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

8.7 KiB
Raw Blame History

vr-shopxo-plugin 架构决策报告

文档版本: v1.0 | 日期: 2026-04-15 | 发起: CouncilFrontendDev + BackendArchitect + SecurityEngineer 关联 Issue: #9 | 状态: FINAL


1. 背景与问题

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

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

已知状态(商品 112 实测):

  • is_exist_many_spec = 0ShopXO 认为无多规格)
  • goods_spec_base 表为空(无任何 SKU
  • spec_base_id_map 指向不存在的 DB 记录ID 1001/1002/1003
  • ShopXO 防超卖机制完全未启用

2. 两种架构方向

方案 A每座=SKU 方案 B每 Zone=SKU
SKU 粒度 每个具体座位一行inventory=1 每个 ZoneA/B/C一行inventory=Zone 座位数
防超卖 ShopXO 原生原子扣库存(BuyService dec() 自建 FOR UPDATE 锁,需并发逻辑
多 Zone 混买 每座一行 goods_params后端原子处理 前端分组,后端共享 Zone 库存
后台复杂度 10000+ SKU 行插件自管Hook 隐藏) Zone 数量少,后台友好
与 ShopXO 生态 完全对齐 绕过 spec 校验

3. 四问评议结论

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

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

  • ShopXO 的 GoodsSpecificationsInsert() 每次商品保存时 DELETE 所有现有 spec 后重建10K+ 座位场景不可用。
  • 可行路径:直接 SQL INSERTsxo_goods_spec_typesxo_goods_spec_basesxo_goods_spec_value 三表。
  • 性能10000 座位 ≈ 3-4 秒(需分批 500 条/批提交)。
  • 初始化一次,座位模板绑定时生成,后续不变。
  • ShopXO 防超卖依赖 BuyService.php:1677-1681dec() 机制MySQL 条件原子扣减 UPDATE SET inventory = inventory - N WHERE inventory >= NTOCTOU 窗口极小(选座模式并发低 + InnoDB 行锁),推荐接受此风险

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

结论:推荐方案乙(最小修复集),紧急程度中等。

最小修复集:

-- Step 1: 启用多规格
UPDATE sxo_goods SET is_exist_many_spec = 1 WHERE id = 112;

-- Step 2: 写入 $vr- 规格维度
INSERT INTO sxo_goods_spec_type (goods_id, name, value, add_time) VALUES
(112, '$vr-场馆', '[{"name":"国家体育馆","images":""}]', UNIX_TIMESTAMP()),
(112, '$vr-分区', '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]', UNIX_TIMESTAMP()),
(112, '$vr-时段', '[{"name":"2026-05-01 19:00","images":""}]', UNIX_TIMESTAMP());

-- Step 3: 幂等保护(在 TicketService::issueTicket() 中添加 spec_base_id=0 fallback

真正的批量 SKU 生成在 Phase 3「座位模板绑定」时完成。

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

结论低风险确认安全。SecurityEngineer + FrontendDev 双重确认)

  • ThinkPHP 模板解析机制{$var} 默认 HTML 转义输出,{:expr} 执行表达式但需要 $var 存在。
  • $vr-场馆 作为 spec name 字符串存储在 DB 中,不作为 PHP 变量名,不触发变量插值。
  • parseVar 正则 \$[a-zA-Z_](?>\w*)$vr-场馆 中仅匹配 $vr,剩余 -场馆 留在原地,生成无效 PHP 代码,无 XSS 风险。
  • {{$spec.name}} 中的 spec name 是属性值ThinkPHP 不会二次解析为模板语法。
  • ShopXO spec name 字段无字符过滤,数据库 varchar 类型允许 $ 字符。
  • 唯一注意ShopXO 后台规格管理页可能将 $vr- 显示不当(纯展示问题,不影响安全)。

Q4方案 A vs B 最终推荐?

结论:明确推荐方案 A每个座位一个 SKU。三方一致。

维度 方案 A推荐 方案 B
防超卖 ShopXO 原生原子扣库存DB 层保证 自建 FOR UPDATE 锁,需自己写并发逻辑
实现复杂度 后端需批量生成 1 万+ SKU前端 submit() 需改为逐座提交 后端简单;前端按 Zone 分组即可
多 Zone 混买 每座一行 goods_params后端原子处理体验流畅 前端分组但后端共享 Zone 库存,复杂度高
后台可维护性 10000+ SKU 行,但可 Hook 隐藏(插件自管) Zone 数量少,后台友好
调试/故障排查 每个 SKU 独立,可追溯 共享库存,出问题难以定位
与 ShopXO 生态 完全对齐,无缝集成 绕过 spec 校验,部分 ShopXO 功能失效
TOCTOU 风险 极小(选座并发低 + InnoDB 行锁兜底) 可控(显式锁)

方案 B 的唯一优势SKU 数量少)在演唱会 10000+ 座场景下不成立——插件自建独立 SKU 管理页面ShopXO 原生规格管理页通过 Hook 隐藏插件 SKU优势消失。


