vr-shopxo-plugin/plan.md

14 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: SecurityEngineer]
  • Q4: 方案 A vs 方案 B 最终推荐 [Done: FrontendDev]
  • Final: council-output/ARCHITECTURE_DECISION.md — 汇总三方推荐 + 最终结论 [Done: FrontendDev]

Claim 状态

任务 Claim 状态
Q1 [Done: BackendArchitect]
Q2 [Done: BackendArchitect]
Q3 [Done: SecurityEngineer]
Q4 [Done: FrontendDev]
最终输出 [Done: FrontendDev]

依赖关系

  • 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 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 后台)

SecurityEngineer Round 2 分析Q3

SecurityEngineer 在 Round 2 进行了 ThinkPHP View 层的 $vr- 前缀安全审计,结论:无高危风险。

FrontendDev Round 2 深入分析

Q4 最终推荐:方案 A每个座位一个 SPEC/SKU—— 明确推荐

经过代码级验证后,确认推荐方案 A。

核心发现

发现 1当前 ticket_detail.html 的 submit() 是 Plan B 模式

检查 shopxo/app/plugins/vr_ticket/view/goods/ticket_detail.html 第 413-418 行:

var goodsParams = JSON.stringify([{
    goods_id: this.goodsId,
    spec_base_id: this.sessionSpecId,   // ← session spec_id不是 seat spec_base_id
    stock: this.selectedSeats.length,   // ← seat count不是 1
    extension_data: extensionData
}]);

→ 当前实现是 Plan B(按场次买多个座位),specBaseIdMap 已声明但未接入 submit 逻辑。

发现 2BuyService 依赖 spec_base 原子扣库存

BuyService.php 第 113-115 行:

$goods['price'] = (float) $goods_base['data']['spec_base']['price'];
$goods['original_price'] = (float) $goods_base['data']['spec_base']['original_price'];
$goods['spec_base_id'] = $goods_base['data']['spec_base']['id'];

→ ShopXO 购买流程从 spec_base 表读取库存并原子扣减。spec_base 为空时购买走裸商品逻辑inventory 字段在 goods 表)。

方案 A vs B 最终对比

维度 方案 A每座=SKU 方案 B每 Zone=SKU
防超卖 ShopXO 原生原子扣库存stock=1DB 层保证 自建 FOR UPDATE 锁,需自己写并发逻辑
实现复杂度 后端需批量生成 1 万+ SKU前端 submit() 需改为逐座提交 后端简单;前端按 Zone 分组即可
多 Zone 混买 每座一行 goods_params后端原子处理 前端分组但后端共享 Zone 库存,复杂度高
后台可维护性 10000+ SKU 行,但可 Hook 隐藏 Zone 数量少,后台友好
调试/故障排查 每个 SKU 独立,可追溯 共享库存,出问题难以定位
与 ShopXO 生态 完全对齐,无缝集成 绕过 spec 校验,部分 ShopXO 功能失效
当前代码适配成本 ticket_detail.html submit() 需重构 基本无需改动

Plan A 前端实现路径ticket_detail.html

关键修改:将 submit() 从"session-level 提交"改为"seat-level 逐座提交"

// Plan A: 每座一行 goods_params逐座购买
this.selectedSeats.forEach(function(seat) {
    var seatSpecBaseId = app.specBaseIdMap[seat.row + '_' + seat.col]?.spec_base_id;
    // 如果 spec_base_id 存在,走 ShopXO 原生购买
    // 否则走 Plan B 回退逻辑
});

specBaseIdMap 数据结构已就位(从后端 PHP 注入),前端只需接入即可。

Q3 验证:$vr- 前缀安全

结论:低风险,确认安全。

证据:

  1. ticket_detail.html 使用 ThinkPHP 模板 {:$goods.title|default='...'} —— $goods 是 PHP 变量,不是模板表达式
  2. $vr_seat_template.seat_map 是 PHP 对象访问,|json_encode|raw 是模板过滤器链,|raw 仅用于跳过 HTML 转义,不触发变量插值
  3. ThinkPHP {$var} 默认转义输出;{:$expr} 执行表达式但需要 $var 存在
  4. $vr- 作为 spec name 字符串存储在 DB 中,不作为 PHP 变量名

唯一需注意ShopXO 后台规格管理页可能将 $vr- 显示不当(纯展示问题,不影响安全)。

Q2 前端视角最小修复

当前 ticket_detail.htmlloadSoldSeats() 是 TODOPlan A 需要:

  1. 后端生成 spec_base SKUsBackendArchitect 负责)
  2. 前端 loadSoldSeats() 调用 API 查询各 seat spec_base 的库存状态

最小可行路径:先让购买流程能跑通,再迭代优化


行动项(优先级排序)

优先级 行动项 负责
P0 紧急修复商品 112 broken state BackendArchitect
P1 实现方案 A 批量 SKU 生成GoodsSpecificationsInsert 直接 SQL BackendArchitect
P2 重构 ticket_detail.html submit() 接入 specBaseIdMap FrontendDev
P3 Hook 隐藏插件 SKUspec_base_id_map key = seat_id FrontendDev

共识投票

[CONSENSUS: YES] — Round 3 完成,所有 Q1-Q4 分析完成,最终决策报告已输出(方案 A


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

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

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

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

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

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