vr-shopxo-plugin/plan.md

13 KiB
Raw Blame History

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

版本v1.2(最终合并版)| 日期2026-04-15 | Agentcouncil/FrontendDev + BackendArchitect + SecurityEngineer 关联Issue #9 | 状态FINAL


任务背景

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
Q3 vr- 前缀方案是否有隐患ShopXO 内部是否对 有特殊处理? SecurityEngineer + FrontendDev
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 + FrontendDev]
  • Q4: 方案 A vs 方案 B 最终推荐 [Done: 所有成员] — 三方一致推荐方案 A
  • Final: council-output/ARCHITECTURE_DECISION.md [Done: FrontendDev]

Claim 状态

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

依赖关系

  • Q1BackendArchitect先完成后 Q4 才能给出完整推荐
  • Q3SecurityEngineer可与 Q1 并行
  • Q2 可独立完成,紧急程度由 BackendArchitect 判定
  • 三方分析完成后FrontendDev 主笔 Round 3 最终报告

各成员 Round 1 初判

BackendArchitect 初判

Q1 初步判断Plan A 后台批量生成 SKU 可行。ShopXO 的 goods_spec_base 是标准 MySQL 表,插件可直接 INSERT。

Q2 初步判断:当前 broken 状态暂不需要立即修复。购买流程走的是裸商品逻辑is_exist_many_spec=0需要明确购买流程最终走哪条路后再修。

Q4 初步判断:倾向 方案 A。ShopXO 原生防超卖机制比自建锁更可靠DB 层面原子操作)。

FrontendDev 初判Q1-Q4 分析)

Q1:结论:可行,但实现路径复杂。 无现成批量 API需要插件自管Hook 隐藏。SKU 数量 = 座位数10000+)。 Q2:结论:需要立即修复,推荐最小方案。 Q3:结论:低风险,但需实测确认。 Q4 推荐方案 A每个座位一个 SPEC/SKU。安全性+数据一致性优先。

SecurityEngineer 初判Q2/Q3/Q4

Q2:依赖 Q1/Q4标记为 blocked。 Q3ThinkPHP View 层可能对 $ 有变量插值行为需要代码验证Round 2 执行)。 Q4:初步倾向 方案 A


各成员 Round 2 深入分析

BackendArchitect Round 2 深入分析Q1+Q2

详细分析见 docs/ROUND2_ANALYSIS.md

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

  • 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 >= N
  • TOCTOU 窗口极小(选座模式并发低 + InnoDB 行锁),推荐接受此风险
  • 性能10000 座位 ≈ 3-4 秒(需分批 500 条/批提交)

Q2 结论:推荐方案乙(最小修复集)

  • UPDATE goods SET is_exist_many_spec=1 WHERE id=112
  • 写入 $vr- 规格维度到 sxo_goods_spec_type
  • 幂等保护:票生成逻辑已有 spec_base_id 冗余

Q4 初步推荐:方案 A

  • 原子性已验证BuyService dec 机制)
  • 数据完整性高(每个座位 inventory=1
  • 票务链路清晰spec_base_id → 座位直接映射)

SecurityEngineer Round 2 分析Q3

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

FrontendDev Round 2 深入分析Q3+Q4

Q3 结论:$vr- 前缀安全

  • ThinkPHP {$var} 默认做 HTML 转义,$vr- 不会被解析为 PHP 变量
  • |raw 仅跳过 HTML 转义,不会执行变量插值
  • ThinkPHP parseVar 正则对连字符 - 的处理会阻断 $vr- 的完整解析
  • ShopXO spec name 存 DB 无过滤,但渲染层安全

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

核心发现

  1. 当前 ticket_detail.html submit() 是 Plan B 模式,specBaseIdMap 已声明但未接入 submit 逻辑
  2. ShopXO 购买流程从 spec_base 表读取库存并原子扣减

方案 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 功能失效

Plan A 前端实现路径

关键修改:将 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 注入),前端只需接入即可。


各成员 Round 3 最终推荐

BackendArchitect Round 3 最终推荐Q1+Q2+Q4

