refactor: 常量化订单状态 + 支付渠道/提供商分离显示 + H5自动跳转
- 新增 src/lib/constants.ts,集中管理 ORDER_STATUS / PAYMENT_TYPE / PAYMENT_PREFIX 等常量
- 后端 service/status/timeout/limits 全量替换魔法字符串为 ORDER_STATUS.*
- PaymentTypeMeta 新增 provider 字段,分离 sublabel(选择器展示)与 provider(提供商名称)
- getPaymentDisplayInfo() 返回 { channel, provider } 用于用户端/管理端展示
- 支持通过 PAYMENT_SUBLABEL_* 环境变量覆盖默认 sublabel
- PaymentQRCode: H5 支付自动跳转(含易支付微信 weixin:// scheme 兜底)
- 订单列表/详情页:显示可读的渠道名+提供商,不再暴露内部标识符
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,13 @@ const envSchema = z.object({
|
||||
NEXT_PUBLIC_APP_URL: z.string().url(),
|
||||
PAY_HELP_IMAGE_URL: optionalTrimmedString,
|
||||
PAY_HELP_TEXT: optionalTrimmedString,
|
||||
|
||||
// ── 支付方式前端描述(sublabel)覆盖,不设置则使用默认值 ──
|
||||
PAYMENT_SUBLABEL_ALIPAY: optionalTrimmedString,
|
||||
PAYMENT_SUBLABEL_ALIPAY_DIRECT: optionalTrimmedString,
|
||||
PAYMENT_SUBLABEL_WXPAY: optionalTrimmedString,
|
||||
PAYMENT_SUBLABEL_WXPAY_DIRECT: optionalTrimmedString,
|
||||
PAYMENT_SUBLABEL_STRIPE: optionalTrimmedString,
|
||||
});
|
||||
|
||||
export type Env = z.infer<typeof envSchema>;
|
||||
|
||||
51
src/lib/constants.ts
Normal file
51
src/lib/constants.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/** 订单状态 */
|
||||
export const ORDER_STATUS = {
|
||||
PENDING: 'PENDING',
|
||||
PAID: 'PAID',
|
||||
RECHARGING: 'RECHARGING',
|
||||
COMPLETED: 'COMPLETED',
|
||||
EXPIRED: 'EXPIRED',
|
||||
CANCELLED: 'CANCELLED',
|
||||
FAILED: 'FAILED',
|
||||
REFUNDING: 'REFUNDING',
|
||||
REFUNDED: 'REFUNDED',
|
||||
REFUND_FAILED: 'REFUND_FAILED',
|
||||
} as const;
|
||||
|
||||
export type OrderStatus = (typeof ORDER_STATUS)[keyof typeof ORDER_STATUS];
|
||||
|
||||
/** 终态状态集合(不再轮询) */
|
||||
export const TERMINAL_STATUSES = new Set<string>([
|
||||
ORDER_STATUS.COMPLETED,
|
||||
ORDER_STATUS.FAILED,
|
||||
ORDER_STATUS.CANCELLED,
|
||||
ORDER_STATUS.EXPIRED,
|
||||
ORDER_STATUS.REFUNDED,
|
||||
ORDER_STATUS.REFUND_FAILED,
|
||||
]);
|
||||
|
||||
/** 退款相关状态 */
|
||||
export const REFUND_STATUSES = new Set<string>([
|
||||
ORDER_STATUS.REFUNDING,
|
||||
ORDER_STATUS.REFUNDED,
|
||||
ORDER_STATUS.REFUND_FAILED,
|
||||
]);
|
||||
|
||||
/** 支付方式标识 */
|
||||
export const PAYMENT_TYPE = {
|
||||
ALIPAY: 'alipay',
|
||||
ALIPAY_DIRECT: 'alipay_direct',
|
||||
WXPAY: 'wxpay',
|
||||
WXPAY_DIRECT: 'wxpay_direct',
|
||||
STRIPE: 'stripe',
|
||||
} as const;
|
||||
|
||||
/** 支付方式前缀(用于 startsWith 判断) */
|
||||
export const PAYMENT_PREFIX = {
|
||||
ALIPAY: 'alipay',
|
||||
WXPAY: 'wxpay',
|
||||
STRIPE: 'stripe',
|
||||
} as const;
|
||||
|
||||
/** 需要页面跳转(而非二维码)的支付方式 */
|
||||
export const REDIRECT_PAYMENT_TYPES = new Set<string>([PAYMENT_TYPE.ALIPAY_DIRECT]);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getEnv } from '@/lib/config';
|
||||
import { ORDER_STATUS } from '@/lib/constants';
|
||||
import { initPaymentProviders, paymentRegistry } from '@/lib/payment';
|
||||
import { getMethodFeeRate } from './fee';
|
||||
|
||||
@@ -72,7 +73,7 @@ export async function queryMethodLimits(paymentTypes: string[]): Promise<Record<
|
||||
by: ['paymentType'],
|
||||
where: {
|
||||
paymentType: { in: paymentTypes },
|
||||
status: { in: ['PAID', 'RECHARGING', 'COMPLETED'] },
|
||||
status: { in: [ORDER_STATUS.PAID, ORDER_STATUS.RECHARGING, ORDER_STATUS.COMPLETED] },
|
||||
paidAt: { gte: todayStart },
|
||||
},
|
||||
_sum: { amount: true },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { getEnv } from '@/lib/config';
|
||||
import { ORDER_STATUS } from '@/lib/constants';
|
||||
import { generateRechargeCode } from './code-gen';
|
||||
import { getMethodDailyLimit } from './limits';
|
||||
import { getMethodFeeRate, calculatePayAmount } from './fee';
|
||||
@@ -44,7 +45,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
|
||||
}
|
||||
|
||||
const pendingCount = await prisma.order.count({
|
||||
where: { userId: input.userId, status: 'PENDING' },
|
||||
where: { userId: input.userId, status: ORDER_STATUS.PENDING },
|
||||
});
|
||||
if (pendingCount >= MAX_PENDING_ORDERS) {
|
||||
throw new OrderError('TOO_MANY_PENDING', `Too many pending orders (${MAX_PENDING_ORDERS})`, 429);
|
||||
@@ -57,7 +58,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
|
||||
const dailyAgg = await prisma.order.aggregate({
|
||||
where: {
|
||||
userId: input.userId,
|
||||
status: { in: ['PAID', 'RECHARGING', 'COMPLETED'] },
|
||||
status: { in: [ORDER_STATUS.PAID, ORDER_STATUS.RECHARGING, ORDER_STATUS.COMPLETED] },
|
||||
paidAt: { gte: todayStart },
|
||||
},
|
||||
_sum: { amount: true },
|
||||
@@ -77,7 +78,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
|
||||
const methodAgg = await prisma.order.aggregate({
|
||||
where: {
|
||||
paymentType: input.paymentType,
|
||||
status: { in: ['PAID', 'RECHARGING', 'COMPLETED'] },
|
||||
status: { in: [ORDER_STATUS.PAID, ORDER_STATUS.RECHARGING, ORDER_STATUS.COMPLETED] },
|
||||
paidAt: { gte: todayStart },
|
||||
},
|
||||
_sum: { amount: true },
|
||||
@@ -158,7 +159,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
|
||||
amount: input.amount,
|
||||
payAmount,
|
||||
feeRate,
|
||||
status: 'PENDING',
|
||||
status: ORDER_STATUS.PENDING,
|
||||
paymentType: input.paymentType,
|
||||
userName: user.username,
|
||||
userBalance: user.balance,
|
||||
@@ -231,7 +232,7 @@ export async function cancelOrderCore(options: {
|
||||
|
||||
// 2. DB 更新 (WHERE status='PENDING' 保证幂等)
|
||||
const result = await prisma.order.updateMany({
|
||||
where: { id: orderId, status: 'PENDING' },
|
||||
where: { id: orderId, status: ORDER_STATUS.PENDING },
|
||||
data: { status: finalStatus, updatedAt: new Date() },
|
||||
});
|
||||
|
||||
@@ -240,7 +241,7 @@ export async function cancelOrderCore(options: {
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
orderId,
|
||||
action: finalStatus === 'EXPIRED' ? 'ORDER_EXPIRED' : 'ORDER_CANCELLED',
|
||||
action: finalStatus === ORDER_STATUS.EXPIRED ? 'ORDER_EXPIRED' : 'ORDER_CANCELLED',
|
||||
detail: auditDetail,
|
||||
operator,
|
||||
},
|
||||
@@ -258,13 +259,13 @@ export async function cancelOrder(orderId: string, userId: number): Promise<Canc
|
||||
|
||||
if (!order) throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
||||
if (order.userId !== userId) throw new OrderError('FORBIDDEN', 'Forbidden', 403);
|
||||
if (order.status !== 'PENDING') throw new OrderError('INVALID_STATUS', 'Order cannot be cancelled', 400);
|
||||
if (order.status !== ORDER_STATUS.PENDING) throw new OrderError('INVALID_STATUS', 'Order cannot be cancelled', 400);
|
||||
|
||||
return cancelOrderCore({
|
||||
orderId: order.id,
|
||||
paymentTradeNo: order.paymentTradeNo,
|
||||
paymentType: order.paymentType,
|
||||
finalStatus: 'CANCELLED',
|
||||
finalStatus: ORDER_STATUS.CANCELLED,
|
||||
operator: `user:${userId}`,
|
||||
auditDetail: 'User cancelled order',
|
||||
});
|
||||
@@ -277,13 +278,13 @@ export async function adminCancelOrder(orderId: string): Promise<CancelOutcome>
|
||||
});
|
||||
|
||||
if (!order) throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
||||
if (order.status !== 'PENDING') throw new OrderError('INVALID_STATUS', 'Order cannot be cancelled', 400);
|
||||
if (order.status !== ORDER_STATUS.PENDING) throw new OrderError('INVALID_STATUS', 'Order cannot be cancelled', 400);
|
||||
|
||||
return cancelOrderCore({
|
||||
orderId: order.id,
|
||||
paymentTradeNo: order.paymentTradeNo,
|
||||
paymentType: order.paymentType,
|
||||
finalStatus: 'CANCELLED',
|
||||
finalStatus: ORDER_STATUS.CANCELLED,
|
||||
operator: 'admin',
|
||||
auditDetail: 'Admin cancelled order',
|
||||
});
|
||||
@@ -330,10 +331,10 @@ export async function confirmPayment(input: {
|
||||
const result = await prisma.order.updateMany({
|
||||
where: {
|
||||
id: order.id,
|
||||
status: { in: ['PENDING', 'EXPIRED'] },
|
||||
status: { in: [ORDER_STATUS.PENDING, ORDER_STATUS.EXPIRED] },
|
||||
},
|
||||
data: {
|
||||
status: 'PAID',
|
||||
status: ORDER_STATUS.PAID,
|
||||
amount: paidAmount,
|
||||
paymentTradeNo: input.tradeNo,
|
||||
paidAt: new Date(),
|
||||
@@ -392,13 +393,13 @@ export async function executeRecharge(orderId: string): Promise<void> {
|
||||
if (!order) {
|
||||
throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
||||
}
|
||||
if (order.status === 'COMPLETED') {
|
||||
if (order.status === ORDER_STATUS.COMPLETED) {
|
||||
return;
|
||||
}
|
||||
if (isRefundStatus(order.status)) {
|
||||
throw new OrderError('INVALID_STATUS', 'Refund-related order cannot recharge', 400);
|
||||
}
|
||||
if (order.status !== 'PAID' && order.status !== 'FAILED') {
|
||||
if (order.status !== ORDER_STATUS.PAID && order.status !== ORDER_STATUS.FAILED) {
|
||||
throw new OrderError('INVALID_STATUS', `Order cannot recharge in status ${order.status}`, 400);
|
||||
}
|
||||
|
||||
@@ -412,7 +413,7 @@ export async function executeRecharge(orderId: string): Promise<void> {
|
||||
|
||||
await prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: { status: 'COMPLETED', completedAt: new Date() },
|
||||
data: { status: ORDER_STATUS.COMPLETED, completedAt: new Date() },
|
||||
});
|
||||
|
||||
await prisma.auditLog.create({
|
||||
@@ -427,7 +428,7 @@ export async function executeRecharge(orderId: string): Promise<void> {
|
||||
await prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
status: 'FAILED',
|
||||
status: ORDER_STATUS.FAILED,
|
||||
failedAt: new Date(),
|
||||
failedReason: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
@@ -455,15 +456,15 @@ function assertRetryAllowed(order: { status: string; paidAt: Date | null }): voi
|
||||
throw new OrderError('INVALID_STATUS', 'Refund-related order cannot retry', 400);
|
||||
}
|
||||
|
||||
if (order.status === 'FAILED' || order.status === 'PAID') {
|
||||
if (order.status === ORDER_STATUS.FAILED || order.status === ORDER_STATUS.PAID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (order.status === 'RECHARGING') {
|
||||
if (order.status === ORDER_STATUS.RECHARGING) {
|
||||
throw new OrderError('CONFLICT', 'Order is recharging, retry later', 409);
|
||||
}
|
||||
|
||||
if (order.status === 'COMPLETED') {
|
||||
if (order.status === ORDER_STATUS.COMPLETED) {
|
||||
throw new OrderError('INVALID_STATUS', 'Order already completed', 400);
|
||||
}
|
||||
|
||||
@@ -490,10 +491,10 @@ export async function retryRecharge(orderId: string): Promise<void> {
|
||||
const result = await prisma.order.updateMany({
|
||||
where: {
|
||||
id: orderId,
|
||||
status: { in: ['FAILED', 'PAID'] },
|
||||
status: { in: [ORDER_STATUS.FAILED, ORDER_STATUS.PAID] },
|
||||
paidAt: { not: null },
|
||||
},
|
||||
data: { status: 'PAID', failedAt: null, failedReason: null },
|
||||
data: { status: ORDER_STATUS.PAID, failedAt: null, failedReason: null },
|
||||
});
|
||||
|
||||
if (result.count === 0) {
|
||||
@@ -511,7 +512,7 @@ export async function retryRecharge(orderId: string): Promise<void> {
|
||||
}
|
||||
|
||||
const derived = deriveOrderState(latest);
|
||||
if (derived.rechargeStatus === 'recharging' || latest.status === 'PAID') {
|
||||
if (derived.rechargeStatus === 'recharging' || latest.status === ORDER_STATUS.PAID) {
|
||||
throw new OrderError('CONFLICT', 'Order is recharging, retry later', 409);
|
||||
}
|
||||
|
||||
@@ -553,7 +554,7 @@ export interface RefundResult {
|
||||
export async function processRefund(input: RefundInput): Promise<RefundResult> {
|
||||
const order = await prisma.order.findUnique({ where: { id: input.orderId } });
|
||||
if (!order) throw new OrderError('NOT_FOUND', 'Order not found', 404);
|
||||
if (order.status !== 'COMPLETED') {
|
||||
if (order.status !== ORDER_STATUS.COMPLETED) {
|
||||
throw new OrderError('INVALID_STATUS', 'Only completed orders can be refunded', 400);
|
||||
}
|
||||
|
||||
@@ -580,8 +581,8 @@ export async function processRefund(input: RefundInput): Promise<RefundResult> {
|
||||
}
|
||||
|
||||
const lockResult = await prisma.order.updateMany({
|
||||
where: { id: input.orderId, status: 'COMPLETED' },
|
||||
data: { status: 'REFUNDING' },
|
||||
where: { id: input.orderId, status: ORDER_STATUS.COMPLETED },
|
||||
data: { status: ORDER_STATUS.REFUNDING },
|
||||
});
|
||||
if (lockResult.count === 0) {
|
||||
throw new OrderError('CONFLICT', 'Order status changed, refresh and retry', 409);
|
||||
@@ -609,7 +610,7 @@ export async function processRefund(input: RefundInput): Promise<RefundResult> {
|
||||
await prisma.order.update({
|
||||
where: { id: input.orderId },
|
||||
data: {
|
||||
status: 'REFUNDED',
|
||||
status: ORDER_STATUS.REFUNDED,
|
||||
refundAmount: new Prisma.Decimal(refundAmount.toFixed(2)),
|
||||
refundReason: input.reason || null,
|
||||
refundAt: new Date(),
|
||||
@@ -631,7 +632,7 @@ export async function processRefund(input: RefundInput): Promise<RefundResult> {
|
||||
await prisma.order.update({
|
||||
where: { id: input.orderId },
|
||||
data: {
|
||||
status: 'REFUND_FAILED',
|
||||
status: ORDER_STATUS.REFUND_FAILED,
|
||||
failedAt: new Date(),
|
||||
failedReason: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ORDER_STATUS, REFUND_STATUSES } from '@/lib/constants';
|
||||
|
||||
export type RechargeStatus = 'not_paid' | 'paid_pending' | 'recharging' | 'success' | 'failed' | 'closed';
|
||||
|
||||
export interface OrderStatusLike {
|
||||
@@ -6,9 +8,13 @@ export interface OrderStatusLike {
|
||||
completedAt?: Date | string | null;
|
||||
}
|
||||
|
||||
const CLOSED_STATUSES = new Set(['EXPIRED', 'CANCELLED', 'REFUNDING', 'REFUNDED', 'REFUND_FAILED']);
|
||||
|
||||
const REFUND_STATUSES = new Set(['REFUNDING', 'REFUNDED', 'REFUND_FAILED']);
|
||||
const CLOSED_STATUSES = new Set<string>([
|
||||
ORDER_STATUS.EXPIRED,
|
||||
ORDER_STATUS.CANCELLED,
|
||||
ORDER_STATUS.REFUNDING,
|
||||
ORDER_STATUS.REFUNDED,
|
||||
ORDER_STATUS.REFUND_FAILED,
|
||||
]);
|
||||
|
||||
function hasDate(value: Date | string | null | undefined): boolean {
|
||||
return Boolean(value);
|
||||
@@ -19,7 +25,7 @@ export function isRefundStatus(status: string): boolean {
|
||||
}
|
||||
|
||||
export function isRechargeRetryable(order: OrderStatusLike): boolean {
|
||||
return hasDate(order.paidAt) && order.status === 'FAILED' && !isRefundStatus(order.status);
|
||||
return hasDate(order.paidAt) && order.status === ORDER_STATUS.FAILED && !isRefundStatus(order.status);
|
||||
}
|
||||
|
||||
export function deriveOrderState(order: OrderStatusLike): {
|
||||
@@ -28,17 +34,17 @@ export function deriveOrderState(order: OrderStatusLike): {
|
||||
rechargeStatus: RechargeStatus;
|
||||
} {
|
||||
const paymentSuccess = hasDate(order.paidAt);
|
||||
const rechargeSuccess = hasDate(order.completedAt) || order.status === 'COMPLETED';
|
||||
const rechargeSuccess = hasDate(order.completedAt) || order.status === ORDER_STATUS.COMPLETED;
|
||||
|
||||
if (rechargeSuccess) {
|
||||
return { paymentSuccess, rechargeSuccess: true, rechargeStatus: 'success' };
|
||||
}
|
||||
|
||||
if (order.status === 'RECHARGING') {
|
||||
if (order.status === ORDER_STATUS.RECHARGING) {
|
||||
return { paymentSuccess, rechargeSuccess: false, rechargeStatus: 'recharging' };
|
||||
}
|
||||
|
||||
if (order.status === 'FAILED') {
|
||||
if (order.status === ORDER_STATUS.FAILED) {
|
||||
return { paymentSuccess, rechargeSuccess: false, rechargeStatus: 'failed' };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { ORDER_STATUS } from '@/lib/constants';
|
||||
import { cancelOrderCore } from './service';
|
||||
|
||||
const INTERVAL_MS = 30_000; // 30 seconds
|
||||
@@ -7,7 +8,7 @@ let timer: ReturnType<typeof setInterval> | null = null;
|
||||
export async function expireOrders(): Promise<number> {
|
||||
const orders = await prisma.order.findMany({
|
||||
where: {
|
||||
status: 'PENDING',
|
||||
status: ORDER_STATUS.PENDING,
|
||||
expiresAt: { lt: new Date() },
|
||||
},
|
||||
select: {
|
||||
@@ -27,7 +28,7 @@ export async function expireOrders(): Promise<number> {
|
||||
orderId: order.id,
|
||||
paymentTradeNo: order.paymentTradeNo,
|
||||
paymentType: order.paymentType,
|
||||
finalStatus: 'EXPIRED',
|
||||
finalStatus: ORDER_STATUS.EXPIRED,
|
||||
operator: 'timeout',
|
||||
auditDetail: 'Order expired',
|
||||
});
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
ORDER_STATUS,
|
||||
PAYMENT_TYPE,
|
||||
PAYMENT_PREFIX,
|
||||
REDIRECT_PAYMENT_TYPES,
|
||||
} from './constants';
|
||||
|
||||
export interface UserInfo {
|
||||
id?: number;
|
||||
username: string;
|
||||
@@ -15,16 +22,16 @@ export interface MyOrder {
|
||||
export type OrderStatusFilter = 'ALL' | 'PENDING' | 'PAID' | 'COMPLETED' | 'CANCELLED' | 'EXPIRED' | 'FAILED';
|
||||
|
||||
export const STATUS_TEXT_MAP: Record<string, string> = {
|
||||
PENDING: '待支付',
|
||||
PAID: '已支付',
|
||||
RECHARGING: '充值中',
|
||||
COMPLETED: '已完成',
|
||||
EXPIRED: '已超时',
|
||||
CANCELLED: '已取消',
|
||||
FAILED: '失败',
|
||||
REFUNDING: '退款中',
|
||||
REFUNDED: '已退款',
|
||||
REFUND_FAILED: '退款失败',
|
||||
[ORDER_STATUS.PENDING]: '待支付',
|
||||
[ORDER_STATUS.PAID]: '已支付',
|
||||
[ORDER_STATUS.RECHARGING]: '充值中',
|
||||
[ORDER_STATUS.COMPLETED]: '已完成',
|
||||
[ORDER_STATUS.EXPIRED]: '已超时',
|
||||
[ORDER_STATUS.CANCELLED]: '已取消',
|
||||
[ORDER_STATUS.FAILED]: '失败',
|
||||
[ORDER_STATUS.REFUNDING]: '退款中',
|
||||
[ORDER_STATUS.REFUNDED]: '已退款',
|
||||
[ORDER_STATUS.REFUND_FAILED]: '退款失败',
|
||||
};
|
||||
|
||||
export const FILTER_OPTIONS: { key: OrderStatusFilter; label: string }[] = [
|
||||
@@ -64,8 +71,12 @@ export function formatCreatedAt(value: string): string {
|
||||
}
|
||||
|
||||
export interface PaymentTypeMeta {
|
||||
/** 支付渠道名(用户看到的:支付宝 / 微信支付 / Stripe) */
|
||||
label: string;
|
||||
/** 选择器中的辅助说明(易支付 / 官方 / 信用卡 / 借记卡) */
|
||||
sublabel?: string;
|
||||
/** 提供商名称(易支付 / 支付宝 / 微信支付 / Stripe) */
|
||||
provider: string;
|
||||
color: string;
|
||||
selectedBorder: string;
|
||||
selectedBg: string;
|
||||
@@ -79,9 +90,10 @@ export interface PaymentTypeMeta {
|
||||
}
|
||||
|
||||
export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
||||
alipay: {
|
||||
[PAYMENT_TYPE.ALIPAY]: {
|
||||
label: '支付宝',
|
||||
sublabel: '易支付',
|
||||
provider: '易支付',
|
||||
color: '#00AEEF',
|
||||
selectedBorder: 'border-cyan-400',
|
||||
selectedBg: 'bg-cyan-50',
|
||||
@@ -90,9 +102,10 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
||||
chartBar: { light: 'bg-cyan-500', dark: 'bg-cyan-400' },
|
||||
buttonClass: 'bg-[#00AEEF] hover:bg-[#009dd6] active:bg-[#008cbe]',
|
||||
},
|
||||
alipay_direct: {
|
||||
[PAYMENT_TYPE.ALIPAY_DIRECT]: {
|
||||
label: '支付宝',
|
||||
sublabel: '官方直连',
|
||||
sublabel: '官方',
|
||||
provider: '支付宝',
|
||||
color: '#1677FF',
|
||||
selectedBorder: 'border-blue-500',
|
||||
selectedBg: 'bg-blue-50',
|
||||
@@ -101,9 +114,10 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
||||
chartBar: { light: 'bg-blue-500', dark: 'bg-blue-400' },
|
||||
buttonClass: 'bg-[#1677FF] hover:bg-[#0958d9] active:bg-[#003eb3]',
|
||||
},
|
||||
wxpay: {
|
||||
[PAYMENT_TYPE.WXPAY]: {
|
||||
label: '微信支付',
|
||||
sublabel: '易支付',
|
||||
provider: '易支付',
|
||||
color: '#2BB741',
|
||||
selectedBorder: 'border-green-500',
|
||||
selectedBg: 'bg-green-50',
|
||||
@@ -112,9 +126,10 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
||||
chartBar: { light: 'bg-green-500', dark: 'bg-green-400' },
|
||||
buttonClass: 'bg-[#2BB741] hover:bg-[#24a038] active:bg-[#1d8a2f]',
|
||||
},
|
||||
wxpay_direct: {
|
||||
[PAYMENT_TYPE.WXPAY_DIRECT]: {
|
||||
label: '微信支付',
|
||||
sublabel: '官方直连',
|
||||
sublabel: '官方',
|
||||
provider: '微信支付',
|
||||
color: '#07C160',
|
||||
selectedBorder: 'border-green-600',
|
||||
selectedBg: 'bg-green-50',
|
||||
@@ -123,9 +138,10 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
||||
chartBar: { light: 'bg-emerald-500', dark: 'bg-emerald-400' },
|
||||
buttonClass: 'bg-[#07C160] hover:bg-[#06ad56] active:bg-[#05994c]',
|
||||
},
|
||||
stripe: {
|
||||
[PAYMENT_TYPE.STRIPE]: {
|
||||
label: 'Stripe',
|
||||
sublabel: '信用卡 / 借记卡',
|
||||
provider: 'Stripe',
|
||||
color: '#635bff',
|
||||
selectedBorder: 'border-[#635bff]',
|
||||
selectedBg: 'bg-[#635bff]/10',
|
||||
@@ -135,25 +151,32 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
||||
},
|
||||
};
|
||||
|
||||
/** 获取支付方式的显示名称(如 '支付宝(官方直连)') */
|
||||
/** 获取支付方式的显示名称(如 '支付宝(官方)') */
|
||||
export function getPaymentTypeLabel(type: string): string {
|
||||
const meta = PAYMENT_TYPE_META[type];
|
||||
if (!meta) return type;
|
||||
return meta.sublabel ? `${meta.label}(${meta.sublabel})` : meta.label;
|
||||
}
|
||||
|
||||
/** 获取支付渠道和提供商的结构化信息 */
|
||||
export function getPaymentDisplayInfo(type: string): { channel: string; provider: string } {
|
||||
const meta = PAYMENT_TYPE_META[type];
|
||||
if (!meta) return { channel: type, provider: '' };
|
||||
return { channel: meta.label, provider: meta.provider };
|
||||
}
|
||||
|
||||
/** 获取基础支付方式图标类型(alipay_direct → alipay) */
|
||||
export function getPaymentIconType(type: string): string {
|
||||
if (type.startsWith('alipay')) return 'alipay';
|
||||
if (type.startsWith('wxpay')) return 'wxpay';
|
||||
if (type.startsWith('stripe')) return 'stripe';
|
||||
if (type.startsWith(PAYMENT_PREFIX.ALIPAY)) return PAYMENT_PREFIX.ALIPAY;
|
||||
if (type.startsWith(PAYMENT_PREFIX.WXPAY)) return PAYMENT_PREFIX.WXPAY;
|
||||
if (type.startsWith(PAYMENT_PREFIX.STRIPE)) return PAYMENT_PREFIX.STRIPE;
|
||||
return type;
|
||||
}
|
||||
|
||||
/** 获取支付方式的元数据,带合理的 fallback */
|
||||
export function getPaymentMeta(type: string): PaymentTypeMeta {
|
||||
const base = getPaymentIconType(type);
|
||||
return PAYMENT_TYPE_META[type] || PAYMENT_TYPE_META[base] || PAYMENT_TYPE_META.alipay;
|
||||
return PAYMENT_TYPE_META[type] || PAYMENT_TYPE_META[base] || PAYMENT_TYPE_META[PAYMENT_TYPE.ALIPAY];
|
||||
}
|
||||
|
||||
/** 获取支付方式图标路径 */
|
||||
@@ -168,30 +191,40 @@ export function getPaymentChannelLabel(type: string): string {
|
||||
|
||||
/** 支付类型谓词函数 */
|
||||
export function isStripeType(type: string | undefined | null): boolean {
|
||||
return !!type?.startsWith('stripe');
|
||||
return !!type?.startsWith(PAYMENT_PREFIX.STRIPE);
|
||||
}
|
||||
|
||||
export function isWxpayType(type: string | undefined | null): boolean {
|
||||
return !!type?.startsWith('wxpay');
|
||||
return !!type?.startsWith(PAYMENT_PREFIX.WXPAY);
|
||||
}
|
||||
|
||||
export function isAlipayType(type: string | undefined | null): boolean {
|
||||
return !!type?.startsWith('alipay');
|
||||
return !!type?.startsWith(PAYMENT_PREFIX.ALIPAY);
|
||||
}
|
||||
|
||||
/** alipay_direct 使用页面跳转而非二维码 */
|
||||
/** 该支付方式需要页面跳转(而非二维码) */
|
||||
export function isRedirectPayment(type: string | undefined | null): boolean {
|
||||
return type === 'alipay_direct';
|
||||
return !!type && REDIRECT_PAYMENT_TYPES.has(type);
|
||||
}
|
||||
|
||||
/** 用自定义 sublabel 覆盖默认值 */
|
||||
export function applySublabelOverrides(overrides: Record<string, string>): void {
|
||||
for (const [type, sublabel] of Object.entries(overrides)) {
|
||||
if (PAYMENT_TYPE_META[type]) {
|
||||
PAYMENT_TYPE_META[type] = { ...PAYMENT_TYPE_META[type], sublabel };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusBadgeClass(status: string, isDark: boolean): string {
|
||||
if (['COMPLETED', 'PAID'].includes(status)) {
|
||||
if (status === ORDER_STATUS.COMPLETED || status === ORDER_STATUS.PAID) {
|
||||
return isDark ? 'bg-emerald-500/20 text-emerald-200' : 'bg-emerald-100 text-emerald-700';
|
||||
}
|
||||
if (status === 'PENDING') {
|
||||
if (status === ORDER_STATUS.PENDING) {
|
||||
return isDark ? 'bg-blue-500/20 text-blue-200' : 'bg-blue-100 text-blue-700';
|
||||
}
|
||||
if (['CANCELLED', 'EXPIRED', 'FAILED'].includes(status)) {
|
||||
const GREY_STATUSES = new Set<string>([ORDER_STATUS.CANCELLED, ORDER_STATUS.EXPIRED, ORDER_STATUS.FAILED]);
|
||||
if (GREY_STATUSES.has(status)) {
|
||||
return isDark ? 'bg-slate-600 text-slate-200' : 'bg-slate-100 text-slate-700';
|
||||
}
|
||||
return isDark ? 'bg-slate-700 text-slate-200' : 'bg-slate-100 text-slate-700';
|
||||
|
||||
Reference in New Issue
Block a user