213 lines
6.4 KiB
PHP
213 lines
6.4 KiB
PHP
<?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 = \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,
|
||
];
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|