feat(Phase 2): 完成票务商品前端展示层
- Goods.php: item_type=ticket 时加载 ticket_detail.html 模板 - SeatSkuService.php: 新增 GetGoodsViewData() 供前端模板使用 - TicketService.php: onOrderPaid 改用 sxo_order_detail 表 + JSON spec 解析 关联: Phase 2 前台展示层完成council/SecurityEngineer
parent
dc63cff77c
commit
7bd8967648
|
|
@ -135,6 +135,19 @@ class Goods extends Common
|
|||
MyViewAssign($assign);
|
||||
// 钩子
|
||||
$this->PluginsHook($goods_id, $goods);
|
||||
|
||||
// 票务商品:加载自定义模板并注入座位数据
|
||||
if (($goods['item_type'] ?? '') === 'ticket') {
|
||||
$viewData = \app\plugins\vr_ticket\service\SeatSkuService::GetGoodsViewData($goods_id);
|
||||
MyViewAssign([
|
||||
'vr_seat_template' => $viewData['vr_seat_template'] ?? null,
|
||||
'goods_spec_data' => $viewData['goods_spec_data'] ?? [],
|
||||
]);
|
||||
// 使用绝对路径 + fetch() 方法,让 Think 驱动正确解析 include 路径
|
||||
$tplFile = ROOT . 'app' . DS . 'plugins' . DS . 'vr_ticket' . DS . 'view' . DS . 'goods' . DS . 'ticket_detail.html';
|
||||
return \think\facade\View::fetch($tplFile, $assign);
|
||||
}
|
||||
|
||||
return MyView();
|
||||
}
|
||||
MyViewAssign('msg', MyLang('goods.goods_no_data_tips'));
|
||||
|
|
|
|||
|
|
@ -348,4 +348,102 @@ class SeatSkuService extends BaseService
|
|||
|
||||
return ['code' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品前端展示数据(供 ticket_detail.html 模板使用)
|
||||
*
|
||||
* @param int $goodsId
|
||||
* @return array ['vr_seat_template' => [...], 'goods_spec_data' => [...]]
|
||||
*/
|
||||
public static function GetGoodsViewData(int $goodsId): array
|
||||
{
|
||||
// 读取 vr_goods_config
|
||||
$goods = \think\facade\Db::name('goods')->find($goodsId);
|
||||
$vrGoodsConfig = json_decode($goods['vr_goods_config'] ?? '', true);
|
||||
|
||||
if (empty($vrGoodsConfig) || !is_array($vrGoodsConfig)) {
|
||||
return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
|
||||
}
|
||||
|
||||
// 取第一个配置块(单模板模式)
|
||||
$config = $vrGoodsConfig[0];
|
||||
$templateId = intval($config['template_id'] ?? 0);
|
||||
if ($templateId <= 0) {
|
||||
return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
|
||||
}
|
||||
|
||||
// 读取座位模板(包含 seat_map 和 spec_base_id_map)
|
||||
$seatTemplate = \think\facade\Db::name(self::table('seat_templates'))
|
||||
->where('id', $templateId)
|
||||
->find();
|
||||
|
||||
// 解码 seat_map JSON(存储时是 JSON 字符串)
|
||||
if (!empty($seatTemplate['seat_map'])) {
|
||||
$decoded = json_decode($seatTemplate['seat_map'], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$seatTemplate['seat_map'] = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// 解码 spec_base_id_map JSON
|
||||
if (!empty($seatTemplate['spec_base_id_map'])) {
|
||||
$decoded = json_decode($seatTemplate['spec_base_id_map'], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$seatTemplate['spec_base_id_map'] = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// 构建场次列表(goods_spec_data)
|
||||
$sessions = $config['sessions'] ?? [];
|
||||
$goodsSpecData = [];
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
$start = $session['start'] ?? '';
|
||||
$end = $session['end'] ?? '';
|
||||
$timeRange = $start && $end ? "{$start}-{$end}" : ($start ?: $end);
|
||||
|
||||
// 查找该场次对应的 spec_base_id
|
||||
$specValue = \think\facade\Db::name('goods_spec_value')
|
||||
->alias('sv')
|
||||
->join('goods_spec_base sb', 'sb.id = sv.goods_spec_base_id')
|
||||
->where('sv.goods_id', $goodsId)
|
||||
->where('sv.value', $timeRange)
|
||||
->where('sb.price', '>', 0)
|
||||
->find();
|
||||
|
||||
$goodsSpecData[] = [
|
||||
'spec_id' => $specValue['goods_spec_base_id'] ?? 0,
|
||||
'spec_name' => $timeRange,
|
||||
'price' => $specValue['price'] ?? floatval($goods['price'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
// 如果没有从配置读取到场次,尝试从数据库直接读取场次类规格值
|
||||
if (empty($goodsSpecData)) {
|
||||
$sessionValues = \think\facade\Db::name('goods_spec_value')
|
||||
->alias('sv')
|
||||
->join('goods_spec_base sb', 'sb.id = sv.goods_spec_base_id')
|
||||
->field('sv.goods_spec_base_id as spec_id, sv.value as spec_name, sb.price')
|
||||
->where('sv.goods_id', $goodsId)
|
||||
->where('sb.price', '>', 0)
|
||||
->order('sb.id asc')
|
||||
->select()->toArray();
|
||||
|
||||
foreach ($sessionValues as $sv) {
|
||||
if (preg_match('/^\d{2}:\d{2}-\d{2}:\d{2}$/', $sv['spec_name'])) {
|
||||
$goodsSpecData[] = [
|
||||
'spec_id' => $sv['spec_id'],
|
||||
'spec_name' => $sv['spec_name'],
|
||||
'price' => floatval($sv['price']),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'vr_seat_template' => $seatTemplate ?: null,
|
||||
'goods_spec_data' => $goodsSpecData,
|
||||
'goods_config' => $config,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class TicketService extends BaseService
|
|||
}
|
||||
|
||||
// 查询订单
|
||||
$order = \think\facade\Db::name('Order')->find($order_id);
|
||||
$order = \think\facade\Db::name('order')->find($order_id);
|
||||
if (empty($order) || $order['pay_status'] != 1) {
|
||||
BaseService::log('onOrderPaid: order not paid or not found', ['order_id' => $order_id], 'warning');
|
||||
return false;
|
||||
|
|
@ -41,16 +41,51 @@ class TicketService extends BaseService
|
|||
return true; // 不是票务商品,不报错
|
||||
}
|
||||
|
||||
// 查询商品快照(规格信息)
|
||||
$order_goods = \think\facade\Db::name('OrderGoods')
|
||||
// 查询订单明细(规格信息存储在 spec JSON 字段)
|
||||
$order_goods = \think\facade\Db::name('order_detail')
|
||||
->where('order_id', $order_id)
|
||||
->select();
|
||||
|
||||
if (empty($order_goods)) {
|
||||
BaseService::log('onOrderPaid: no order goods', ['order_id' => $order_id], 'error');
|
||||
BaseService::log('onOrderPaid: no order detail', ['order_id' => $order_id], 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 逐个生成票(每个规格选项 = 一张票)
|
||||
// 逐行解析 spec JSON,提取座位信息
|
||||
foreach ($order_goods as &$og) {
|
||||
$spec_list = json_decode($og['spec'] ?? '[]', true);
|
||||
$spec_name = '';
|
||||
$spec_base_id = 0;
|
||||
|
||||
if (is_array($spec_list)) {
|
||||
// 优先取座位号,其次分区名
|
||||
foreach ($spec_list as $spec_item) {
|
||||
$type = $spec_item['type'] ?? '';
|
||||
$value = $spec_item['value'] ?? '';
|
||||
if ($type === '$vr-座位号') {
|
||||
$spec_name = $value;
|
||||
break;
|
||||
} elseif ($type === '$vr-分区' && !$spec_name) {
|
||||
$spec_name = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试通过座位名反向查找 spec_base_id
|
||||
if ($spec_name) {
|
||||
$spec_base = \think\facade\Db::name('goods_spec_value')
|
||||
->where('goods_id', $order['goods_id'])
|
||||
->where('value', $spec_name)
|
||||
->find();
|
||||
$spec_base_id = $spec_base['goods_spec_base_id'] ?? 0;
|
||||
}
|
||||
|
||||
$og['_parsed_spec_name'] = $spec_name;
|
||||
$og['_parsed_spec_base_id'] = $spec_base_id;
|
||||
}
|
||||
unset($og);
|
||||
|
||||
// 逐个生成票(每个订单明细行 = 一张票)
|
||||
$count = 0;
|
||||
foreach ($order_goods as $og) {
|
||||
$ticket_id = self::issueTicket($order, $og);
|
||||
|
|
@ -70,21 +105,24 @@ class TicketService extends BaseService
|
|||
/**
|
||||
* 发放单张票
|
||||
*
|
||||
* @param array $order 订单数据
|
||||
* @param array $order_goods 订单商品数据(包含 spec_base_id)
|
||||
* @param array $order 订单数据
|
||||
* @param array $og 订单商品数据(来自 vrt_order_detail,已解析 _parsed_spec_name 等字段)
|
||||
* @return int 票ID
|
||||
*/
|
||||
public static function issueTicket($order, $order_goods)
|
||||
public static function issueTicket($order, $og)
|
||||
{
|
||||
// P0-1 幂等保护:同一订单+同一规格只发一张票
|
||||
$spec_name = $og['_parsed_spec_name'] ?? '';
|
||||
$spec_base_id = $og['_parsed_spec_base_id'] ?? 0;
|
||||
|
||||
// P0-1 幂等保护:同一订单+同一座位名只发一张票
|
||||
$existing = \think\facade\Db::name(BaseService::table('tickets'))
|
||||
->where('order_id', $order['id'])
|
||||
->where('spec_base_id', $order_goods['spec_base_id'] ?? 0)
|
||||
->where('seat_info', $spec_name)
|
||||
->find();
|
||||
if (!empty($existing)) {
|
||||
BaseService::log('issueTicket: idempotent_skip', [
|
||||
'order_id' => $order['id'],
|
||||
'spec_base_id'=> $order_goods['spec_base_id'] ?? 0,
|
||||
'order_id' => $order['id'],
|
||||
'seat_info' => $spec_name,
|
||||
], 'info');
|
||||
return $existing['id'];
|
||||
}
|
||||
|
|
@ -93,14 +131,14 @@ class TicketService extends BaseService
|
|||
|
||||
// 构建 QR 数据
|
||||
$qr_payload = [
|
||||
'id' => 0, // 写入后再更新
|
||||
'code' => $ticket_code,
|
||||
'event' => $order['goods_id'],
|
||||
'seat' => $order_goods['spec_name'] ?? '', // 规格名=座位信息
|
||||
'id' => 0, // 写入后再更新
|
||||
'code' => $ticket_code,
|
||||
'event'=> $order['goods_id'],
|
||||
'seat' => $spec_name,
|
||||
];
|
||||
$qr_data = BaseService::encryptQrData($qr_payload);
|
||||
|
||||
// 观演人信息(从订单扩展字段读取,由购票页表单写入)
|
||||
// 观演人信息:优先从 order.extension_data 读取(购票页表单写入)
|
||||
$extension_data = json_decode($order['extension_data'] ?? '{}', true);
|
||||
$attendee = $extension_data['attendee'] ?? [];
|
||||
|
||||
|
|
@ -111,19 +149,19 @@ class TicketService extends BaseService
|
|||
'order_no' => $order['order_no'],
|
||||
'goods_id' => $order['goods_id'],
|
||||
'goods_snapshot' => json_encode([
|
||||
'goods_name' => $order['goods_name'] ?? '',
|
||||
'spec_name' => $order_goods['spec_name'] ?? '',
|
||||
'price' => $order_goods['goods_price'] ?? 0,
|
||||
'goods_name' => $og['title'] ?? '',
|
||||
'spec_name' => $spec_name,
|
||||
'price' => $og['price'] ?? 0,
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'user_id' => $order['user_id'],
|
||||
'ticket_code' => $ticket_code,
|
||||
'qr_data' => $qr_data,
|
||||
'seat_info' => $order_goods['spec_name'] ?? '',
|
||||
'spec_base_id' => $order_goods['spec_base_id'] ?? 0,
|
||||
'seat_info' => $spec_name,
|
||||
'spec_base_id' => $spec_base_id,
|
||||
'real_name' => $attendee['real_name'] ?? '',
|
||||
'phone' => $attendee['phone'] ?? '',
|
||||
'id_card' => $attendee['id_card'] ?? '',
|
||||
'verify_status' => 0, // 0=未核销
|
||||
'verify_status' => 0,
|
||||
'issued_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
|
|
@ -145,7 +183,7 @@ class TicketService extends BaseService
|
|||
* 核销票(事务保护 + 悲观锁防并发)
|
||||
*
|
||||
* @param string $ticket_code 票码
|
||||
* @param int $verifier_id 核销员ID
|
||||
* @param int $verifier_id 核销员ID
|
||||
* @return array [code, msg]
|
||||
*/
|
||||
public static function verifyTicket($ticket_code, $verifier_id)
|
||||
|
|
@ -178,8 +216,8 @@ class TicketService extends BaseService
|
|||
->update([
|
||||
'verify_status' => 1,
|
||||
'verify_time' => $now,
|
||||
'verifier_id' => $verifier_id,
|
||||
'updated_at' => $now,
|
||||
'verifier_id' => $verifier_id,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// 写入核销记录
|
||||
|
|
@ -201,20 +239,20 @@ class TicketService extends BaseService
|
|||
'verifier_id' => $verifier_id,
|
||||
]);
|
||||
|
||||
// 审计日志(失败也记录,便于追溯异常)
|
||||
// 审计日志
|
||||
AuditService::logVerify(
|
||||
$ticket['id'],
|
||||
$ticket_code,
|
||||
$verifier_id,
|
||||
$verifier['name'] ?? '',
|
||||
'success',
|
||||
0 // 原状态(核销前一定是 0)
|
||||
0
|
||||
);
|
||||
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '核销成功',
|
||||
'data' => [
|
||||
'code' => 0,
|
||||
'msg' => '核销成功',
|
||||
'data' => [
|
||||
'seat_info' => $ticket['seat_info'],
|
||||
'real_name' => $ticket['real_name'],
|
||||
'goods_name' => json_decode($ticket['goods_snapshot'] ?? '{}', true)['goods_name'] ?? '',
|
||||
|
|
@ -239,7 +277,6 @@ class TicketService extends BaseService
|
|||
if ($status !== null) {
|
||||
$where['verify_status'] = $status;
|
||||
}
|
||||
|
||||
return \think\facade\Db::name(BaseService::table('tickets'))
|
||||
->where($where)
|
||||
->order('created_at', 'desc')
|
||||
|
|
@ -248,9 +285,6 @@ class TicketService extends BaseService
|
|||
|
||||
/**
|
||||
* 生成 QR 码图片 URL
|
||||
*
|
||||
* @param string $ticket_code
|
||||
* @return string QR码图片URL
|
||||
*/
|
||||
public static function getQrCodeUrl($ticket_code)
|
||||
{
|
||||
|
|
@ -258,7 +292,6 @@ class TicketService extends BaseService
|
|||
'type' => 'vr_ticket',
|
||||
'code' => $ticket_code,
|
||||
]));
|
||||
|
||||
return request()->domain().request()->root() . '?s=index/qrcode/index&content=' . urlencode($content) . '&size=8&level=H';
|
||||
return request()->domain() . request()->root() . '?s=index/qrcode/index&content=' . urlencode($content) . '&size=8&level=H';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue