alias('t') ->join('order o', 't.order_id = o.id', 'LEFT') ->where('o.user_id', $userId) ->where('o.pay_status', 1) // 已支付 ->where('o.status', '<>', 3) // 未删除 ->field('t.*') ->order('t.issued_at', 'desc') ->select() ->toArray(); if (empty($tickets)) { return []; } // 批量获取商品信息 $goodsIds = array_filter(array_column($tickets, 'goods_id')); $goodsMap = []; if (!empty($goodsIds)) { $goodsMap = \think\facade\Db::name('Goods') ->where('id', 'in', $goodsIds) ->column('title', 'id'); } // 格式化数据 $result = []; foreach ($tickets as $ticket) { // 生成短码 $shortCode = self::shortCodeEncode($ticket['goods_id'], $ticket['id']); // 解析座位信息(从 seat_info 中提取场次/场馆) $seatInfo = self::parseSeatInfo($ticket['seat_info'] ?? ''); $result[] = [ 'id' => $ticket['id'], 'goods_id' => $ticket['goods_id'], 'goods_title' => $goodsMap[$ticket['goods_id']] ?? '已下架商品', 'seat_info' => $ticket['seat_info'] ?? '', 'session_time' => $seatInfo['session'] ?? '', 'venue_name' => $seatInfo['venue'] ?? '', 'real_name' => $ticket['real_name'] ?? '', 'phone' => self::maskPhone($ticket['phone'] ?? ''), 'verify_status' => $ticket['verify_status'], 'issued_at' => $ticket['issued_at'], 'short_code' => $shortCode, ]; } return $result; } /** * 获取票详情 * * @param int $ticketId 票ID * @param int $userId 用户ID(用于权限校验) * @return array|null */ public static function getTicketDetail(int $ticketId, int $userId): ?array { $ticket = \think\facade\Db::name('vr_tickets') ->alias('t') ->join('order o', 't.order_id = o.id', 'LEFT') ->where('t.id', $ticketId) ->where('o.user_id', $userId) ->field('t.*') ->find(); if (empty($ticket)) { return null; } // 获取商品信息 $goods = \think\facade\Db::name('Goods')->find($ticket['goods_id']); $seatInfo = self::parseSeatInfo($ticket['seat_info'] ?? ''); // 生成短码 $shortCode = self::shortCodeEncode($ticket['goods_id'], $ticket['id']); // 生成 QR payload $qrData = self::getQrPayload($ticket); return [ 'id' => $ticket['id'], 'goods_id' => $ticket['goods_id'], 'goods_title' => $goods['title'] ?? '已下架商品', 'goods_image' => $goods['images'] ?? '', 'seat_info' => $ticket['seat_info'] ?? '', 'session_time' => $seatInfo['session'] ?? '', 'venue_name' => $seatInfo['venue'] ?? '', 'real_name' => $ticket['real_name'] ?? '', 'phone' => self::maskPhone($ticket['phone'] ?? ''), 'verify_status' => $ticket['verify_status'], 'verify_time' => $ticket['verify_time'] ?? 0, 'issued_at' => $ticket['issued_at'], 'short_code' => $shortCode, 'qr_payload' => $qrData['payload'], 'qr_expires_at' => $qrData['expires_at'], 'qr_expires_in' => $qrData['expires_in'], ]; } /** * 生成 QR payload * * QR 有效期 30 分钟,动态生成,不存储 * * @param array $ticket 票数据 * @return array ['payload' => string, 'expires_at' => int, 'expires_in' => int] */ public static function getQrPayload(array $ticket): array { $now = time(); $expiresAt = $now + self::QR_TTL; $payload = [ 'id' => $ticket['id'], 'g' => $ticket['goods_id'], 'iat' => $now, 'exp' => $expiresAt, ]; $encoded = self::signQrPayload($payload); return [ 'payload' => $encoded, 'expires_at' => $expiresAt, 'expires_in' => self::QR_TTL, ]; } /** * 强制刷新 QR payload * 重新生成一个新的 QR payload(有效期重新计算) * * @param int $ticketId 票ID * @param int $userId 用户ID * @return array|null */ public static function refreshQrPayload(int $ticketId, int $userId): ?array { // 直接调用 getTicketDetail,它会重新生成 QR return self::getTicketDetail($ticketId, $userId); } /** * 解析座位信息 * * seat_info 格式:场次|场馆|演播室|分区|座位号 * 例如:2026-06-01 20:00|国家体育馆|主要展厅|A区|A1 * * @param string $seatInfo * @return array */ private static function parseSeatInfo(string $seatInfo): array { $parts = explode('|', $seatInfo); return [ 'session' => $parts[0] ?? '', 'venue' => $parts[1] ?? '', 'room' => $parts[2] ?? '', 'section' => $parts[3] ?? '', 'seat' => $parts[4] ?? '', ]; } /** * 手机号脱敏 * * @param string $phone * @return string */ private static function maskPhone(string $phone): string { if (empty($phone) || strlen($phone) < 7) { return $phone; } return substr($phone, 0, 3) . '****' . substr($phone, -4); } }