vr-shopxo-plugin/reviews/council-phase2-assessment.md

9.4 KiB
Raw Blame History

VR 演唱会票务小程序 Phase 2 技术评估报告

日期2026-04-21 协作产出BackendArchitect、FrontendDev、FirstPrinciples 源码依据BuyService.php、GoodsCartService.php、SeatSkuService.php、ticket_detail.html


执行摘要

Phase 2 完成了对 4 个已知问题的根因分析,识别了 1 个未被提及的潜在 Bug多场次并给出了分优先级的修复方案。

结论:所有 4 个问题均为可修复的技术问题,无架构级障碍。


问题总览

# 问题 优先级 根因分类
1 购买提交流程失效 P0 GET→POST 机制错误 + 参数契约不匹配
2 缩放时舞台不跟随 P1 DOM 结构导致 transform 不共享
3 spec 加载机制回滚 P1 ShopXO spec 匹配机制不兼容 + API 端点缺失
4 商品详情/图片加载 P2 模板未引入 ShopXO 内容渲染组件

新增发现(潜在 Bug

# 问题 优先级
5 GetGoodsViewData() 只返回第一个场次 P2 潜在

Issue 1P0购买提交流程失效

根因分析(三层叠加)

第一层(致命)location.href 产生 GET 请求,但 Buy::Index() 只在 data_post 为真时调用 BuyDataStorage()

// Buy.php:56-62
public function Index()
{
    if($this->data_post) {
        BuyService::BuyDataStorage($this->user['id'], $this->data_post);
        return MyRedirect(MyUrl('index/buy/index'));
    } else {
        // GET 分支:从 session 读取URL 参数从未被读取
        $buy_data = BuyService::BuyDataRead($this->user['id']);
    }
}

goods_params URL 参数从未被读取 → BuyDataStorage 从未被调用 → BuyDataRead 返回空 → "商品数据为空"。

第二层(严重):字段名不匹配。

  • 前端发送:goods_paramsJSON 数组)
  • ShopXO 期望:goods_dataJSON 数组)

第三层(中等):规格匹配机制不兼容。

  • 当前:spec_base_id: parseInt(specBaseId) — 直接传 ID
  • ShopXOspec: [{type, value}] — 通过 type:value 字符串匹配 GoodsSpecValue 表

推荐修复

前端FrontendDev

// 隐藏表单 POST最小化变更复用 Buy 链路)
submit: function() {
    var goodsDataList = this.selectedSeats.map(function(seat, i) {
        var specBaseId = self.specBaseIdMap[seat.seatKey] || self.sessionSpecId;
        return {
            goods_id: self.goodsId,
            spec: [{type: '座位', value: seat.seatKey}],  // ShopXO 规格格式
            stock: 1
        };
    });

    var form = document.createElement('form');
    form.method = 'POST';
    form.action = MyUrl('index/buy/index');
    var input = document.createElement('input');
    input.name = 'goods_data';
    input.value = Base64.encode(JSON.stringify(goodsDataList));
    form.appendChild(input);
    document.body.appendChild(form);
    form.submit();
}

关于 extension_data观演人信息ShopXO 原生不支持 extension_dataOrderDetail 表无此字段)。推荐方案:新增 vr_order_attendee 表,在 BuyService::OrderInsert() 后存储观演人信息。

关键提醒FirstPrinciples

Issue 1 的准确描述是「Buy 传输机制损坏」,而非「购物车格式错误」。

当前代码实际上绕过了购物车,直接进入 Buy 链路。这说明直觉上的「绕过购物车」需求并不存在——只是 submit() 的传递方式错了。


Issue 2P1缩放时舞台不跟随

根因分析

<div class="vr-seat-map-wrapper">
    <div class="vr-stage">舞 台</div>       <!-- 平级 sibling -->
    <div class="vr-seat-rows" id="seatRows"></div>
</div>

CSS transform: scale() 只作用于应用元素的子树。舞台和座位行是平级元素,缩放座位时舞台不动。

推荐修复

方案:将舞台和座位包裹在同一 zoom 容器内FrontendDev 实施)

<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;
}

注意:舞台的 border-radius 在缩放后可能变形,需在 zoom 场景下单独调整。


Issue 3P1spec 加载机制回滚

根因分析

问题 AloadSoldSeats() 是空 stub无任何网络请求。

loadSoldSeats: function() {
    // TODO: 从后端加载已售座位
    // $.get(...);  // 空,无任何调用
}

