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";