Q1 最终结论:可行。必须旁路 GoodsSpecificationsInsert(),走直接 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 后台,无"管理困难"问题

FrontendDev Round 3 最终推荐Q3+Q4

三方一致推荐 方案 A每个座位一个 ShopXO SKU

最终决策报告:council-output/ARCHITECTURE_DECISION.md


行动项(优先级排序)

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

共识投票

成员 CONSENSUS
BackendArchitect [CONSENSUS: YES] — 推荐方案 ARound 2/3 分析完成
SecurityEngineer [CONSENSUS: YES] — $vr- 前缀低风险,方案 A 推荐
FrontendDev [CONSENSUS: YES] — 方案 A 推荐,前端配合方案清晰

全票通过:采纳方案 A


Issue #9 执行计划 — Round 4P0 修复)

执行日期2026-04-15 | 目标:方案 A 全量落地

任务清单

  • P0-A: BaseService::initGoodsSpecs() — 修复商品 112 broken state [Claimed: BackendArchitect]
  • P0-B: SeatSkuService::BatchGenerate() — 批量生成座位级 SKU [Claimed: BackendArchitect]
  • P1: ticket_detail.html submit() 重构 — seat-level goods_params [Claimed: FrontendDev]
  • P1-Verification: 前端实测验证(商品 112 购买流程) [Claimed: FrontendDev]

阶段划分

阶段 内容 负责
Draft BackendArchitect: P0-A + P0-B 实现FrontendDev: submit() 重构 双线并行
Review 互相 review对方代码确认接口对齐 双线并行
Finalize 合并到 main实测验证 共同

依赖关系

  • P0-A 完成后P0-B 才能验证 spec_type 维度是否存在
  • P0-A + P0-B 完成后,前端 submit() 重构才有正确的 spec_base_id 可用
  • 前端实测依赖后端 SKU 已生成

P1 详细执行计划

当前状态ticket_detail.html 第 413-418 行)

var goodsParams = JSON.stringify([{
    goods_id: this.goodsId,
    spec_base_id: this.sessionSpecId,   // ← Zone 级别,只有 1 个
    stock: this.selectedSeats.length,    // ← 数量,但 ShopXO 不知道具体是哪些座位
    extension_data: extensionData
}]);

重构目标:每座一行 goods_params

// 每座一行,逐座提交
var goodsParamsList = [];
this.selectedSeats.forEach(function(seat) {
    var seatKey = seat.row + '_' + seat.col;
    var specBaseId = app.specBaseIdMap[seatKey]?.spec_base_id || app.sessionSpecId;
    goodsParamsList.push({
        goods_id: app.goodsId,
        spec_base_id: specBaseId,
        stock: 1,
        extension_data: JSON.stringify({
            attendee: attendees.find(function(a) { return a._seat === seatKey; }),
            seat: seat
        })
    });
});
var goodsParams = JSON.stringify(goodsParamsList);

关键改动点

  1. submit() 改为遍历 selectedSeats,每座一行 goods_params
  2. spec_base_idspecBaseIdMap[seatKey] 获取Plan A座位级 SKU
  3. stock 固定为 1每个 SKU 对应一个座位)
  4. extension_data 改为 seat-level每座携带自己的信息
  5. 保留 sessionSpecId 作为 fallbackPlan B 回退)

P0-B 返回值接口约定

BackendArchitect 的 BatchGenerate() 返回值需包含:

[
    'total' => 100,           // 生成数量
    'spec_base_id_map' => [   // seatKey → spec_base_id 映射
        'A_1' => ['spec_base_id' => 2001, 'zone_id' => 'zone1', 'row' => 'A', 'col' => 1],
        'B_2' => ['spec_base_id' => 2002, 'zone_id' => 'zone1', 'row' => 'B', 'col' => 2],
        ...
    ]
]

前端期望 specBaseIdMap 格式:

  • Key: row_col(如 "A_1"
  • Value: {spec_base_id: number, zone_id: string, row: string, col: number}

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

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

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

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

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

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