From c01e14ee7067613f07299a1e8d19a566d9c48066 Mon Sep 17 00:00:00 2001 From: Council Date: Wed, 15 Apr 2026 20:40:39 +0800 Subject: [PATCH] council(plan): FrontendDev - Round 1 plan for editor solution research Q1: JSON editor complexity assessment + ShopXO DIY components Q2: BackendArchitect investigates page replacement feasibility Final output: council-output/EDITOR_RESEARCH.md Co-Authored-By: Claude Opus 4.6 --- plan.md | 347 +++++++++----------------------------------------------- 1 file changed, 56 insertions(+), 291 deletions(-) diff --git a/plan.md b/plan.md index afd25ea..c02f551 100644 --- a/plan.md +++ b/plan.md @@ -1,33 +1,49 @@ -# vr-shopxo-plugin 架构决策评议 — plan.md +# vr-shopxo-plugin 编辑器方案调研 — plan.md -> 版本:v1.2(最终合并版)| 日期:2026-04-15 | Agent:council/FrontendDev + BackendArchitect + SecurityEngineer -> 关联:Issue #9 | 状态:FINAL +> 版本:v1.0(Round 1 初稿)| 日期:2026-04-15 | Agent:council/FrontendDev +> 背景:ShopXO 票务插件后台编辑器设计方案调研(Q1 JSON 编辑器复杂度评估 + ShopXO DIY 组件参考) --- ## 任务背景 -Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题:VR 演唱会票务商品中 ShopXO SPEC 与 SKU 的绑定方案。 +vr-shopxo-plugin 是 ShopXO 票务插件,需要调研后台编辑器设计方案。 -**已知事实:** -- ShopXO `goods_spec_base`(SKU表)当前为空,商品 112 的 `is_exist_many_spec=0` -- `spec_base_id_map` 中的 ID(如 1001/1002/1003)在 DB 中不存在 -- ShopXO 防超卖机制(原子扣 inventory)完全未启用 +**已知 seat_map JSON 结构**: +```json +{ + "map": ["AAAAAA", "BBBBBB", "CCCCCC"], + "seats": { "A": { "price": 899, "color": "#e74c3c", "label": "VIP区" }, ... }, + "row_labels": ["A", "B", "C"], + "sections": [{ "name": "VIP区", "color": "..." }, ...] +} +``` +venue 字段完全不存在(硬编码 "国家体育馆")。 -**两种架构方向:** -- **方案 A**:每个座位 = 一个 SKU(stock=1),ShopXO 原生防超卖 -- **方案 B**:每个 Zone = 一个 SKU(stock=Zone座位数),自建 FOR UPDATE 防超卖 +**引入"场馆"后的嵌套层级**: +``` +venue(name/address/image) + └── seat_map(map/seats/row_labels/sections/zones) +``` --- -## 核心问题(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 最终推荐(实现成本 / 安全性 / 可维护性) | 所有成员 | +| **Q1** | JSON 编辑器复杂度评估:ShopXO 是否有现成组件?4 层嵌套 Vue3 编辑器实现成本?JSON vs 拆表方案成本对比? | FrontendDev | +| **Q2** | 商品发布页替换方案(替换页面)可行性:`plugins_view_admin_goods_save` 能否完全替换表单? | BackendArchitect | + +--- + +## 任务清单 + +- [ ] **Q1.1**: 调研 ShopXO 后台是否有现成 JSON 编辑器组件(ShopXO DIY 组件) `[Claimed: FrontendDev]` +- [ ] **Q1.2**: 评估 4 层嵌套 Vue3 + JSON Schema form 编辑器复杂度(代码量/工时) `[Claimed: FrontendDev]` +- [ ] **Q1.3**: JSON 编辑器 vs 拆表方案开发和维护成本对比 `[Claimed: FrontendDev]` +- [ ] **Q2**: 商品发布页替换方案可行性(BackendArchitect 并行调研) `[Claimed: BackendArchitect]` +- [ ] **Final**: 输出 `council-output/EDITOR_RESEARCH.md` 并给出明确推荐 `[Claimed: FrontendDev]` --- @@ -35,294 +51,43 @@ Phase 0/1/2 已完成基础骨架,暴露了一个 P0 架构问题:VR 演唱 | 阶段 | 内容 | 负责 | |------|------|------| -| Round 1 | 独立评议 + plan.md 合并 | 所有成员 | -| Round 2 | 各成员深入分析(后台实现路径、安全评估、前端方案) | 所有成员 | -| Round 3 | 综合推荐 + 输出最终决策报告 + `council-output/ARCHITECTURE_DECISION.md` | FrontendDev 主笔 | - ---- - -## 任务清单 - -- [x] **Q1**: 方案 A 批量生成 SKU 路径 `[Done: BackendArchitect]` ✅ -- [x] **Q2**: 商品 112 broken 状态紧急修复 `[Done: BackendArchitect]` ✅ -- [x] **Q3**: $vr- 前缀安全评估 `[Done: SecurityEngineer + FrontendDev]` ✅ -- [x] **Q4**: 方案 A vs 方案 B 最终推荐 `[Done: 所有成员]` ✅ — 三方一致推荐方案 A -- [x] **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] | +| **Round 1(规划)** | 各自创建 plan.md | 所有成员 | +| **Round 2(执行)** | 深入调研 + 代码级确认 | FrontendDev + BackendArchitect | +| **Round 3(综合)** | 输出 EDITOR_RESEARCH.md + 最终推荐 | FrontendDev | --- ## 依赖关系 -- Q1(BackendArchitect)先完成,后 Q4 才能给出完整推荐 -- Q3(SecurityEngineer)可与 Q1 并行 -- Q2 可独立完成,紧急程度由 BackendArchitect 判定 -- 三方分析完成后,FrontendDev 主笔 Round 3 最终报告 +- Q2(BackendArchitect)先完成,后端替换可行性影响前端方案选择 +- Q1.1 调研 ShopXO DIY 组件是 Q1.2 的前置 --- -## 各成员 Round 1 初判 +## 调研路径 -### BackendArchitect 初判 +### Q1 调研路径(FrontendDev) -**Q1 初步判断**:Plan A 后台批量生成 SKU **可行**。ShopXO 的 `goods_spec_base` 是标准 MySQL 表,插件可直接 INSERT。 +1. 检查 `shopxo/` 目录中是否存在 DIY JSON 编辑器组件 + - `static/diy/js/entry/index-*.js` — Vue3 SPA 组件 + - `custom` 组件类型参考 +2. 如果无现成组件,评估 Vue3 + JSON Schema form 实现方案 +3. 对比 JSON 单表 vs 拆多表方案的开发和维护成本 -**Q2 初步判断**:当前 broken 状态**暂不需要立即修复**。购买流程走的是裸商品逻辑(is_exist_many_spec=0),需要明确购买流程最终走哪条路后再修。 +### Q2 调研路径(BackendArchitect) -**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。 -**Q3**:ThinkPHP View 层可能对 `$` 有变量插值行为,需要代码验证(Round 2 执行)。 -**Q4**:初步倾向 **方案 A**。 +1. 检查 `app/admin/controller/Goods.php` 中 SaveInfo() 和 Save() 方法 +2. 确认 `plugins_view_admin_goods_save` 钩子调用位置和可替换性 +3. 验证替换后数据能否正常保存 --- -## 各成员 Round 2 深入分析 +## Claim 状态 -### BackendArchitect Round 2 深入分析(Q1+Q2) - -详细分析见 `docs/ROUND2_ANALYSIS.md`。 - -**Q1 结论:可行,但必须旁路 `GoodsSpecificationsInsert()`** - -- `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` -- 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=1),DB 层保证 | 自建 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 逐座提交": - -```javascript -// 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 优势不成立**:插件自管 SKU(Hook 隐藏),不走 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]` — 推荐方案 A,Round 2/3 分析完成 | -| SecurityEngineer | `[CONSENSUS: YES]` — $vr- 前缀低风险,方案 A 推荐 | -| FrontendDev | `[CONSENSUS: YES]` — 方案 A 推荐,前端配合方案清晰 | - -**全票通过:采纳方案 A** - ---- - -## Issue #9 执行计划 — Round 4(P0 修复) - -> 执行日期:2026-04-15 | 目标:方案 A 全量落地 - -### 任务清单 - -- [x] **P0-A**: `BaseService::initGoodsSpecs()` — 修复商品 112 broken state `[Claimed: BackendArchitect]` -- [x] **P0-B**: `SeatSkuService::BatchGenerate()` — 批量生成座位级 SKU `[Claimed: BackendArchitect]` -- [x] **P1**: `ticket_detail.html` submit() 重构 — seat-level goods_params `[Done: 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 行)**: -```javascript -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: -```javascript -// 每座一行,逐座提交 -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_id` 从 `specBaseIdMap[seatKey]` 获取(Plan A:座位级 SKU) -3. `stock` 固定为 1(每个 SKU 对应一个座位) -4. `extension_data` 改为 seat-level,每座携带自己的信息 -5. 保留 `sessionSpecId` 作为 fallback(Plan B 回退) - -### P0-B 返回值接口约定 - -BackendArchitect 的 `BatchGenerate()` 返回值需包含: -```php -[ - '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}` - -### P1 实现记录 - -**已完成的改动**(`ticket_detail.html`): - -1. **`renderSeatMap()`**:为每个座位 div 新增 `data-row-label` 和 `data-col-num` 属性,用于生成与 `specBaseIdMap` key 格式一致的 seatKey -2. **`toggleSeat()`**:将 seatKey 从 `rowIndex_colIndex`(如 `"0_0"`)改为 `rowLabel_colNum`(如 `"A_1"`),匹配 `specBaseIdMap` key 格式 -3. **`removeSeat()`**:改用 `[data-row-label][data-col-num]` 选择器查找座位元素 -4. **`submit()`**:从 Zone 级别提交(1 行 goods_params)改为座位级提交(N 行 goods_params,每座 stock=1,spec_base_id 从 `specBaseIdMap[seat.seatKey]` 获取);降级兜底:若 specBaseIdMap 无对应 key,使用 sessionSpecId - -**seatKey 格式约定**: -- 格式:`{rowLabel}_{colNum}`,例如 `"A_1"`, `"B_5"` -- 与 BackendArchitect `SeatSkuService::BatchGenerate()` 返回的 `spec_base_id_map` key 格式保持一致 - ---- - -## Round 3 安全审计结果(保留,仅供参考) - -### Task S1 — Admin 鉴权覆盖完整性审查 ✅ 验证通过 -### Task S2 — SQL 注入风险审计 ✅ 无注入风险 -### Task S3 — XSS / CSRF 防护检查 ✅ 通过 -### Task S5 — IDOR 水平越权检查 ✅ 通过 -### Task S4 — 敏感操作审计日志设计 ✅ 设计完成 +| 任务 | 状态 | +|------|------| +| Q1.1 | [Claimed: FrontendDev] | +| Q1.2 | [Claimed: FrontendDev] | +| Q1.3 | [Claimed: FrontendDev] | +| Q2 | [Claimed: BackendArchitect] | +| Final Output | [Pending: FrontendDev] |