# 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 实测状态(问题根因):** ```sql 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-50(Zone 数量) | | 并发安全 | InnoDB 行锁 + 选座低并发窗口 | 自建锁,高并发风险 | | 可维护性 | 依赖 ShopXO 原生机制,有据可查 | 插件黑盒,故障排查困难 | 方案 B 的"SKU 少"优势在演唱会 10K+ 场景不成立:插件自管 SKU,通过 Hook 隐藏插件专用规格,不走 ShopXO 原生规格管理页面。 --- ## 最终推荐 ### 采纳方案:方案 A — 每个座位一个 ShopXO SKU(stock=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: ```php $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(); ``` 翻译为 SQL:UPDATE 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 — 推荐方案 A,Round 2/3 分析完成 - SecurityEngineer: CONSENSUS: YES — $vr- 前缀低风险,方案 A 推荐 - FrontendDev: CONSENSUS: YES — 方案 A 推荐,前端配合方案清晰 全票通过:采纳方案 A