vr-shopxo-plugin/council-output/ARCHITECTURE_DECISION.md

5.2 KiB
Raw Blame History

vr-shopxo-plugin 架构决策报告

关联Issue #9 日期2026-04-15 参与council/BackendArchitect + SecurityEngineer + FrontendDev 轮次Round 3最终


背景

vr-shopxo-plugin 是 ShopXO 票务插件核心场景VR 演唱会票务小程序用户选座→下单→QR 核销。

Phase 0/1/2 已完成基础骨架,暴露 P0 架构问题ShopXO SPEC 与 SKU 的绑定方案。

当前商品 112 实测状态(问题根因):

is_exist_many_spec = 0   -- ShopXO 认为无多规格
spec_base  =            -- 没有任何 SKU
spec_base_id_map  {A:1001, B:1002, C:1003}  -- 这些 ID 在 DB 里不存在!

ShopXO 防超卖机制完全未启用,购买走裸商品逻辑。


核心问题与结论

Q1方案 A 后台批量生成 SKU 路径是否可行?

结论:可行,但必须旁路 GoodsSpecificationsInsert()

  • ShopXO GoodsService::GoodsSpecificationsInsert() 每次商品保存时 DELETE 所有现有 spec 后重建10K+ 座位场景不可用
  • 可行路径:直接 SQL INSERT 到 sxo_goods_spec_type、sxo_goods_spec_base、sxo_goods_spec_value 三表
  • 性能10000 座位约 3-4 秒(分批 500 条/批提交)
  • 关键spec_base_id_map[seat_id] → actual_db_id 映射必须在 INSERT 后即时重建

Q2商品 112 broken 状态是否需要紧急修复?

结论:推荐方案乙(最小修复集)

  1. UPDATE sxo_goods SET is_exist_many_spec = 1 WHERE id = 112
  2. INSERT $vr- spec_type场馆/分区、时段三个维度)
  3. TicketService::issueTicket() 添加 spec_base_id = 0 的幂等保护 fallback

紧急程度中等,不影响当前票务逻辑运行,但应在 Phase 3 批量 SKU 生成前完成。

Q3$vr- 前缀方案是否有隐患?

结论低风险SecurityEngineer + FrontendDev 确认)

  • ThinkPHP {$var} 默认做 HTML 转义,$vr- 不会被解析为 PHP 变量
  • |raw 仅跳过 HTML 转义,不会执行变量插值
  • ThinkPHP parseVar 正则对连字符 - 的处理会阻断 $vr- 的完整解析
  • ShopXO spec name 数据库字段无字符过滤,存储层安全

Q4方案 A vs 方案 B 最终推荐

结论:一致推荐方案 A每个座位一个 SKU

三方一致理由汇总:

维度 方案 A每座位一个 SKU 方案 B每个 Zone 一个 SKU
防超卖 ShopXO 原生 dec() 原子扣减 自建 FOR UPDATE 锁(死锁风险)
数据一致性 每个座位 inventory=1事务保护 Zone 库存需插件自己维护
购买流程 ShopXO 原生流程完整走通 需 Hook 旁路购买逻辑
票务链路 spec_base_id 直接映射座位 需反向解析 seat_id
SKU 数量 10000+插件自管ShopXO 后台隐藏) 10-50Zone 数量)
并发安全 InnoDB 行锁 + 选座低并发窗口 自建锁,高并发风险
可维护性 依赖 ShopXO 原生机制,有据可查 插件黑盒,故障排查困难

方案 B 的"SKU 少"优势在演唱会 10K+ 场景不成立:插件自管 SKU通过 Hook 隐藏插件专用规格,不走 ShopXO 原生规格管理页面。


最终推荐

采纳方案:方案 A — 每个座位一个 ShopXO SKUstock=1

实现路径

  1. Phase 3 批量 SKU 生成BackendArchitect 负责)

    • 创建 SeatSkuService::BatchGenerate():直接 SQL INSERT 批量生成 SKU
    • 旁路 GoodsSpecificationsInsert(),避免每次商品保存清空重建
    • 分批提交500 条/批),初始化一次
  2. 紧急修复BackendArchitect 负责)

    • UPDATE is_exist_many_spec = 1
    • INSERT $vr- spec_type场馆/分区/时段)
    • TicketService::issueTicket() 的 spec_base_id=0 幂等保护
  3. 插件规格隔离FrontendDev 负责)

    • 通过 Hook 隐藏插件专用 SKU不出现在 ShopXO 规格管理页)
    • 建立独立"座位 SKU 管理"页面

技术细节ShopXO 原生防超卖机制

BuyService.php:1677-1681

$where = [
    ['id', '=', $base['data']['spec_base']['id']],
    ['goods_id', '=', $v['goods_id']],
    ['inventory', '>=', $v['buy_number']],
];
Db::name('GoodsSpecBase')->where($where)->dec('inventory', $v['buy_number'])->update();

翻译为 SQLUPDATE goods_spec_base SET inventory = inventory - N WHERE inventory >= N

这是 MySQL 层面的条件原子扣减TOCTOU 窗口极小(选座模式已在前端锁定具体座位,并发窗口远低于总库存),推荐接受此风险。


行动项

优先级 行动项 负责
P0 创建 SeatSkuService::BatchGenerate() — 直接 SQL INSERT 批量生成 SKU BackendArchitect
P0 执行 Q2 最小修复集UPDATE is_exist_many_spec + INSERT $vr- spec_type BackendArchitect
P1 TicketService::issueTicket() 添加 spec_base_id=0 幂等保护 BackendArchitect
P2 通过 Hook 隐藏插件专用 SKU建立独立座位 SKU 管理页面 FrontendDev

共识

  • BackendArchitect: CONSENSUS: YES — 推荐方案 ARound 2/3 分析完成
  • SecurityEngineer: CONSENSUS: YES — $vr- 前缀低风险,方案 A 推荐
  • FrontendDev: CONSENSUS: YES — 方案 A 推荐,前端配合方案清晰

全票通过:采纳方案 A