vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/service/WalletService.php

266 lines
9.7 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
/**
* VR票务插件 - 票夹服务C端
*
* 核心功能:
* 1. 获取用户票列表
* 2. 获取票详情
* 3. 生成/缓存 QR payload
*
* @package vr_ticket\service
*/
namespace app\plugins\vr_ticket\service;
require_once __DIR__ . '/BaseService.php';
class WalletService extends BaseService
{
/**
* QR 有效期(秒)
*/
const QR_TTL = 1800; // 30分钟
/**
* 获取用户所有票
*
* @param int $userId 用户ID
* @return array
*/
public static function getUserTickets(int $userId): array
{
// 直接查询 tickets 表user_id 已存在)
$tickets = \think\facade\Db::name('vr_tickets')
->where('user_id', $userId)
->order('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 解析5维 pipe 格式),兜底从 goods_snapshot 解析
$seatInfo = self::parseSeatInfo($ticket['seat_info'] ?? '');
$snapshot = json_decode($ticket['goods_snapshot'] ?? '{}', true);
$snapshotKeys = array_filter(['session' => $snapshot['session'] ?? '', 'venue' => $snapshot['venue'] ?? '', 'studio' => $snapshot['studio'] ?? '', 'section' => $snapshot['section'] ?? '', 'seat' => $snapshot['seat'] ?? '']);
if (empty($seatInfo['session']) && !empty($snapshotKeys)) {
$seatInfo = array_merge($seatInfo, $snapshotKeys);
}
// goods_snapshot 里没有 session/venue 时,从商品表补全
if (empty($seatInfo['session']) || empty($seatInfo['venue'])) {
$goodsTitle = $goodsMap[$ticket['goods_id']] ?? '已下架商品';
$goods = \think\facade\Db::name('Goods')->where('id', $ticket['goods_id'])->find();
$vrConfig = json_decode($goods['vr_goods_config'] ?? '', true);
if (!empty($vrConfig[0]['template_id'])) {
$template = \think\facade\Db::name('vr_seat_templates')
->where('id', $vrConfig[0]['template_id'])->find();
$seatMap = json_decode($template['seat_map'] ?? '{}', true);
if (empty($seatInfo['venue'])) $seatInfo['venue'] = $template['name'] ?? '';
if (empty($seatInfo['session'])) {
$sessions = $vrConfig[0]['sessions'] ?? [];
$seatInfo['session'] = !empty($sessions[0]['start']) && !empty($sessions[0]['end'])
? ($sessions[0]['start'] . '-' . $sessions[0]['end']) : '';
}
}
}
if (empty($seatInfo['venue'])) $seatInfo['venue'] = $snapshot['venue'] ?? '';
if (empty($seatInfo['session'])) $seatInfo['session'] = $snapshot['session'] ?? '';
$result[] = [
'id' => $ticket['id'],
'goods_id' => $ticket['goods_id'],
'goods_title' => $goodsMap[$ticket['goods_id']] ?? '已下架商品',
'seat_info' => $ticket['seat_info'] ?? '', // 完整 5 维(保留)
'seat_number' => self::parseSeatNumber($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,
'seat_number' => self::parseSeatNumber($ticket['seat_info'] ?? ''),
];
}
return $result;
}
/**
* 提取座位号seat_info 最后一个 | 分段)
* @param string $seatInfo 完整 5 维坐席信息
* @return string 仅座位号
*/
public static function parseSeatNumber(string $seatInfo): string
{
if (empty($seatInfo)) return '';
$parts = explode('|', $seatInfo);
return end($parts) ?: '';
}
/**
* 获取票详情
*
* @param int $ticketId 票ID
* @param int $userId 用户ID用于权限校验
* @return array|null
*/
public static function getTicketDetail(int $ticketId, int $userId): ?array
{
// 直接查询 tickets 表(包含 user_id
$ticket = \think\facade\Db::name('vr_tickets')
->where('id', $ticketId)
->where('user_id', $userId)
->find();
if (empty($ticket)) {
return null;
}
// 获取商品信息
$goods = \think\facade\Db::name('Goods')->find($ticket['goods_id']);
$seatInfo = self::parseSeatInfo($ticket['seat_info'] ?? '');
$snapshot = json_decode($ticket['goods_snapshot'] ?? '{}', true);
// 兜底补全:从 snapshot 补 seat_info 缺失字段
if (empty($seatInfo['venue']) || empty($seatInfo['session'])) {
$vrConfig = json_decode($goods['vr_goods_config'] ?? '', true);
if (!empty($vrConfig[0]['template_id'])) {
$template = \think\facade\Db::name('vr_seat_templates')
->where('id', $vrConfig[0]['template_id'])->find();
if (empty($seatInfo['venue'])) $seatInfo['venue'] = $template['name'] ?? '';
if (empty($seatInfo['session'])) {
$sessions = $vrConfig[0]['sessions'] ?? [];
$seatInfo['session'] = !empty($sessions[0]['start']) && !empty($sessions[0]['end'])
? ($sessions[0]['start'] . '-' . $sessions[0]['end']) : '';
}
}
}
if (empty($seatInfo['venue'])) $seatInfo['venue'] = $snapshot['venue'] ?? '';
if (empty($seatInfo['session'])) $seatInfo['session'] = $snapshot['session'] ?? '';
// 生成短码
$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,
'seat_number' => self::parseSeatNumber($ticket['seat_info'] ?? ''),
'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);
}
}