492 lines
16 KiB
PHP
492 lines
16 KiB
PHP
<?php
|
||
/**
|
||
* VR票务插件 - C端票夹API控制器
|
||
*
|
||
* 路由机制(PluginsService::PluginsApiCall):
|
||
* URL: /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=list
|
||
* → pluginsname=vr_ticket, pluginscontrol=ticket, pluginsaction=list
|
||
* → class = \app\plugins\vr_ticket\api\Ticket (ucfirst('ticket') = 'Ticket')
|
||
* → method = ucfirst('list') = 'list'
|
||
* → app/plugins/vr_ticket/api/Ticket.php::list() ✓
|
||
*
|
||
* @package vr_ticket\api
|
||
*/
|
||
|
||
namespace app\plugins\vr_ticket\api;
|
||
|
||
use app\plugins\vr_ticket\service\WalletService;
|
||
|
||
/**
|
||
* C端票夹 API
|
||
*/
|
||
class Ticket
|
||
{
|
||
private static function getUserId()
|
||
{
|
||
// 方式1:X-Token / Authorization header(JS 发送方式)
|
||
$token = request()->header('X-Token') ?: request()->header('Authorization', '');
|
||
if (!empty($token)) {
|
||
$token = trim(str_replace('Bearer ', '', $token));
|
||
}
|
||
|
||
// 方式1.5:UniApp 以 query 参数传入 token(&token=xxx)
|
||
if (empty($token)) {
|
||
$token = input('token', '', null);
|
||
}
|
||
|
||
if (!empty($token)) {
|
||
// 优先用 vrt_user_platform.token 查 DB(App 登录场景)
|
||
$user = \app\service\UserService::UserTokenData($token);
|
||
if (!empty($user) && !empty($user['id'])) {
|
||
return intval($user['id']);
|
||
}
|
||
|
||
// 如果没查到,说明是 web 登录 token(存在 user_info cookie 里,不在 vrt_user_platform)
|
||
// 尝试从 user_info cookie 直接解码(cookie 内容 = 用户 JSON)
|
||
$userInfoCookie = request()->cookie('user_info');
|
||
if (!empty($userInfoCookie)) {
|
||
$decoded = urldecode($userInfoCookie);
|
||
$userData = json_decode($decoded, true);
|
||
if (!empty($userData) && !empty($userData['id'])) {
|
||
return intval($userData['id']);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 方式2:ShopXO 标准方式(session / cookie,适用于页面直接访问场景)
|
||
$user = \app\service\UserService::LoginUserInfo();
|
||
if (!empty($user) && !empty($user['id'])) {
|
||
return intval($user['id']);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 返回未登录错误
|
||
*
|
||
* @return Json
|
||
*/
|
||
private static function unauthorized(string $msg = '请先登录')
|
||
{
|
||
return [
|
||
'code' => 401,
|
||
'msg' => $msg,
|
||
'data' => [],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 返回成功响应
|
||
*
|
||
* @param mixed $data
|
||
* @param string $msg
|
||
* @return Json
|
||
*/
|
||
private static function success($data = [], string $msg = 'success')
|
||
{
|
||
return [
|
||
'code' => 0,
|
||
'msg' => $msg,
|
||
'data' => $data,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 返回错误响应
|
||
*
|
||
* @param string $msg
|
||
* @param int $code
|
||
* @return Json
|
||
*/
|
||
private static function error(string $msg = '请求失败', int $code = -1)
|
||
{
|
||
return [
|
||
'code' => $code,
|
||
'msg' => $msg,
|
||
'data' => [],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取用户票列表
|
||
*
|
||
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=list
|
||
*
|
||
* 支持两种模式:
|
||
*
|
||
* 【模式 1:传统分页】
|
||
* - page: 页码,默认 1
|
||
* - page_size: 每页数量,默认 20,最大 100
|
||
*
|
||
* 【模式 2:瀑布流】(last_id > 0 或传入 limit 时启用)
|
||
* - last_id: 游标 ID,0 表示首次加载
|
||
* - limit: 每次拉取数量,默认 20,最大 100
|
||
* - order_by: 排序方向,desc(降序,历史) / asc(升序,新数据),默认 desc
|
||
*
|
||
* 通用筛选参数:
|
||
* - order_id: 按单个订单ID筛选
|
||
* - order_ids: 按多个订单ID批量筛选,逗号分隔,如 41,42,43
|
||
* - goods_id: 按商品ID筛选
|
||
* - status: 按核销状态筛选 (0=未核销, 1=已核销, 2=已退款)
|
||
*
|
||
* @return Json
|
||
*/
|
||
public function list()
|
||
{
|
||
$userId = self::getUserId();
|
||
if (empty($userId)) {
|
||
return self::unauthorized();
|
||
}
|
||
|
||
// 解析筛选参数
|
||
$orderId = input('order_id', 0, 'intval');
|
||
$orderIdsStr = input('order_ids', '', 'trim');
|
||
$orderIds = !empty($orderIdsStr) ? array_filter(array_map('intval', explode(',', $orderIdsStr))) : null;
|
||
$goodsId = input('goods_id', 0, 'intval');
|
||
$status = input('status', null, 'intval');
|
||
|
||
// 判断模式:瀑布流 vs 分页
|
||
$lastId = input('last_id', 0, 'intval');
|
||
$hasLimitParam = input('limit') !== null && input('limit') !== '';
|
||
|
||
if ($lastId > 0 || $hasLimitParam) {
|
||
// 瀑布流模式
|
||
$limit = min(100, max(1, intval(input('limit', 20))));
|
||
$orderByInput = strtolower(trim(input('order_by', 'desc')));
|
||
$orderBy = in_array($orderByInput, ['asc', 'desc']) ? $orderByInput : 'desc';
|
||
|
||
try {
|
||
$result = WalletService::getUserTicketsWaterfall(
|
||
$userId,
|
||
$lastId,
|
||
$orderBy,
|
||
$limit,
|
||
$orderId,
|
||
$orderIds,
|
||
$goodsId,
|
||
$status
|
||
);
|
||
|
||
return self::success([
|
||
'tickets' => $result['list'],
|
||
'has_more' => $result['has_more'],
|
||
'last_id' => $result['last_id'],
|
||
'count' => $result['count'],
|
||
]);
|
||
} catch (\Exception $e) {
|
||
return self::error('获取票列表失败: ' . $e->getMessage());
|
||
}
|
||
} else {
|
||
// 传统分页模式(保持兼容)
|
||
$page = max(1, intval(input('page', 1)));
|
||
$pageSize = min(100, max(1, intval(input('page_size', 20))));
|
||
|
||
try {
|
||
$result = WalletService::getUserTicketsPaginated(
|
||
$userId,
|
||
$orderId,
|
||
$orderIds,
|
||
$goodsId,
|
||
$status,
|
||
$page,
|
||
$pageSize
|
||
);
|
||
|
||
return self::success([
|
||
'tickets' => $result['list'],
|
||
'total' => $result['total'],
|
||
'page' => $result['page'],
|
||
'page_size' => $result['page_size'],
|
||
'pages' => $result['pages'],
|
||
]);
|
||
} catch (\Exception $e) {
|
||
return self::error('获取票列表失败: ' . $e->getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户票列表(tickets 别名,兼容文档格式)
|
||
*
|
||
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=tickets
|
||
*
|
||
* @return Json
|
||
*/
|
||
public function tickets()
|
||
{
|
||
$userId = self::getUserId();
|
||
if (empty($userId)) {
|
||
return self::unauthorized();
|
||
}
|
||
|
||
try {
|
||
$tickets = WalletService::getUserTickets($userId);
|
||
|
||
return self::success([
|
||
'tickets' => $tickets,
|
||
'count' => count($tickets),
|
||
]);
|
||
} catch (\Exception $e) {
|
||
return self::error('获取票列表失败: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取票详情(含 QR payload)
|
||
*
|
||
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=detail&id=X
|
||
*
|
||
* @return Json
|
||
*/
|
||
public function detail()
|
||
{
|
||
$userId = self::getUserId();
|
||
if (empty($userId)) {
|
||
return self::unauthorized();
|
||
}
|
||
|
||
$ticketId = input('id', 0, 'intval');
|
||
if ($ticketId <= 0) {
|
||
return self::error('参数错误:票ID无效');
|
||
}
|
||
|
||
try {
|
||
$ticket = WalletService::getTicketDetail($ticketId, $userId);
|
||
|
||
if (empty($ticket)) {
|
||
return self::error('票不存在或无权访问', -404);
|
||
}
|
||
|
||
return self::success([
|
||
'ticket' => $ticket,
|
||
]);
|
||
} catch (\Exception $e) {
|
||
return self::error('获取票详情失败: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 强制刷新 QR payload
|
||
*
|
||
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=refreshQr&id=X
|
||
*
|
||
* @return Json
|
||
*/
|
||
public function refreshQr()
|
||
{
|
||
$userId = self::getUserId();
|
||
if (empty($userId)) {
|
||
return self::unauthorized();
|
||
}
|
||
|
||
$ticketId = input('id', 0, 'intval');
|
||
if ($ticketId <= 0) {
|
||
return self::error('参数错误:票ID无效');
|
||
}
|
||
|
||
try {
|
||
$ticket = WalletService::refreshQrPayload($ticketId, $userId);
|
||
|
||
if (empty($ticket)) {
|
||
return self::error('票不存在或无权访问', -404);
|
||
}
|
||
|
||
return self::success([
|
||
'ticket' => $ticket,
|
||
]);
|
||
} catch (\Exception $e) {
|
||
return self::error('刷新QR失败: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 扫码核销票
|
||
*
|
||
* POST /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=verify
|
||
* Body: { ticket_code: "AB12..." }
|
||
*
|
||
* 鉴权:用户必须是 vr_verifiers 表中 status=1 的记录
|
||
* @return Json
|
||
*/
|
||
public function verify()
|
||
{
|
||
$userId = self::getUserId();
|
||
if (empty($userId)) {
|
||
return self::unauthorized();
|
||
}
|
||
|
||
// 鉴权:检查该 C 端用户是否是授权核销员
|
||
$verifier = \think\facade\Db::name('vr_verifiers')
|
||
->where('user_id', $userId)
|
||
->where('status', 1)
|
||
->find();
|
||
if (empty($verifier)) {
|
||
return self::error('你不是授权核销员,无权核销', -403);
|
||
}
|
||
$verifier_id = $verifier['id'];
|
||
|
||
$ticket_code = input('ticket_code', '', null, 'trim');
|
||
if (empty($ticket_code)) {
|
||
return self::error('票码不能为空');
|
||
}
|
||
|
||
// 自动识别短码 vs UUID:短码长度 < 20 且不含连字符
|
||
$is_short_code = (strlen($ticket_code) < 20 && strpos($ticket_code, '-') === false);
|
||
|
||
if ($is_short_code) {
|
||
$result = \app\plugins\vr_ticket\service\TicketService::verifyByShortCode($ticket_code, $verifier_id);
|
||
} else {
|
||
$result = \app\plugins\vr_ticket\service\TicketService::verifyTicket($ticket_code, $verifier_id);
|
||
}
|
||
|
||
return [
|
||
'code' => $result['code'],
|
||
'msg' => $result['msg'],
|
||
'data' => $result['data'] ?? [],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 我的核销记录
|
||
*
|
||
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=myVerifications
|
||
*
|
||
* @return Json
|
||
*/
|
||
public function myVerifications()
|
||
{
|
||
$userId = self::getUserId();
|
||
if (empty($userId)) {
|
||
return self::unauthorized();
|
||
}
|
||
|
||
// 鉴权:检查该 C 端用户是否是授权核销员
|
||
$verifier = \think\facade\Db::name('vr_verifiers')
|
||
->where('user_id', $userId)
|
||
->where('status', 1)
|
||
->find();
|
||
if (empty($verifier)) {
|
||
return self::error('你不是授权核销员', -403);
|
||
}
|
||
$verifier_id = $verifier['id'];
|
||
|
||
// 分页参数
|
||
$page = max(1, intval(input('page', 1)));
|
||
$page_size = min(100, max(10, intval(input('page_size', 20))));
|
||
|
||
$where = ['verifier_id' => $verifier_id];
|
||
|
||
$list = \think\facade\Db::name('vr_verifications')
|
||
->where($where)
|
||
->order('id', 'desc')
|
||
->page($page, $page_size)
|
||
->select()
|
||
->toArray();
|
||
$total = \think\facade\Db::name('vr_verifications')
|
||
->where($where)
|
||
->count();
|
||
|
||
// 关联票和商品信息
|
||
$ticket_ids = array_filter(array_column($list, 'ticket_id'));
|
||
$tickets_map = [];
|
||
$user_ids = [];
|
||
if (!empty($ticket_ids)) {
|
||
$tickets_raw = \think\facade\Db::name('vr_tickets')
|
||
->where('id', 'in', $ticket_ids)
|
||
->select()
|
||
->toArray();
|
||
foreach ($tickets_raw as $t) {
|
||
$tickets_map[$t['id']] = $t;
|
||
if (!empty($t['user_id'])) {
|
||
$user_ids[] = $t['user_id'];
|
||
}
|
||
}
|
||
}
|
||
|
||
$users_map = [];
|
||
if (!empty($user_ids)) {
|
||
$users_raw = \think\facade\Db::name('User')
|
||
->where('id', 'in', array_unique($user_ids))
|
||
->select()
|
||
->toArray();
|
||
foreach ($users_raw as $u) {
|
||
$users_map[$u['id']] = !empty($u['nickname']) ? $u['nickname'] : (!empty($u['username']) ? $u['username'] : '用户' . $u['id']);
|
||
}
|
||
}
|
||
|
||
$goods_ids = array_filter(array_unique(array_column($list, 'goods_id')));
|
||
$goods_map = [];
|
||
if (!empty($goods_ids)) {
|
||
$goods_list = \think\facade\Db::name('Goods')
|
||
->where('id', 'in', $goods_ids)
|
||
->column('title', 'id');
|
||
$goods_map = $goods_list;
|
||
}
|
||
|
||
$result = [];
|
||
foreach ($list as $v) {
|
||
$ticket = $tickets_map[$v['ticket_id']] ?? [];
|
||
|
||
// 提取短码(从 qr_data 的前半部分)
|
||
$short_code = '';
|
||
if (!empty($ticket['qr_data']) && strpos($ticket['qr_data'], '|') !== false) {
|
||
$short_code = explode('|', $ticket['qr_data'], 2)[0];
|
||
} else {
|
||
$short_code = !empty($ticket['ticket_code']) ? substr($ticket['ticket_code'], 0, 8) : '';
|
||
}
|
||
|
||
$buyer_name = $users_map[$ticket['user_id'] ?? 0] ?? '';
|
||
|
||
$result[] = [
|
||
'id' => $v['id'],
|
||
'ticket_id' => $v['ticket_id'],
|
||
'ticket_code' => $v['ticket_code'],
|
||
'short_code' => $short_code,
|
||
'seat_info' => $ticket['seat_info'] ?? '',
|
||
'real_name' => $ticket['real_name'] ?? '',
|
||
'phone' => $ticket['phone'] ?? '',
|
||
'order_no' => $ticket['order_no'] ?? '',
|
||
'buyer_name' => $buyer_name,
|
||
'goods_title' => $goods_map[$v['goods_id']] ?? '已下架商品',
|
||
'created_at' => $v['created_at'],
|
||
];
|
||
}
|
||
|
||
return self::success([
|
||
'list' => $result,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'page_size' => $page_size,
|
||
'pages' => ceil($total / $page_size),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 检测当前登录用户是否为授权核销员
|
||
*
|
||
* GET /api.php?s=plugins/index&pluginsname=vr_ticket&pluginscontrol=ticket&pluginsaction=checkVerifier
|
||
*
|
||
* 轻量接口,用于前端快速判断是否展示核销员入口
|
||
* @return Json
|
||
*/
|
||
public function checkVerifier()
|
||
{
|
||
$userId = self::getUserId();
|
||
if (empty($userId)) {
|
||
return self::unauthorized();
|
||
}
|
||
|
||
$verifier = \think\facade\Db::name('vr_verifiers')
|
||
->where('user_id', $userId)
|
||
->where('status', 1)
|
||
->find();
|
||
|
||
return self::success([
|
||
'is_verifier' => !empty($verifier),
|
||
'verifier_id' => $verifier['id'] ?? 0,
|
||
'verifier_name' => $verifier['name'] ?? '',
|
||
]);
|
||
}
|
||
}
|