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

8.3 KiB
Raw Blame History

Council Phase 2 技术评估报告

协作产出 | 日期2026-04-21 参与 AgentBackendArchitect、FrontendDev、FirstPrinciples


问题总览

# 问题 优先级 根因分类
1 购物车提交格式错误 P0 API 传递方式 + 参数契约不匹配
2 缩放时舞台元素不跟随 P1 DOM 结构导致 CSS transform 不共享
3 spec 加载问题(已回滚) P1 ShopXO 规格匹配机制 + API 链路不明确
4 商品详情/图片加载 P2 模板未引入 ShopXO 商品内容组件

Issue 1P0购物车提交格式错误

根因分析(三层)

第一层(致命)location.href 产生 GET 请求,但 Buy::Index() 只在 $this->data_post 时处理下单逻辑。

// Buy.php:58-61
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 表

推荐修复(前后端)

前端BackendArchitect + FrontendDev 联合)

// 方案 A隐藏表单 POST最小化变更
submit: function() {
    var goods_data = this.selectedSeats.map(function(seat, i) {
        return {
            goods_id: self.goodsId,
            spec: [{type: '座位', value: seat.seatKey}],  // ← ShopXO 规格格式
            stock: 1,
            extension_data: JSON.stringify({attendee: attendeeData[i], seat: {...}})
        };
    });

    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 = JSON.stringify(goods_data);
    form.appendChild(input);
    document.body.appendChild(form);
    form.submit();
}

后端BackendArchitect

  • 方案 B推荐在插件中新增 VrTicketBuy 控制器,复用 BuyService 链路,但绕过 ShopXO 的 spec 匹配(直接通过 spec_base_id 查询)
  • 方案 CBuyService::BuyInitialize 中增加插件扩展点,允许 vr_ticket 插件注入自定义的规格处理逻辑

API 设计建议

当前实现是「用 GET 做 POST 的事」。正确的做法是:

  1. 隐藏表单 POST goods_dataindex/buy/indexShopXO 原生)
  2. 或者插件新增端点POST goods_dataplugins/vr_ticket/buy-direct

Issue 2P1缩放时舞台元素不跟随

根因分析

<div class="vr-seat-map-wrapper">
    <div class="vr-stage">舞 台</div>      <!-- 舞台wrapper 直接子元素 -->
    <div class="vr-seat-rows" id="seatRows"></div>  <!-- 座位行 -->
</div>

CSS transform: scale() 只作用于应用元素的子树。.vr-stage.vr-seat-rows 是平级,没有共同的 transform 容器。

推荐修复FrontendDev

方案:将舞台和座位行包裹在同一 zoom 容器内

<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(X),舞台和座位同步缩放。

注意:舞台的 border-radius: 50% 50% 0 0 / 20px 20px 0 0 弧形在缩放后可能变形,需要在 zoom 场景下调整。


Issue 3P1spec 加载问题

根因分析

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

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

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

问题 B:插件 API 端点未建立,导致前端 loadSoldSeats() 是 TODO stub。

推荐修复

后端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"]}}

前端在选中场次后调用此接口,标记已售座位。

关于 ShopXO spec 机制:如果 VR 票务已经生成了 GoodsSpecBase 记录,最直接的方式是让插件维护座位→规格的映射,并提供独立的已售座位查询 API而不是依赖 ShopXO 的规格匹配流程。


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

根因分析

ticket_detail.html 是插件独立模板ShopXO 商品的 content_web 和图片数据由 Goods.php Index() 加载,但插件模板可能未正确引入这些数据。

推荐修复

确认 ticket_detail.html 是否需要 ShopXO 商品内容渲染。如果需要,应该在模板中引入 ShopXO 的商品内容组件:

  • 商品详情:$goods['content_web'] 由 GoodsService 处理
  • 商品图册:通过 ResourcesService 获取

如果票务 UI 不需要 ShopXO 商品内容区(票务详情页有自己的布局),则此问题可降级为「确认不需要」。


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

  1. P0 的真正来源submit() 的 URL 重定向方式错了,修复后 Buy 链路本身是可用的——不需要重构 spec 系统。

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

  3. 购物车对票务无价值:当前实现已经在用 Buy 链路,不是 Cart 链路。说明直觉上的「绕过购物车」需求其实不存在——只是 submit() 的传递方式错了。

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

  5. 多场次 bugGetGoodsViewData() 只返回第一个配置的场次(取 validConfigs[0])。如果一个商品有多个场次配置,只显示第一个——这是潜在的 bug。

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


修复优先级与分工

优先级 问题 负责方 修复说明
P0 Issue 1 submit() BackendArchitect + FrontendDev 改用隐藏表单 POST复用 Buy 链路
P1 Issue 2 舞台缩放 FrontendDev 新增 zoom wrapper 容器
P1 Issue 3 spec 加载 BackendArchitect 新增插件 API 端点
P2 Issue 4 商品详情 延后 确认是否需要

附录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

Council Phase 2 技术评估报告 — 由 BackendArchitect、FrontendDev、FirstPrinciples 协作完成