13 KiB
vr-shopxo-plugin 架构决策评议 — plan.md
版本:v1.1(合并版)| 日期:2026-04-15 | Agent:council/FrontendDev + BackendArchitect + SecurityEngineer 关联:Issue #9
任务背景
Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题:VR 演唱会票务商品中 ShopXO SPEC 与 SKU 的绑定方案。
已知事实:
- ShopXO
goods_spec_base(SKU表)当前为空,商品 112 的is_exist_many_spec=0 spec_base_id_map中的 ID(如 1001/1002/1003)在 DB 中不存在- ShopXO 防超卖机制(原子扣 inventory)完全未启用
两种架构方向:
- 方案 A:每个座位 = 一个 SKU(stock=1),ShopXO 原生防超卖
- 方案 B:每个 Zone = 一个 SKU(stock=Zone座位数),自建 FOR UPDATE 防超卖
核心问题(4问)
| # | 问题 | 负责 |
|---|---|---|
| Q1 | 方案 A 后台批量生成 SKU 路径是否可行?ShopXO 是否有批量 API? | BackendArchitect |
| Q2 | 当前商品 112 的 broken 状态(is_exist_many_spec=0 + spec_base 空)是否需要紧急修复?最小修复集? | BackendArchitect + SecurityEngineer |
| Q3 | vr- 前缀方案是否有隐患?ShopXO 内部是否对 有特殊处理? |
SecurityEngineer |
| Q4 | 方案 A vs 方案 B 最终推荐(实现成本 / 安全性 / 可维护性) | 所有成员 |
阶段划分
| 阶段 | 内容 | 负责 |
|---|---|---|
| Round 1(本轮) | 独立评议 + plan.md 合并 | 所有成员 |
| Round 2 | 各成员深入分析(后台实现路径、安全评估、前端方案) | 所有成员 |
| Round 3 | 综合推荐 + 输出最终决策报告 + council-output/ARCHITECTURE_DECISION.md |
FrontendDev 主笔 |
任务清单
- Q1: 方案 A 批量生成 SKU 路径
[Done: BackendArchitect]✅ - Q2: 商品 112 broken 状态紧急修复
[Done: BackendArchitect]✅ - Q3: $vr- 前缀安全评估
[Done: FrontendDev]✅ (ThinkPHP {$var} 默认转义,|raw 仅跳过HTML转义) - Q4: 方案 A vs 方案 B 最终推荐
[Done: council/BackendArchitect]✅ — 全部成员一致推荐方案A - Final:
council-output/ARCHITECTURE_DECISION.md— FrontendDev 主笔汇总三方推荐 + 最终结论[Pending: FrontendDev]
Claim 状态
| 任务 | Claim 状态 |
|---|---|
| Q1 | [Done: BackendArchitect] |
| Q2 | [Done: BackendArchitect] |
| Q3 | [Done: FrontendDev] |
| Q4 | [Done: council/BackendArchitect] — 全员一致推荐方案A |
| 最终输出 | [Pending: FrontendDev] — 主笔 council-output/ARCHITECTURE_DECISION.md |
依赖关系
- Q1(BackendArchitect)先完成,后 Q4 才能给出完整推荐
- Q3(SecurityEngineer)可与 Q1 并行
- Q2 可独立完成,紧急程度由 BackendArchitect 判定
- 三方分析完成后,FrontendDev 主笔 Round 3 最终报告
各成员 Round 1 初判
BackendArchitect 初判
Q1 初步判断:Plan A 后台批量生成 SKU 可行。ShopXO 的 goods_spec_base 是标准 MySQL 表,插件可直接 INSERT。但需要确认:
- ShopXO 商品保存时是否校验 spec_base 的 referential integrity
- 上万座位时批量 INSERT 的性能
- spec_base_id_map 中的 ID 是否需要与 ShopXO 内部 ID 对齐
Q2 初步判断:当前 broken 状态暂不需要立即修复。购买流程走的是裸商品逻辑(is_exist_many_spec=0),对 Phase 3 的购买流程设计反而是参考点——需要明确购买流程最终走哪条路后再修。
Q4 初步判断:倾向 方案 A。ShopXO 原生防超卖机制比自建锁更可靠(DB 层面原子操作),且不破坏 ShopXO 生态完整性。
FrontendDev 初判(Q1-Q4 分析)
Q1 分析:方案 A 批量生成 SKU 路径
结论:可行,但实现路径复杂。
ShopXO spec_base 生成机制:
- 商品保存时,
GoodsService::Save()调用SpecService::Save()逐条写入sxo_goods_spec_base - 没有现成的批量 API — 需要在插件初始化/商品绑定时,批量调用
SpecService或直接 SQL INSERT - 方案 A 的 SKU 数量 = 座位数(一场演唱会可能 10000+ 个座位)
- 前端配合:uni-app 需要维护
seat_id → spec_base_id映射(已在spec_base_id_map中) - 关键风险:商品规格管理页面会显示 10000+ 行 SKU,可能导致 ShopXO 后台崩溃
- 解决方向:插件专用规格不出现在 ShopXO 原生规格管理页,通过 Hook 隐藏;建立独立的"座位 SKU 管理"页面
Q2 分析:商品 112 broken state 最小修复集
结论:需要立即修复,推荐最小方案。
根因:is_exist_many_spec=0 意味着 ShopXO 认为此商品无多规格,spec_base 表自然为空(从未生成过 SKU)。
最小修复路径(不破坏现有数据):
- 方案甲(最小侵入):在
plugins_service_goods_save_endHook 中,检测商品有venue_data且$vr-spec 存在时,强制将is_exist_many_spec设为 1,但不写 spec_base 表(绕过 ShopXO spec 机制,完全走插件自定义逻辑) - 方案乙(规范做法):调用
SpecService::Save()为每个座位生成一条 spec_base 记录(inventory=1, price 从 seat_type 读取)
推荐方案甲(最小修复):
- 优势:无需重建 SKU,不影响现有订单数据
- 代价:
is_exist_many_spec变成"脏 flag",但这是 ShopXO 的内部状态,插件不依赖它做业务 - 操作:一条 UPDATE + 一条 Hook 注入
Q3 分析:$vr- 前缀隐患
结论:低风险,但需实测确认。
ShopXO spec name 字段无字符过滤,数据库 varchar 类型允许 $ 字符。潜在风险点:
- ThinkPHP 的
__isset()/ 动态属性访问可能对$敏感(但 spec name 存 DB 而非 PHP 属性,低风险) - 前端模板渲染时,
$vr-字符串可能触发 Vue/JS 的变量插值解析({{ $vr-场馆 }})—— 这是真实风险 - ShopXO 原生规格管理页面可能将
$视为特殊字符处理
需要验证:uni-app 端 spec value 的渲染方式(是纯文本还是模板字符串?)
Q4 最终推荐:方案 A vs 方案 B
推荐:方案 A(每个座位一个 SPEC/SKU)
理由:
- 安全性:ShopXO 原生原子扣库存防超卖,经过大量生产验证;方案 B 的自建 FOR UPDATE 锁在高并发下有死锁风险
- 数据一致性:方案 A 的 stock = 1,ShopXO 购买流程自带事务保护;方案 B 的 Zone stock 需要插件自己维护一致性和并发安全
- 多 Zone 混买:方案 A 前端每 Zone 一个 goods_params 行,后端按 seat_id 原子购买,体验流畅;方案 B 前端分组但后端共享 Zone stock,反而增加了前端分组逻辑的复杂度
- 维护性:方案 A 依赖 ShopXO 原生机制,故障排查有据可查;方案 B 是"黑盒",出问题只能靠插件自己
- $vr- 前缀:spec_base_id_map 的 key 可以是 seat_id,无需改 ShopXO spec name 存储
方案 B 的唯一优势:SKU 数量少(Zone 数量 vs 座位数量),后台管理简单。但这个优势在演唱会 10000 座场景下不如安全和一致性重要。
SecurityEngineer 初判(Q2/Q3/Q4)
Q2:紧急修复优先级
当前状态:商品 112 的 broken 状态(is_exist_many_spec=0 + spec_base 空)
- ShopXO 防超卖机制完全未启用
- spec_base_id_map 指向不存在的 DB 记录
最小修复集:必须立即修复,但需确认走方案 A 还是 B
- Pending — 方案确定后,填充 spec_base 表(每个 SKU 一行)
- Pending — 设置 is_exist_many_spec = 1
- Pending — 关联 spec_base_id_map 与实际 seat 数据
结论:Q2 依赖 Q1/Q4 的输出,暂标记为 blocked。
Q3:$vr- 前缀安全隐患
已知事实:
- ShopXO spec name 允许特殊字符($、-、中文均无过滤)
- ThinkPHP 模板引擎(View)可能对 $ 有变量插值行为
风险点:
- View 层:Tpl 模板中
{:$spec_name}是否会解析 $vr- 作为 PHP 变量? - DB 层:spec name 入库是否经过转义?
- API 层:spec name 作为 JSON key 时是否安全?
结论:需要代码验证(Round 2 执行)。
Q4:方案 A vs B 最终推荐
初步倾向:方案 A(每个座位一个 SKU)
理由:
- 安全性:ShopXO 原生原子扣库存,无需自建锁,超卖风险最低
- 正确性:与 ShopXO SPEC 机制对齐,is_exist_many_spec=1 时原生防超卖生效
- 可追溯性:每个 SKU 独立订单项,核销链路清晰
BackendArchitect Round 3 最终推荐(Q1+Q2+Q4)
最终 Q4 推荐:方案 A(每个座位一个 SKU)
基于 Round 2 完整分析,各问题最终结论:
Q1 最终结论:可行。必须旁路 GoodsSpecificationsInsert()(因为它每次都 DELETE+重建),走直接 SQL INSERT 路径。性能:10000 座位 ≈ 3-4 秒(分批 500 条/批)。关键:spec_base_id_map[seat_id] → actual_db_id 映射必须在 INSERT 后即时重建。
Q2 最终结论:推荐方案乙(最小修复集):
UPDATE sxo_goods SET is_exist_many_spec=1 WHERE id=112INSERT $vr- spec_type(场馆/分区/时段三行)- 幂等保护:
TicketService::issueTicket()中对spec_base_id=0做 fallback
Q3 最终结论(汇入 SecurityEngineer + FrontendDev 确认):低风险。ThinkPHP {$var} 默认 HTML 转义,$vr- 不会触发变量解析。
Q4 最终推荐:方案 A,理由汇总:
- ShopXO 原生原子防超卖:
BuyService::dec()= MySQL 条件原子扣减,无需自建锁 - TOCTOU 风险可接受:选座模式并发窗口极小,InnoDB 行锁提供最后保护
- 票务链路清晰:
spec_base_id直接映射座位,票生成无需反向解析 - 方案 B 优势不成立:插件自管 SKU(Hook 隐藏),不走 ShopXO 后台,无"管理困难"问题
Round 3 行动项(BackendArchitect):
| 优先级 | 行动项 |
|---|---|
| P0 | 创建 SeatSkuService::BatchGenerate() — 直接 SQL INSERT 批量生成 SKU |
| P0 | 执行 Q2 最小修复集:UPDATE is_exist_many_spec + INSERT $vr- spec_type |
| P1 | TicketService::issueTicket() 添加 spec_base_id=0 幂等保护 |
| P2 | 插件独立 SKU 管理页面(隔离 ShopXO 原生规格管理)← FrontendDev |
BackendArchitect Round 2 深入分析(Q1+Q2)
详细分析见 docs/ROUND2_ANALYSIS.md。核心结论:
Q1 结论:可行,但必须旁路 GoodsSpecificationsInsert()
- ShopXO 的
GoodsSpecificationsInsert()在每次商品保存时DELETE所有现有 spec 后重建,10K+ 座位场景不可用 - 可行路径:直接 SQL INSERT 到
sxo_goods_spec_type、sxo_goods_spec_base、sxo_goods_spec_value三表 - 关键代码:
BuyService.php:1677-1681的dec()机制 = MySQL 条件原子扣减UPDATE SET inventory = inventory - N WHERE inventory >= N,ShopXO 防超卖依赖此机制 - TOCTOU 窗口极小(选座模式并发低 + InnoDB 行锁),推荐接受此风险
- 性能:10000 座位 = ~3-4 秒(需分批 500 条/批提交)
Q2 结论:推荐方案乙(最小修复集)
is_exist_many_spec=0→ 执行UPDATE goods SET is_exist_many_spec=1 WHERE id=112- 写入
$vr-规格维度到sxo_goods_spec_type - 幂等保护:票生成逻辑已有
spec_base_id冗余,不依赖 DB 引用
Q4 初步推荐:方案 A
- 原子性已验证(BuyService dec 机制)
- 数据完整性高(每个座位 inventory=1)
- 票务链路清晰(spec_base_id → 座位直接映射)
- 方案 B 的"SKU 少"优势在演唱会 10K+ 场景不成立(插件自管,不走 ShopXO 后台)
FrontendDev Round 2 深入分析(Q3+Q4)
Q3 结论:$vr- 前缀安全 ✅
- ThinkPHP
{$var}默认做 HTML 转义,$vr- 不会被解析为 PHP 变量 |raw仅跳过 HTML 转义,不会执行变量插值- ShopXO spec name 存 DB 无过滤,但渲染层安全
Q4 结论:推荐方案 A(每个座位一个 SKU)
- ShopXO 原生
BuyService.php:1677的 dec() 机制提供原子防超卖 - 当前
submit()是 Plan B 模式,specBaseIdMap 未接入 - 需重构 submit() 按 seat_id 分组,每组单独 spec_base_id
行动项(优先级排序)
| 优先级 | 行动项 | 负责 |
|---|---|---|
| P0 | 紧急修复商品 112 broken state | BackendArchitect |
| P1 | 实现方案 A 批量 SKU 生成 | BackendArchitect |
| P2 | 隔离 ShopXO 规格管理页面(Hook 隐藏插件 SKU) | FrontendDev |
共识投票
[CONSENSUS: YES] — BackendArchitect Round 3 完成。Q1-Q4 全部完成,三方一致推荐方案 A。等待 FrontendDev 输出最终 council-output/ARCHITECTURE_DECISION.md 后,Council 全部完成。