diff --git a/src/__tests__/app/pay/alipay-short-link-route.test.ts b/src/__tests__/app/pay/alipay-short-link-route.test.ts index 351fc98..ede7734 100644 --- a/src/__tests__/app/pay/alipay-short-link-route.test.ts +++ b/src/__tests__/app/pay/alipay-short-link-route.test.ts @@ -190,9 +190,7 @@ describe('GET /pay/[orderId]', () => { ); const response = await GET( - createRequest( - 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148', - ), + createRequest('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148'), { params: Promise.resolve({ orderId: 'order-001' }), }, diff --git a/src/__tests__/lib/alipay/client.test.ts b/src/__tests__/lib/alipay/client.test.ts index a69786b..e1c1982 100644 --- a/src/__tests__/lib/alipay/client.test.ts +++ b/src/__tests__/lib/alipay/client.test.ts @@ -1,4 +1,3 @@ - import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('@/lib/config', () => ({ diff --git a/src/__tests__/lib/order/status-access.test.ts b/src/__tests__/lib/order/status-access.test.ts index 6524918..39ed8d4 100644 --- a/src/__tests__/lib/order/status-access.test.ts +++ b/src/__tests__/lib/order/status-access.test.ts @@ -1,4 +1,3 @@ - import { describe, expect, it, vi } from 'vitest'; vi.mock('@/lib/config', () => ({ diff --git a/src/__tests__/payment-flow.test.ts b/src/__tests__/payment-flow.test.ts index 79d0388..c141612 100644 --- a/src/__tests__/payment-flow.test.ts +++ b/src/__tests__/payment-flow.test.ts @@ -101,12 +101,7 @@ function shouldAutoRedirect(opts: { qrCode?: string | null; isMobile: boolean; }): boolean { - return ( - !opts.expired && - !isStripeType(opts.paymentType) && - !!opts.payUrl && - (opts.isMobile || !opts.qrCode) - ); + return !opts.expired && !isStripeType(opts.paymentType) && !!opts.payUrl && (opts.isMobile || !opts.qrCode); } // ============================================================ @@ -381,9 +376,7 @@ describe('Payment Flow - PC/Mobile, QR/Redirect', () => { }); it('Mobile: uses H5 order, returns payUrl (no qrCode)', async () => { - mockWxpayCreateH5Order.mockResolvedValue( - 'https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx123', - ); + mockWxpayCreateH5Order.mockResolvedValue('https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx123'); const request: CreatePaymentRequest = { orderId: 'order-wx-002', diff --git a/src/app/admin/dashboard/page.tsx b/src/app/admin/dashboard/page.tsx index d30095d..d3a96ff 100644 --- a/src/app/admin/dashboard/page.tsx +++ b/src/app/admin/dashboard/page.tsx @@ -39,33 +39,34 @@ function DashboardContent() { const isDark = theme === 'dark'; const isEmbedded = uiMode === 'embedded'; - const text = locale === 'en' - ? { - missingToken: 'Missing admin token', - missingTokenHint: 'Please access the admin page from the Sub2API platform.', - invalidToken: 'Invalid admin token', - requestFailed: 'Request failed', - loadFailed: 'Failed to load data', - title: 'Dashboard', - subtitle: 'Recharge order analytics and insights', - daySuffix: 'd', - orders: 'Order Management', - refresh: 'Refresh', - loading: 'Loading...', - } - : { - missingToken: '缺少管理员凭证', - missingTokenHint: '请从 Sub2API 平台正确访问管理页面', - invalidToken: '管理员凭证无效', - requestFailed: '请求失败', - loadFailed: '加载数据失败', - title: '数据概览', - subtitle: '充值订单统计与分析', - daySuffix: '天', - orders: '订单管理', - refresh: '刷新', - loading: '加载中...', - }; + const text = + locale === 'en' + ? { + missingToken: 'Missing admin token', + missingTokenHint: 'Please access the admin page from the Sub2API platform.', + invalidToken: 'Invalid admin token', + requestFailed: 'Request failed', + loadFailed: 'Failed to load data', + title: 'Dashboard', + subtitle: 'Recharge order analytics and insights', + daySuffix: 'd', + orders: 'Order Management', + refresh: 'Refresh', + loading: 'Loading...', + } + : { + missingToken: '缺少管理员凭证', + missingTokenHint: '请从 Sub2API 平台正确访问管理页面', + invalidToken: '管理员凭证无效', + requestFailed: '请求失败', + loadFailed: '加载数据失败', + title: '数据概览', + subtitle: '充值订单统计与分析', + daySuffix: '天', + orders: '订单管理', + refresh: '刷新', + loading: '加载中...', + }; const [days, setDays] = useState(30); const [data, setData] = useState(null); @@ -138,7 +139,8 @@ function DashboardContent() { <> {DAYS_OPTIONS.map((d) => ( ))} @@ -162,7 +164,7 @@ function DashboardContent() { )} {loading ? ( -
{text.loading}
+
{text.loading}
) : data ? (
@@ -190,9 +192,7 @@ function DashboardPageFallback() { export default function DashboardPage() { return ( - } - > + }> ); diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 8bebbd1..671aea4 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -52,67 +52,68 @@ function AdminContent() { const isDark = theme === 'dark'; const isEmbedded = uiMode === 'embedded'; - const text = locale === 'en' - ? { - missingToken: 'Missing admin token', - missingTokenHint: 'Please access the admin page from the Sub2API platform.', - invalidToken: 'Invalid admin token', - requestFailed: 'Request failed', - loadOrdersFailed: 'Failed to load orders', - retryConfirm: 'Retry recharge for this order?', - retryFailed: 'Retry failed', - retryRequestFailed: 'Retry request failed', - cancelConfirm: 'Cancel this order?', - cancelFailed: 'Cancel failed', - cancelRequestFailed: 'Cancel request failed', - loadDetailFailed: 'Failed to load order details', - title: 'Order Management', - subtitle: 'View and manage all recharge orders', - dashboard: 'Dashboard', - refresh: 'Refresh', - loading: 'Loading...', - statuses: { - '': 'All', - PENDING: 'Pending', - PAID: 'Paid', - RECHARGING: 'Recharging', - COMPLETED: 'Completed', - EXPIRED: 'Expired', - CANCELLED: 'Cancelled', - FAILED: 'Recharge failed', - REFUNDED: 'Refunded', - }, - } - : { - missingToken: '缺少管理员凭证', - missingTokenHint: '请从 Sub2API 平台正确访问管理页面', - invalidToken: '管理员凭证无效', - requestFailed: '请求失败', - loadOrdersFailed: '加载订单列表失败', - retryConfirm: '确认重试充值?', - retryFailed: '重试失败', - retryRequestFailed: '重试请求失败', - cancelConfirm: '确认取消该订单?', - cancelFailed: '取消失败', - cancelRequestFailed: '取消请求失败', - loadDetailFailed: '加载订单详情失败', - title: '订单管理', - subtitle: '查看和管理所有充值订单', - dashboard: '数据概览', - refresh: '刷新', - loading: '加载中...', - statuses: { - '': '全部', - PENDING: '待支付', - PAID: '已支付', - RECHARGING: '充值中', - COMPLETED: '已完成', - EXPIRED: '已超时', - CANCELLED: '已取消', - FAILED: '充值失败', - REFUNDED: '已退款', - }, - }; + const text = + locale === 'en' + ? { + missingToken: 'Missing admin token', + missingTokenHint: 'Please access the admin page from the Sub2API platform.', + invalidToken: 'Invalid admin token', + requestFailed: 'Request failed', + loadOrdersFailed: 'Failed to load orders', + retryConfirm: 'Retry recharge for this order?', + retryFailed: 'Retry failed', + retryRequestFailed: 'Retry request failed', + cancelConfirm: 'Cancel this order?', + cancelFailed: 'Cancel failed', + cancelRequestFailed: 'Cancel request failed', + loadDetailFailed: 'Failed to load order details', + title: 'Order Management', + subtitle: 'View and manage all recharge orders', + dashboard: 'Dashboard', + refresh: 'Refresh', + loading: 'Loading...', + statuses: { + '': 'All', + PENDING: 'Pending', + PAID: 'Paid', + RECHARGING: 'Recharging', + COMPLETED: 'Completed', + EXPIRED: 'Expired', + CANCELLED: 'Cancelled', + FAILED: 'Recharge failed', + REFUNDED: 'Refunded', + }, + } + : { + missingToken: '缺少管理员凭证', + missingTokenHint: '请从 Sub2API 平台正确访问管理页面', + invalidToken: '管理员凭证无效', + requestFailed: '请求失败', + loadOrdersFailed: '加载订单列表失败', + retryConfirm: '确认重试充值?', + retryFailed: '重试失败', + retryRequestFailed: '重试请求失败', + cancelConfirm: '确认取消该订单?', + cancelFailed: '取消失败', + cancelRequestFailed: '取消请求失败', + loadDetailFailed: '加载订单详情失败', + title: '订单管理', + subtitle: '查看和管理所有充值订单', + dashboard: '数据概览', + refresh: '刷新', + loading: '加载中...', + statuses: { + '': '全部', + PENDING: '待支付', + PAID: '已支付', + RECHARGING: '充值中', + COMPLETED: '已完成', + EXPIRED: '已超时', + CANCELLED: '已取消', + FAILED: '充值失败', + REFUNDED: '已退款', + }, + }; const [orders, setOrders] = useState([]); const [total, setTotal] = useState(0); @@ -321,7 +322,9 @@ function AdminContent() { /> {/* Order Detail */} - {detailOrder && setDetailOrder(null)} dark={isDark} locale={locale} />} + {detailOrder && ( + setDetailOrder(null)} dark={isDark} locale={locale} /> + )} ); } @@ -339,9 +342,7 @@ function AdminPageFallback() { export default function AdminPage() { return ( - } - > + }> ); diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index fe4c262..5ffb1b6 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -15,7 +15,10 @@ export async function GET(request: NextRequest) { const token = request.nextUrl.searchParams.get('token')?.trim(); if (!token) { - return NextResponse.json({ error: locale === 'en' ? 'Missing token parameter' : '缺少 token 参数' }, { status: 401 }); + return NextResponse.json( + { error: locale === 'en' ? 'Missing token parameter' : '缺少 token 参数' }, + { status: 401 }, + ); } try { @@ -28,7 +31,10 @@ export async function GET(request: NextRequest) { } if (tokenUser.id !== userId) { - return NextResponse.json({ error: locale === 'en' ? 'Forbidden to access this user' : '无权访问该用户信息' }, { status: 403 }); + return NextResponse.json( + { error: locale === 'en' ? 'Forbidden to access this user' : '无权访问该用户信息' }, + { status: 403 }, + ); } const env = getEnv(); @@ -77,9 +83,7 @@ export async function GET(request: NextRequest) { helpImageUrl: env.PAY_HELP_IMAGE_URL ?? null, helpText: env.PAY_HELP_TEXT ?? null, stripePublishableKey: - enabledTypes.includes('stripe') && env.STRIPE_PUBLISHABLE_KEY - ? env.STRIPE_PUBLISHABLE_KEY - : null, + enabledTypes.includes('stripe') && env.STRIPE_PUBLISHABLE_KEY ? env.STRIPE_PUBLISHABLE_KEY : null, sublabelOverrides: Object.keys(sublabelOverrides).length > 0 ? sublabelOverrides : null, }, }); @@ -89,6 +93,9 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: locale === 'en' ? 'User not found' : '用户不存在' }, { status: 404 }); } console.error('Get user error:', error); - return NextResponse.json({ error: locale === 'en' ? 'Failed to fetch user info' : '获取用户信息失败' }, { status: 500 }); + return NextResponse.json( + { error: locale === 'en' ? 'Failed to fetch user info' : '获取用户信息失败' }, + { status: 500 }, + ); } } diff --git a/src/app/api/wxpay/notify/route.ts b/src/app/api/wxpay/notify/route.ts index 880c749..de006d2 100644 --- a/src/app/api/wxpay/notify/route.ts +++ b/src/app/api/wxpay/notify/route.ts @@ -22,15 +22,11 @@ export async function POST(request: NextRequest) { return Response.json({ code: 'SUCCESS', message: '成功' }); } const success = await handlePaymentNotify(notification, provider.name); - return Response.json( - success ? { code: 'SUCCESS', message: '成功' } : { code: 'FAIL', message: '处理失败' }, - { status: success ? 200 : 500 }, - ); + return Response.json(success ? { code: 'SUCCESS', message: '成功' } : { code: 'FAIL', message: '处理失败' }, { + status: success ? 200 : 500, + }); } catch (error) { console.error('Wxpay notify error:', error); - return Response.json( - { code: 'FAIL', message: '处理失败' }, - { status: 500 }, - ); + return Response.json({ code: 'FAIL', message: '处理失败' }, { status: 500 }); } } diff --git a/src/app/globals.css b/src/app/globals.css index a346e3a..669fdf1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -8,5 +8,11 @@ body { background: var(--background); color: var(--foreground); - font-family: system-ui, -apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + font-family: + system-ui, + -apple-system, + 'PingFang SC', + 'Hiragino Sans GB', + 'Microsoft YaHei', + sans-serif; } diff --git a/src/app/pay/orders/page.tsx b/src/app/pay/orders/page.tsx index 3a1bcac..96afe70 100644 --- a/src/app/pay/orders/page.tsx +++ b/src/app/pay/orders/page.tsx @@ -30,8 +30,16 @@ function OrdersContent() { const text = { missingAuth: pickLocaleText(locale, '缺少认证信息', 'Missing authentication information'), - visitOrders: pickLocaleText(locale, '请从 Sub2API 平台正确访问订单页面', 'Please open the orders page from Sub2API'), - sessionExpired: pickLocaleText(locale, '登录态已失效,请从 Sub2API 重新进入支付页。', 'Session expired. Please re-enter from Sub2API.'), + visitOrders: pickLocaleText( + locale, + '请从 Sub2API 平台正确访问订单页面', + 'Please open the orders page from Sub2API', + ), + sessionExpired: pickLocaleText( + locale, + '登录态已失效,请从 Sub2API 重新进入支付页。', + 'Session expired. Please re-enter from Sub2API.', + ), loadFailed: pickLocaleText(locale, '订单加载失败,请稍后重试。', 'Failed to load orders. Please try again later.'), networkError: pickLocaleText(locale, '网络错误,请稍后重试。', 'Network error. Please try again later.'), switchingMobileTab: pickLocaleText(locale, '正在切换到移动端订单 Tab...', 'Switching to mobile orders tab...'), @@ -40,7 +48,11 @@ function OrdersContent() { backToPay: pickLocaleText(locale, '返回充值', 'Back to Top Up'), loading: pickLocaleText(locale, '加载中...', 'Loading...'), userPrefix: pickLocaleText(locale, '用户', 'User'), - authError: pickLocaleText(locale, '缺少认证信息,请从 Sub2API 平台正确访问订单页面', 'Missing authentication information. Please open the orders page from Sub2API.'), + authError: pickLocaleText( + locale, + '缺少认证信息,请从 Sub2API 平台正确访问订单页面', + 'Missing authentication information. Please open the orders page from Sub2API.', + ), }; const [isIframeContext, setIsIframeContext] = useState(true); diff --git a/src/app/pay/page.tsx b/src/app/pay/page.tsx index 8a4a7b6..4c7b1ab 100644 --- a/src/app/pay/page.tsx +++ b/src/app/pay/page.tsx @@ -156,8 +156,7 @@ function PayContent() { } } } - } catch { - } + } catch {} }; const loadMoreOrders = async () => { @@ -203,7 +202,11 @@ function PayContent() {

{pickLocaleText(locale, '缺少认证信息', 'Missing authentication info')}

- {pickLocaleText(locale, '请从 Sub2API 平台正确访问充值页面', 'Please open the recharge page from the Sub2API platform')} + {pickLocaleText( + locale, + '请从 Sub2API 平台正确访问充值页面', + 'Please open the recharge page from the Sub2API platform', + )}

@@ -216,7 +219,11 @@ function PayContent() {

{pickLocaleText(locale, '用户不存在', 'User not found')}

- {pickLocaleText(locale, '请检查链接是否正确,或联系管理员', 'Please check whether the link is correct or contact the administrator')} + {pickLocaleText( + locale, + '请检查链接是否正确,或联系管理员', + 'Please check whether the link is correct or contact the administrator', + )}

@@ -272,15 +279,33 @@ function PayContent() { if (!res.ok) { const codeMessages: Record = { - INVALID_TOKEN: pickLocaleText(locale, '认证已失效,请重新从平台进入充值页面', 'Authentication expired. Please re-enter the recharge page from the platform'), - USER_INACTIVE: pickLocaleText(locale, '账户已被禁用,无法充值,请联系管理员', 'This account is disabled and cannot be recharged. Please contact the administrator'), - TOO_MANY_PENDING: pickLocaleText(locale, '您有过多待支付订单,请先完成或取消现有订单后再试', 'You have too many pending orders. Please complete or cancel existing orders first'), - USER_NOT_FOUND: pickLocaleText(locale, '用户不存在,请检查链接是否正确', 'User not found. Please check whether the link is correct'), + INVALID_TOKEN: pickLocaleText( + locale, + '认证已失效,请重新从平台进入充值页面', + 'Authentication expired. Please re-enter the recharge page from the platform', + ), + USER_INACTIVE: pickLocaleText( + locale, + '账户已被禁用,无法充值,请联系管理员', + 'This account is disabled and cannot be recharged. Please contact the administrator', + ), + TOO_MANY_PENDING: pickLocaleText( + locale, + '您有过多待支付订单,请先完成或取消现有订单后再试', + 'You have too many pending orders. Please complete or cancel existing orders first', + ), + USER_NOT_FOUND: pickLocaleText( + locale, + '用户不存在,请检查链接是否正确', + 'User not found. Please check whether the link is correct', + ), DAILY_LIMIT_EXCEEDED: data.error, METHOD_DAILY_LIMIT_EXCEEDED: data.error, PAYMENT_GATEWAY_ERROR: data.error, }; - setError(codeMessages[data.code] || data.error || pickLocaleText(locale, '创建订单失败', 'Failed to create order')); + setError( + codeMessages[data.code] || data.error || pickLocaleText(locale, '创建订单失败', 'Failed to create order'), + ); return; } @@ -481,11 +506,24 @@ function PayContent() { {pickLocaleText(locale, '支付说明', 'Payment Notes')}
    -
  • {pickLocaleText(locale, '订单完成后会自动到账', 'Balance will be credited automatically after the order completes')}
  • -
  • {pickLocaleText(locale, '如需历史记录请查看「我的订单」', 'Check "My Orders" for payment history')}
  • +
  • + {pickLocaleText( + locale, + '订单完成后会自动到账', + 'Balance will be credited automatically after the order completes', + )} +
  • +
  • + {pickLocaleText( + locale, + '如需历史记录请查看「我的订单」', + 'Check "My Orders" for payment history', + )} +
  • {config.maxDailyAmount > 0 && (
  • - {pickLocaleText(locale, '每日最大充值', 'Maximum daily recharge')} ¥{config.maxDailyAmount.toFixed(2)} + {pickLocaleText(locale, '每日最大充值', 'Maximum daily recharge')} ¥ + {config.maxDailyAmount.toFixed(2)}
  • )}
@@ -551,18 +589,17 @@ function PayContent() { /> )} - -{step === 'result' && orderResult && finalOrderState && ( - -)} + {step === 'result' && orderResult && finalOrderState && ( + + )} {helpImageOpen && helpImageUrl && (
} - > + }> ); diff --git a/src/app/pay/result/page.tsx b/src/app/pay/result/page.tsx index a573dc4..8a4c656 100644 --- a/src/app/pay/result/page.tsx +++ b/src/app/pay/result/page.tsx @@ -1,4 +1,3 @@ - 'use client'; import { useSearchParams } from 'next/navigation'; @@ -58,27 +57,60 @@ function closeCurrentWindow() { function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale, hasAccessToken: boolean) { if (!order) { return locale === 'en' - ? { label: 'Payment Error', color: 'text-red-600', icon: '✗', message: hasAccessToken ? 'Unable to load the order status. Please try again later.' : 'Missing order access token. Please go back to the recharge page.' } - : { label: '支付异常', color: 'text-red-600', icon: '✗', message: hasAccessToken ? '未查询到订单状态,请稍后重试。' : '订单访问凭证缺失,请返回原充值页查看订单结果。' }; + ? { + label: 'Payment Error', + color: 'text-red-600', + icon: '✗', + message: hasAccessToken + ? 'Unable to load the order status. Please try again later.' + : 'Missing order access token. Please go back to the recharge page.', + } + : { + label: '支付异常', + color: 'text-red-600', + icon: '✗', + message: hasAccessToken ? '未查询到订单状态,请稍后重试。' : '订单访问凭证缺失,请返回原充值页查看订单结果。', + }; } if (order.rechargeSuccess) { return locale === 'en' - ? { label: 'Recharge Successful', color: 'text-green-600', icon: '✓', message: 'Your balance has been credited successfully.' } + ? { + label: 'Recharge Successful', + color: 'text-green-600', + icon: '✓', + message: 'Your balance has been credited successfully.', + } : { label: '充值成功', color: 'text-green-600', icon: '✓', message: '余额已成功到账!' }; } if (order.paymentSuccess) { if (order.rechargeStatus === 'paid_pending' || order.rechargeStatus === 'recharging') { return locale === 'en' - ? { label: 'Top-up Processing', color: 'text-blue-600', icon: '⟳', message: 'Payment succeeded, and the balance top-up is being processed.' } + ? { + label: 'Top-up Processing', + color: 'text-blue-600', + icon: '⟳', + message: 'Payment succeeded, and the balance top-up is being processed.', + } : { label: '充值处理中', color: 'text-blue-600', icon: '⟳', message: '支付成功,余额正在充值中...' }; } if (order.rechargeStatus === 'failed') { return locale === 'en' - ? { label: 'Payment Successful', color: 'text-amber-600', icon: '!', message: 'Payment succeeded, but the balance top-up has not completed yet. Please check again later or contact the administrator.' } - : { label: '支付成功', color: 'text-amber-600', icon: '!', message: '支付成功,但余额充值暂未完成,请稍后查看订单结果或联系管理员。' }; + ? { + label: 'Payment Successful', + color: 'text-amber-600', + icon: '!', + message: + 'Payment succeeded, but the balance top-up has not completed yet. Please check again later or contact the administrator.', + } + : { + label: '支付成功', + color: 'text-amber-600', + icon: '!', + message: '支付成功,但余额充值暂未完成,请稍后查看订单结果或联系管理员。', + }; } } @@ -90,7 +122,12 @@ function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale if (order.status === 'EXPIRED') { return locale === 'en' - ? { label: 'Order Expired', color: 'text-gray-500', icon: '⏰', message: 'This order has expired. Please create a new order.' } + ? { + label: 'Order Expired', + color: 'text-gray-500', + icon: '⏰', + message: 'This order has expired. Please create a new order.', + } : { label: '订单已超时', color: 'text-gray-500', icon: '⏰', message: '订单已超时,请重新充值。' }; } @@ -224,11 +261,7 @@ function ResultContent() {
) ) : ( - )} diff --git a/src/app/pay/stripe-popup/page.tsx b/src/app/pay/stripe-popup/page.tsx index 39671f7..0efd5c4 100644 --- a/src/app/pay/stripe-popup/page.tsx +++ b/src/app/pay/stripe-popup/page.tsx @@ -19,12 +19,20 @@ function StripePopupContent() { const text = { init: pickLocaleText(locale, '正在初始化...', 'Initializing...'), orderId: pickLocaleText(locale, '订单号', 'Order ID'), - loadFailed: pickLocaleText(locale, '支付组件加载失败,请关闭窗口重试', 'Failed to load payment component. Please close the window and try again.'), + loadFailed: pickLocaleText( + locale, + '支付组件加载失败,请关闭窗口重试', + 'Failed to load payment component. Please close the window and try again.', + ), payFailed: pickLocaleText(locale, '支付失败,请重试', 'Payment failed. Please try again.'), closeWindow: pickLocaleText(locale, '关闭窗口', 'Close window'), redirecting: pickLocaleText(locale, '正在跳转到支付页面...', 'Redirecting to payment page...'), loadingForm: pickLocaleText(locale, '正在加载支付表单...', 'Loading payment form...'), - successClosing: pickLocaleText(locale, '支付成功,窗口即将自动关闭...', 'Payment successful. This window will close automatically...'), + successClosing: pickLocaleText( + locale, + '支付成功,窗口即将自动关闭...', + 'Payment successful. This window will close automatically...', + ), closeWindowManually: pickLocaleText(locale, '手动关闭窗口', 'Close window manually'), processing: pickLocaleText(locale, '处理中...', 'Processing...'), payAmount: pickLocaleText(locale, `支付 ¥${amount.toFixed(2)}`, `Pay ¥${amount.toFixed(2)}`), @@ -191,11 +199,17 @@ function StripePopupContent() { {'¥'} {amount.toFixed(2)} -

{text.orderId}: {orderId}

+

+ {text.orderId}: {orderId} +

{stripeError ? (
-
{stripeError}
+
+ {stripeError} +
-

{text.orderId}: {orderId}

+

+ {text.orderId}: {orderId} +

{!stripeLoaded ? ( @@ -238,9 +252,7 @@ function StripePopupContent() { ) : stripeSuccess ? (
{'✓'}
-

- {text.successClosing} -

+

{text.successClosing}

))} diff --git a/src/components/PaymentForm.tsx b/src/components/PaymentForm.tsx index 5d4f70c..fd58231 100644 --- a/src/components/PaymentForm.tsx +++ b/src/components/PaymentForm.tsx @@ -223,12 +223,15 @@ export default function PaymentForm({ !isValid && (() => { const num = parseFloat(customAmount); - let msg = locale === 'en' - ? 'Amount must be within range and support up to 2 decimal places' - : '金额需在范围内,且最多支持 2 位小数(精确到分)'; + let msg = + locale === 'en' + ? 'Amount must be within range and support up to 2 decimal places' + : '金额需在范围内,且最多支持 2 位小数(精确到分)'; if (!isNaN(num)) { - if (num < minAmount) msg = locale === 'en' ? `Minimum per transaction: ¥${minAmount}` : `单笔最低充值 ¥${minAmount}`; - else if (num > effectiveMax) msg = locale === 'en' ? `Maximum per transaction: ¥${effectiveMax}` : `单笔最高充值 ¥${effectiveMax}`; + if (num < minAmount) + msg = locale === 'en' ? `Minimum per transaction: ¥${minAmount}` : `单笔最低充值 ¥${minAmount}`; + else if (num > effectiveMax) + msg = locale === 'en' ? `Maximum per transaction: ¥${effectiveMax}` : `单笔最高充值 ¥${effectiveMax}`; } return
{msg}
; })()} @@ -252,7 +255,13 @@ export default function PaymentForm({ type="button" disabled={isUnavailable} onClick={() => !isUnavailable && setPaymentType(type)} - title={isUnavailable ? (locale === 'en' ? 'Daily limit reached, please use another payment method' : '今日充值额度已满,请使用其他支付方式') : undefined} + title={ + isUnavailable + ? locale === 'en' + ? 'Daily limit reached, please use another payment method' + : '今日充值额度已满,请使用其他支付方式' + : undefined + } className={[ 'relative flex h-[58px] flex-col items-center justify-center rounded-lg border px-3 transition-all sm:flex-1', isUnavailable @@ -260,7 +269,7 @@ export default function PaymentForm({ ? 'cursor-not-allowed border-slate-700 bg-slate-800/50 opacity-50' : 'cursor-not-allowed border-gray-200 bg-gray-50 opacity-50' : isSelected - ? `${meta?.selectedBorder || 'border-blue-500'} ${dark ? (meta?.selectedBgDark || 'bg-blue-950') : (meta?.selectedBg || 'bg-blue-50')} ${dark ? 'text-slate-100' : 'text-slate-900'} shadow-sm` + ? `${meta?.selectedBorder || 'border-blue-500'} ${dark ? meta?.selectedBgDark || 'bg-blue-950' : meta?.selectedBg || 'bg-blue-50'} ${dark ? 'text-slate-100' : 'text-slate-900'} shadow-sm` : dark ? 'border-slate-700 bg-slate-900 text-slate-200 hover:border-slate-500' : 'border-gray-300 bg-white text-slate-700 hover:border-gray-400', @@ -271,7 +280,9 @@ export default function PaymentForm({ {displayInfo.channel || type} {isUnavailable ? ( - {locale === 'en' ? 'Daily limit reached' : '今日额度已满'} + + {locale === 'en' ? 'Daily limit reached' : '今日额度已满'} + ) : displayInfo.sublabel ? ( {locale === 'en' - ? 'The selected payment method has reached today\'s limit. Please switch to another method.' + ? "The selected payment method has reached today's limit. Please switch to another method." : '所选支付方式今日额度已满,请切换到其他支付方式'}

); @@ -331,9 +342,7 @@ export default function PaymentForm({
{locale === 'en' diff --git a/src/components/PaymentQRCode.tsx b/src/components/PaymentQRCode.tsx index 28118e0..cbda0fc 100644 --- a/src/components/PaymentQRCode.tsx +++ b/src/components/PaymentQRCode.tsx @@ -4,12 +4,7 @@ import { useEffect, useMemo, useState, useCallback, useRef } from 'react'; import QRCode from 'qrcode'; import type { Locale } from '@/lib/locale'; import type { PublicOrderStatusSnapshot } from '@/lib/order/status'; -import { - isStripeType, - getPaymentMeta, - getPaymentIconSrc, - getPaymentChannelLabel, -} from '@/lib/pay-utils'; +import { isStripeType, getPaymentMeta, getPaymentIconSrc, getPaymentChannelLabel } from '@/lib/pay-utils'; import { buildOrderStatusUrl } from '@/lib/order/status-url'; import { TERMINAL_STATUSES } from '@/lib/constants'; @@ -84,24 +79,38 @@ export default function PaymentQRCode({ scanPay: locale === 'en' ? 'Please scan with your payment app' : '请使用支付应用扫码支付', back: locale === 'en' ? 'Back' : '返回', cancelOrder: locale === 'en' ? 'Cancel Order' : '取消订单', - h5Hint: locale === 'en' ? 'After payment, please return to this page. The system will confirm automatically.' : '支付完成后请返回此页面,系统将自动确认', + h5Hint: + locale === 'en' + ? 'After payment, please return to this page. The system will confirm automatically.' + : '支付完成后请返回此页面,系统将自动确认', paid: locale === 'en' ? 'Order Paid' : '订单已支付', paidCancelBlocked: - locale === 'en' ? 'This order has already been paid and cannot be cancelled. The recharge will be credited automatically.' : '该订单已支付完成,无法取消。充值将自动到账。', + locale === 'en' + ? 'This order has already been paid and cannot be cancelled. The recharge will be credited automatically.' + : '该订单已支付完成,无法取消。充值将自动到账。', backToRecharge: locale === 'en' ? 'Back to Recharge' : '返回充值', credited: locale === 'en' ? 'Credited ¥' : '到账 ¥', - stripeLoadFailed: locale === 'en' ? 'Failed to load payment component. Please refresh and try again.' : '支付组件加载失败,请刷新页面重试', - initFailed: locale === 'en' ? 'Payment initialization failed. Please go back and try again.' : '支付初始化失败,请返回重试', + stripeLoadFailed: + locale === 'en' + ? 'Failed to load payment component. Please refresh and try again.' + : '支付组件加载失败,请刷新页面重试', + initFailed: + locale === 'en' ? 'Payment initialization failed. Please go back and try again.' : '支付初始化失败,请返回重试', loadingForm: locale === 'en' ? 'Loading payment form...' : '正在加载支付表单...', payFailed: locale === 'en' ? 'Payment failed. Please try again.' : '支付失败,请重试', successProcessing: locale === 'en' ? 'Payment successful, processing your order...' : '支付成功,正在处理订单...', processing: locale === 'en' ? 'Processing...' : '处理中...', payNow: locale === 'en' ? 'Pay' : '支付', popupBlocked: - locale === 'en' ? 'Popup was blocked by your browser. Please allow popups for this site and try again.' : '弹出窗口被浏览器拦截,请允许本站弹出窗口后重试', + locale === 'en' + ? 'Popup was blocked by your browser. Please allow popups for this site and try again.' + : '弹出窗口被浏览器拦截,请允许本站弹出窗口后重试', redirectingPrefix: locale === 'en' ? 'Redirecting to ' : '正在跳转到', redirectingSuffix: locale === 'en' ? '...' : '...', - redirectRetryHint: locale === 'en' ? 'If the payment app does not open automatically, go back and try again.' : '如未自动拉起支付应用,请返回上一页后重新发起支付。', + redirectRetryHint: + locale === 'en' + ? 'If the payment app does not open automatically, go back and try again.' + : '如未自动拉起支付应用,请返回上一页后重新发起支付。', notRedirectedPrefix: locale === 'en' ? 'Not redirected? Open ' : '未跳转?点击前往', goPaySuffix: locale === 'en' ? '' : '', gotoPrefix: locale === 'en' ? 'Open ' : '前往', @@ -327,8 +336,7 @@ export default function PaymentQRCode({ onStatusChange(data); } } - } catch { - } + } catch {} }, [orderId, onStatusChange, statusAccessToken]); useEffect(() => { @@ -372,8 +380,7 @@ export default function PaymentQRCode({ } else { await pollStatus(); } - } catch { - } + } catch {} }; const meta = getPaymentMeta(paymentType || 'alipay'); @@ -412,7 +419,9 @@ export default function PaymentQRCode({ {amount.toFixed(2)}
)} -
+
{expired ? t.expired : `${t.remaining}: ${timeLeft}`}
@@ -428,9 +437,7 @@ export default function PaymentQRCode({ dark ? 'border-slate-700' : 'border-gray-300', ].join(' ')} > -

- {t.initFailed} -

+

{t.initFailed}

) : !stripeLoaded ? (
@@ -440,10 +447,14 @@ export default function PaymentQRCode({
) : stripeError && !stripeLib ? ( -
{stripeError}
+
+ {stripeError} +
) : ( <>
{stripeSubmitting ? ( @@ -505,7 +514,10 @@ export default function PaymentQRCode({ ) : shouldAutoRedirect ? ( <>
-
+
{`${t.redirectingPrefix}${channelLabel}${t.redirectingSuffix}`} @@ -517,11 +529,11 @@ export default function PaymentQRCode({ className={`flex w-full items-center justify-center gap-2 rounded-lg py-3 font-medium text-white shadow-md ${meta.buttonClass}`} > {iconSrc && {channelLabel}} - {redirected ? `${t.notRedirectedPrefix}${channelLabel}` : `${t.gotoPrefix}${channelLabel}${t.gotoSuffix}`} + {redirected + ? `${t.notRedirectedPrefix}${channelLabel}` + : `${t.gotoPrefix}${channelLabel}${t.gotoSuffix}`} -

- {t.h5Hint} -

+

{t.h5Hint}

) : ( <> @@ -584,9 +596,7 @@ export default function PaymentQRCode({ onClick={handleCancel} className={[ 'flex-1 rounded-lg border py-2 text-sm', - dark - ? 'border-red-700 text-red-400 hover:bg-red-900/30' - : 'border-red-300 text-red-600 hover:bg-red-50', + dark ? 'border-red-700 text-red-400 hover:bg-red-900/30' : 'border-red-300 text-red-600 hover:bg-red-50', ].join(' ')} > {t.cancelOrder} diff --git a/src/components/admin/DailyChart.tsx b/src/components/admin/DailyChart.tsx index 4d7a7e7..997d180 100644 --- a/src/components/admin/DailyChart.tsx +++ b/src/components/admin/DailyChart.tsx @@ -85,7 +85,9 @@ export default function DailyChart({ data, dark, locale = 'zh' }: DailyChartProp

{chartTitle}

-

{emptyText}

+

+ {emptyText} +

); } @@ -121,7 +123,11 @@ export default function DailyChart({ data, dark, locale = 'zh' }: DailyChartProp tickLine={false} width={60} /> - } /> + + } + /> - {currency}{entry.totalAmount.toLocaleString()} + {currency} + {entry.totalAmount.toLocaleString()} {entry.orderCount} diff --git a/src/components/admin/OrderDetail.tsx b/src/components/admin/OrderDetail.tsx index e0c916c..317d482 100644 --- a/src/components/admin/OrderDetail.tsx +++ b/src/components/admin/OrderDetail.tsx @@ -49,77 +49,78 @@ interface OrderDetailProps { export default function OrderDetail({ order, onClose, dark, locale = 'zh' }: OrderDetailProps) { const currency = locale === 'en' ? '$' : '¥'; - const text = locale === 'en' - ? { - title: 'Order Details', - auditLogs: 'Audit Logs', - operator: 'Operator', - emptyLogs: 'No logs', - close: 'Close', - yes: 'Yes', - no: 'No', - orderId: 'Order ID', - userId: 'User ID', - userName: 'Username', - email: 'Email', - amount: 'Amount', - status: 'Status', - paymentSuccess: 'Payment Success', - rechargeSuccess: 'Recharge Success', - rechargeStatus: 'Recharge Status', - paymentChannel: 'Payment Channel', - provider: 'Provider', - rechargeCode: 'Recharge Code', - paymentTradeNo: 'Payment Trade No.', - clientIp: 'Client IP', - sourceHost: 'Source Host', - sourcePage: 'Source Page', - createdAt: 'Created At', - expiresAt: 'Expires At', - paidAt: 'Paid At', - completedAt: 'Completed At', - failedAt: 'Failed At', - failedReason: 'Failure Reason', - refundAmount: 'Refund Amount', - refundReason: 'Refund Reason', - refundAt: 'Refunded At', - forceRefund: 'Force Refund', - } - : { - title: '订单详情', - auditLogs: '审计日志', - operator: '操作者', - emptyLogs: '暂无日志', - close: '关闭', - yes: '是', - no: '否', - orderId: '订单号', - userId: '用户ID', - userName: '用户名', - email: '邮箱', - amount: '金额', - status: '状态', - paymentSuccess: '支付成功', - rechargeSuccess: '充值成功', - rechargeStatus: '充值状态', - paymentChannel: '支付渠道', - provider: '提供商', - rechargeCode: '充值码', - paymentTradeNo: '支付单号', - clientIp: '客户端IP', - sourceHost: '来源域名', - sourcePage: '来源页面', - createdAt: '创建时间', - expiresAt: '过期时间', - paidAt: '支付时间', - completedAt: '完成时间', - failedAt: '失败时间', - failedReason: '失败原因', - refundAmount: '退款金额', - refundReason: '退款原因', - refundAt: '退款时间', - forceRefund: '强制退款', - }; + const text = + locale === 'en' + ? { + title: 'Order Details', + auditLogs: 'Audit Logs', + operator: 'Operator', + emptyLogs: 'No logs', + close: 'Close', + yes: 'Yes', + no: 'No', + orderId: 'Order ID', + userId: 'User ID', + userName: 'Username', + email: 'Email', + amount: 'Amount', + status: 'Status', + paymentSuccess: 'Payment Success', + rechargeSuccess: 'Recharge Success', + rechargeStatus: 'Recharge Status', + paymentChannel: 'Payment Channel', + provider: 'Provider', + rechargeCode: 'Recharge Code', + paymentTradeNo: 'Payment Trade No.', + clientIp: 'Client IP', + sourceHost: 'Source Host', + sourcePage: 'Source Page', + createdAt: 'Created At', + expiresAt: 'Expires At', + paidAt: 'Paid At', + completedAt: 'Completed At', + failedAt: 'Failed At', + failedReason: 'Failure Reason', + refundAmount: 'Refund Amount', + refundReason: 'Refund Reason', + refundAt: 'Refunded At', + forceRefund: 'Force Refund', + } + : { + title: '订单详情', + auditLogs: '审计日志', + operator: '操作者', + emptyLogs: '暂无日志', + close: '关闭', + yes: '是', + no: '否', + orderId: '订单号', + userId: '用户ID', + userName: '用户名', + email: '邮箱', + amount: '金额', + status: '状态', + paymentSuccess: '支付成功', + rechargeSuccess: '充值成功', + rechargeStatus: '充值状态', + paymentChannel: '支付渠道', + provider: '提供商', + rechargeCode: '充值码', + paymentTradeNo: '支付单号', + clientIp: '客户端IP', + sourceHost: '来源域名', + sourcePage: '来源页面', + createdAt: '创建时间', + expiresAt: '过期时间', + paidAt: '支付时间', + completedAt: '完成时间', + failedAt: '失败时间', + failedReason: '失败原因', + refundAmount: '退款金额', + refundReason: '退款原因', + refundAt: '退款时间', + forceRefund: '强制退款', + }; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { diff --git a/src/components/admin/OrderTable.tsx b/src/components/admin/OrderTable.tsx index 465463c..5ce4c3b 100644 --- a/src/components/admin/OrderTable.tsx +++ b/src/components/admin/OrderTable.tsx @@ -32,37 +32,38 @@ interface OrderTableProps { export default function OrderTable({ orders, onRetry, onCancel, onViewDetail, dark, locale = 'zh' }: OrderTableProps) { const currency = locale === 'en' ? '$' : '¥'; - const text = locale === 'en' - ? { - orderId: 'Order ID', - userName: 'Username', - email: 'Email', - notes: 'Notes', - amount: 'Amount', - status: 'Status', - paymentMethod: 'Payment', - source: 'Source', - createdAt: 'Created At', - actions: 'Actions', - retry: 'Retry', - cancel: 'Cancel', - empty: 'No orders', - } - : { - orderId: '订单号', - userName: '用户名', - email: '邮箱', - notes: '备注', - amount: '金额', - status: '状态', - paymentMethod: '支付方式', - source: '来源', - createdAt: '创建时间', - actions: '操作', - retry: '重试', - cancel: '取消', - empty: '暂无订单', - }; + const text = + locale === 'en' + ? { + orderId: 'Order ID', + userName: 'Username', + email: 'Email', + notes: 'Notes', + amount: 'Amount', + status: 'Status', + paymentMethod: 'Payment', + source: 'Source', + createdAt: 'Created At', + actions: 'Actions', + retry: 'Retry', + cancel: 'Cancel', + empty: 'No orders', + } + : { + orderId: '订单号', + userName: '用户名', + email: '邮箱', + notes: '备注', + amount: '金额', + status: '状态', + paymentMethod: '支付方式', + source: '来源', + createdAt: '创建时间', + actions: '操作', + retry: '重试', + cancel: '取消', + empty: '暂无订单', + }; const thCls = `px-4 py-3 text-left text-xs font-medium uppercase ${dark ? 'text-slate-400' : 'text-gray-500'}`; const tdMuted = `whitespace-nowrap px-4 py-3 text-sm ${dark ? 'text-slate-400' : 'text-gray-500'}`; @@ -133,7 +134,8 @@ export default function OrderTable({ orders, onRetry, onCancel, onViewDetail, da {order.userEmail || '-'} {order.userNotes || '-'} - {currency}{order.amount.toFixed(2)} + {currency} + {order.amount.toFixed(2)} -

- {title} -

+

{title}

{data.map((method) => { const meta = getPaymentMeta(method.paymentType); @@ -56,7 +54,8 @@ export default function PaymentMethodChart({ data, dark, locale = 'zh' }: Paymen
{label} - {currency}{method.amount.toLocaleString()} · {method.percentage}% + {currency} + {method.amount.toLocaleString()} · {method.percentage}%
diff --git a/src/components/admin/RefundDialog.tsx b/src/components/admin/RefundDialog.tsx index 93ce537..25dd66a 100644 --- a/src/components/admin/RefundDialog.tsx +++ b/src/components/admin/RefundDialog.tsx @@ -74,10 +74,7 @@ export default function RefundDialog({ return (
e.stopPropagation()} >

{text.title}

@@ -90,7 +87,10 @@ export default function RefundDialog({
{text.amount}
-
{currency}{amount.toFixed(2)}
+
+ {currency} + {amount.toFixed(2)} +
{warning && ( diff --git a/src/lib/alipay/codec.ts b/src/lib/alipay/codec.ts index 8ab0c9a..5aa591a 100644 --- a/src/lib/alipay/codec.ts +++ b/src/lib/alipay/codec.ts @@ -4,7 +4,10 @@ const BODY_CHARSET_RE = /(?:^|&)charset=([^&]+)/i; function normalizeCharset(charset: string | null | undefined): string | null { if (!charset) return null; - const normalized = charset.trim().replace(/^['"]|['"]$/g, '').toLowerCase(); + const normalized = charset + .trim() + .replace(/^['"]|['"]$/g, '') + .toLowerCase(); if (!normalized) return null; switch (normalized) { @@ -67,9 +70,7 @@ export function decodeAlipayPayload(rawBody: string | Buffer, headers: Record, privateKey: string) /** 用支付宝公钥验证签名(回调验签:排除 sign 和 sign_type) */ export function verifySign(params: Record, alipayPublicKey: string, sign: string): boolean { const filtered = Object.entries(params) - .filter(([key, value]) => key !== 'sign' && key !== 'sign_type' && value !== '' && value !== undefined && value !== null) + .filter( + ([key, value]) => key !== 'sign' && key !== 'sign_type' && value !== '' && value !== undefined && value !== null, + ) .sort(([a], [b]) => a.localeCompare(b)); const signStr = filtered.map(([key, value]) => `${key}=${value}`).join('&'); diff --git a/src/lib/order/service.ts b/src/lib/order/service.ts index 442b5c8..ea05a4d 100644 --- a/src/lib/order/service.ts +++ b/src/lib/order/service.ts @@ -62,7 +62,11 @@ export async function createOrder(input: CreateOrderInput): Promise= MAX_PENDING_ORDERS) { throw new OrderError( 'TOO_MANY_PENDING', - message(locale, `待支付订单过多(最多 ${MAX_PENDING_ORDERS} 笔)`, `Too many pending orders (${MAX_PENDING_ORDERS})`), + message( + locale, + `待支付订单过多(最多 ${MAX_PENDING_ORDERS} 笔)`, + `Too many pending orders (${MAX_PENDING_ORDERS})`, + ), 429, ); } @@ -228,13 +232,21 @@ export async function createOrder(input: CreateOrderInput): Promise { function assertRetryAllowed(order: { status: string; paidAt: Date | null }, locale: Locale): void { if (!order.paidAt) { - throw new OrderError('INVALID_STATUS', message(locale, '订单未支付,不允许重试', 'Order is not paid, retry denied'), 400); + throw new OrderError( + 'INVALID_STATUS', + message(locale, '订单未支付,不允许重试', 'Order is not paid, retry denied'), + 400, + ); } if (isRefundStatus(order.status)) { - throw new OrderError('INVALID_STATUS', message(locale, '退款相关订单不允许重试', 'Refund-related order cannot retry'), 400); + throw new OrderError( + 'INVALID_STATUS', + message(locale, '退款相关订单不允许重试', 'Refund-related order cannot retry'), + 400, + ); } if (order.status === ORDER_STATUS.FAILED || order.status === ORDER_STATUS.PAID) { @@ -586,14 +603,22 @@ function assertRetryAllowed(order: { status: string; paidAt: Date | null }, loca } if (order.status === ORDER_STATUS.RECHARGING) { - throw new OrderError('CONFLICT', message(locale, '订单正在充值中,请稍后重试', 'Order is recharging, retry later'), 409); + throw new OrderError( + 'CONFLICT', + message(locale, '订单正在充值中,请稍后重试', 'Order is recharging, retry later'), + 409, + ); } if (order.status === ORDER_STATUS.COMPLETED) { throw new OrderError('INVALID_STATUS', message(locale, '订单已完成', 'Order already completed'), 400); } - throw new OrderError('INVALID_STATUS', message(locale, '仅已支付和失败订单允许重试', 'Only paid and failed orders can retry'), 400); + throw new OrderError( + 'INVALID_STATUS', + message(locale, '仅已支付和失败订单允许重试', 'Only paid and failed orders can retry'), + 400, + ); } export async function retryRecharge(orderId: string, locale: Locale = 'zh'): Promise { @@ -638,7 +663,11 @@ export async function retryRecharge(orderId: string, locale: Locale = 'zh'): Pro const derived = deriveOrderState(latest); if (derived.rechargeStatus === 'recharging' || latest.status === ORDER_STATUS.PAID) { - throw new OrderError('CONFLICT', message(locale, '订单正在充值中,请稍后重试', 'Order is recharging, retry later'), 409); + throw new OrderError( + 'CONFLICT', + message(locale, '订单正在充值中,请稍后重试', 'Order is recharging, retry later'), + 409, + ); } if (derived.rechargeStatus === 'success') { @@ -646,10 +675,18 @@ export async function retryRecharge(orderId: string, locale: Locale = 'zh'): Pro } if (isRefundStatus(latest.status)) { - throw new OrderError('INVALID_STATUS', message(locale, '退款相关订单不允许重试', 'Refund-related order cannot retry'), 400); + throw new OrderError( + 'INVALID_STATUS', + message(locale, '退款相关订单不允许重试', 'Refund-related order cannot retry'), + 400, + ); } - throw new OrderError('CONFLICT', message(locale, '订单状态已变更,请刷新后重试', 'Order status changed, refresh and retry'), 409); + throw new OrderError( + 'CONFLICT', + message(locale, '订单状态已变更,请刷新后重试', 'Order status changed, refresh and retry'), + 409, + ); } await prisma.auditLog.create({ @@ -682,7 +719,11 @@ export async function processRefund(input: RefundInput): Promise { const order = await prisma.order.findUnique({ where: { id: input.orderId } }); if (!order) throw new OrderError('NOT_FOUND', message(locale, '订单不存在', 'Order not found'), 404); if (order.status !== ORDER_STATUS.COMPLETED) { - throw new OrderError('INVALID_STATUS', message(locale, '仅已完成订单允许退款', 'Only completed orders can be refunded'), 400); + throw new OrderError( + 'INVALID_STATUS', + message(locale, '仅已完成订单允许退款', 'Only completed orders can be refunded'), + 400, + ); } const rechargeAmount = Number(order.amount); @@ -716,7 +757,11 @@ export async function processRefund(input: RefundInput): Promise { data: { status: ORDER_STATUS.REFUNDING }, }); if (lockResult.count === 0) { - throw new OrderError('CONFLICT', message(locale, '订单状态已变更,请刷新后重试', 'Order status changed, refresh and retry'), 409); + throw new OrderError( + 'CONFLICT', + message(locale, '订单状态已变更,请刷新后重试', 'Order status changed, refresh and retry'), + 409, + ); } try { diff --git a/src/lib/order/status.ts b/src/lib/order/status.ts index 30a9fe8..b87475e 100644 --- a/src/lib/order/status.ts +++ b/src/lib/order/status.ts @@ -101,7 +101,8 @@ export function getOrderDisplayState( label: '支付成功', color: 'text-amber-600', icon: '!', - message: '支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。', + message: + '支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。', }; } } diff --git a/src/lib/pay-utils.ts b/src/lib/pay-utils.ts index 6b349e0..902b70d 100644 --- a/src/lib/pay-utils.ts +++ b/src/lib/pay-utils.ts @@ -1,9 +1,4 @@ -import { - ORDER_STATUS, - PAYMENT_TYPE, - PAYMENT_PREFIX, - REDIRECT_PAYMENT_TYPES, -} from './constants'; +import { ORDER_STATUS, PAYMENT_TYPE, PAYMENT_PREFIX, REDIRECT_PAYMENT_TYPES } from './constants'; import type { Locale } from './locale'; export interface UserInfo { @@ -211,7 +206,10 @@ export function getPaymentTypeLabel(type: string, locale: Locale = 'zh'): string return locale === 'en' ? `${meta.label} (${meta.provider})` : `${meta.label}(${meta.provider})`; } -export function getPaymentDisplayInfo(type: string, locale: Locale = 'zh'): { channel: string; provider: string; sublabel?: string } { +export function getPaymentDisplayInfo( + type: string, + locale: Locale = 'zh', +): { channel: string; provider: string; sublabel?: string } { const meta = getPaymentText(type, locale); return { channel: meta.label, provider: meta.provider, sublabel: meta.sublabel }; } diff --git a/src/lib/sub2api/client.ts b/src/lib/sub2api/client.ts index 814a62e..1e1e8d8 100644 --- a/src/lib/sub2api/client.ts +++ b/src/lib/sub2api/client.ts @@ -125,12 +125,7 @@ export async function subtractBalance( } } -export async function addBalance( - userId: number, - amount: number, - notes: string, - idempotencyKey: string, -): Promise { +export async function addBalance(userId: number, amount: number, notes: string, idempotencyKey: string): Promise { const env = getEnv(); const response = await fetch(`${env.SUB2API_BASE_URL}/api/v1/admin/users/${userId}/balance`, { method: 'POST',