# VR 演唱会票务小程序 Phase 2 技术评估报告(修正版) > 日期:2026-04-21 > 协作产出:BackendArchitect、FrontendDev、FirstPrinciples > 修正:大头 + 西莉雅(2026-04-21 上午) > 源码依据:BuyService.php、GoodsCartService.php、SeatSkuService.php、ticket_detail.html、vr_tickets install.sql --- ## 执行摘要 Phase 2 完成 4 个已知问题的根因分析 + 1 个新发现潜在 Bug。**经大头确认后,修正了 FirstPrinciples 的关键错误结论。** **核心修正**:FirstPrinciples「购物车对票务无价值」的结论是**错误的**。Buy 链路是正确方向,ShopXO 原生支持多 SKU 合并下单 + extension_data 透传 + onOrderPaid 写入 vr_tickets。只需修复 submit() 的传递方式。 --- ## 问题总览 | # | 问题 | 优先级 | 根因 | |---|------|--------|------| | 1 | 购买提交流程失效 | **P0** | GET→POST 机制错误 + spec 字段格式错误 | | 2 | 缩放时舞台不跟随 | **P1** | DOM 结构导致 transform 不共享 | | 3 | spec 加载问题(已回滚) | **P1** | loadSoldSeats() 是空 stub + 需 sold_seats API | | 4 | 商品详情/图片加载 | **P2** | 模板未引入内容组件 | **新发现**: | # | 问题 | 优先级 | |---|------|--------| | 5 | GetGoodsViewData() 只返回第一个场次 | **P2 潜在** | --- ## Issue 1(P0):购买提交流程失效 ### 根因(三层叠加) **第一层(致命)**:`location.href` 产生 GET,但 `Buy::Index()` 只在 POST 时调用 `BuyDataStorage()`。 ```php // Buy.php:58-61 public function Index() { if($this->data_post) { BuyService::BuyDataStorage($user_id, $this->data_post); // ← POST 才执行 return MyRedirect(MyUrl('index/buy/index')); } else { $buy_data = BuyService::BuyDataRead($user_id); // GET → 读 session → 空 } } ``` → `goods_params` URL 参数从未被读取 → `BuyDataStorage` 未被调用 → buy 确认页收不到数据 → "商品数据为空"。 **第二层(严重)**:字段名 `goods_params` vs 期望的 `goods_data`。 **第三层(中等)**:spec 格式不匹配: - 当前:`spec_base_id: int`(直接传 ID) - ShopXO:`spec: [{type, value}]` 字符串匹配 GoodsSpecValue 表 ### ShopXO Buy 链路完全支持多座位合并下单 **ShopXO 原生能力验证**: - `BuyService::BuyGoods` 第86行:`foreach($params['goods_data'] as $v)` — 原生遍历多 SKU - `BuyService::OrderInsertHandle` 第773行:`'extension_data' => json_encode($v['order_base']['extension_data'])` — 原生写入 extension_data - `vr_tickets` install.sql 已有:`real_name`, `phone`, `id_card` 字段 ✅ - `TicketService::issueTicket()` 第141行:从 `$order['extension_data']` 读取观演人 ✅ ### 正确修复方案(只需改 submit()) ```javascript // var self = this; — 原始代码第6行已有此声明,确保 submit() 上方作用域有 var self = this submit: function() { var self = this; // 如作用域内已有则忽略此行 // 1. 收集观演人 var inputs = document.querySelectorAll('#attendeeList input'); var attendeeData = {}; inputs.forEach(function(input) { var idx = input.dataset.index; var field = input.dataset.field; if (!attendeeData[idx]) attendeeData[idx] = {}; attendeeData[idx][field] = input.value; }); // 2. 构建 ShopXO 原生 goods_data 格式 // // ⚠️ 【必须】order_base.extension_data 是 BuyService 隐含契约(第86行 $v['order_base']) // 必须嵌套在 order_base 内!不能平铺在 goods_data 第一层! // ⚠️ 【必须】直接传 JSON 字符串,不需要 base64 // BuyService 第60行判断:!is_array($_POST['goods_data']) → json_decode() // ShopXO 标准 buy/index.html 第871行也是直接输出 JSON 字符串,不 base64 var goodsDataList = this.selectedSeats.map(function(seat, i) { var specBaseId = self.specBaseIdMap[seat.seatKey] || self.sessionSpecId; return { goods_id: self.goodsId, spec: [{type: '$vr-座位号', value: seat.seatKey}], stock: 1, order_base: { // ← 必须嵌套!不能平铺! extension_data: { attendee: { real_name: attendeeData[i]?.real_name || '', phone: attendeeData[i]?.phone || '', id_card: attendeeData[i]?.id_card || '' } } } }; }); // 3. 隐藏表单 POST 到 Buy 链路 // // ⚠️ requestUrl 来自 PHP 模板注入(ticket_detail.html 第6行): // var requestUrl = ''; // 确认 Goods.php 在传给票务模板时已在 $assign 中注入 host_url var form = document.createElement('form'); form.method = 'POST'; form.action = requestUrl + '?s=index/buy/index'; // 用模板注入的全局 requestUrl 变量 var input = document.createElement('input'); input.name = 'goods_data'; input.value = JSON.stringify(goodsDataList); // 直接 JSON,BuyService 自动处理 form.appendChild(input); document.body.appendChild(form); form.submit(); } ``` **完整数据流**(ShopXO 原生,无需扩展): ``` submit() POST goods_data(含 order_base.extension_data) → Buy::Index → BuyDataStorage(user_id, data_post) [存入 session] → 跳转确认页(GET)→ form hidden field 携带 goods_data → Buy::Add → BuyGoods → OrderInsertHandle → order.extension_data 写入 Order 表 → 支付成功 → onOrderPaid → issueTicket() → 从 $order['extension_data'] 读取观演人 → 写入 vr_tickets(real_name/phone/id_card) ✅ ``` --- ## Issue 2(P1):缩放时舞台不跟随 ### 根因 `.vr-stage` 和 `.vr-seat-rows` 是平级兄弟元素,transform 只作用于子树。 ### 修复方案 ```html