vr-shopxo-plugin/fix_backlog_tickets.php

304 lines
9.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
/**
* 补发历史已支付订单的票(修复 goods_id 从 order_detail 而非 order 主表取值)
*
* 问题根因onOrderPaid 从 vrt_order 表取 goods_id字段不存在实际在 vrt_order_detail
* 修复后onOrderPaid 已改为从 order_detail 取 goods_id
* 此脚本用于补发已支付但未生票的历史订单
*
* 使用方式(直接在宿主机执行):
* php fix_backlog_tickets.php
*
* 或在 Docker PHP 容器内执行:
* docker exec shopxo-php php /var/www/html/fix_backlog_tickets.php
*/
echo "========================================\n";
echo "VR票务 - 历史订单补票脚本\n";
echo "========================================\n\n";
// ============================================================
// 连接数据库
// ============================================================
$MYSQL_HOST = '172.23.0.2';
$MYSQL_PORT = 3306;
$MYSQL_USER = 'root';
$MYSQL_PASS = 'shopxo_root_2024';
$MYSQL_DB = 'vrticket';
// VR_TICKET_SECRET与 BaseService 保持一致,使用默认值)
$VR_SECRET = '8935b3a3-a7b4-4e3d-8c1f-9b7e2a6f5d4c';
try {
$pdo = new PDO(
"mysql:host={$MYSQL_HOST};port={$MYSQL_PORT};dbname={$MYSQL_DB};charset=utf8mb4",
$MYSQL_USER,
$MYSQL_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage() . "\n");
}
echo "[配置] 数据库: {$MYSQL_DB}\n\n";
// ============================================================
// 工具函数
// ============================================================
function vr_now(): int {
return time();
}
function vr_generate_uuid(): string {
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
function vr_get_goods_key(int $goods_id, string $secret): string {
return substr(hash_hmac('sha256', (string)$goods_id, $secret), 0, 16);
}
function vr_feistel_round(int $R, int $round, string $key): int {
$hmac = hash_hmac('sha256', $R . '.' . $round, $key, true);
$val = (ord($hmac[0]) << 16) | (ord($hmac[1]) << 8) | ord($hmac[2]);
return $val & 0x7FFFF;
}
function vr_feistel_encode(int $packed, string $key): string {
$L = ($packed >> 19) & 0x1FFFFF;
$R = $packed & 0x7FFFF;
for ($i = 0; $i < 8; $i++) {
$round_key = hash_hmac('sha256', pack('V', $i), $key, true);
$F = (ord($round_key[0]) << 16) | (ord($round_key[1]) << 8) | ord($round_key[2]);
$L_new = $R;
$R_new = ($L ^ $F) & 0x7FFFF;
$L = $L_new;
$R = $R_new;
}
$result = (($L & 0x1FFFFF) << 19) | ($R & 0x7FFFF);
return base_convert($result, 10, 36);
}
function vr_short_code_encode(int $goods_id, int $ticket_id, string $secret): string {
if ($goods_id > 0xFFFFFF) {
throw new Exception("goods_id 超出范围: {$goods_id}");
}
if ($ticket_id <= 0) {
throw new Exception("ticket_id 必须为正整数: {$ticket_id}");
}
$goods_part = str_pad(base_convert($goods_id, 10, 36), 4, '0', STR_PAD_LEFT);
$ticket_int = intval(base_convert($ticket_id, 10, 36));
$key = vr_get_goods_key($goods_id, $secret);
$obfuscated = vr_feistel_encode($ticket_int, $key);
return strtolower($goods_part . $obfuscated);
}
function vr_sign_qr_payload(int $id, int $goods_id, int $now, string $secret): string {
$sign_str = "{$id}.{$goods_id}.{$now}." . ($now + 1800);
$sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8);
$payload = json_encode([
'id' => $id,
'g' => $goods_id,
'iat' => $now,
'exp' => $now + 1800,
'sig' => $sig,
], JSON_UNESCAPED_UNICODE);
return base64_encode($payload);
}
// 解析 spec JSON提取 $vr-座位号 类型的值
function extract_seat_from_spec(string $spec): string {
$list = json_decode($spec, true);
if (!is_array($list)) {
return '';
}
foreach ($list as $item) {
$type = $item['type'] ?? '';
$value = $item['value'] ?? '';
if ($type === '$vr-座位号') {
return $value;
}
}
// fallback: 取 $vr-分区
foreach ($list as $item) {
$type = $item['type'] ?? '';
$value = $item['value'] ?? '';
if ($type === '$vr-分区') {
return $value;
}
}
return '';
}
// 判断是否为票务商品
function is_ticket_goods(PDO $pdo, int $goods_id): bool {
$stmt = $pdo->prepare("SELECT item_type FROM vrt_goods WHERE id = ?");
$stmt->execute([$goods_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return false;
}
return ($row['item_type'] ?? '') === 'ticket';
}
// ============================================================
// 主流程
// ============================================================
// Step 1: 查找已支付订单中vrt_vr_tickets 里还没有票的订单
$sql = "
SELECT
o.id AS order_id,
o.order_no,
o.user_id,
o.extension_data,
od.id AS detail_id,
od.goods_id,
od.title,
od.price,
od.spec
FROM vrt_order o
JOIN vrt_order_detail od ON o.id = od.order_id
WHERE o.pay_status = 1
AND NOT EXISTS (
SELECT 1 FROM vrt_vr_tickets t
WHERE t.order_id = o.id
AND t.seat_info = :seat_placeholder
)
ORDER BY o.id, od.id
";
$stmt = $pdo->prepare($sql);
$stmt->execute(['seat_placeholder' => '']);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "[Step 1] 找到 " . count($rows) . " 个待处理的订单明细行\n\n";
// 先按 order_id 分组,看看哪些订单需要处理
$ordersById = [];
foreach ($rows as $row) {
$ordersById[$row['order_id']] = true;
}
echo "涉及订单: " . implode(', ', array_keys($ordersById)) . "\n\n";
// Step 2: 逐行处理,发放票
$totalNewTickets = 0;
$skipped = 0;
$pdo->beginTransaction();
foreach ($rows as $row) {
$order_id = (int)$row['order_id'];
$detail_id = (int)$row['detail_id'];
$goods_id = (int)$row['goods_id'];
$order_no = $row['order_no'];
$user_id = (int)$row['user_id'];
$title = $row['title'];
$price = (float)$row['price'];
$spec = $row['spec'];
$extension_data = $row['extension_data'] ?? '{}';
// 判断是否为票务商品
if (!is_ticket_goods($pdo, $goods_id)) {
echo " [SKIP] order_id={$order_id} goods_id={$goods_id} 不是票务商品\n";
$skipped++;
continue;
}
// 解析座位信息
$seat_info = extract_seat_from_spec($spec);
if (empty($seat_info)) {
echo " [WARN] order_id={$order_id} goods_id={$goods_id} 无法从 spec 提取座位信息\n";
$skipped++;
continue;
}
// 幂等检查:同一 order_id + seat_info 是否已有票
$checkStmt = $pdo->prepare("SELECT id FROM vrt_vr_tickets WHERE order_id = ? AND seat_info = ?");
$checkStmt->execute([$order_id, $seat_info]);
if ($checkStmt->fetch()) {
echo " [SKIP] order_id={$order_id} seat_info={$seat_info} 已存在票,幂等跳过\n";
$skipped++;
continue;
}
// 生成票数据
$ticket_code = vr_generate_uuid();
$now = vr_now();
$goods_snapshot = json_encode([
'goods_name' => $title,
'spec_name' => $seat_info,
'price' => $price,
], JSON_UNESCAPED_UNICODE);
// 插入票
$insertSql = "INSERT INTO vrt_vr_tickets
(order_id, order_no, goods_id, goods_snapshot, user_id, ticket_code, qr_data,
seat_info, spec_base_id, real_name, phone, id_card, verify_status, issued_at, created_at, updated_at)
VALUES
(:order_id, :order_no, :goods_id, :goods_snapshot, :user_id, :ticket_code, :qr_data,
:seat_info, :spec_base_id, :real_name, :phone, :id_card, 0, :issued_at, :created_at, :updated_at)";
$insertStmt = $pdo->prepare($insertSql);
// 生成短码和 QR payload先插入获取自增 ID
// 短码需要 ticket_id所以分两步先插入占位再更新
$insertStmt->execute([
':order_id' => $order_id,
':order_no' => $order_no,
':goods_id' => $goods_id,
':goods_snapshot' => $goods_snapshot,
':user_id' => $user_id,
':ticket_code' => $ticket_code,
':qr_data' => '', // 占位
':seat_info' => $seat_info,
':spec_base_id' => 0,
':real_name' => '',
':phone' => '',
':id_card' => '',
':issued_at' => $now,
':created_at' => $now,
':updated_at' => $now,
]);
$ticket_id = (int)$pdo->lastInsertId();
// 生成短码和 QR payload
$short_code = vr_short_code_encode($goods_id, $ticket_id, $VR_SECRET);
$qr_payload = vr_sign_qr_payload($ticket_id, $goods_id, $now, $VR_SECRET);
$qr_data = $short_code . '|' . $qr_payload;
// 更新 qr_data
$updateStmt = $pdo->prepare("UPDATE vrt_vr_tickets SET qr_data = :qr_data WHERE id = :id");
$updateStmt->execute([':qr_data' => $qr_data, ':id' => $ticket_id]);
// 写入观演人信息
$attendee = ['real_name' => '', 'phone' => '', 'id_card' => ''];
$extData = json_decode($extension_data, true);
if (isset($extData['attendee'])) {
$attendee = array_merge($attendee, $extData['attendee']);
}
$updateAttendee = $pdo->prepare("UPDATE vrt_vr_tickets SET real_name = :rn, phone = :ph, id_card = :ic WHERE id = :id");
$updateAttendee->execute([
':rn' => $attendee['real_name'] ?? '',
':ph' => $attendee['phone'] ?? '',
':ic' => $attendee['id_card'] ?? '',
':id' => $ticket_id,
]);
$totalNewTickets++;
echo " [OK] order_id={$order_id}, ticket_id={$ticket_id}, goods_id={$goods_id}, seat={$seat_info}\n";
}
$pdo->commit();
echo "\n========================================\n";
echo "完成!\n";
echo "补发票数: {$totalNewTickets}\n";
echo "跳过数: {$skipped}\n";
echo "========================================\n";