vr-shopxo-plugin/plan.md

13 KiB
Raw Blame History

vr-shopxo-plugin 架构决策评议 — plan.md

版本v1.1(合并版)| 日期2026-04-15 | Agentcouncil/FrontendDev + BackendArchitect + SecurityEngineer 关联Issue #9


任务背景

Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题VR 演唱会票务商品中 ShopXO SPEC 与 SKU 的绑定方案。

已知事实:

  • ShopXO goods_spec_baseSKU表当前为空商品 112 的 is_exist_many_spec=0
  • spec_base_id_map 中的 ID如 1001/1002/1003在 DB 中不存在
  • ShopXO 防超卖机制(原子扣 inventory完全未启用

两种架构方向:

  • 方案 A:每个座位 = 一个 SKUstock=1ShopXO 原生防超卖
  • 方案 B:每个 Zone = 一个 SKUstock=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 — 汇总三方推荐 + 最终结论 [Done: council/BackendArchitect] — 全票通过方案 A

Claim 状态

任务 Claim 状态
Q1 [Done: BackendArchitect]
Q2 [Done: BackendArchitect]
Q3 [Done: FrontendDev]
Q4 [Done: council/BackendArchitect] — 全员一致推荐方案A
最终输出 [Done: council/BackendArchitect] — council-output/ARCHITECTURE_DECISION.md 已输出,方案 A 全票通过

依赖关系

  • Q1BackendArchitect先完成后 Q4 才能给出完整推荐
  • Q3SecurityEngineer可与 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

最小修复路径(不破坏现有数据):

  1. 方案甲(最小侵入):在 plugins_service_goods_save_end Hook 中,检测商品有 venue_data$vr- spec 存在时,强制将 is_exist_many_spec 设为 1但不写 spec_base 表(绕过 ShopXO spec 机制,完全走插件自定义逻辑)
  2. 方案乙(规范做法):调用 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

理由:

  1. 安全性ShopXO 原生原子扣库存防超卖,经过大量生产验证;方案 B 的自建 FOR UPDATE 锁在高并发下有死锁风险
  2. 数据一致性:方案 A 的 stock = 1ShopXO 购买流程自带事务保护;方案 B 的 Zone stock 需要插件自己维护一致性和并发安全
  3. 多 Zone 混买:方案 A 前端每 Zone 一个 goods_params 行,后端按 seat_id 原子购买,体验流畅;方案 B 前端分组但后端共享 Zone stock反而增加了前端分组逻辑的复杂度
  4. 维护性:方案 A 依赖 ShopXO 原生机制,故障排查有据可查;方案 B 是"黑盒",出问题只能靠插件自己
  5. $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

理由:

  1. 安全性ShopXO 原生原子扣库存,无需自建锁,超卖风险最低
  2. 正确性:与 ShopXO SPEC 机制对齐is_exist_many_spec=1 时原生防超卖生效
  3. 可追溯性:每个 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 最终结论:推荐方案乙(最小修复集):

  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

Q3 最终结论(汇入 SecurityEngineer + FrontendDev 确认低风险。ThinkPHP {$var} 默认 HTML 转义,$vr- 不会触发变量解析。

Q4 最终推荐:方案 A,理由汇总:

  1. ShopXO 原生原子防超卖BuyService::dec() = MySQL 条件原子扣减,无需自建锁
  2. TOCTOU 风险可接受选座模式并发窗口极小InnoDB 行锁提供最后保护
  3. 票务链路清晰spec_base_id 直接映射座位,票生成无需反向解析
  4. 方案 B 优势不成立:插件自管 SKUHook 隐藏),不走 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 INSERTsxo_goods_spec_typesxo_goods_spec_basesxo_goods_spec_value 三表
  • 关键代码:BuyService.php:1677-1681dec() 机制 = MySQL 条件原子扣减 UPDATE SET inventory = inventory - N WHERE inventory >= NShopXO 防超卖依赖此机制
  • 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。council-output/ARCHITECTURE_DECISION.md 已输出,方案 A 全票通过。


Round 3 安全审计结果(保留,仅供参考)

Task S1 — Admin 鉴权覆盖完整性审查 验证通过

Task S2 — SQL 注入风险审计 无注入风险

Task S3 — XSS / CSRF 防护检查 通过

Task S5 — IDOR 水平越权检查 通过

Task S4 — 敏感操作审计日志设计 设计完成