vr-shopxo-plugin/docs/PHASE2_DEVELOPMENT_LOG.md

224 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Phase 2 — 后台管理开发日志
> 状态:✅ 完成
> 时间2026-04-15 04:36 — 14:30 CST
> 执行方式Council 讨论BackendArchitect + SecurityEngineer + FrontendDev5轮+ 西莉雅独立资料归档
---
## 1. 目标
完成 vr-shopxo-plugin 后台管理页面开发,涵盖:
1. 座位模板管理CRUD
2. 电子票列表 / 详情 / 导出
3. 核销员管理(增删改查)
4. 核销记录查询
5. Admin 控制器鉴权修复P1
6. 审计日志(敏感操作记录)
---
## 2. 交付物清单
### 2.1 数据库层
| 表名 | 说明 | 关键字段 |
|------|------|---------|
| `vr_seat_templates` | 座位模板 | name, category_id, seat_map, spec_base_id_map, status |
| `vr_tickets` | 电子票 | ticket_code, qr_data, verify_status, verifier_id, real_name/phone/id_card |
| `vr_verifiers` | 核销员 | user_id, name, status |
| `vr_verifications` | 核销记录 | ticket_id, verifier_id, verifier_name, goods_id, created_at |
| `vr_audit_log` | 审计日志append-only | action, operator_id, target_type, target_id, client_ip, user_agent, request_id, extra |
**索引:**
```sql
vr_tickets: KEY idx_goods_id(id), idx_ticket_code(ticket_code), idx_verify_status(verify_status)
vr_verifications: KEY idx_ticket_id(ticket_id), idx_verifier_id(verifier_id)
vr_audit_log: KEY idx_action, idx_operator_id, idx_target, idx_created_at
```
---
### 2.2 控制器层
| 文件 | 职责 | 关键方法 |
|------|------|---------|
| `admin/controller/Base.php` | 鉴权基类(继承 Common | `__construct()` → IsLogin + IsPower |
| `admin/controller/SeatTemplate.php` | 座位模板 CRUD | list / save / delete |
| `admin/controller/Ticket.php` | 电子票管理 | list / detail / verify / export |
| `admin/controller/Verifier.php` | 核销员管理 | list / save / delete |
| `admin/controller/Verification.php` | 核销记录查询 | list |
---
### 2.3 服务层
| 文件 | 职责 |
|------|------|
| `service/BaseService.php` | 基础工具table前缀/UUID/AES QR加密/AdminPowerMenu |
| `service/TicketService.php` | 核销事务(含 FOR UPDATE 悲观锁) |
| `service/AuditService.php` | 审计日志12种操作类型append-only |
---
### 2.4 视图层
| 路径 | 说明 |
|------|------|
| `admin/view/seat_template/list.html` | 座位模板列表 |
| `admin/view/seat_template/save.html` | 座位模板新增/编辑 |
| `admin/view/ticket/list.html` | 电子票列表(含搜索/筛选/导出) |
| `admin/view/ticket/detail.html` | 电子票详情 + QR码 + 手动核销 |
| `admin/view/verifier/list.html` | 核销员列表 |
| `admin/view/verifier/save.html` | 核销员新增/编辑 |
| `admin/view/verification/list.html` | 核销记录列表 |
---
## 3. 关键修复记录
### P0插件后台鉴权失败卡死 4-5 小时的历史问题)
**根因:** 插件 `Base` 只调用 `AdminService::LoginInfo()`,跳过了 ShopXO `Common` 的完整鉴权链(`IsLogin()` + `IsPower()` + `FormTableInit()`)。
**修复:**
```php
// 修复前(错误)
class Base {
public function __construct() {
\app\service\AdminService::LoginInfo(); // 只填充 $this->admin跳过权限检查
}
}
// 修复后(正确)
abstract class Base extends \app\admin\controller\Common {
public function __construct() {
parent::__construct(); // 触发完整鉴权链
}
}
```
**修复后鉴权链:**
```
ThinkPHP → admin.php → Common::__construct()
→ AdminService::LoginInfo() 填充 $this->admin
→ AdminPowerService::PowerMenuInit() 权限菜单
→ ViewInit() 视图初始化
→ 插件控制器extends Base
→ parent::__construct() → 自动获得上述所有鉴权
```
---
### P1Verifier.php CONCAT SQL 语法错误
**问题:** `column('CONCAT(nickname, "/", username)')` — ThinkPHP `column()` 不支持原始 SQL。
**修复:** 改用 `select()` + PHP 遍历拼接:
```php
$users_raw = Db::name('User')->where('id', 'in', $user_ids)->select();
$users = [];
foreach ($users_raw as $u) {
$users[$u['id']] = ($u['nickname'] ?: '') . '/' . ($u['username'] ?: '');
}
```
---
### P1Verification.php column() 多字段映射 Bug
**问题:** `column('seat_info,real_name,goods_id', 'id')` — 返回结构与代码预期不符。
**修复:** 同样改用 `select()` + PHP 关联数组。
---
### P1导出按钮 GET → POST
**问题:** `ticket/list.html` 的导出按钮是 `<a href>` GET 链接,但 `Ticket::export()` 要求 `IS_AJAX_POST`
**修复:** 改为 `<button id="export-btn">`JS 动态创建 POST form 提交:
```js
$('#export-btn').on('click', function() {
var $form = $('<form method="post" target="_blank">...');
$form.submit().remove();
});
```
---
### P1TicketService::verifyTicket() 缺少事务保护
**问题:** 并发核销同一张票可能导致重复核销。
**修复:** 事务包裹 + `lock(true)` 悲观锁:
```php
Db::transaction(function() use ($ticket_code, $verifier_id) {
$ticket = Db::name('tickets')
->where('ticket_code', $ticket_code)
->lock(true) // = FOR UPDATE SKIP LOCKED
->find();
// 状态校验 + 更新 + 写入 vr_verifications
});
```
---
### P2Ticket::export() OOM 风险
**问题:** 大数据量时 `select()` 一次性加载所有记录到内存。
**修复:** 改用 `cursor()` 游标遍历:
```php
$rows = Db::name('plugins_vr_tickets')->where($where)->order('id', 'desc')->cursor();
```
---
## 4. 安全审计结果S1-S5
### S1 ✅ Admin 鉴权覆盖完整性
- `Base extends Common` 后,完整鉴权链自动继承
- 所有子控制器无需单独处理鉴权
### S2 ✅ SQL 注入无风险
- 所有输入均通过 ThinkPHP 查询构造器(自动绑定参数)
- 无原始 SQL 拼接
- `Verifier.php:45` / `Verification.php:55` CONCAT bug 已修复
### S3 ✅ XSS / CSRF 低风险
- CSRF Token 由 ShopXO 统一保护
- 关键操作delete / verify / export均有 `IS_AJAX_POST` 检查
- admin 上下文天然降低 XSS 风险
### S4 ✅ 审计日志设计完整
- `vr_audit_log` append-only 表
- 记录 action / operator_id / client_ip / user_agent / request_id / extra
- 4 索引优化查询性能
### S5 ✅ IDOR 水平越权
- admin 上下文(需登录 + 插件菜单权限)
- `Ticket::detail()` 无 owner checkadmin 上下文已限制)
---
## 5. Phase 3 待办
- [ ] **FR-3**:座位图可视化编辑器(需产品需求确认 Canvas/SVG/Grid
- [ ] **BR-2**:插件独立权限体系(核销员 vs 超级管理员菜单隔离)
- [ ] **R5 盲区**IDOR 归属校验(核销员是否只能看自己的记录?)
- [ ] **BR-5**核销通知队列化think_queue
- [ ] **FR-4**CSV 导出携带搜索条件keywords / verify_status
- [ ] **FR-4**Excel .xlsx 格式支持
---
## 6. 经验教训
1. **插件 Base 必须继承 `Common`**ShopXO 的鉴权链是 `Common` 构造方法的一部分,不能绕过
2. **ThinkPHP `column()` 不支持 CONCAT**:需用 `select()` + PHP 拼接
3. **导出按钮必须是 POST**`IS_AJAX_POST` 检查存在,安全要求不能降级
4. **大量数据导出用 `cursor()`**:防止 OOM
5. **Council 超纲执行**:说"只讨论不动手"时 agents 仍会实现——需提前设 worktree 隔离