# vr-shopxo-plugin 架构决策评议 — plan.md > 版本:v1.0 | 制定日期:2026-04-15 | Agent:council/BackendArchitect > 关联: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 防超卖 --- ## 阶段划分 | 阶段 | 内容 | 负责 | |------|------|------| | Round 1 | 独立评议 + plan.md 合并 | 所有成员 | | Round 2 | 各成员深入分析(后台实现路径、安全评估、前端方案) | 所有成员 | | Round 3 | 综合推荐 + 输出最终决策报告 | 所有成员 | --- ## Agent 任务分配 | Agent | 主要评议方向 | |-------|------------| | BackendArchitect | Q1(Plan A 后台批量 SKU 生成可行性)+ Q4(最终推荐) | | SecurityEngineer | Q3($vr- 前缀安全风险)+ Q4(安全性维度) | | FrontendDev | 前端方案 A/B 的实现差异 + Q4(前端实现成本) | --- ## 任务清单 ### Q1 — Plan A 后台批量生成 SKU 路径评估 `[Pending: BackendArchitect]` - [ ] 分析 ShopXO spec_base 表写入路径 - [ ] 确认是否需要修改 ShopXO 核心代码还是通过插件可完成 - [ ] 评估批量生成的性能(上万座位场景) - [ ] 给出可行性结论 ### Q2 — 商品 112 broken 状态紧急修复 `[Pending: BackendArchitect]` - [ ] 评估 is_exist_many_spec=0 + spec_base 空的实际影响 - [ ] 确定最小修复集(是否需要立即修复) - [ ] 制定修复方案 ### Q3 — $vr- 前缀安全评估 `[Pending: SecurityEngineer]` - [ ] 检查 ShopXO 对带 $ 字符 spec name 的处理逻辑 - [ ] 识别潜在冲突或注入风险 - [ ] 给出安全结论 ### Q4 — 方案 A vs 方案 B 最终推荐 `[Pending: all]` - [ ] BackendArchitect:从实现成本、ShopXO 原生机制利用角度评议 - [ ] SecurityEngineer:从防超卖安全性角度评议 - [ ] FrontendDev:从前端复杂度角度评议 ### 最终输出 - [ ] `council-output/ARCHITECTURE_DECISION.md` — 汇总三方推荐 + 最终结论(Round 3) --- ## 依赖关系 - Q1(BackendArchitect)先完成,后 Q4 才能给出完整推荐 - Q3(SecurityEngineer)可与 Q1 并行 - Q2 可独立完成,紧急程度由 BackendArchitect 判定 --- ## Claim 状态 | 任务 | Claim 状态 | |------|-----------| | Q1 | [Pending: BackendArchitect] | | Q2 | [Pending: BackendArchitect] | | Q3 | [Pending: SecurityEngineer] | | Q4 | [Pending: all] | | 最终输出 | [Pending: all] | --- ## 本轮(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 生态完整性。 --- ### Task S2 — SQL 注入风险审计 ✅ 无注入风险 **审查范围**:Phase 2 所有控制器 + TicketService | 控制器 | 查询点 | 输入处理 | 结论 | |--------|--------|----------|------| | SeatTemplate::list | `name` like, `status` | `null` + `intval()` | ✅ 安全 | | Ticket::list | `keywords` multi-field like | `trim()` + 查询构造器绑定 | ✅ 安全 | | Ticket::verify | `ticket_code`, `verifier_id` | `trim()` + `intval()` | ✅ 安全 | | Verification::list | date range | `strtotime()` 绑定 | ✅ 安全 | **无原始 SQL 执行,无字符串拼接注入。** ⚠️ **P1 Bug(已修复)**:`Verifier.php:45` ThinkPHP `column()` 不支持直接传 CONCAT SQL,已改用 `select()` + PHP 拼接 --- ### Task S3 — XSS / CSRF 防护检查 ✅ 通过 | 方面 | 状态 | |------|------| | CSRF Token (POST) | ✅ ShopXO 保护 | | XSS(存储型) | ✅ 低风险(admin 上下文) | | 关键操作 guard | ✅ `delete()` / `verify()` / `export()` 均有 `IS_AJAX_POST` 检查 | --- ### Task S5 — IDOR 水平越权检查 ✅ 通过 Admin 上下文(所有控制器需登录 admin + 插件菜单权限)下访问控制正确。 --- ### 安全任务更新 - [x] **Task S1** — Admin 鉴权覆盖完整性 — `[Done: council/SecurityEngineer]` - [x] **Task S2** — SQL 注入风险审计 — `[Done: council/SecurityEngineer]` - [x] **Task S3** — XSS / CSRF 防护检查 — `[Done: council/SecurityEngineer]` - [x] **Task S5** — IDOR / 水平越权测试用例编写 — `[Done: council/SecurityEngineer]` - [x] **Task S4** — 敏感操作审计日志设计 — `[Done: council/BackendArchitect]` --- ## Task S4 — 敏感操作审计日志设计 ✅ 设计完成 **表结构**:`vr_audit_log` 已在 `EventListener.php` 中定义(第99-121行),字段如下: | 字段 | 类型 | 说明 | |------|------|------| | `action` | VARCHAR(60) | 操作类型:verify/export/refund/disable/enable/delete | | `operator_id` | BIGINT | 操作用户ID(admin) | | `operator_name` | VARCHAR(90) | 操作用户名(冗余) | | `target_type` | VARCHAR(60) | 对象类型:ticket/verifier/seat_template | | `target_id` | BIGINT | 对象ID | | `target_desc` | VARCHAR(255) | 对象描述(冗余,便于查询) | | `client_ip` | VARCHAR(45) | 客户端IP(支持IPv6) | | `user_agent` | VARCHAR(512) | User-Agent | | `request_id` | VARCHAR(64) | 请求追踪ID(UUID) | | `extra` | LONGTEXT | 附加数据JSON(变更前后快照) | | `created_at` | INT UNSIGNED | 操作时间戳 | **索引**:`idx_action` / `idx_operator_id` / `idx_target(target_type,target_id)` / `idx_created_at` **AuditService 接口设计**(待 Phase 3 实现): ```php // service/AuditService.php class AuditService { // 记录操作 public static function log($action, $target_type, $target_id, $extra = []); // 自动从 Common 控制器获取 admin 上下文 private static function getAdminContext(); // 生成请求追踪ID private static function makeRequestId(); } ``` **集成点**(Phase 3 实现): | 控制器 | 方法 | action 值 | extra 快照 | |--------|------|-----------|-----------| | Ticket | `verify()` | `verify` | verify_status=0→1, verifier_id | | Ticket | `export()` | `export` | goods_id, count | | Ticket | `refund()` | `refund` | verify_status=0→2 | | Verifier | `delete()` | `disable_verifier` | verifier_id, name | | Verifier | `save()` | `enable_verifier` | verifier_id, name | | SeatTemplate | `save()` | `edit_template` | template_id, name | | SeatTemplate | `delete()` | `disable_template` | template_id, name | > **防篡改策略**:表为 append-only,不提供 UPDATE/DELETE 接口;`operator_name` 冗余存储防止审计日志与 admin 表不同步时丢失身份。 --- ## BackendArchitect Round 4 — P1 Bug Fix ### Verification.php:55 — `column()` 多字段映射 Bug(P1 已修复) **问题**:`ThinkPHP column()` 不支持多字段映射,`column('seat_info,real_name,goods_id', 'id')` 返回结构与代码预期不符,导致核销记录列表页 `seat_info` / `real_name` / `goods_id` 为空。 **修复**:改用 `select()` + PHP foreach 拼接为 `$tickets[id] => row` 关联数组。 **文件**:`admin/controller/Verification.php` 第51-63行 --- ## FrontendDev Round 4 — P1 Bug Fix ### ticket/list.html — 导出按钮 IS_AJAX_POST 不匹配 Bug(P1) **问题**:`ticket/list.html:35` 导出按钮为 `` 链接(GET 请求),但 `Ticket.php:export()` 要求 `IS_AJAX_POST`,导致点击"导出CSV"按钮返回"非法请求"错误。 **修复**: - 视图:`ticket/list.html` 第35行 → `` 链接改为 `