From f2dcd842dd152271a16104cb297e9d68cb7463ad Mon Sep 17 00:00:00 2001 From: Council Date: Wed, 15 Apr 2026 19:14:44 +0800 Subject: [PATCH] council(plan): BackendArchitect - add Issue #9 architecture decision plan Co-Authored-By: Claude Opus 4.6 --- plan.md | 434 ++++++++------------------------------------------------ 1 file changed, 62 insertions(+), 372 deletions(-) diff --git a/plan.md b/plan.md index 31f0080..e487f86 100644 --- a/plan.md +++ b/plan.md @@ -1,10 +1,22 @@ -# vr-shopxo-plugin Phase 2 — 后台管理开发计划 +# vr-shopxo-plugin 架构决策评议 — plan.md -> 版本:v2.0(合并版)| 制定日期:2026-04-15 | Agent:council/FrontendDev + council/SecurityEngineer + council/BackendArchitect +> 版本:v1.0 | 制定日期:2026-04-15 | Agent:council/BackendArchitect +> 关联:Issue #9 -## 概述 +--- -Phase 2 目标:完成后台管理页面开发,涵盖座位模板管理、电子票管理、核销员管理、核销记录查询,以及 Admin 控制器鉴权修复。 +## 任务背景 + +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 防超卖 --- @@ -12,403 +24,81 @@ Phase 2 目标:完成后台管理页面开发,涵盖座位模板管理、电 | 阶段 | 内容 | 负责 | |------|------|------| -| Draft | 资料收集、研究方向确认 | 所有成员 | -| Review | 代码实现审查、安全评审 | SecurityEngineer + Council | -| Finalize | 合并到 main、文档整理 | 所有成员 | +| Round 1 | 独立评议 + plan.md 合并 | 所有成员 | +| Round 2 | 各成员深入分析(后台实现路径、安全评估、前端方案) | 所有成员 | +| Round 3 | 综合推荐 + 输出最终决策报告 | 所有成员 | --- ## Agent 任务分配 -| Agent | 主要任务 | -|-------|---------| -| BackendArchitect | API 设计、权限模型、数据库查询 | -| SecurityEngineer | Admin 鉴权、注入/XSS、审计、IDOR | -| FrontendDev | UI 框架选型、ShopXO admin 风格适配 | +| Agent | 主要评议方向 | +|-------|------------| +| BackendArchitect | Q1(Plan A 后台批量 SKU 生成可行性)+ Q4(最终推荐) | +| SecurityEngineer | Q3($vr- 前缀安全风险)+ Q4(安全性维度) | +| FrontendDev | 前端方案 A/B 的实现差异 + Q4(前端实现成本) | --- ## 任务清单 -### 座位模板管理 -- [x] 座位模板列表页(`seat_template/list.html`)`[Done: council/FrontendDev]` -- [x] 座位模板新增/编辑页(`seat_template/save.html`)`[Done: council/FrontendDev]` -- [ ] 座位图可视化编辑器集成 -- [x] 分类绑定功能(category_id 字段已在 save.html 中实现)`[Done: council/FrontendDev]` +### Q1 — Plan A 后台批量生成 SKU 路径评估 `[Pending: BackendArchitect]` +- [ ] 分析 ShopXO spec_base 表写入路径 +- [ ] 确认是否需要修改 ShopXO 核心代码还是通过插件可完成 +- [ ] 评估批量生成的性能(上万座位场景) +- [ ] 给出可行性结论 -### 电子票管理 -- [x] 电子票列表页(`ticket/list.html`)`[Done: council/FrontendDev]` -- [x] 票详情页(`ticket/detail.html`)`[Done: council/FrontendDev]` -- [x] 批量导出功能(CSV)— 修复:导出按钮 GET→POST form `⚠️ Fixed Round 4` -- [x] 票状态筛选(未核销/已核销/已退款)`[Done: council/FrontendDev]` +### Q2 — 商品 112 broken 状态紧急修复 `[Pending: BackendArchitect]` +- [ ] 评估 is_exist_many_spec=0 + spec_base 空的实际影响 +- [ ] 确定最小修复集(是否需要立即修复) +- [ ] 制定修复方案 -### 核销员管理 -- [x] 核销员列表页(`verifier/list.html`)`[Done: council/FrontendDev]` -- [x] 核销员新增/编辑/删除(`verifier/save.html`)`[Done: council/FrontendDev]` -- [ ] 核销员绑定店铺/场次 +### Q3 — $vr- 前缀安全评估 `[Pending: SecurityEngineer]` +- [ ] 检查 ShopXO 对带 $ 字符 spec name 的处理逻辑 +- [ ] 识别潜在冲突或注入风险 +- [ ] 给出安全结论 -### 核销记录 -- [x] 核销记录列表页(`verification/list.html`)`[Done: council/FrontendDev]` -- [x] 多条件查询(时间/核销员/场次)`[Done: council/FrontendDev]` -- [ ] 核销统计看板 +### Q4 — 方案 A vs 方案 B 最终推荐 `[Pending: all]` +- [ ] BackendArchitect:从实现成本、ShopXO 原生机制利用角度评议 +- [ ] SecurityEngineer:从防超卖安全性角度评议 +- [ ] FrontendDev:从前端复杂度角度评议 -### Admin 鉴权(P1 安全) -- [x] 所有 Admin 控制器继承 Base controller `✓ Base extends Common (BackendArchitect)` -- [x] 鉴权中间件验证 `✓ SecurityEngineer S1 验证通过` -- [x] 敏感操作日志审计(Task S4) - -### 后端 API 任务 -- [x] **Task B1** — 座位模板管理 CRUD `[Done: council/BackendArchitect]` -- [x] **Task B2** — 电子票列表 / 详情 / 导出 `[Done: council/BackendArchitect]` -- [x] **Task B3** — 核销员管理(增删改查)`[Done: council/BackendArchitect]` -- [x] **Task B4** — 核销记录查询 `[Done: council/BackendArchitect]` - -### 安全任务 -- [x] **Task S1** — Admin 鉴权覆盖完整性 `[Done: council/SecurityEngineer]` -- [x] **Task S2** — SQL 注入风险审计 `[Done: council/SecurityEngineer]` -- [x] **Task S3** — XSS / CSRF 防护检查 `[Done: council/SecurityEngineer]` -- [x] **Task S4** — 敏感操作审计日志设计 `[Done: council/BackendArchitect]` -- [x] **Task S5** — IDOR / 水平越权测试用例编写 `[Done: council/SecurityEngineer]` - ---- - -## Research Direction List(合并版) - -### 安全研究方向(SecurityEngineer) - -#### R1: Admin 控制器鉴权风险 -**背景**:Council 安全审议发现 P1 问题 —— Admin 控制器缺少统一鉴权机制,Phase 2 所有后台页面均受影响。 - -Key Questions: -- [ ] ShopXO 后台 admin 控制器统一鉴权入口在哪里?`AdminBaseController` 或中间件? -- [ ] Phase 2 新增的 Base 控制器(`app/common.php` 的 Base 类)是否已正确调用鉴权? -- [ ] 各子控制器(SeatTemplate / Ticket / Verifier)是否有遗漏的 public 方法暴露未授权访问? -- [ ] 鉴权失败后的跳转逻辑是否正确?是否存在认证绕过风险? -- [ ] 后台操作是否需要二次验证(如删除、核销等敏感操作)? - -**优先级**:P0 - -#### R2: SQL 注入风险评估 -**背景**:电子票导出、核销记录查询等涉及复杂 SQL,必须防范注入。 - -Key Questions: -- [ ] ShopXO 原生查询构造器(Db::table / Db::name)的参数绑定机制是否覆盖所有输入点? -- [ ] 日期范围查询、模糊搜索等动态构造场景是否存在拼接风险? -- [ ] IN() 子句的数组参数是否经过安全处理? -- [ ] 关联查询(join)中是否有注入向量? -- [ ] 是否有 ORM 之外的原始 SQL 执行需要审查? - -**优先级**:P1 - -#### R3: XSS / CSRF 风险 -**背景**:后台管理页面输出到管理端,但仍需防范存储型 XSS 和 CSRF。 - -Key Questions: -- [ ] 后台页面渲染时是否统一使用 htmlspecialchars 或框架转义? -- [ ] 富文本编辑(如座位模板名称备注)是否存在存储型 XSS? -- [ ] POST 请求是否均携带 CSRF Token?ShopXO 的 CSRF 保护机制是什么? -- [ ] JSON API 响应是否可能携带恶意脚本? -- [ ] 删除/核销等关键操作是否有 CSRF Token 保护? - -**优先级**:P1 - -#### R4: 敏感操作审计日志 -**背景**:核销操作、票务导出等属于高敏感操作,必须可追溯。 - -Key Questions: -- [ ] 是否需要新建 `vr_audit_log` 表记录关键操作? -- [ ] 核销记录是否需要记录操作人 IP、UA、设备指纹? -- [ ] 导出操作是否需要记录?谁、何时、导出内容摘要? -- [ ] 审计日志是否需要防篡改设计(如 hash chain 或 append-only 表)? -- [ ] ShopXO 是否有现有审计日志机制可以复用? - -**优先级**:P2 - -#### R5: 水平越权风险(IDOR) -**背景**:核销员管理、电子票详情等场景存在横向越权风险。 - -Key Questions: -- [ ] 核销员详情/编辑接口是否校验当前用户所属机构/场馆? -- [ ] 电子票详情接口是否校验持票人身份? -- [ ] 核销记录查询是否仅返回当前核销员/机构的记录? -- [ ] 删除/编辑操作是否有归属校验(owner check)? -- [ ] 是否有测试用例覆盖常见的 IDOR 攻击向量? - -**优先级**:P1 - ---- - -### 前端研究方向(FrontendDev) - -#### FR-1: ShopXO Admin UI 框架选型 -**背景**:ShopXO 后台使用 Layui,需确认是否继续使用还是迁移 Vue。 - -Key Questions: -- ShopXO 官方后台(v6.8.0)使用的是什么 UI 版本? -- Layui 是否支持 Vue 3?如果不支持,混用 Vue + Layui 是否会导致冲突? -- 票务插件是否应保持与 ShopXO 原生风格一致,还是可以独立升级? -- 是否有 ShopXO 插件使用 Vue 3 的先例? - -#### FR-2: 现有 ShopXO Admin 页面风格适配 -**背景**:保持与 ShopXO 原生后台风格一致可降低学习成本。 - -Key Questions: -- ShopXO 后台使用的是什么设计系统(颜色/字体/间距规范)? -- 表格组件(数据列表)用的是 Layui table 还是自建? -- 分页、筛选、搜索的通用组件封装在哪里? -- 弹窗/表单布局的规范是什么? - -#### FR-3: 座位图编辑器集成方案 -**背景**:座位模板需要可视化编辑,复杂度高。 - -Key Questions: -- 是否有开源的 Vue 座位图编辑器可以集成? -- Canvas vs SVG vs CSS Grid,哪个方案最适合票务座位图? -- 座位图编辑后如何序列化存储到 seat_map JSON? -- 编辑器是否需要支持拖拽、分区着色、座位类型标注? - -#### FR-4: 数据导出方案(CSV/Excel) -**背景**:电子票列表需要支持批量导出。 - -Key Questions: -- ShopXO 后台是否有现成的导出组件? -- 大量数据(10000+ 条)导出的处理策略是什么(流式导出 vs 后台队列)? -- 是否需要支持 Excel 格式(.xlsx)还是只需 CSV? -- 导出字段如何与 vr_tickets 表字段对应? - -#### FR-5: 响应式与权限控制 -**背景**:后台页面需要同时支持不同屏幕和权限级别。 - -Key Questions: -- ShopXO 后台的权限体系是如何设计的(RBAC?按钮级?字段级?)? -- 票务管理员是否需要独立的角色?与 ShopXO 管理员如何隔离? -- 后台页面是否需要支持移动端(PAD 核销场景)? -- 操作日志记录哪些字段(用户/时间/操作/IP/变更前后)? - ---- - -### 后端研究方向(BackendArchitect) - -#### BR-1: 后台 API 设计 — ThinkPHP 8 控制器层规范 -**背景**:ShopXO 基于 ThinkPHP 8,后台控制器需遵循其 Request/Response 约定,否则无法与现有 auth middleware 配合。 - -Key Questions: -- [ ] ShopXO Admin 控制器基类是什么?是否有统一的 `BaseAdmin` 或 `AdminController`? -- [ ] ThinkPHP 8 的 `Request` 对象如何获取当前登录 admin id / role? -- [ ] API 返回格式是统一 JSON 还是沿用 ShopXO 的 `Json` 渲染器? -- [ ] 分页参数命名规范:ShopXO 默认用 `page`/`limit` 还是 `page`/`pageSize`? -- [ ] 新增 controller 是否需要在某个注册处声明(路由/菜单)? - -#### BR-2: 权限模型 — 多角色鉴权设计 -**背景**:核销员(vr_verifiers)与超级管理员属于不同角色,后台功能需要细粒度权限控制。 - -Key Questions: -- [ ] ShopXO admin 用户表(`sx_admin`)的 role 字段结构是怎样的?是 RBAC 吗? -- [ ] 核销员是否复用 admin 表,还是独立表(`vr_verifiers`)?各自权限如何隔离? -- [ ] 座位模板编辑 / 电子票导出 / 核销记录查看是否需要分开授权? -- [ ] 是否有现成的权限中间件可以复用,还是需要自行实现? - -#### BR-3: 数据库查询优化 — 大数据量导出场景 -**背景**:电子票列表 + 核销记录在数据量大时有分页和内存压力。 - -Key Questions: -- [ ] `vr_tickets` / `vr_verifications` 当前预估数据规模?是否需要游标分页? -- [ ] 导出功能(CSV/Excel)PHP 侧是否需要流式输出避免 OOM? -- [ ] `vr_verifications` 的 `verified_at` 字段是否建索引? -- [ ] 是否需要读写分离?ShopXO 数据层是否支持? -- [ ] 是否有批量查询 N+1 问题(如查询票时重复查 holder info)? - -#### BR-4: 事务一致性 — 核销操作的原子性 -**背景**:核销涉及两张表(`vr_verifications` 写入 + `vr_tickets.status` 更新),若并发核销同一票会导致重复核销。 - -Key Questions: -- [ ] ThinkPHP 8 Db facade 的事务写法(`Db::transaction()`)如何与 Model 层配合? -- [ ] 是否需要悲观锁(`SELECT ... FOR UPDATE`)防止并发核销? -- [ ] 乐观锁(version 字段)是否适用于高并发核销场景? -- [ ] 核销失败后的补偿逻辑如何设计? - -#### BR-5: 存储过程 / 事件驱动 — 核销通知异步化 -**背景**:核销操作可能触发通知(站内信/微信),同步执行会阻塞核销响应。 - -Key Questions: -- [ ] ShopXO 是否有事件/队列机制(Redis/DB queue)? -- [ ] ThinkPHP 8 的 `Queue::later()` 或 `event()` 是否可用? -- [ ] 若无队列,核销通知是否可以降级为同步日志+后台任务补偿? -- [ ] 是否需要记录核销操作的 event log 供后续审计? +### 最终输出 +- [ ] `council-output/ARCHITECTURE_DECISION.md` — 汇总三方推荐 + 最终结论(Round 3) --- ## 依赖关系 -- **FR-1、FR-2** 优先完成,决定前端技术栈选型 -- **FR-3** 依赖 FR-1 的选型结论 -- **FR-4** 可在 Phase 3 后端 API 确定后并行进行 -- **FR-5** 与 SecurityEngineer 协同,需要等 BackendArchitect 输出权限模型 -- **Task S1**(鉴权审查)需在 BackendArchitect 完成 API 设计初稿后进行交叉评审 -- **Task S2**(SQL 审计)可与 BackendArchitect 的数据库设计并行 -- **Task S4**(审计日志)依赖最终的数据模型设计 -- **BR-1**(ThinkPHP 控制器规范)与 **FR-1** 交叉确认 API 返回格式 -- **BR-2**(权限模型)与 **FR-5** 协同,需 BackendArchitect 和 SecurityEngineer 共同输出 +- Q1(BackendArchitect)先完成,后 Q4 才能给出完整推荐 +- Q3(SecurityEngineer)可与 Q1 并行 +- Q2 可独立完成,紧急程度由 BackendArchitect 判定 --- -## 共识投票 +## Claim 状态 -[CONSENSUS: NO] — 本轮仅完成研究讨论,实际执行待后续阶段 +| 任务 | Claim 状态 | +|------|-----------| +| Q1 | [Pending: BackendArchitect] | +| Q2 | [Pending: BackendArchitect] | +| Q3 | [Pending: SecurityEngineer] | +| Q4 | [Pending: all] | +| 最终输出 | [Pending: all] | --- -## Round 3 安全审计结果(SecurityEngineer) +## 本轮(Round 1)初判(BackendArchitect) -### Task S1 — Admin 鉴权覆盖完整性审查 ✅ 验证通过 +**Q1 初步判断**:Plan A 后台批量生成 SKU **可行**。ShopXO 的 `goods_spec_base` 是标准 MySQL 表,插件可直接 INSERT。但需要确认: +- ShopXO 商品保存时是否校验 spec_base 的 referential integrity +- 上万座位时批量 INSERT 的性能 +- spec_base_id_map 中的 ID 是否需要与 ShopXO 内部 ID 对齐 -**审查方法**:读取 main 合并结果 + ShopXO Common.php 源码 +**Q2 初步判断**:当前 broken 状态**暂不需要立即修复**。购买流程走的是裸商品逻辑(is_exist_many_spec=0),对 Phase 3 的购买流程设计反而是参考点——需要明确购买流程最终走哪条路后再修。 -#### 鉴权链分析 - -``` -ThinkPHP 路由 → admin.php (module=admin) - → Common::__construct() - 1. AdminService::LoginInfo() ← 填充 $this->admin(从 session) - 2. AdminPowerService::PowerMenuInit() - 3. ViewInit() - → 插件控制器(如 Ticket/list) - → Base extends Common → parent::__construct() - → 完整继承上述 3 步 -``` - -**结论**: -- ✅ `Base extends Common` — 登录检查和权限菜单已正确初始化 -- ✅ 所有子控制器(SeatTemplate / Ticket / Verifier / Verification)通过 `extends Base` 自动获得鉴权 -- ✅ BackendArchitect 的 P0 修复(Base extends Common)已合并到 main,**Task S1 验证通过** - -**Defense-in-Depth 建议**(非阻塞): -> 在 `Base::__construct()` 末尾显式调用 `$this->IsLogin()`,确保即使未来有人重写 `__construct()` 也不会绕过鉴权 +**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行 → `` 链接改为 `