diff --git a/src/__tests__/lib/alipay/provider.test.ts b/src/__tests__/lib/alipay/provider.test.ts index be21b9b..f22ddb6 100644 --- a/src/__tests__/lib/alipay/provider.test.ts +++ b/src/__tests__/lib/alipay/provider.test.ts @@ -37,21 +37,21 @@ describe('AlipayProvider', () => { }); describe('metadata', () => { - it('should have name "alipay"', () => { - expect(provider.name).toBe('alipay'); + it('should have name "alipay-direct"', () => { + expect(provider.name).toBe('alipay-direct'); }); it('should have providerKey "alipay"', () => { expect(provider.providerKey).toBe('alipay'); }); - it('should support "alipay" payment type', () => { - expect(provider.supportedTypes).toEqual(['alipay']); + it('should support "alipay_direct" payment type', () => { + expect(provider.supportedTypes).toEqual(['alipay_direct']); }); it('should have default limits', () => { expect(provider.defaultLimits).toEqual({ - alipay: { singleMax: 1000, dailyMax: 10000 }, + alipay_direct: { singleMax: 1000, dailyMax: 10000 }, }); }); }); diff --git a/src/app/api/orders/route.ts b/src/app/api/orders/route.ts index b751cec..115e515 100644 --- a/src/app/api/orders/route.ts +++ b/src/app/api/orders/route.ts @@ -6,7 +6,7 @@ import { getEnv } from '@/lib/config'; const createOrderSchema = z.object({ user_id: z.number().int().positive(), amount: z.number().positive(), - payment_type: z.enum(['alipay', 'wxpay', 'stripe']), + payment_type: z.string().min(1), src_host: z.string().max(253).optional(), src_url: z.string().max(2048).optional(), }); diff --git a/src/app/pay/page.tsx b/src/app/pay/page.tsx index 1c8f73b..89e9e68 100644 --- a/src/app/pay/page.tsx +++ b/src/app/pay/page.tsx @@ -15,7 +15,7 @@ interface OrderResult { amount: number; payAmount?: number; status: string; - paymentType: 'alipay' | 'wxpay' | 'stripe'; + paymentType: string; payUrl?: string | null; qrCode?: string | null; clientSecret?: string | null; diff --git a/src/components/PaymentForm.tsx b/src/components/PaymentForm.tsx index 03915d5..f0def70 100644 --- a/src/components/PaymentForm.tsx +++ b/src/components/PaymentForm.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { PAYMENT_TYPE_META } from '@/lib/pay-utils'; +import { PAYMENT_TYPE_META, getPaymentIconType } from '@/lib/pay-utils'; export interface MethodLimitInfo { available: boolean; @@ -99,14 +99,15 @@ export default function PaymentForm({ }; const renderPaymentIcon = (type: string) => { - if (type === 'alipay') { + const iconType = getPaymentIconType(type); + if (iconType === 'alipay') { return ( ); } - if (type === 'wxpay') { + if (iconType === 'wxpay') { return ( @@ -116,7 +117,7 @@ export default function PaymentForm({ ); } - if (type === 'stripe') { + if (iconType === 'stripe') { return ( { if (!isStripe || !clientSecret || !stripePublishableKey) return; @@ -313,7 +313,7 @@ export default function PaymentQRCode({ } }; - const isWx = paymentType === 'wxpay'; + const isWx = paymentType?.startsWith('wxpay'); const iconSrc = isStripe ? '' : isWx ? '/icons/wxpay.svg' : '/icons/alipay.svg'; const channelLabel = isStripe ? 'Stripe' : isWx ? '\u5FAE\u4FE1' : '\u652F\u4ED8\u5B9D'; const iconBgClass = isStripe ? 'bg-[#635bff]' : isWx ? 'bg-[#07C160]' : 'bg-[#1677FF]'; diff --git a/src/components/admin/OrderDetail.tsx b/src/components/admin/OrderDetail.tsx index 43f7946..06cc7c6 100644 --- a/src/components/admin/OrderDetail.tsx +++ b/src/components/admin/OrderDetail.tsx @@ -1,5 +1,7 @@ 'use client'; +import { getPaymentTypeLabel } from '@/lib/pay-utils'; + interface AuditLog { id: string; action: string; @@ -53,7 +55,7 @@ export default function OrderDetail({ order, onClose, dark }: OrderDetailProps) { label: 'Payment OK', value: order.paymentSuccess ? 'yes' : 'no' }, { label: 'Recharge OK', value: order.rechargeSuccess ? 'yes' : 'no' }, { label: 'Recharge Status', value: order.rechargeStatus || '-' }, - { label: '支付方式', value: order.paymentType === 'alipay' ? '支付宝' : '微信支付' }, + { label: '支付方式', value: getPaymentTypeLabel(order.paymentType) }, { label: '充值码', value: order.rechargeCode }, { label: '支付单号', value: order.paymentTradeNo || '-' }, { label: '客户端IP', value: order.clientIp || '-' }, diff --git a/src/components/admin/OrderTable.tsx b/src/components/admin/OrderTable.tsx index 6a7331a..a2db253 100644 --- a/src/components/admin/OrderTable.tsx +++ b/src/components/admin/OrderTable.tsx @@ -1,5 +1,7 @@ 'use client'; +import { getPaymentTypeLabel } from '@/lib/pay-utils'; + interface Order { id: string; userId: number; @@ -92,15 +94,7 @@ export default function OrderTable({ orders, onRetry, onCancel, onViewDetail, da {statusInfo.label} - - {order.paymentType === 'alipay' - ? '支付宝' - : order.paymentType === 'wechat' - ? '微信支付' - : order.paymentType === 'stripe' - ? 'Stripe' - : order.paymentType} - + {getPaymentTypeLabel(order.paymentType)} {order.srcHost || '-'} {new Date(order.createdAt).toLocaleString('zh-CN')} diff --git a/src/components/admin/PaymentMethodChart.tsx b/src/components/admin/PaymentMethodChart.tsx index 0fe3bdf..df21ef1 100644 --- a/src/components/admin/PaymentMethodChart.tsx +++ b/src/components/admin/PaymentMethodChart.tsx @@ -1,5 +1,7 @@ 'use client'; +import { getPaymentTypeLabel } from '@/lib/pay-utils'; + interface PaymentMethod { paymentType: string; amount: number; @@ -13,8 +15,10 @@ interface PaymentMethodChartProps { } const TYPE_CONFIG: Record = { - alipay: { label: '支付宝', light: 'bg-blue-500', dark: 'bg-blue-400' }, - wechat: { label: '微信支付', light: 'bg-green-500', dark: 'bg-green-400' }, + alipay: { label: '支付宝(易支付)', light: 'bg-cyan-500', dark: 'bg-cyan-400' }, + alipay_direct: { label: '支付宝(官方)', light: 'bg-blue-500', dark: 'bg-blue-400' }, + wxpay: { label: '微信支付(易支付)', light: 'bg-green-500', dark: 'bg-green-400' }, + wxpay_direct: { label: '微信支付(官方)', light: 'bg-emerald-500', dark: 'bg-emerald-400' }, stripe: { label: 'Stripe', light: 'bg-purple-500', dark: 'bg-purple-400' }, }; @@ -48,7 +52,7 @@ export default function PaymentMethodChart({ data, dark }: PaymentMethodChartPro
{data.map((method) => { const config = TYPE_CONFIG[method.paymentType] || { - label: method.paymentType, + label: getPaymentTypeLabel(method.paymentType), light: 'bg-gray-500', dark: 'bg-gray-400', }; diff --git a/src/lib/alipay/provider.ts b/src/lib/alipay/provider.ts index 59e5a0e..ddacf53 100644 --- a/src/lib/alipay/provider.ts +++ b/src/lib/alipay/provider.ts @@ -14,11 +14,11 @@ import { getEnv } from '@/lib/config'; import type { AlipayTradeQueryResponse, AlipayTradeRefundResponse, AlipayTradeCloseResponse } from './types'; export class AlipayProvider implements PaymentProvider { - readonly name = 'alipay'; + readonly name = 'alipay-direct'; readonly providerKey = 'alipay'; - readonly supportedTypes: PaymentType[] = ['alipay']; + readonly supportedTypes: PaymentType[] = ['alipay_direct']; readonly defaultLimits = { - alipay: { singleMax: 1000, dailyMax: 10000 }, + alipay_direct: { singleMax: 1000, dailyMax: 10000 }, }; async createPayment(request: CreatePaymentRequest): Promise { diff --git a/src/lib/config.ts b/src/lib/config.ts index 5382f1b..de8716a 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -67,6 +67,11 @@ const envSchema = z.object({ .optional() .transform((v) => (v !== undefined ? Number(v) : undefined)) .pipe(z.number().min(0).optional()), + MAX_DAILY_AMOUNT_ALIPAY_DIRECT: z + .string() + .optional() + .transform((v) => (v !== undefined ? Number(v) : undefined)) + .pipe(z.number().min(0).optional()), MAX_DAILY_AMOUNT_WXPAY: z .string() .optional() diff --git a/src/lib/easy-pay/client.ts b/src/lib/easy-pay/client.ts index e3ab35a..d2d0de7 100644 --- a/src/lib/easy-pay/client.ts +++ b/src/lib/easy-pay/client.ts @@ -5,7 +5,7 @@ import type { EasyPayCreateResponse, EasyPayQueryResponse, EasyPayRefundResponse export interface CreatePaymentOptions { outTradeNo: string; amount: string; - paymentType: 'alipay' | 'wxpay'; + paymentType: string; clientIp: string; productName: string; } @@ -20,7 +20,7 @@ function normalizeCidList(cid?: string): string | undefined { return normalized || undefined; } -function resolveCid(paymentType: 'alipay' | 'wxpay'): string | undefined { +function resolveCid(paymentType: string): string | undefined { const env = getEnv(); if (paymentType === 'alipay') { return normalizeCidList(env.EASY_PAY_CID_ALIPAY) || normalizeCidList(env.EASY_PAY_CID); diff --git a/src/lib/pay-utils.ts b/src/lib/pay-utils.ts index c019fe3..ac6b0e3 100644 --- a/src/lib/pay-utils.ts +++ b/src/lib/pay-utils.ts @@ -75,19 +75,36 @@ export interface PaymentTypeMeta { export const PAYMENT_TYPE_META: Record = { alipay: { label: '支付宝', - sublabel: 'ALIPAY', + sublabel: '易支付', color: '#00AEEF', selectedBorder: 'border-cyan-400', selectedBg: 'bg-cyan-50', iconBg: 'bg-[#00AEEF]', }, + alipay_direct: { + label: '支付宝', + sublabel: '官方直连', + color: '#1677FF', + selectedBorder: 'border-blue-500', + selectedBg: 'bg-blue-50', + iconBg: 'bg-[#1677FF]', + }, wxpay: { label: '微信支付', + sublabel: '易支付', color: '#2BB741', selectedBorder: 'border-green-500', selectedBg: 'bg-green-50', iconBg: 'bg-[#2BB741]', }, + wxpay_direct: { + label: '微信支付', + sublabel: '官方直连', + color: '#07C160', + selectedBorder: 'border-green-600', + selectedBg: 'bg-green-50', + iconBg: 'bg-[#07C160]', + }, stripe: { label: 'Stripe', sublabel: '信用卡 / 借记卡', @@ -98,6 +115,21 @@ export const PAYMENT_TYPE_META: Record = { }, }; +/** 获取支付方式的显示名称(如 '支付宝(官方直连)') */ +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; +} + +/** 获取基础支付方式图标类型(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'; + return type; +} + export function getStatusBadgeClass(status: string, isDark: boolean): string { if (['COMPLETED', 'PAID'].includes(status)) { return isDark ? 'bg-emerald-500/20 text-emerald-200' : 'bg-emerald-100 text-emerald-700'; diff --git a/src/lib/payment/index.ts b/src/lib/payment/index.ts index 16a85a7..d8ae284 100644 --- a/src/lib/payment/index.ts +++ b/src/lib/payment/index.ts @@ -36,7 +36,7 @@ export function initPaymentProviders(): void { if (!env.ALIPAY_APP_ID || !env.ALIPAY_PRIVATE_KEY) { throw new Error('PAYMENT_PROVIDERS 含 alipay,但缺少 ALIPAY_APP_ID 或 ALIPAY_PRIVATE_KEY'); } - paymentRegistry.register(new AlipayProvider()); + paymentRegistry.register(new AlipayProvider()); // 注册 alipay_direct } if (providers.includes('stripe')) { diff --git a/src/lib/payment/types.ts b/src/lib/payment/types.ts index 470a40b..21a2f2b 100644 --- a/src/lib/payment/types.ts +++ b/src/lib/payment/types.ts @@ -1,5 +1,16 @@ /** Unified payment method types across all providers */ -export type PaymentType = 'alipay' | 'wxpay' | 'stripe'; +export type PaymentType = string; + +/** + * 从复合 key 中提取基础支付方式(如 'alipay_direct' → 'alipay') + * 用于传给第三方 API 时映射回标准名称 + */ +export function getBasePaymentType(type: string): string { + if (type.startsWith('alipay')) return 'alipay'; + if (type.startsWith('wxpay')) return 'wxpay'; + if (type.startsWith('stripe')) return 'stripe'; + return type; +} /** Request to create a payment with any provider */ export interface CreatePaymentRequest {