vr-shopxo-plugin/docs/COUNCIL_PHASE2_ASSESSMENT_C...

10 KiB
Raw Blame History

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 1P0购买提交流程失效

根因(三层叠加)

第一层(致命)location.href 产生 GETBuy::Index() 只在 POST 时调用 BuyDataStorage()

// 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
  • ShopXOspec: [{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()

// 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 = '<?php echo Config("shopxo.host_url"); ?>';
    //   确认 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);  // 直接 JSONBuyService 自动处理
    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 2P1缩放时舞台不跟随

根因

.vr-stage.vr-seat-rows 是平级兄弟元素transform 只作用于子树。

修复方案

<div class="vr-seat-map-wrapper">
    <div class="vr-zoom-container" id="zoomContainer">
        <div class="vr-stage">舞 台</div>
        <div class="vr-seat-rows" id="seatRows"></div>
    </div>
</div>
.vr-zoom-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    transform-origin: center top;
    transition: transform 0.2s ease;
}

缩放 JS 操作 #zoomContainertransform: scale(),舞台和座位同步缩放。


Issue 3P1spec 加载问题(已回滚)

根因

  • loadSoldSeats() 是空 TODO stub无任何 AJAX 调用
  • 后端无 sold_seats API 端点

修复方案

后端:新增 plugins/vr_ticket/index/soldSeats API 端点

GET /?s=api/plugins/index&pluginsname=vr_ticket&pluginscontrol=index&pluginsaction=soldSeats
Query: goods_id, spec_base_id
Response: {code: 0, data: {sold_seats: ["A_1", "A_2", "B_5"]}}

前端loadSoldSeats() 调用该接口,标记 .sold class。


Issue 4P2商品详情/图片加载

  • $goods['content'] 正常渲染
  • $goods['images']⚠️ 数据存在但未使用
  • .goods-detail-content CSS⚠️ 缺失

如需展示商品图片,在模板中添加图片渲染逻辑。如票务详情页不需要 ShopXO 商品内容,可降级为「确认不需要」。


Issue 5P2 潜在GetGoodsViewData 只返回第一个场次

SeatSkuService::GetGoodsViewData() 第368行返回 validConfigs[0],多场次商品只显示第一个场次。

修复方向

修改返回值格式为数组,前端根据选中场次索引读取对应数据。


第一性原则视角(修正后)

  1. Issue 1 是「传输机制损坏」,不是「流程错误」Buy 链路完全正确,多 SKU 合并下单是 ShopXO 原生能力,不需要绕过。

  2. extension_data 存储完全在 ShopXO 生态内order.extension_dataonOrderPaidvr_tickets 全链路原生打通,不需要新建表或扩展字段。

  3. spec_base_id_map 是性能缓存:如果 onOrderPaid 能通过 seatKeyspec value 字符串)查询 spec_base_idmap 可以去掉。但保留是合理的优化。

  4. onOrderPaid 是座位唯一性权威(未审计):在 Issue 1 修复部署前,必须验证此 Hook 是否正确实现了座位锁定(幂等 + FOR UPDATE。这是防双售的核心。

  5. onOrderPaid spec 匹配存在潜在 bug⚠️ 新增)BatchGenerate 写入 GoodsSpecValue.value 的格式是 "{$venueName}-{$roomName}-{$char}-{$rowLabel}{$col}"(如 "场馆A-放映室1-A-A3座"),而前端 seatKey 格式是 "roomId_A_3",两者不匹配。TicketService::issueTicket 第57-77行通过 type='$vr-座位号' 匹配 GoodsSpecValue.value 的逻辑会失效。目前不影响功能是因为幂等靠 seat_info 字段(不需要 spec_base_id但如果未来需要精确关联此处需修复 value 写入格式或改为读 GoodsSpecBase.extends.seat_key。

  6. 最小修复范围:只需修改 submit() 函数POST + 正确 goods_data 格式 + extension_data。不需要重构 spec 系统,不需要新建表,不需要绕过 Buy 链路。


修复优先级

优先级 Issue 负责 修复说明
P0 Issue 1 submit() FrontendDev 改隐藏表单 POST正确构造 goods_data + extension_data
P1 Issue 2 舞台缩放 FrontendDev 新增 zoom wrapper 容器
P1 Issue 3 spec 加载 BackendArchitect 新增 sold_seats API + 前端调用
P2 Issue 4 商品详情 FrontendDev 确认是否需要,补充 CSS
P2 Issue 5 多场次 BackendArchitect GetGoodsViewData 返回数组格式

附录ShopXO Buy 链路关键代码索引

文件 行号 说明
Buy.php 58-61 Index() — POST/GET 分支BuyDataStorage/BuyDataRead
BuyService.php 51-62 BuyGoods — goods_data 参数校验 + base64/JSON 解码
BuyService.php 86 foreach($params['goods_data'] as $v) — 多 SKU 原生遍历
BuyService.php 104-109 GoodsSpecDetail 调用 — spec.value 字符串匹配
BuyService.php 773 OrderInsertHandle — extension_data 写入 order 表
BuyService.php 1932 BuyDataStorage — 21600s TTL session 缓存
Buy/index.html 871 确认表单 hidden goods_data field原生包含
TicketService.php 141-143 issueTicket — 从 $order['extension_data'] 读观演人
SeatSkuService.php ~368 GetGoodsViewData — validConfigs[0] 多场次 Bug

VR 演唱会票务小程序 Phase 2 技术评估 — Council 协作完成2026-04-21 修正版