feat(ticket): 添加水票功能支持

- 在订单模型中增加formId字段用于标识商品ID
- 更新统一扫码组件以支持水票和礼品卡核销
- 实现水票列表页面,包含我的水票和核销记录两个标签页
- 添加水票核销二维码生成功能
- 支持水票的分页加载和搜索功能
- 实现水票核销记录的展示
- 添加水票状态变更历史追踪
- 更新订单状态判断逻辑以支持特定商品完成状态
- 扩展扫码验证功能以处理水票业务类型
This commit is contained in:
2026-02-04 11:00:54 +08:00
parent a3c952d092
commit f96918bf86
7 changed files with 484 additions and 90 deletions

View File

@@ -5,6 +5,7 @@ import {
parseQRContent
} from '@/api/passport/qr-login';
import { getShopGiftByCode, updateShopGift, decryptQrData } from "@/api/shop/shopGift";
import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket';
import { useUser } from "@/hooks/useUser";
import { isValidJSON } from "@/utils/jsonUtils";
import dayjs from 'dayjs';
@@ -29,6 +30,15 @@ export enum ScanType {
UNKNOWN = 'unknown' // 未知类型
}
type VerificationBusinessType = 'gift' | 'ticket';
interface TicketVerificationPayload {
userTicketId: number;
qty?: number;
userId?: number;
t?: number;
}
/**
* 统一扫码结果
*/
@@ -73,7 +83,11 @@ export function useUnifiedQRScan() {
// 1. 检查是否为JSON格式核销二维码
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if (json.businessType === 'gift' && json.token && json.data) {
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
return ScanType.VERIFICATION;
}
// Allow plaintext (non-encrypted) ticket verification payload for debugging/internal use.
if (json.userTicketId) {
return ScanType.VERIFICATION;
}
}
@@ -130,35 +144,79 @@ export function useUnifiedQRScan() {
throw new Error('您没有核销权限');
}
let code = '';
let businessType: VerificationBusinessType = 'gift';
let decryptedOrRaw = '';
// 判断是否为加密的JSON格式
if (isValidJSON(scanResult)) {
const json = JSON.parse(scanResult);
if (json.businessType === 'gift' && json.token && json.data) {
// 解密获取核销码
if ((json.businessType === 'gift' || json.businessType === 'ticket') && json.token && json.data) {
businessType = json.businessType;
// 解密获取核销内容
const decryptedData = await decryptQrData({
token: json.token,
encryptedData: json.data
});
if (decryptedData) {
code = decryptedData.toString();
decryptedOrRaw = decryptedData.toString();
} else {
throw new Error('解密失败');
}
} else if (json.userTicketId) {
businessType = 'ticket';
decryptedOrRaw = scanResult.trim();
}
} else {
// 直接使用扫码结果作为核销
code = scanResult.trim();
// 直接使用扫码结果作为核销内容
decryptedOrRaw = scanResult.trim();
}
if (!code) {
if (!decryptedOrRaw) {
throw new Error('无法获取有效的核销码');
}
// 验证核销码
const gift = await getShopGiftByCode(code);
if (businessType === 'ticket') {
if (!isValidJSON(decryptedOrRaw)) {
throw new Error('水票核销信息格式错误');
}
const payload = JSON.parse(decryptedOrRaw) as TicketVerificationPayload;
const userTicketId = Number(payload.userTicketId);
const qty = Math.max(1, Number(payload.qty || 1));
if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
throw new Error('水票核销信息无效');
}
const ticket = await getGltUserTicket(userTicketId);
if (!ticket) throw new Error('水票不存在');
if (ticket.status === 1) throw new Error('该水票已冻结');
const available = Number(ticket.availableQty || 0);
const used = Number(ticket.usedQty || 0);
if (available < qty) throw new Error('水票可用次数不足');
await updateGltUserTicket({
...ticket,
availableQty: available - qty,
usedQty: used + qty
});
return {
type: ScanType.VERIFICATION,
data: {
businessType: 'ticket',
ticket: {
...ticket,
availableQty: available - qty,
usedQty: used + qty
},
qty
},
message: `核销成功(已使用${qty}次)`
};
}
// 验证礼品卡核销码
const gift = await getShopGiftByCode(decryptedOrRaw);
if (!gift) {
throw new Error('核销码无效');
@@ -187,7 +245,7 @@ export function useUnifiedQRScan() {
return {
type: ScanType.VERIFICATION,
data: gift,
data: { businessType: 'gift', gift },
message: '核销成功'
};
}, [isAdmin]);