4. 最终推荐

采用方案 A每个座位 = 一个 ShopXO SKUstock=1

推荐理由(综合三方)

  1. 安全性最优ShopXO 原生原子扣库存防超卖,经过生产验证,无需自建锁。
  2. 数据一致性:每个座位 inventory=1ShopXO 购买流程自带事务保护TOCTOU 窗口极小(选座模式下并发度远低于总库存)。
  3. 票务链路清晰spec_base_id 直接对应座位,票生成逻辑无需反向解析,核销链路可追溯。
  4. 多 Zone 混买体验好:前端每 Zone 一个 goods_params 行,后端按 seat_id 原子购买,体验流畅。
  5. 与 ShopXO 生态对齐:完全走 ShopXO 原生购买流程,故障排查有据可查,升级兼容性好。
  6. $vr- 前缀安全:无 ThinkPHP 变量插值风险,无 XSS 风险,完全隔离于用户规格。

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 窗口极小(选座模式已在前端锁定具体座位,请求打到后端时并发度远低于总库存),推荐接受此风险。


5. 行动项(优先级排序)

优先级 行动项 负责 依赖
P0 执行 Q2 最小修复集:UPDATE is_exist_many_spec=1 + 写入 $vr- spec_type + spec_base_id=0 幂等保护 BackendArchitect
P0 创建 SeatSkuService::BatchGenerate():直接 SQL INSERT 批量生成 SKU分批 500 条) BackendArchitect P0 完成后
P1 重构 ticket_detail.html submit():从 session-level 提交改为 seat-level 逐座提交,接入 specBaseIdMap FrontendDev P0 完成后
P2 实现 loadSoldSeats():查询各 seat spec_base 的库存状态 FrontendDev P0 完成后
P3 Hook 隐藏插件 SKU插件 SKU 不出现在 ShopXO 原生规格管理页 FrontendDev P1 完成后
P3 设计插件独立 SKU 管理页面(隔离 ShopXO 原生规格管理) FrontendDev 远期

6. 各成员立场

成员 Q1 Q2 Q3 Q4 最终推荐
BackendArchitect 可行,旁路 GoodsSpecificationsInsert 推荐方案乙 方案 A
FrontendDev 可行但复杂(需 Hook 隐藏 SKU 行) 推荐方案甲(最小侵入) 低风险安全 方案 A
SecurityEngineer blocked待 Q4 确认) 低风险安全 方案 A

全票通过:采纳方案 A


7. 附录

A. 关键代码路径

  • 购买原子扣库存BuyService.php:1677-1681dec() 机制
  • 规格插入(禁用)GoodsService.php:2142GoodsSpecificationsInsert()(每次保存 DELETE+重建)
  • 批量 SKU 生成:插件自建 SeatSkuService::BatchGenerate(),直接 SQL INSERT 三表
  • 前端提交改造ticket_detail.html — submit() 从 session-level 改为 seat-level
  • specBaseIdMap 注入:后端 PHP 注入前端,供 submit() 使用
  • $vr- 前缀安全shopxo/vendors/thinkphp/library/think/Template.php:837-955parseVar 正则

B. 缩写说明

  • SKU = ShopXO goods_spec_base 表中的一条记录(一个规格组合)
  • spec_base_id = SKU 的主键 ID
  • spec_base_id_map = 插件内存/缓存中的 seat_id → spec_base_id 映射
  • TOCTOU = Time-of-check to time-of-use并发竞态窗口
  • goods_params = 购买请求中的规格参数数组