diff --git a/plan.md b/plan.md index e8e5da8..ce5bbd3 100644 --- a/plan.md +++ b/plan.md @@ -1,33 +1,21 @@ -# vr-shopxo-plugin 架构决策评议 — plan.md +# vr-shopxo-plugin P0 修复执行计划 — plan.md -> 版本:v1.1(合并版)| 日期:2026-04-15 | Agent:council/FrontendDev + BackendArchitect + SecurityEngineer -> 关联:Issue #9 +> 版本:v1.0 | 日期:2026-04-15 | Agent:BackendArchitect + FrontendDev +> 关联: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 防超卖 +方案 A 已全票通过(见 `council-output/ARCHITECTURE_DECISION.md`)。现在进入**执行阶段**,按优先级实施三个任务。 --- -## 核心问题(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 最终推荐(实现成本 / 安全性 / 可维护性) | 所有成员 | +- [ ] **P0-A**: `BaseService::initGoodsSpecs()` — 商品 112 最小修复集 `[Claimed: BackendArchitect]` +- [ ] **P0-B**: `SeatSkuService::BatchGenerate()` — 批量生成座位级 SKU `[Claimed: BackendArchitect]` +- [ ] **P1**: `ticket_detail.html` submit() 重构 — seat-level 逐座提交 `[Claimed: FrontendDev]` --- @@ -35,19 +23,85 @@ Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题:VR 演唱 | 阶段 | 内容 | 负责 | |------|------|------| -| Round 1(本轮)| 独立评议 + plan.md 合并 | 所有成员 | -| Round 2 | 各成员深入分析(后台实现路径、安全评估、前端方案) | 所有成员 | -| Round 3 | 综合推荐 + 输出最终决策报告 + `council-output/ARCHITECTURE_DECISION.md` | FrontendDev 主笔 | +| Draft | 各成员编写执行代码 | BackendArchitect (P0-A/P0-B), FrontendDev (P1) | +| Review | 代码互审,验证 SQL 正确性 | BackendArchitect 审 P1, FrontendDev 审 P0-A/P0-B | +| Finalize | 合并到 main,实测验证 | 所有成员 | --- -## 任务清单 +## P0-A 详细设计 -- [x] **Q1**: 方案 A 批量生成 SKU 路径 `[Done: BackendArchitect]` ✅ -- [x] **Q2**: 商品 112 broken 状态紧急修复 `[Done: BackendArchitect]` ✅ -- [x] **Q3**: $vr- 前缀安全评估 `[Done: FrontendDev]` ✅ (ThinkPHP {$var} 默认转义,|raw 仅跳过HTML转义) -- [x] **Q4**: 方案 A vs 方案 B 最终推荐 `[Done: council/BackendArchitect]` ✅ — 全部成员一致推荐方案A -- [x] **Final**: `council-output/ARCHITECTURE_DECISION.md` — 汇总三方推荐 + 最终结论 `[Done: council/BackendArchitect]` ✅ — 全票通过方案 A +**文件**: `plugins/vr_ticket/service/BaseService.php` + +**方法**: `public static function initGoodsSpecs(int $goodsId): bool` + +**逻辑**: +1. UPDATE `is_exist_many_spec=1` WHERE `id=$goodsId`(幂等) +2. 检查 `$vr-场馆`/`$vr-分区`/`$vr-时段` 是否存在(按 name 查 `goods_spec_type`),不存在则 INSERT +3. 使用 `INSERT IGNORE` 或 `ON DUPLICATE KEY` 防止重复 + +**关键 SQL**: +```sql +UPDATE sxo_goods SET is_exist_many_spec = 1 WHERE id = $goodsId; + +INSERT IGNORE INTO sxo_goods_spec_type (goods_id, name, value, add_time) VALUES +($goodsId, '$vr-场馆', '[{"name":"国家体育馆","images":""}]', UNIX_TIMESTAMP()), +($goodsId, '$vr-分区', '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]', UNIX_TIMESTAMP()), +($goodsId, '$vr-时段', '[{"name":"2026-05-01 19:00","images":""}]', UNIX_TIMESTAMP()); +``` + +**验证**: 执行后 `SELECT * FROM sxo_goods_spec_type WHERE goods_id=112` 确认 3 条 spec_type 记录。 + +--- + +## P0-B 详细设计 + +**文件**: `plugins/vr_ticket/service/SeatSkuService.php`(新建) + +**方法**: `public static function BatchGenerate(int $goodsId, int $seatTemplateId): array` + +**返回值**: +```php +[ + 'total' => 100, // 生成的 SKU 总数 + 'batch' => 1, // 批次数 + 'spec_base_id_map' => ['A1_1' => 2001, 'A1_2' => 2002, ...] // seatId => spec_base_id +] +``` + +**核心逻辑**: +1. 从 `vr_seat_template` 读取 seat_map(zones → rows → seats) +2. 从 zone 配置获取 price +3. 遍历每个座位,生成 `goods_spec_base` 行(inventory=1,price 从 zone.price 获取) +4. 同时写入 `goods_spec_value`(spec_type_id × 4 维度 = 4 行/座位) +5. **必须旁路 `GoodsSpecificationsInsert()`** — 直接 SQL INSERT +6. 分批:500 条/批,10000 座位约 20 批 + +**关键表结构**: +- `sxo_goods_spec_base`: id (PK auto), goods_id, spec_base, price, inventory, color, images, weight, stock +- `sxo_goods_spec_value`: id (PK auto), goods_id, spec_base_id (FK), spec_type_id (FK), `spec_value` (JSON) + +**幂等**: 先 DELETE 已存在的座位级 SKU(spec_type_id IN (venue,zone,time,seat_num)),再重建。 + +--- + +## P1 详细设计(FrontendDev) + +**文件**: `plugins/vr_ticket/view/goods/ticket_detail.html` + +**逻辑**: +1. `submit()` 改为遍历 `this.selectedSeats` +2. 每个座位从 `app.specBaseIdMap[seatId]` 获取 `spec_base_id` +3. 构造 `goods_params` 数组,每个座位一行 +4. 降级策略:`spec_base_id` 不存在时走原 Plan B 逻辑 + +--- + +## 依赖关系 + +- P0-A 和 P0-B 可并行开发 +- P1 依赖 P0-B 完成后注入 `specBaseIdMap` 数据 +- P0-A 完成后需在 ShopXO 容器实测验证 --- @@ -55,218 +109,17 @@ Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题:VR 演唱 | 任务 | Claim 状态 | |------|-----------| -| Q1 | [Done: BackendArchitect] | -| Q2 | [Done: BackendArchitect] | -| Q3 | [Done: FrontendDev] | -| Q4 | [Done: council/BackendArchitect] — 全员一致推荐方案A | -| 最终输出 | [Done: council/BackendArchitect] — `council-output/ARCHITECTURE_DECISION.md` 已输出,方案 A 全票通过 | +| P0-A | [Claimed: BackendArchitect] | +| P0-B | [Claimed: BackendArchitect] | +| P1 | [Claimed: FrontendDev] | --- -## 依赖关系 - -- 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)。 - -最小修复路径(不破坏现有数据): -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 = 1,ShopXO 购买流程自带事务保护;方案 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 优势不成立**:插件自管 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。`council-output/ARCHITECTURE_DECISION.md` 已输出,方案 A 全票通过。 - ---- - -## Round 3 安全审计结果(保留,仅供参考) - -### Task S1 — Admin 鉴权覆盖完整性审查 ✅ 验证通过 -### Task S2 — SQL 注入风险审计 ✅ 无注入风险 -### Task S3 — XSS / CSRF 防护检查 ✅ 通过 -### Task S5 — IDOR 水平越权检查 ✅ 通过 -### Task S4 — 敏感操作审计日志设计 ✅ 设计完成 +## 执行顺序 + +1. BackendArchitect: P0-A 代码 + SQL 验证 +2. BackendArchitect: P0-B SeatSkuService::BatchGenerate() +3. FrontendDev: P1 submit() 重构 +4. BackendArchitect: 合并到 main +5. 容器实测:商品 112 `initGoodsSpecs(112)` → 验证 is_exist_many_spec=1 + 3条spec_type +6. 容器实测:`BatchGenerate(112, $templateId)` → 验证座位级 SKU 生成