council(draft): FrontendDev - Issue #9 plan.md: 架构决策评议计划
Round 1 输出: - Q1: 方案A批量SKU可行但需独立管理页面 - Q2: 最小修复集=Hook注入is_exist_many_spec=1 - Q3: $vr-前缀低风险,需实测确认前端渲染 - Q4: 推荐方案A(每座位=SKU),安全性+一致性优先 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>refactor/vr-ticket-20260416
parent
829564b019
commit
6b8f3ec0de
485
plan.md
485
plan.md
|
|
@ -1,10 +1,29 @@
|
|||
# vr-shopxo-plugin Phase 2 — 后台管理开发计划
|
||||
# vr-shopxo-plugin Issue #9 — 架构决策评议计划
|
||||
|
||||
> 版本:v2.0(合并版)| 制定日期:2026-04-15 | Agent:council/FrontendDev + council/SecurityEngineer + council/BackendArchitect
|
||||
> 版本:v1.0 | 日期:2026-04-15 | Agent:council/FrontendDev(主笔)
|
||||
|
||||
## 概述
|
||||
## 任务背景
|
||||
|
||||
Phase 2 目标:完成后台管理页面开发,涵盖座位模板管理、电子票管理、核销员管理、核销记录查询,以及 Admin 控制器鉴权修复。
|
||||
Phase 0/1/2 已完成骨架,但暴露了 P0 架构问题:当前商品 112 的 spec_base 表为空(broken state),spec_base_id_map 中的 ID 在 DB 不存在,ShopXO 原生防超卖机制完全未启用。需要评议方案 A vs B 并给出明确推荐。
|
||||
|
||||
---
|
||||
|
||||
## 核心问题(4问)
|
||||
|
||||
### Q1: 方案 A 的后台批量生成 SKU 路径是否可行?
|
||||
- 具体实现方式?ShopXO 是否有批量创建 SKU 的 API/代码路径?
|
||||
- 前端(uni-app)如何配合?
|
||||
|
||||
### Q2: 当前商品 112 的 broken 状态需要立即修复吗?最小修复集是什么?
|
||||
- `is_exist_many_spec=0` + `spec_base` 空的根因是什么?
|
||||
- 最小修复:不改 spec_base 表,仅改 `is_exist_many_spec` flag?还是重建 SKU?
|
||||
|
||||
### Q3: $vr- 前缀方案是否有隐患?
|
||||
- ShopXO 内部逻辑是否对带 `$` 的 spec name 做特殊处理?
|
||||
- spec_value 的 name/label 字段是否允许 `$` 字符?
|
||||
|
||||
### Q4: 方案 A vs 方案 B 的最终推荐
|
||||
- 考虑:实现成本、安全性(防超卖)、可维护性、多 Zone 混买体验
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -12,403 +31,97 @@ Phase 2 目标:完成后台管理页面开发,涵盖座位模板管理、电
|
|||
|
||||
| 阶段 | 内容 | 负责 |
|
||||
|------|------|------|
|
||||
| Draft | 资料收集、研究方向确认 | 所有成员 |
|
||||
| Review | 代码实现审查、安全评审 | SecurityEngineer + Council |
|
||||
| Finalize | 合并到 main、文档整理 | 所有成员 |
|
||||
|
||||
---
|
||||
|
||||
## Agent 任务分配
|
||||
|
||||
| Agent | 主要任务 |
|
||||
|-------|---------|
|
||||
| BackendArchitect | API 设计、权限模型、数据库查询 |
|
||||
| SecurityEngineer | Admin 鉴权、注入/XSS、审计、IDOR |
|
||||
| FrontendDev | UI 框架选型、ShopXO admin 风格适配 |
|
||||
| **Round 1**(本轮)| 分析 4 个问题,建立共识框架,输出推荐 | FrontendDev + BackendArchitect + SecurityEngineer 独立输出 |
|
||||
| **Round 2** | 交叉评审其他成员的分析,补充或反驳 | 所有成员 |
|
||||
| **Round 3** | 最终报告合并,输出 `council-output/ARCHITECTURE_DECISION.md` | FrontendDev 主笔 |
|
||||
|
||||
---
|
||||
|
||||
## 任务清单
|
||||
|
||||
### 座位模板管理
|
||||
- [x] 座位模板列表页(`seat_template/list.html`)`[Done: council/FrontendDev]`
|
||||
- [x] 座位模板新增/编辑页(`seat_template/save.html`)`[Done: council/FrontendDev]`
|
||||
- [ ] 座位图可视化编辑器集成
|
||||
- [x] 分类绑定功能(category_id 字段已在 save.html 中实现)`[Done: council/FrontendDev]`
|
||||
|
||||
### 电子票管理
|
||||
- [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]`
|
||||
|
||||
### 核销员管理
|
||||
- [x] 核销员列表页(`verifier/list.html`)`[Done: council/FrontendDev]`
|
||||
- [x] 核销员新增/编辑/删除(`verifier/save.html`)`[Done: council/FrontendDev]`
|
||||
- [ ] 核销员绑定店铺/场次
|
||||
|
||||
### 核销记录
|
||||
- [x] 核销记录列表页(`verification/list.html`)`[Done: council/FrontendDev]`
|
||||
- [x] 多条件查询(时间/核销员/场次)`[Done: council/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 供后续审计?
|
||||
- [ ] **Task FD-1**: FrontendDev — 分析 Q1-Q4,输出推荐(plan.md 本文件)
|
||||
- [ ] **Task FD-2**: FrontendDev — 评审 BackendArchitect / SecurityEngineer 的分析
|
||||
- [ ] **Task FD-3**: FrontendDev — 撰写最终报告 `council-output/ARCHITECTURE_DECISION.md`
|
||||
- [ ] **Task FD-4**: FrontendDev — 更新 plan.md 和 ARCHITECTURE.md,合并到 main
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
- **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 共同输出
|
||||
- BackendArchitect 输出后端视角(ShopXO spec_base 机制、批量生成可能性)
|
||||
- SecurityEngineer 输出安全视角($vr- 前缀风险、防超卖方案安全性)
|
||||
- FrontendDev 输出前端视角(多 Zone 混买 UX、$vr- 前端处理)
|
||||
- 三方分析完成后,合并为最终报告
|
||||
|
||||
---
|
||||
|
||||
## 行动项(FrontendDev Round 1 输出)
|
||||
|
||||
### 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,反而增加了前端分组逻辑的复杂度(与我们"多 Zone 混买前端分组"的初衷不符)
|
||||
4. **维护性**:方案 A 依赖 ShopXO 原生机制,故障排查有据可查;方案 B 是"黑盒",出问题只能靠插件自己
|
||||
5. **$vr- 前缀**:spec_base_id_map 的 key 可以是 seat_id,无需改 ShopXO spec name 存储
|
||||
|
||||
**方案 B 的唯一优势**:SKU 数量少(Zone 数量 vs 座位数量),后台管理简单。但这个优势在演唱会 10000 座场景下不如安全和一致性重要。
|
||||
|
||||
---
|
||||
|
||||
## 行动项(优先级排序)
|
||||
|
||||
1. **【P0】紧急修复商品 112 broken state**:Hook 注入 `is_exist_many_spec=1`,使插件能正常识别票务商品(推荐方案甲,最小侵入)
|
||||
2. **【P1】实现方案 A 批量 SKU 生成**:在 `SeatTemplateService::BindToGoods()` 中,座位模板绑定商品时,批量 INSERT spec_base 记录(inventory=1, price 从 seat_type 读取)
|
||||
3. **【P2】隔离 ShopXO 规格管理页面**:Hook 隐藏票务商品的原生规格列表,建立独立座位 SKU 管理视图
|
||||
|
||||
---
|
||||
|
||||
## 共识投票
|
||||
|
||||
[CONSENSUS: NO] — 本轮仅完成研究讨论,实际执行待后续阶段
|
||||
[CONSENSUS: NO] — 本轮仅完成分析,执行待后续阶段
|
||||
|
||||
---
|
||||
|
||||
## Round 3 安全审计结果(SecurityEngineer)
|
||||
|
||||
### Task S1 — Admin 鉴权覆盖完整性审查 ✅ 验证通过
|
||||
|
||||
**审查方法**:读取 main 合并结果 + ShopXO Common.php 源码
|
||||
|
||||
#### 鉴权链分析
|
||||
|
||||
```
|
||||
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()` 也不会绕过鉴权
|
||||
|
||||
---
|
||||
|
||||
### 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行
|
||||
|
||||
---
|
||||
|
||||
## 共识投票
|
||||
|
||||
[CONSENSUS: YES] — 所有 Phase 2 安全任务 S1-S5 全部完成;前端视图全部就位;P1 导出按钮 bug 已修复;Task S4(审计日志设计)完成。Phase 2 收尾。
|
||||
*Round 1 完成,输出存档。等待 BackendArchitect 和 SecurityEngineer 的分析结果。*
|
||||
|
|
|
|||
Loading…
Reference in New Issue