16 KiB
Phase 4 API 文档(已实现部分)
状态:截至 2026-04-23,Phase 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_id(5维规格维度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_id、idx_goods_id、idx_verify_status、idx_created_at、idx_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_id(0-1679615),ticket_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 key(HMAC-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, ]- 处理流程:
- 判断是否为票务商品(
isTicketGoods) - 查询订单明细,解析5维 spec(
$vr-场次/$vr-场馆/$vr-演播室/$vr-分区/$vr-座位号) - 逐行调用
issueTicket()生成票
- 判断是否为票务商品(
- 返回:成功发放票数(0表示跳过)
issueTicket(array $order, array $og): int ⚡ 单票发放
内部方法,供 onOrderPaid 调用。
- 参数:
$order(订单数据),$og(订单商品行,已解析_parsed_spec_name等字段) - 流程:
- 幂等保护:同一
order_id + seat_info已有票则跳过 - 生成 UUID
ticket_code INSERT占位qr_data=''→ 获取ticket_id- 生成短码:
shortCodeEncode(goods_id, ticket_id) - 生成 QR payload:
signQrPayload(['id' => ticket_id, ...]) UPDATE qr_data = short_code + '|' + payload- 写入观演人信息(
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
三、后台管理 API(Admin)
所有 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 有效期 | 1800s(30min) | signQrPayload 中 exp = iat + 1800 |
| 缓存阈值 | 900s(15min) | 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.js(v3.11.5) - QR码:前端用
$('.view-qrcode-init').qrcode({text: value}),后端用/?s=index/qrcode/index