305 lines
12 KiB
Markdown
305 lines
12 KiB
Markdown
# 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` 导出按钮为 `<a>` 链接(GET 请求),但 `Ticket.php:export()` 要求 `IS_AJAX_POST`,导致点击"导出CSV"按钮返回"非法请求"错误。
|
||
|
||
**修复**:
|
||
- 视图:`ticket/list.html` 第35行 → `<a>` 链接改为 `<button type="button" id="export-btn">`,JS 动态创建 `<form method="post">` 提交
|
||
- 控制器:`Ticket.php:export()` 保留 `IS_AJAX_POST` 检查不变(保持安全),注释更新说明 POST-only 设计
|
||
|
||
**文件**:`admin/view/ticket/list.html` 第35行 + 第92-98行
|
||
|
||
---
|
||
|
||
## Issue #9 — 架构决策:票务 SKU 方案评议
|
||
|
||
> 核心问题:VR 演唱会票务"每个座位一个 SKU"(方案 A)vs "每个 Zone 一个 SKU"(方案 B)
|
||
|
||
### 评议问题清单
|
||
|
||
| # | 问题 | 优先级 |
|
||
|---|------|--------|
|
||
| Q1 | 方案 A 后台批量生成 SKU 路径是否可行?ShopXO 是否有批量 API? | P0 |
|
||
| Q2 | 当前商品 112 的 broken 状态(is_exist_many_spec=0 + spec_base 空)是否需要紧急修复?最小修复集? | P0 |
|
||
| Q3 | $vr- 前缀方案是否有隐患?ShopXO 内部是否对 $ 有特殊处理? | P1 |
|
||
| Q4 | 方案 A vs 方案 B 最终推荐(实现成本 / 安全性 / 可维护性) | P0 |
|
||
|
||
### 任务分配
|
||
|
||
| Agent | 负责问题 | 行动项 |
|
||
|-------|---------|--------|
|
||
| SecurityEngineer | Q2/Q3/Q4 | 安全评估 + 最终推荐 |
|
||
| BackendArchitect | Q1 | SKU 生成路径分析 |
|
||
| FrontendDev | Q4 | 前端实现成本评估 |
|
||
|
||
### 阶段计划
|
||
|
||
- [ ] **Round 1**(本轮):各 Agent 独立分析 → 更新 plan.md → 合并到 main
|
||
- [ ] **Round 2**:执行研究(代码探索 + 分析)→ 输出分析报告到 plan.md
|
||
- [ ] **Round 3**:Cross-review → 汇总 → 写入 `council-output/ARCHITECTURE_DECISION.md`
|
||
|
||
### 安全工程师分析(SecurityEngineer)
|
||
|
||
#### 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 独立订单项,核销链路清晰
|
||
|
||
**需 Round 2 验证**:
|
||
- 方案 A 后台 SKU 批量生成是否可行(BackendArchitect 输出)
|
||
- $vr- 前缀在 View 层是否安全(SecurityEngineer 验证)
|
||
|
||
### 行动项(优先级排序)
|
||
|
||
1. **[Claimed: council/SecurityEngineer]** Q3 — 验证 $vr- 前缀在 ThinkPHP View 层是否安全(Round 2)
|
||
2. **[Claimed: council/SecurityEngineer]** Q2 — 方案确定后给出最小修复集(Round 3)
|
||
3. **[Pending]** Q4 — 综合输出最终推荐报告(Round 3)
|
||
|
||
---
|
||
|
||
## 共识投票
|
||
|
||
[CONSENSUS: NO] — Phase 2 收尾;Issue #9 架构决策研究待 Round 2 执行
|