vr-shopxo-plugin/docs/PHASE2_DEVELOPMENT_LOG.md

7.2 KiB
Raw Permalink Blame History

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

索引:

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

P1Verifier.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'] ?: '');
}

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 提交:

$('#export-btn').on('click', function() {
    var $form = $('<form method="post" target="_blank">...');
    $form.submit().remove();
});

P1TicketService::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
});

P2Ticket::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: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-4CSV 导出携带搜索条件keywords / verify_status
  • FR-4Excel .xlsx 格式支持

6. 经验教训

  1. 插件 Base 必须继承 CommonShopXO 的鉴权链是 Common 构造方法的一部分,不能绕过
  2. ThinkPHP column() 不支持 CONCAT:需用 select() + PHP 拼接
  3. 导出按钮必须是 POSTIS_AJAX_POST 检查存在,安全要求不能降级
  4. 大量数据导出用 cursor():防止 OOM
  5. Council 超纲执行:说"只讨论不动手"时 agents 仍会实现——需提前设 worktree 隔离