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

249 lines
9.0 KiB
PHP
Raw Normal View History

<?php
/**
* VR票务插件 - 票夹服务C端
*
* 核心功能:
* 1. 获取用户票列表
* 2. 获取票详情
* 3. 生成/缓存 QR payload
*
* @package vr_ticket\service
*/
namespace app\plugins\vr_ticket\service;
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'] ?? '',
'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
{
// 直接查询 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,
'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);
}
}