From 7bd8967648f75f477e37b8fa1f56a253299a8d6b Mon Sep 17 00:00:00 2001 From: Council Date: Mon, 20 Apr 2026 05:22:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(Phase=202):=20=E5=AE=8C=E6=88=90=E7=A5=A8?= =?UTF-8?q?=E5=8A=A1=E5=95=86=E5=93=81=E5=89=8D=E7=AB=AF=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Goods.php: item_type=ticket 时加载 ticket_detail.html 模板 - SeatSkuService.php: 新增 GetGoodsViewData() 供前端模板使用 - TicketService.php: onOrderPaid 改用 sxo_order_detail 表 + JSON spec 解析 关联: Phase 2 前台展示层完成 --- shopxo/app/index/controller/Goods.php | 13 +++ .../vr_ticket/service/SeatSkuService.php | 98 ++++++++++++++++ .../vr_ticket/service/TicketService.php | 107 ++++++++++++------ 3 files changed, 181 insertions(+), 37 deletions(-) diff --git a/shopxo/app/index/controller/Goods.php b/shopxo/app/index/controller/Goods.php index af0e662..603cea9 100755 --- a/shopxo/app/index/controller/Goods.php +++ b/shopxo/app/index/controller/Goods.php @@ -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')); diff --git a/shopxo/app/plugins/vr_ticket/service/SeatSkuService.php b/shopxo/app/plugins/vr_ticket/service/SeatSkuService.php index 5b2c3e4..6259330 100644 --- a/shopxo/app/plugins/vr_ticket/service/SeatSkuService.php +++ b/shopxo/app/plugins/vr_ticket/service/SeatSkuService.php @@ -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, + ]; + } } diff --git a/shopxo/app/plugins/vr_ticket/service/TicketService.php b/shopxo/app/plugins/vr_ticket/service/TicketService.php index ffe7399..2b4b61f 100644 --- a/shopxo/app/plugins/vr_ticket/service/TicketService.php +++ b/shopxo/app/plugins/vr_ticket/service/TicketService.php @@ -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'; } }