vr-shopxo-plugin/docs/PHASE_4_API.md

16 KiB
Raw Blame History

Phase 4 API 文档(已实现部分)

状态:截至 2026-04-23Phase 4.1/4.2 已完成B端核销进行中 目的:记录已实现 API避免后续 agent 重复发明轮子


一、数据库表结构

1.1 vr_tickets电子票

用途:存储用户已购票,每行 = 一张票。

字段 类型 说明
id BIGINT UNSIGNED AUTO_INCREMENT 票ID主键
order_id BIGINT UNSIGNED 关联订单ID
order_no CHAR(60) 订单号
goods_id BIGINT UNSIGNED 票务商品ID
goods_snapshot TEXT 商品快照JSON含名称/规格/价格)
user_id BIGINT UNSIGNED 持有用户ID
ticket_code CHAR(36) UNIQUE UUID v4 票码(核销主键)
qr_data TEXT 格式:短码|QR_payload(例:003a2hgmgety|eyJ...
seat_info VARCHAR(255) 座位描述(如 A区-3排-15座
spec_base_id BIGINT UNSIGNED spec_base_id5维规格维度ID
real_name VARCHAR(60) 观演人姓名
phone CHAR(15) 观演人手机
id_card CHAR(18) 观演人身份证
verify_status TINYINT UNSIGNED 0=未核销1=已核销2=已退款
verify_time INT UNSIGNED 核销时间戳
verifier_id BIGINT UNSIGNED 核销员ID
issued_at INT UNSIGNED 票发放时间
created_at INT UNSIGNED 创建时间
updated_at INT UNSIGNED 更新时间

索引idx_user_ididx_goods_ididx_verify_statusidx_created_atidx_spec_base_id

qr_data 格式详解

short_code|base64(payload)
例如003a2hgmgety|eyJpZCI6NDgyODE1LCJnIjoxMTgsImlhdCI6MTc0NTI4...

payload = base64(json({
    "id":    482815,     // ticket_id
    "g":     118,        // goods_id
    "iat":   1745286000,// 签发时间
    "exp":   1745287800,// 过期时间iat+1800s=30min
    "sig":   "A3F9B2C1" // HMAC-SHA256签名前8字符
}))

1.2 vr_verifiers核销员

字段 类型 说明
id BIGINT UNSIGNED 核销员ID
user_id BIGINT UNSIGNED 关联的 ShopXO 用户ID
name VARCHAR(60) 核销员名称
status TINYINT UNSIGNED 1=启用0=禁用
created_at INT UNSIGNED 创建时间

1.3 vr_verifications核销记录

字段 类型 说明
id BIGINT UNSIGNED 记录ID
ticket_id BIGINT UNSIGNED 关联票ID
ticket_code CHAR(36) 票码(冗余)
verifier_id BIGINT UNSIGNED 核销员ID
verifier_name VARCHAR(60) 核销员名称(冗余)
goods_id BIGINT UNSIGNED 商品ID
created_at INT UNSIGNED 核销时间

1.4 vr_audit_log审计日志

字段 类型 说明
action VARCHAR(60) 操作类型(如 verify, refund, export
operator_id BIGINT UNSIGNED 操作人ID后台 admin_id
operator_name VARCHAR(90) 操作人名称
target_type VARCHAR(60) 对象类型(ticket, verifier, seat_template
target_id BIGINT UNSIGNED 对象ID
target_desc VARCHAR(255) 对象描述(冗余,人工可读)
client_ip VARCHAR(45) 客户端IP
user_agent VARCHAR(512) User-Agent
request_id VARCHAR(64) 请求追踪ID同一次 HTTP 请求内的多个操作共用)
extra LONGTEXT 附加数据JSON
created_at INT UNSIGNED 操作时间

二、Service 层 API内部调用

所有方法均在 app\plugins\vr_ticket\service\ 下。

2.1 BaseService

命名空间app\plugins\vr_ticket\service\BaseService

短码编解码

shortCodeEncode(int $goods_id, int $ticket_id): string

  • 输入goods_id0-1679615ticket_id正整数
  • 输出:短码(小写字母,格式:4位goods_id明文 + 变长混淆ticket_id
  • 示例:shortCodeEncode(118, 1)003a2hgmgety

shortCodeDecode(string $code, ?int $goods_id_hint = null): array

  • 输入:短码字符串,可选 goods_id_hint校验用可省略
  • 输出:['goods_id' => int, 'ticket_id' => int]
  • 示例:shortCodeDecode('003a2hgmgety')['goods_id' => 118, 'ticket_id' => 1]
  • 特性:O(1) 解码无需暴力搜索前4位直接取 goods_id

QR 签名

signQrPayload(array $payload): string

  • 输入:['id' => int, 'g' => int, 'iat' => int, 'exp' => int]
  • 输出base64 编码字符串payload + sig 字段)
  • 算法:sig = substr(HMAC-SHA256("{$id}.{$g}.{$iat}.{$exp}", secret), 0, 8)
  • 密钥:VR_TICKET_SECRET 环境变量(或硬编码测试密钥 8935b3...

verifyQrPayload(string $encoded): array|null

  • 输入base64 编码字符串
  • 输出:验证失败返回 null;成功返回 ['id' => int, 'g' => int, 'exp' => int]
  • 验证内容:过期检查(exp < now+ 签名校验HMAC-SHA256

其他工具方法

generateUuid(): string — 生成 UUID v4 票码 encryptQrData(array $data, ?int $expire = null): string — AES-256-CBC 加密 decryptQrData(string $encoded): array|null — 解密 isTicketGoods(int $goods_id): bool — 判断商品是否为票务商品 table(string $name): string — 返回带前缀的表名 vr_ + $name now(): int — 当前时间戳 getGoodsKey(int $goods_id): string — 派生 per-goods keyHMAC-SHA256前16字节 getVrSecret(): string — 获取主密钥(需配置 VR_TICKET_SECRET AdminPowerMenu(): array — 返回后台权限菜单配置


2.2 TicketService

命名空间app\plugins\vr_ticket\service\TicketService(继承 BaseService

出票链路

onOrderPaid(array $params): bool 核心入口

plugins_service_order_pay_success_handle_end Hook 触发。

  • 触发时机:微信支付成功回调,pay_status=1 已写入 DB 后
  • $params 结构:
    [
      'business_id' => 123,        // 订单ID
      'business_ids' => [123],     // 批量订单ID数组
      'user_id' => 456,
      'order' => [...],            // 订单完整数据(含 extension_data
      'order_id' => 123,
    ]
    
  • 处理流程:
    1. 判断是否为票务商品(isTicketGoods
    2. 查询订单明细解析5维 spec$vr-场次/$vr-场馆/$vr-演播室/$vr-分区/$vr-座位号
    3. 逐行调用 issueTicket() 生成票
  • 返回成功发放票数0表示跳过

issueTicket(array $order, array $og): int 单票发放

内部方法,供 onOrderPaid 调用。

  • 参数:$order(订单数据),$og(订单商品行,已解析 _parsed_spec_name 等字段)
  • 流程:
    1. 幂等保护:同一 order_id + seat_info 已有票则跳过
    2. 生成 UUID ticket_code
    3. INSERT 占位 qr_data='' → 获取 ticket_id
    4. 生成短码:shortCodeEncode(goods_id, ticket_id)
    5. 生成 QR payloadsignQrPayload(['id' => ticket_id, ...])
    6. UPDATE qr_data = short_code + '|' + payload
    7. 写入观演人信息(real_name/phone/id_card
  • 返回ticket_id失败返回 0

核销

verifyTicket(string $ticket_code, int $verifier_id): array UUID核销

  • 参数UUID 票码(vr_tickets.ticket_code核销员ID
  • 内部使用 FOR UPDATE 悲观锁
  • 返回:['code' => 0, 'msg' => '核销成功', 'data' => ['seat_info' => ..., 'real_name' => ..., 'goods_name' => ...]]
  • 错误码:code=-1 票不存在,code=-2 已核销,code=-3 已退款,code=-999 系统异常

verifyByShortCode(string $short_code, int $verifier_id): array 短码核销

  • 参数:短码(如 003a2hgmgety核销员ID
  • 流程:shortCodeDecode(code) → 解析 {goods_id, ticket_id} → DB查询 → verifyTicketById()
  • 特点:无需知道 goods_id从短码前4位直接解析

verifyTicketById(int $ticket_id, int $verifier_id): array — 内部方法ticket_id 核销

QR 数据

getQrData(int $ticket_id, int $user_id): array — 获取用户票的 QR 数据

  • 校验:票必须属于该用户(user_id 匹配)
  • 返回:
    // 有效期 > 15分钟900s返回缓存
    ['code' => 0, 'data' => [
        'short_code' => '003a2hgmgety',
        'payload' => 'eyJ...',
        'cached' => true,
        'expires_in' => 1724,  // 秒
    ]]
    
    // 有效期 ≤ 15分钟刷新后返回
    ['code' => 0, 'data' => [
        'short_code' => '003a2hgmgety',
        'payload' => 'eyJ...(new)',  // 新签名
        'cached' => false,
        'expires_in' => 1800,
    ]]
    
  • 错误:code=-1 票不存在,code=-2 已核销,code=-3 已退款

getQrCodeUrl(string $ticket_code): string — 生成 QR 码图片 URL

  • 调用 ShopXO 内置 /?s=index/qrcode/index&content=...&size=8&level=H
  • 内容base64(JSON({type: 'vr_ticket', code: UUID}))

用户票查询

getUserTickets(int $user_id, ?int $status = null): array

  • 返回用户所有票(可筛选核销状态)

2.3 AuditService

命名空间app\plugins\vr_ticket\service\AuditService

log(string $action, string $targetType, int $targetId, array $extra = [], string $targetDesc = ''): int|false

记录审计日志,异常不阻断主流程。

  • $action 常量:ACTION_VERIFY / ACTION_REFUND / ACTION_EXPORT / ACTION_DISABLE_VERIFIER / ACTION_ENABLE_VERIFIER / ACTION_DELETE_VERIFIER / ACTION_DELETE_TEMPLATE
  • $targetType 常量:TARGET_TICKET / TARGET_VERIFIER / TARGET_TEMPLATE / TARGET_GOODS
  • 返回日志ID 或 false(失败时)

logVerify(int $ticketId, string $ticketCode, int $verifierId, string $verifierName, string $result, int $oldStatus) — 便捷包装

logExport(int $goodsId, array $filter, int $count) — 记录导出操作

search(array $params): array — 查询审计日志(分页)

  • 支持:action, operator_id, target_type, target_id, date_from, date_to, page, limit

三、后台管理 APIAdmin

所有 Admin 方法在 app\plugins\vr_ticket\admin\Admin.php(继承 Common)。

3.1 电子票管理

方法 URL 说明 鉴权
TicketList() GET /plugins/vr_ticket/admin/ticketList 票列表(支持关键词/状态/商品筛选) Admin
TicketDetail() GET /plugins/vr_ticket/admin/ticketDetail?id=X 票详情 Admin
TicketVerify() POST /plugins/vr_ticket/admin/ticketVerify 手动核销JSON API Admin
TicketExport() POST /plugins/vr_ticket/admin/ticketExport 导出票列表 CSV Admin

TicketVerify 请求

POST /plugins/vr_ticket/admin/ticketVerify
Body: { "ticket_code": "uuid-xxx", "verifier_id": 1 }

3.2 核销员管理

方法 URL 说明
VerifierList() GET /plugins/vr_ticket/admin/verifierList 核销员列表
VerifierSave() GET/POST /plugins/vr_ticket/admin/verifierSave 添加/编辑核销员
VerifierDelete() POST /plugins/vr_ticket/admin/verifierDelete 禁用核销员

3.3 核销记录

方法 URL 说明
VerificationList() GET /plugins/vr_ticket/admin/verificationList 核销记录列表(支持核销员/日期筛选)

3.4 场馆/座位模板

方法 URL 说明
VenueList() GET /plugins/vr_ticket/admin/venueList 场馆列表seat_map v3 格式)
VenueSave() GET/POST /plugins/vr_ticket/admin/venueSave 添加/编辑场馆(支持 Base64 传输 seat_map JSON
VenueDelete() POST /plugins/vr_ticket/admin/venueDelete 删除场馆(支持硬删除 value=hard
VenueEnable() POST /plugins/vr_ticket/admin/venueEnable 启用场馆

3.5 座位模板Phase 2 遗留)

方法 URL 说明
SeatTemplateList() GET /plugins/vr_ticket/admin/seatTemplateList 座位模板列表
SeatTemplateSave() GET/POST /plugins/vr_ticket/admin/seatTemplateSave 添加/编辑模板
SeatTemplateDelete() POST /plugins/vr_ticket/admin/seatTemplateDelete 删除模板
SeatTemplateEnable() POST /plugins/vr_ticket/admin/seatTemplateEnable 启用模板

3.6 辅助 API

SoldSeats() GET /plugins/vr_ticket/admin/soldSeats?goods_id=X&spec_base_id=Y

  • 返回:{"code":0,"data":{"sold_seats":[]}}
  • 当前版本返回空数组TODO接入真实已售座位查询

四、Hook 触发点

Hook 在 app\plugins\vr_ticket\Hook.php 中注册。

Hook 名称 触发时机 处理方法
plugins_service_order_pay_success_handle_end 订单支付成功pay_status=1 TicketService::onOrderPaid($params)
plugins_service_admin_menu_data 后台菜单渲染 Hook::AdminSidebarInit() 注入 VR票务菜单

五、QR 验证流程(核销机)

5.1 QR 本地验证(离线可行)

// 步骤1解析 base64 payload
$payload = json_decode(base64_decode($qr_string), true);

// 步骤2检查过期
if ($payload['exp'] < time()) {
    return '已过期,拒绝';
}

// 步骤3验签防篡改
$sign_str = "{$payload['id']}.{$payload['g']}.{$payload['iat']}.{$payload['exp']}";
$expected_sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8);
if (!hash_equals($expected_sig, $payload['sig'])) {
    return '伪造,拒绝';
}

// 步骤4联网执行 DB 状态检查(防重放)
// SELECT verify_status FROM vr_tickets WHERE id=? AND ticket_code=? FOR UPDATE
// verify_status == 0 → 放行
// verify_status == 1 → 已核销
// verify_status == 2 → 已退款

5.2 短码核销流程

核销员输入/扫码003a2hgmgety
    ↓
shortCodeDecode('003a2hgmgety')
→ goods_id=118, ticket_id=1
    ↓
DB: SELECT * FROM vr_tickets WHERE id=1 AND goods_id=118 FOR UPDATE
→ 命中 → verify_status==0? → 更新 verify_status=1, verifier_id=X
    ↓
返回:核销成功 + 票面信息

六、关键配置

配置项 说明
VR_TICKET_SECRET 环境变量(.env QR签名 + 短码混淆主密钥
VR_TICKET_QR_SECRET 环境变量(.env AES-256-CBC QR加密密钥尚未启用
表前缀 vrt_ ShopXO 配置的表前缀
QR 有效期 1800s30min signQrPayloadexp = iat + 1800
缓存阈值 900s15min getQrData() 刷新阈值
Feistel 轮数 8 HMAC-XOR 混淆轮数

七、待完成 API避免重复实现

功能 文件 状态
C端票列表 API api/controller/Ticket.php 待建
C端票夹页面 index/Ticket.php 待建
C端票夹 HTML view/goods/ticket_wallet.html 待建
B端扫码核销页 admin/view/ticket/verify.html 进行中
B端核销 API含短码路由 admin/controller/Ticket.php 进行中
票夹聚合查询服务 service/WalletService.php 待建

八、已验证可行的方案

  • ShopXO 插件路由无需手动声明Hooks → 自动映射
  • admin/Admin.php 单文件控制器:所有后台方法在此文件,统一继承 Common(含 IsLogin/IsPower/ViewInit
  • 自动建表Admin 控制器 initialize() 检查并从 install.sql 执行建表
  • session('admin'):后台鉴权从 session 读取管理员信息
  • Base64 绕过净化VenueSave 支持 seat_map_raw 字段Base64 传输,避免 HTML 净化破坏 JSON
  • JsBarcode:已内置 public/static/common/lib/JsBarcode/JsBarcode.all.min.jsv3.11.5
  • QR码:前端用 $('.view-qrcode-init').qrcode({text: value}),后端用 /?s=index/qrcode/index