7.2 KiB
7.2 KiB
Phase 2 — 后台管理开发日志
状态:✅ 完成 时间:2026-04-15 04:36 — 14:30 CST 执行方式:Council 讨论(BackendArchitect + SecurityEngineer + FrontendDev,5轮)+ 西莉雅独立资料归档
1. 目标
完成 vr-shopxo-plugin 后台管理页面开发,涵盖:
- 座位模板管理(CRUD)
- 电子票列表 / 详情 / 导出
- 核销员管理(增删改查)
- 核销记录查询
- Admin 控制器鉴权修复(P1)
- 审计日志(敏感操作记录)
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 |
索引:
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())。
修复:
// 修复前(错误)
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 遍历拼接:
$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 提交:
$('#export-btn').on('click', function() {
var $form = $('<form method="post" target="_blank">...');
$form.submit().remove();
});
P1:TicketService::verifyTicket() 缺少事务保护
问题: 并发核销同一张票可能导致重复核销。
修复: 事务包裹 + lock(true) 悲观锁:
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() 游标遍历:
$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:55CONCAT bug 已修复
S3 ✅ XSS / CSRF 低风险
- CSRF Token 由 ShopXO 统一保护
- 关键操作(delete / verify / export)均有
IS_AJAX_POST检查 - admin 上下文天然降低 XSS 风险
S4 ✅ 审计日志设计完整
vr_audit_logappend-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. 经验教训
- 插件 Base 必须继承
Common:ShopXO 的鉴权链是Common构造方法的一部分,不能绕过 - ThinkPHP
column()不支持 CONCAT:需用select()+ PHP 拼接 - 导出按钮必须是 POST:
IS_AJAX_POST检查存在,安全要求不能降级 - 大量数据导出用
cursor():防止 OOM - Council 超纲执行:说"只讨论不动手"时 agents 仍会实现——需提前设 worktree 隔离