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

270 lines
8.1 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;
class WalletService extends BaseService
{
/**
* QR 有效期(秒)
*/
const QR_TTL = 1800; // 30分钟
/**
* QR 刷新阈值(秒)
* 剩余有效期 > 此值时返回缓存
*/
const QR_REFRESH_THRESHOLD = 900; // 15分钟
/**
* 获取用户所有票
*
* @param int $userId 用户ID
* @return array
*/
public static function getUserTickets(int $userId): array
{
// 查询该用户的所有票(关联订单)
$tickets = \think\facade\Db::name('vr_tickets')
->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,
// 是否需要刷新 QR
'qr_need_refresh' => self::qrNeedsRefresh($ticket['qr_issued_at'] ?? 0),
];
}
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 分钟
* - 剩余有效期 > 15 分钟:返回缓存
* - 剩余有效期 ≤ 15 分钟:刷新
*
* @param array $ticket 票数据
* @return array ['payload' => string, 'expires_at' => int, 'expires_in' => int]
*/
public static function getQrPayload(array $ticket): array
{
$now = time();
$issuedAt = $ticket['qr_issued_at'] ?? 0;
$expiresAt = $issuedAt + self::QR_TTL;
// 检查是否需要刷新
$needsRefresh = ($issuedAt == 0) || (($expiresAt - $now) <= self::QR_REFRESH_THRESHOLD);
if ($needsRefresh) {
// 生成新 QR
$issuedAt = $now;
$expiresAt = $now + self::QR_TTL;
$payload = [
'id' => $ticket['id'],
'g' => $ticket['goods_id'],
'iat' => $issuedAt,
'exp' => $expiresAt,
];
$encoded = self::signQrPayload($payload);
// 回写数据库(更新 qr_issued_at
\think\facade\Db::name('vr_tickets')
->where('id', $ticket['id'])
->update(['qr_issued_at' => $issuedAt]);
} else {
// 返回缓存的 payload
// 重新构建 payload从数据库读取 iat
$payload = [
'id' => $ticket['id'],
'g' => $ticket['goods_id'],
'iat' => $issuedAt,
'exp' => $expiresAt,
];
$encoded = self::signQrPayload($payload);
}
return [
'payload' => $encoded,
'expires_at' => $expiresAt,
'expires_in' => max(0, $expiresAt - $now),
];
}
/**
* 强制刷新 QR payload
*
* @param int $ticketId 票ID
* @param int $userId 用户ID
* @return array|null
*/
public static function refreshQrPayload(int $ticketId, int $userId): ?array
{
// 先清零 qr_issued_at强制刷新
\think\facade\Db::name('vr_tickets')
->where('id', $ticketId)
->update(['qr_issued_at' => 0]);
return self::getTicketDetail($ticketId, $userId);
}
/**
* 检查 QR 是否需要刷新
*
* @param int $qrIssuedAt QR 发放时间戳
* @return bool
*/
public static function qrNeedsRefresh(int $qrIssuedAt): bool
{
if ($qrIssuedAt == 0) {
return true;
}
$now = time();
$expiresAt = $qrIssuedAt + self::QR_TTL;
return (($expiresAt - $now) <= self::QR_REFRESH_THRESHOLD);
}
/**
* 解析座位信息
*
* 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);
}
}