问题 BShopXO 的 GoodsSpecDetail 通过 spec.value 字符串匹配规格,而非直接接受 spec_base_id

// GoodsService.php:2749-2757
$spec = array_column($params['spec'], 'value');
$where['value'] = $spec;
$ids = Db::name('GoodsSpecValue')->where($where)->column('goods_spec_base_id');

VR 票务场景下,每个座位对应独立的 GoodsSpecBase 记录inventory=1。ShopXO 标准流程需要生成 GoodsSpecValue 记录type='座位', value='seatKey')。

推荐修复

后端BackendArchitect:新增插件 API 端点

GET /?s=api/vr-ticket/sold-seats&goods_id=X&spec_base_id=Y
Response: {code: 0, data: {sold_seats: ["A_1", "A_2", "B_5"]}}

前端在选中场次后调用此接口,标记 .sold class。

关于 spec 加载ShopXO 的 spec 数据通过 GetGoodsViewData() 在模板渲染时注入前端。如果已生成了 GoodsSpecBase 记录,最直接的方式是维护座位→规格映射,并提供独立的已售座位查询 API而不是依赖 ShopXO 的规格匹配流程。


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

现状

  • 商品内容$goods['content'] 正常渲染
  • 商品相册$goods['images']⚠️ 数据存在但未使用
    • renderSessions() 依赖 goods_spec_data,不含 images
    • .vr-goods-photos 已定义样式但从未被调用
  • .goods-detail-content CSS⚠️ 缺失

建议

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


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

根因分析

// SeatSkuService.php:BatchGenerate()
return array_merge($data, [
    'goods_spec_data' => $validConfigs[0],  // ← 只取第一个配置
    'vr_seat_template' => $template,
]);

validConfigs[0] 意味着多场次商品只返回第一个场次的座位模板和规格数据。当一个商品配置了多场演唱会时,其余场次对用户不可见。

修复方向

修改 BatchGenerate() 返回格式,将 goods_spec_data 改为数组($validConfigs),前端根据选中场次索引读取对应数据。


第一性原则视角的关键提醒

  1. P0 的真正来源submit() 用 GET 做 POST 的事。Buy 链路本身可用,不需要重构 spec 系统。

  2. spec_base_id_map 是性能缓存:不是业务必需。如果 onOrderPaid 能通过 seatKey 查询到 spec_base_idmap 可以去掉。保留它是合理的优化,但需要确保同步机制。

  3. 购物车对票务无价值:当前实现已绕过购物车进入 Buy 链路。选座的实时性决定了购物车会增加超卖风险,快速购买是正确方向。

  4. 已售座位展示是 P1不是 P0:真正的 P0 是 onOrderPaid 防双售。前端是否实时显示已售状态,是体验优化,不是业务正确性的根本。

  5. onOrderPaid 是座位唯一性权威(未审计):在 P0 修复部署前,必须验证此 Hook 是否正确实现了座位锁定逻辑。若未实现,任何前端修复都无法防止重复销售。

  6. 最小修复范围:只需修复 submit() 的传递方式(隐藏表单 POST。不需要重构 spec 系统,不需要引入实时已售座位更新(除非 spec 加载方案已实施)。


修复优先级与分工

优先级 问题 负责 修复说明
P0 Issue 1 submit() FrontendDev 改用隐藏表单 POST复用 Buy 链路
P1 Issue 2 舞台缩放 FrontendDev 新增 zoom wrapper 容器
P1 Issue 3 spec 加载 BackendArchitect 新增插件 sold_seats API 端点
P2 Issue 4 商品详情 FrontendDev 确认是否需要,补充 CSS
P2 Issue 5 多场次 BackendArchitect BatchGenerate 返回数组格式
FP extension_data BackendArchitect 新增 vr_order_attendee 表

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

文件 行号 说明
Buy.php 56-62 Index() — POST/GET 分支BuyDataStorage/BuyDataRead
BuyService.php ~51 BuyGoods — goods_data 参数校验 + base64 解码
BuyService.php ~173 GoodsSpecificationsHandle — 规格解析
BuyService.php ~104-109 GoodsSpecDetail 调用 — 通过 spec.value 匹配
GoodsService.php 2720-2795 GoodsSpecDetail — type:value 查询 GoodsSpecValue
BuyService.php 1932-1936 BuyDataStorage — session 缓存21600s TTL
SeatSkuService.php BatchGenerate 返回 validConfigs[0] — 多场次 Bug

VR 演唱会票务小程序 Phase 2 技术评估 — Council 协作完成