# Phase 2 — 后台管理开发日志

> 状态：✅ 完成
> 时间：2026-04-15 04:36 — 14:30 CST
> 执行方式：Council 讨论（BackendArchitect + SecurityEngineer + FrontendDev，5轮）+ 西莉雅独立资料归档

---

## 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() → 自动获得上述所有鉴权
```

---

### P1：Verifier.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'] ?: '');
}
```

---

### P1：Verification.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();
});
```

---

### P1：TicketService::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
});
```

---

### P2：Ticket::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 check（admin 上下文已限制）

---

## 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 隔离
