fix(phase4.2): 复用现有 qr_data 字段存储短码|payload

设计调整:
- 复用现有 qr_data 字段,无需改数据库
- qr_data 格式:短码|payload(竖线分隔)
- short_code: BaseService::shortCodeEncode(goods_id, ticket_id)
- payload: BaseService::signQrPayload(id/g/iat/exp)

方法更新:
- getQrData(): 从 qr_data 解析短码和 payload,支持15分钟自动刷新
- verifyByShortCode(): 短码解码 → DB查询 → verifyTicketById()

无需数据库字段变更!
feat/phase4-ticket-wallet
Council 2026-04-23 00:21:41 +08:00
parent 06d0382dd8
commit 969a667928
2 changed files with 32 additions and 21 deletions

View File

@ -20,8 +20,7 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}vr_tickets` (
`goods_snapshot` TEXT DEFAULT NULL COMMENT '商品快照JSON', `goods_snapshot` TEXT DEFAULT NULL COMMENT '商品快照JSON',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`ticket_code` CHAR(36) NOT NULL COMMENT 'UUID票码', `ticket_code` CHAR(36) NOT NULL COMMENT 'UUID票码',
`short_code` VARCHAR(16) DEFAULT NULL COMMENT '短码Feistel混淆', `qr_data` TEXT DEFAULT NULL COMMENT '加密QR内容',
`qr_payload` TEXT DEFAULT NULL COMMENT 'QR签名payloadBase64',
`seat_info` VARCHAR(255) DEFAULT NULL COMMENT '座位信息', `seat_info` VARCHAR(255) DEFAULT NULL COMMENT '座位信息',
`spec_base_id` BIGINT UNSIGNED DEFAULT 0 COMMENT 'spec_base_id', `spec_base_id` BIGINT UNSIGNED DEFAULT 0 COMMENT 'spec_base_id',
`real_name` VARCHAR(60) DEFAULT NULL COMMENT '观演人姓名', `real_name` VARCHAR(60) DEFAULT NULL COMMENT '观演人姓名',
@ -35,7 +34,6 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}vr_tickets` (
`updated_at` INT UNSIGNED DEFAULT 0 COMMENT '更新时间', `updated_at` INT UNSIGNED DEFAULT 0 COMMENT '更新时间',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_ticket_code` (`ticket_code`), UNIQUE KEY `uk_ticket_code` (`ticket_code`),
UNIQUE KEY `uk_short_code` (`short_code`),
KEY `idx_order_id` (`order_id`), KEY `idx_order_id` (`order_id`),
KEY `idx_user_id` (`user_id`), KEY `idx_user_id` (`user_id`),
KEY `idx_goods_id` (`goods_id`), KEY `idx_goods_id` (`goods_id`),

View File

@ -128,9 +128,9 @@ class TicketService extends BaseService
} }
$ticket_code = BaseService::generateUuid(); $ticket_code = BaseService::generateUuid();
$now = BaseService::now();
// Step 1: 先插入获取 ticket_id用于 short_code 生成) // Step 1: 先插入获取 ticket_id用于 short_code 生成)
$now = BaseService::now();
$ticket_id = \think\facade\Db::name(BaseService::table('tickets'))->insertGetId([ $ticket_id = \think\facade\Db::name(BaseService::table('tickets'))->insertGetId([
'order_id' => $order['id'], 'order_id' => $order['id'],
'order_no' => $order['order_no'], 'order_no' => $order['order_no'],
@ -142,6 +142,7 @@ class TicketService extends BaseService
], JSON_UNESCAPED_UNICODE), ], JSON_UNESCAPED_UNICODE),
'user_id' => $order['user_id'], 'user_id' => $order['user_id'],
'ticket_code' => $ticket_code, 'ticket_code' => $ticket_code,
'qr_data' => '', // 占位,生成后更新
'seat_info' => $spec_name, 'seat_info' => $spec_name,
'spec_base_id' => $spec_base_id, 'spec_base_id' => $spec_base_id,
'real_name' => '', 'real_name' => '',
@ -159,6 +160,7 @@ class TicketService extends BaseService
} }
// Step 2: 生成短码goods_id 明文 + ticket_id 混淆) // Step 2: 生成短码goods_id 明文 + ticket_id 混淆)
// 短码存储在 qr_data 中,供前端展示
$short_code = BaseService::shortCodeEncode($order['goods_id'], $ticket_id); $short_code = BaseService::shortCodeEncode($order['goods_id'], $ticket_id);
// Step 3: 生成 QR payloadHMAC-SHA256 签名30分钟有效 // Step 3: 生成 QR payloadHMAC-SHA256 签名30分钟有效
@ -169,13 +171,13 @@ class TicketService extends BaseService
'exp' => $now + 1800, // 30分钟 'exp' => $now + 1800, // 30分钟
]); ]);
// Step 4: 更新 short_code 和 qr_payload // qr_data 格式:短码|QR_payload竖线分隔
$qr_data = $short_code . '|' . $qr_payload;
// Step 4: 更新 qr_data
\think\facade\Db::name(BaseService::table('tickets')) \think\facade\Db::name(BaseService::table('tickets'))
->where('id', $ticket_id) ->where('id', $ticket_id)
->update([ ->update(['qr_data' => $qr_data]);
'short_code' => $short_code,
'qr_payload' => $qr_payload,
]);
// Step 5: 写入观演人信息 // Step 5: 写入观演人信息
$extension_data = json_decode($order['extension_data'] ?? '{}', true); $extension_data = json_decode($order['extension_data'] ?? '{}', true);
@ -449,13 +451,15 @@ class TicketService extends BaseService
} }
/** /**
* 获取票的 QR payload带动态刷新 * 获取票的 QR 数据(短码 + payload
*
* qr_data 格式:短码|payload
* *
* @param int $ticket_id 票ID * @param int $ticket_id 票ID
* @param int $user_id 用户ID校验归属 * @param int $user_id 用户ID校验归属
* @return array [code, data] * @return array [code, data]
*/ */
public static function getQrPayload($ticket_id, $user_id) public static function getQrData($ticket_id, $user_id)
{ {
$ticket = \think\facade\Db::name(BaseService::table('tickets')) $ticket = \think\facade\Db::name(BaseService::table('tickets'))
->where('id', $ticket_id) ->where('id', $ticket_id)
@ -476,19 +480,26 @@ class TicketService extends BaseService
return ['code' => -3, 'msg' => '该票已退款']; return ['code' => -3, 'msg' => '该票已退款'];
} }
$qr_data = $ticket['qr_data'] ?? '';
if (empty($qr_data) || strpos($qr_data, '|') === false) {
return ['code' => -4, 'msg' => 'QR数据异常'];
}
[$short_code, $payload] = explode('|', $qr_data, 2);
// 检查是否需要刷新 QR剩余有效期 < 15分钟 // 检查是否需要刷新 QR剩余有效期 < 15分钟
$qr_payload = $ticket['qr_payload']; if (!empty($payload)) {
if (!empty($qr_payload)) { $decoded = BaseService::verifyQrPayload($payload);
$decoded = BaseService::verifyQrPayload($qr_payload);
if ($decoded !== null && $decoded['exp'] - time() > 900) { if ($decoded !== null && $decoded['exp'] - time() > 900) {
// 有效期 > 15分钟返回缓存 // 有效期 > 15分钟返回缓存
return [ return [
'code' => 0, 'code' => 0,
'msg' => 'success', 'msg' => 'success',
'data' => [ 'data' => [
'payload' => $qr_payload, 'short_code' => $short_code,
'cached' => true, 'payload' => $payload,
'expires_in'=> $decoded['exp'] - time(), 'cached' => true,
'expires_in' => $decoded['exp'] - time(),
], ],
]; ];
} }
@ -504,17 +515,19 @@ class TicketService extends BaseService
]); ]);
// 更新缓存 // 更新缓存
$new_qr_data = $short_code . '|' . $new_payload;
\think\facade\Db::name(BaseService::table('tickets')) \think\facade\Db::name(BaseService::table('tickets'))
->where('id', $ticket_id) ->where('id', $ticket_id)
->update(['qr_payload' => $new_payload, 'updated_at' => $now]); ->update(['qr_data' => $new_qr_data, 'updated_at' => $now]);
return [ return [
'code' => 0, 'code' => 0,
'msg' => 'success', 'msg' => 'success',
'data' => [ 'data' => [
'payload' => $new_payload, 'short_code' => $short_code,
'cached' => false, 'payload' => $new_payload,
'expires_in'=> 1800, 'cached' => false,
'expires_in' => 1800,
], ],
]; ];
} }