style: 统一代码格式 (prettier)
This commit is contained in:
@@ -190,9 +190,7 @@ describe('GET /pay/[orderId]', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await GET(
|
const response = await GET(
|
||||||
createRequest(
|
createRequest('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148'),
|
||||||
'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' }),
|
params: Promise.resolve({ orderId: 'order-001' }),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
vi.mock('@/lib/config', () => ({
|
vi.mock('@/lib/config', () => ({
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
vi.mock('@/lib/config', () => ({
|
vi.mock('@/lib/config', () => ({
|
||||||
|
|||||||
@@ -101,12 +101,7 @@ function shouldAutoRedirect(opts: {
|
|||||||
qrCode?: string | null;
|
qrCode?: string | null;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
return (
|
return !opts.expired && !isStripeType(opts.paymentType) && !!opts.payUrl && (opts.isMobile || !opts.qrCode);
|
||||||
!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 () => {
|
it('Mobile: uses H5 order, returns payUrl (no qrCode)', async () => {
|
||||||
mockWxpayCreateH5Order.mockResolvedValue(
|
mockWxpayCreateH5Order.mockResolvedValue('https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx123');
|
||||||
'https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx123',
|
|
||||||
);
|
|
||||||
|
|
||||||
const request: CreatePaymentRequest = {
|
const request: CreatePaymentRequest = {
|
||||||
orderId: 'order-wx-002',
|
orderId: 'order-wx-002',
|
||||||
|
|||||||
@@ -39,33 +39,34 @@ function DashboardContent() {
|
|||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
const isEmbedded = uiMode === 'embedded';
|
const isEmbedded = uiMode === 'embedded';
|
||||||
|
|
||||||
const text = locale === 'en'
|
const text =
|
||||||
? {
|
locale === 'en'
|
||||||
missingToken: 'Missing admin token',
|
? {
|
||||||
missingTokenHint: 'Please access the admin page from the Sub2API platform.',
|
missingToken: 'Missing admin token',
|
||||||
invalidToken: 'Invalid admin token',
|
missingTokenHint: 'Please access the admin page from the Sub2API platform.',
|
||||||
requestFailed: 'Request failed',
|
invalidToken: 'Invalid admin token',
|
||||||
loadFailed: 'Failed to load data',
|
requestFailed: 'Request failed',
|
||||||
title: 'Dashboard',
|
loadFailed: 'Failed to load data',
|
||||||
subtitle: 'Recharge order analytics and insights',
|
title: 'Dashboard',
|
||||||
daySuffix: 'd',
|
subtitle: 'Recharge order analytics and insights',
|
||||||
orders: 'Order Management',
|
daySuffix: 'd',
|
||||||
refresh: 'Refresh',
|
orders: 'Order Management',
|
||||||
loading: 'Loading...',
|
refresh: 'Refresh',
|
||||||
}
|
loading: 'Loading...',
|
||||||
: {
|
}
|
||||||
missingToken: '缺少管理员凭证',
|
: {
|
||||||
missingTokenHint: '请从 Sub2API 平台正确访问管理页面',
|
missingToken: '缺少管理员凭证',
|
||||||
invalidToken: '管理员凭证无效',
|
missingTokenHint: '请从 Sub2API 平台正确访问管理页面',
|
||||||
requestFailed: '请求失败',
|
invalidToken: '管理员凭证无效',
|
||||||
loadFailed: '加载数据失败',
|
requestFailed: '请求失败',
|
||||||
title: '数据概览',
|
loadFailed: '加载数据失败',
|
||||||
subtitle: '充值订单统计与分析',
|
title: '数据概览',
|
||||||
daySuffix: '天',
|
subtitle: '充值订单统计与分析',
|
||||||
orders: '订单管理',
|
daySuffix: '天',
|
||||||
refresh: '刷新',
|
orders: '订单管理',
|
||||||
loading: '加载中...',
|
refresh: '刷新',
|
||||||
};
|
loading: '加载中...',
|
||||||
|
};
|
||||||
|
|
||||||
const [days, setDays] = useState<number>(30);
|
const [days, setDays] = useState<number>(30);
|
||||||
const [data, setData] = useState<DashboardData | null>(null);
|
const [data, setData] = useState<DashboardData | null>(null);
|
||||||
@@ -138,7 +139,8 @@ function DashboardContent() {
|
|||||||
<>
|
<>
|
||||||
{DAYS_OPTIONS.map((d) => (
|
{DAYS_OPTIONS.map((d) => (
|
||||||
<button key={d} type="button" onClick={() => setDays(d)} className={days === d ? btnActive : btnBase}>
|
<button key={d} type="button" onClick={() => setDays(d)} className={days === d ? btnActive : btnBase}>
|
||||||
{d}{text.daySuffix}
|
{d}
|
||||||
|
{text.daySuffix}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<a href={`/admin?${navParams}`} className={btnBase}>
|
<a href={`/admin?${navParams}`} className={btnBase}>
|
||||||
@@ -162,7 +164,7 @@ function DashboardContent() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={`py-24 text-center ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.loading}</div>
|
<div className={`py-24 text-center ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.loading}</div>
|
||||||
) : data ? (
|
) : data ? (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<DashboardStats summary={data.summary} dark={isDark} locale={locale} />
|
<DashboardStats summary={data.summary} dark={isDark} locale={locale} />
|
||||||
@@ -190,9 +192,7 @@ function DashboardPageFallback() {
|
|||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense fallback={<DashboardPageFallback />}>
|
||||||
fallback={<DashboardPageFallback />}
|
|
||||||
>
|
|
||||||
<DashboardContent />
|
<DashboardContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,67 +52,68 @@ function AdminContent() {
|
|||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
const isEmbedded = uiMode === 'embedded';
|
const isEmbedded = uiMode === 'embedded';
|
||||||
|
|
||||||
const text = locale === 'en'
|
const text =
|
||||||
? {
|
locale === 'en'
|
||||||
missingToken: 'Missing admin token',
|
? {
|
||||||
missingTokenHint: 'Please access the admin page from the Sub2API platform.',
|
missingToken: 'Missing admin token',
|
||||||
invalidToken: 'Invalid admin token',
|
missingTokenHint: 'Please access the admin page from the Sub2API platform.',
|
||||||
requestFailed: 'Request failed',
|
invalidToken: 'Invalid admin token',
|
||||||
loadOrdersFailed: 'Failed to load orders',
|
requestFailed: 'Request failed',
|
||||||
retryConfirm: 'Retry recharge for this order?',
|
loadOrdersFailed: 'Failed to load orders',
|
||||||
retryFailed: 'Retry failed',
|
retryConfirm: 'Retry recharge for this order?',
|
||||||
retryRequestFailed: 'Retry request failed',
|
retryFailed: 'Retry failed',
|
||||||
cancelConfirm: 'Cancel this order?',
|
retryRequestFailed: 'Retry request failed',
|
||||||
cancelFailed: 'Cancel failed',
|
cancelConfirm: 'Cancel this order?',
|
||||||
cancelRequestFailed: 'Cancel request failed',
|
cancelFailed: 'Cancel failed',
|
||||||
loadDetailFailed: 'Failed to load order details',
|
cancelRequestFailed: 'Cancel request failed',
|
||||||
title: 'Order Management',
|
loadDetailFailed: 'Failed to load order details',
|
||||||
subtitle: 'View and manage all recharge orders',
|
title: 'Order Management',
|
||||||
dashboard: 'Dashboard',
|
subtitle: 'View and manage all recharge orders',
|
||||||
refresh: 'Refresh',
|
dashboard: 'Dashboard',
|
||||||
loading: 'Loading...',
|
refresh: 'Refresh',
|
||||||
statuses: {
|
loading: 'Loading...',
|
||||||
'': 'All',
|
statuses: {
|
||||||
PENDING: 'Pending',
|
'': 'All',
|
||||||
PAID: 'Paid',
|
PENDING: 'Pending',
|
||||||
RECHARGING: 'Recharging',
|
PAID: 'Paid',
|
||||||
COMPLETED: 'Completed',
|
RECHARGING: 'Recharging',
|
||||||
EXPIRED: 'Expired',
|
COMPLETED: 'Completed',
|
||||||
CANCELLED: 'Cancelled',
|
EXPIRED: 'Expired',
|
||||||
FAILED: 'Recharge failed',
|
CANCELLED: 'Cancelled',
|
||||||
REFUNDED: 'Refunded',
|
FAILED: 'Recharge failed',
|
||||||
},
|
REFUNDED: 'Refunded',
|
||||||
}
|
},
|
||||||
: {
|
}
|
||||||
missingToken: '缺少管理员凭证',
|
: {
|
||||||
missingTokenHint: '请从 Sub2API 平台正确访问管理页面',
|
missingToken: '缺少管理员凭证',
|
||||||
invalidToken: '管理员凭证无效',
|
missingTokenHint: '请从 Sub2API 平台正确访问管理页面',
|
||||||
requestFailed: '请求失败',
|
invalidToken: '管理员凭证无效',
|
||||||
loadOrdersFailed: '加载订单列表失败',
|
requestFailed: '请求失败',
|
||||||
retryConfirm: '确认重试充值?',
|
loadOrdersFailed: '加载订单列表失败',
|
||||||
retryFailed: '重试失败',
|
retryConfirm: '确认重试充值?',
|
||||||
retryRequestFailed: '重试请求失败',
|
retryFailed: '重试失败',
|
||||||
cancelConfirm: '确认取消该订单?',
|
retryRequestFailed: '重试请求失败',
|
||||||
cancelFailed: '取消失败',
|
cancelConfirm: '确认取消该订单?',
|
||||||
cancelRequestFailed: '取消请求失败',
|
cancelFailed: '取消失败',
|
||||||
loadDetailFailed: '加载订单详情失败',
|
cancelRequestFailed: '取消请求失败',
|
||||||
title: '订单管理',
|
loadDetailFailed: '加载订单详情失败',
|
||||||
subtitle: '查看和管理所有充值订单',
|
title: '订单管理',
|
||||||
dashboard: '数据概览',
|
subtitle: '查看和管理所有充值订单',
|
||||||
refresh: '刷新',
|
dashboard: '数据概览',
|
||||||
loading: '加载中...',
|
refresh: '刷新',
|
||||||
statuses: {
|
loading: '加载中...',
|
||||||
'': '全部',
|
statuses: {
|
||||||
PENDING: '待支付',
|
'': '全部',
|
||||||
PAID: '已支付',
|
PENDING: '待支付',
|
||||||
RECHARGING: '充值中',
|
PAID: '已支付',
|
||||||
COMPLETED: '已完成',
|
RECHARGING: '充值中',
|
||||||
EXPIRED: '已超时',
|
COMPLETED: '已完成',
|
||||||
CANCELLED: '已取消',
|
EXPIRED: '已超时',
|
||||||
FAILED: '充值失败',
|
CANCELLED: '已取消',
|
||||||
REFUNDED: '已退款',
|
FAILED: '充值失败',
|
||||||
},
|
REFUNDED: '已退款',
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const [orders, setOrders] = useState<AdminOrder[]>([]);
|
const [orders, setOrders] = useState<AdminOrder[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
@@ -321,7 +322,9 @@ function AdminContent() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Order Detail */}
|
{/* Order Detail */}
|
||||||
{detailOrder && <OrderDetail order={detailOrder} onClose={() => setDetailOrder(null)} dark={isDark} locale={locale} />}
|
{detailOrder && (
|
||||||
|
<OrderDetail order={detailOrder} onClose={() => setDetailOrder(null)} dark={isDark} locale={locale} />
|
||||||
|
)}
|
||||||
</PayPageLayout>
|
</PayPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -339,9 +342,7 @@ function AdminPageFallback() {
|
|||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense fallback={<AdminPageFallback />}>
|
||||||
fallback={<AdminPageFallback />}
|
|
||||||
>
|
|
||||||
<AdminContent />
|
<AdminContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const token = request.nextUrl.searchParams.get('token')?.trim();
|
const token = request.nextUrl.searchParams.get('token')?.trim();
|
||||||
if (!token) {
|
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 {
|
try {
|
||||||
@@ -28,7 +31,10 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tokenUser.id !== userId) {
|
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();
|
const env = getEnv();
|
||||||
@@ -77,9 +83,7 @@ export async function GET(request: NextRequest) {
|
|||||||
helpImageUrl: env.PAY_HELP_IMAGE_URL ?? null,
|
helpImageUrl: env.PAY_HELP_IMAGE_URL ?? null,
|
||||||
helpText: env.PAY_HELP_TEXT ?? null,
|
helpText: env.PAY_HELP_TEXT ?? null,
|
||||||
stripePublishableKey:
|
stripePublishableKey:
|
||||||
enabledTypes.includes('stripe') && env.STRIPE_PUBLISHABLE_KEY
|
enabledTypes.includes('stripe') && env.STRIPE_PUBLISHABLE_KEY ? env.STRIPE_PUBLISHABLE_KEY : null,
|
||||||
? env.STRIPE_PUBLISHABLE_KEY
|
|
||||||
: null,
|
|
||||||
sublabelOverrides: Object.keys(sublabelOverrides).length > 0 ? sublabelOverrides : 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 });
|
return NextResponse.json({ error: locale === 'en' ? 'User not found' : '用户不存在' }, { status: 404 });
|
||||||
}
|
}
|
||||||
console.error('Get user error:', error);
|
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 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,15 +22,11 @@ export async function POST(request: NextRequest) {
|
|||||||
return Response.json({ code: 'SUCCESS', message: '成功' });
|
return Response.json({ code: 'SUCCESS', message: '成功' });
|
||||||
}
|
}
|
||||||
const success = await handlePaymentNotify(notification, provider.name);
|
const success = await handlePaymentNotify(notification, provider.name);
|
||||||
return Response.json(
|
return Response.json(success ? { code: 'SUCCESS', message: '成功' } : { code: 'FAIL', message: '处理失败' }, {
|
||||||
success ? { code: 'SUCCESS', message: '成功' } : { code: 'FAIL', message: '处理失败' },
|
status: success ? 200 : 500,
|
||||||
{ status: success ? 200 : 500 },
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Wxpay notify error:', error);
|
console.error('Wxpay notify error:', error);
|
||||||
return Response.json(
|
return Response.json({ code: 'FAIL', message: '处理失败' }, { status: 500 });
|
||||||
{ code: 'FAIL', message: '处理失败' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,11 @@
|
|||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,16 @@ function OrdersContent() {
|
|||||||
|
|
||||||
const text = {
|
const text = {
|
||||||
missingAuth: pickLocaleText(locale, '缺少认证信息', 'Missing authentication information'),
|
missingAuth: pickLocaleText(locale, '缺少认证信息', 'Missing authentication information'),
|
||||||
visitOrders: pickLocaleText(locale, '请从 Sub2API 平台正确访问订单页面', 'Please open the orders page from Sub2API'),
|
visitOrders: pickLocaleText(
|
||||||
sessionExpired: pickLocaleText(locale, '登录态已失效,请从 Sub2API 重新进入支付页。', 'Session expired. Please re-enter from Sub2API.'),
|
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.'),
|
loadFailed: pickLocaleText(locale, '订单加载失败,请稍后重试。', 'Failed to load orders. Please try again later.'),
|
||||||
networkError: pickLocaleText(locale, '网络错误,请稍后重试。', 'Network error. Please try again later.'),
|
networkError: pickLocaleText(locale, '网络错误,请稍后重试。', 'Network error. Please try again later.'),
|
||||||
switchingMobileTab: pickLocaleText(locale, '正在切换到移动端订单 Tab...', 'Switching to mobile orders tab...'),
|
switchingMobileTab: pickLocaleText(locale, '正在切换到移动端订单 Tab...', 'Switching to mobile orders tab...'),
|
||||||
@@ -40,7 +48,11 @@ function OrdersContent() {
|
|||||||
backToPay: pickLocaleText(locale, '返回充值', 'Back to Top Up'),
|
backToPay: pickLocaleText(locale, '返回充值', 'Back to Top Up'),
|
||||||
loading: pickLocaleText(locale, '加载中...', 'Loading...'),
|
loading: pickLocaleText(locale, '加载中...', 'Loading...'),
|
||||||
userPrefix: pickLocaleText(locale, '用户', 'User'),
|
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);
|
const [isIframeContext, setIsIframeContext] = useState(true);
|
||||||
|
|||||||
@@ -156,8 +156,7 @@ function PayContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMoreOrders = async () => {
|
const loadMoreOrders = async () => {
|
||||||
@@ -203,7 +202,11 @@ function PayContent() {
|
|||||||
<div className="text-center text-red-500">
|
<div className="text-center text-red-500">
|
||||||
<p className="text-lg font-medium">{pickLocaleText(locale, '缺少认证信息', 'Missing authentication info')}</p>
|
<p className="text-lg font-medium">{pickLocaleText(locale, '缺少认证信息', 'Missing authentication info')}</p>
|
||||||
<p className="mt-2 text-sm text-gray-500">
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
{pickLocaleText(locale, '请从 Sub2API 平台正确访问充值页面', 'Please open the recharge page from the Sub2API platform')}
|
{pickLocaleText(
|
||||||
|
locale,
|
||||||
|
'请从 Sub2API 平台正确访问充值页面',
|
||||||
|
'Please open the recharge page from the Sub2API platform',
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,7 +219,11 @@ function PayContent() {
|
|||||||
<div className="text-center text-red-500">
|
<div className="text-center text-red-500">
|
||||||
<p className="text-lg font-medium">{pickLocaleText(locale, '用户不存在', 'User not found')}</p>
|
<p className="text-lg font-medium">{pickLocaleText(locale, '用户不存在', 'User not found')}</p>
|
||||||
<p className="mt-2 text-sm text-gray-500">
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
{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',
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,15 +279,33 @@ function PayContent() {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const codeMessages: Record<string, string> = {
|
const codeMessages: Record<string, string> = {
|
||||||
INVALID_TOKEN: pickLocaleText(locale, '认证已失效,请重新从平台进入充值页面', 'Authentication expired. Please re-enter the recharge page from the platform'),
|
INVALID_TOKEN: pickLocaleText(
|
||||||
USER_INACTIVE: pickLocaleText(locale, '账户已被禁用,无法充值,请联系管理员', 'This account is disabled and cannot be recharged. Please contact the administrator'),
|
locale,
|
||||||
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'),
|
'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,
|
DAILY_LIMIT_EXCEEDED: data.error,
|
||||||
METHOD_DAILY_LIMIT_EXCEEDED: data.error,
|
METHOD_DAILY_LIMIT_EXCEEDED: data.error,
|
||||||
PAYMENT_GATEWAY_ERROR: 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,11 +506,24 @@ function PayContent() {
|
|||||||
{pickLocaleText(locale, '支付说明', 'Payment Notes')}
|
{pickLocaleText(locale, '支付说明', 'Payment Notes')}
|
||||||
</div>
|
</div>
|
||||||
<ul className={['mt-2 space-y-1 text-sm', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
|
<ul className={['mt-2 space-y-1 text-sm', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
|
||||||
<li>{pickLocaleText(locale, '订单完成后会自动到账', 'Balance will be credited automatically after the order completes')}</li>
|
<li>
|
||||||
<li>{pickLocaleText(locale, '如需历史记录请查看「我的订单」', 'Check "My Orders" for payment history')}</li>
|
{pickLocaleText(
|
||||||
|
locale,
|
||||||
|
'订单完成后会自动到账',
|
||||||
|
'Balance will be credited automatically after the order completes',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{pickLocaleText(
|
||||||
|
locale,
|
||||||
|
'如需历史记录请查看「我的订单」',
|
||||||
|
'Check "My Orders" for payment history',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
{config.maxDailyAmount > 0 && (
|
{config.maxDailyAmount > 0 && (
|
||||||
<li>
|
<li>
|
||||||
{pickLocaleText(locale, '每日最大充值', 'Maximum daily recharge')} ¥{config.maxDailyAmount.toFixed(2)}
|
{pickLocaleText(locale, '每日最大充值', 'Maximum daily recharge')} ¥
|
||||||
|
{config.maxDailyAmount.toFixed(2)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -551,18 +589,17 @@ function PayContent() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{step === 'result' && orderResult && finalOrderState && (
|
||||||
{step === 'result' && orderResult && finalOrderState && (
|
<OrderStatus
|
||||||
<OrderStatus
|
orderId={orderResult.orderId}
|
||||||
orderId={orderResult.orderId}
|
order={finalOrderState}
|
||||||
order={finalOrderState}
|
statusAccessToken={orderResult.statusAccessToken}
|
||||||
statusAccessToken={orderResult.statusAccessToken}
|
onStateChange={setFinalOrderState}
|
||||||
onStateChange={setFinalOrderState}
|
onBack={handleBack}
|
||||||
onBack={handleBack}
|
dark={isDark}
|
||||||
dark={isDark}
|
locale={locale}
|
||||||
locale={locale}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{helpImageOpen && helpImageUrl && (
|
{helpImageOpen && helpImageUrl && (
|
||||||
<div
|
<div
|
||||||
@@ -594,9 +631,7 @@ function PayPageFallback() {
|
|||||||
|
|
||||||
export default function PayPage() {
|
export default function PayPage() {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense fallback={<PayPageFallback />}>
|
||||||
fallback={<PayPageFallback />}
|
|
||||||
>
|
|
||||||
<PayContent />
|
<PayContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
@@ -58,27 +57,60 @@ function closeCurrentWindow() {
|
|||||||
function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale, hasAccessToken: boolean) {
|
function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale, hasAccessToken: boolean) {
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return locale === 'en'
|
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) {
|
if (order.rechargeSuccess) {
|
||||||
return locale === 'en'
|
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: '余额已成功到账!' };
|
: { label: '充值成功', color: 'text-green-600', icon: '✓', message: '余额已成功到账!' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.paymentSuccess) {
|
if (order.paymentSuccess) {
|
||||||
if (order.rechargeStatus === 'paid_pending' || order.rechargeStatus === 'recharging') {
|
if (order.rechargeStatus === 'paid_pending' || order.rechargeStatus === 'recharging') {
|
||||||
return locale === 'en'
|
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: '支付成功,余额正在充值中...' };
|
: { label: '充值处理中', color: 'text-blue-600', icon: '⟳', message: '支付成功,余额正在充值中...' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.rechargeStatus === 'failed') {
|
if (order.rechargeStatus === 'failed') {
|
||||||
return locale === 'en'
|
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') {
|
if (order.status === 'EXPIRED') {
|
||||||
return locale === 'en'
|
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: '订单已超时,请重新充值。' };
|
: { label: '订单已超时', color: 'text-gray-500', icon: '⏰', message: '订单已超时,请重新充值。' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +261,7 @@ function ResultContent() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button type="button" onClick={goBack} className="mt-4 text-sm text-blue-600 underline hover:text-blue-700">
|
||||||
type="button"
|
|
||||||
onClick={goBack}
|
|
||||||
className="mt-4 text-sm text-blue-600 underline hover:text-blue-700"
|
|
||||||
>
|
|
||||||
{text.back}
|
{text.back}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,12 +19,20 @@ function StripePopupContent() {
|
|||||||
const text = {
|
const text = {
|
||||||
init: pickLocaleText(locale, '正在初始化...', 'Initializing...'),
|
init: pickLocaleText(locale, '正在初始化...', 'Initializing...'),
|
||||||
orderId: pickLocaleText(locale, '订单号', 'Order ID'),
|
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.'),
|
payFailed: pickLocaleText(locale, '支付失败,请重试', 'Payment failed. Please try again.'),
|
||||||
closeWindow: pickLocaleText(locale, '关闭窗口', 'Close window'),
|
closeWindow: pickLocaleText(locale, '关闭窗口', 'Close window'),
|
||||||
redirecting: pickLocaleText(locale, '正在跳转到支付页面...', 'Redirecting to payment page...'),
|
redirecting: pickLocaleText(locale, '正在跳转到支付页面...', 'Redirecting to payment page...'),
|
||||||
loadingForm: pickLocaleText(locale, '正在加载支付表单...', 'Loading payment form...'),
|
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'),
|
closeWindowManually: pickLocaleText(locale, '手动关闭窗口', 'Close window manually'),
|
||||||
processing: pickLocaleText(locale, '处理中...', 'Processing...'),
|
processing: pickLocaleText(locale, '处理中...', 'Processing...'),
|
||||||
payAmount: pickLocaleText(locale, `支付 ¥${amount.toFixed(2)}`, `Pay ¥${amount.toFixed(2)}`),
|
payAmount: pickLocaleText(locale, `支付 ¥${amount.toFixed(2)}`, `Pay ¥${amount.toFixed(2)}`),
|
||||||
@@ -191,11 +199,17 @@ function StripePopupContent() {
|
|||||||
{'¥'}
|
{'¥'}
|
||||||
{amount.toFixed(2)}
|
{amount.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.orderId}: {orderId}</p>
|
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||||
|
{text.orderId}: {orderId}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{stripeError ? (
|
{stripeError ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className={`rounded-lg border p-3 text-sm ${isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600'}`}>{stripeError}</div>
|
<div
|
||||||
|
className={`rounded-lg border p-3 text-sm ${isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600'}`}
|
||||||
|
>
|
||||||
|
{stripeError}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => window.close()}
|
onClick={() => window.close()}
|
||||||
@@ -207,9 +221,7 @@ function StripePopupContent() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-[#635bff] border-t-transparent" />
|
<div className="h-8 w-8 animate-spin rounded-full border-2 border-[#635bff] border-t-transparent" />
|
||||||
<span className={`ml-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
<span className={`ml-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.redirecting}</span>
|
||||||
{text.redirecting}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -227,7 +239,9 @@ function StripePopupContent() {
|
|||||||
{'¥'}
|
{'¥'}
|
||||||
{amount.toFixed(2)}
|
{amount.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.orderId}: {orderId}</p>
|
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||||
|
{text.orderId}: {orderId}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!stripeLoaded ? (
|
{!stripeLoaded ? (
|
||||||
@@ -238,9 +252,7 @@ function StripePopupContent() {
|
|||||||
) : stripeSuccess ? (
|
) : stripeSuccess ? (
|
||||||
<div className="py-6 text-center">
|
<div className="py-6 text-center">
|
||||||
<div className="text-5xl text-green-600">{'✓'}</div>
|
<div className="text-5xl text-green-600">{'✓'}</div>
|
||||||
<p className={`mt-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
<p className={`mt-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.successClosing}</p>
|
||||||
{text.successClosing}
|
|
||||||
</p>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => window.close()}
|
onClick={() => window.close()}
|
||||||
@@ -252,7 +264,11 @@ function StripePopupContent() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{stripeError && (
|
{stripeError && (
|
||||||
<div className={`rounded-lg border p-3 text-sm ${isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600'}`}>{stripeError}</div>
|
<div
|
||||||
|
className={`rounded-lg border p-3 text-sm ${isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600'}`}
|
||||||
|
>
|
||||||
|
{stripeError}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
ref={stripeContainerRef}
|
ref={stripeContainerRef}
|
||||||
@@ -264,9 +280,7 @@ function StripePopupContent() {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className={[
|
className={[
|
||||||
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
|
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
|
||||||
stripeSubmitting
|
stripeSubmitting ? 'bg-gray-400 cursor-not-allowed' : getPaymentMeta('stripe').buttonClass,
|
||||||
? 'bg-gray-400 cursor-not-allowed'
|
|
||||||
: getPaymentMeta('stripe').buttonClass,
|
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{stripeSubmitting ? (
|
{stripeSubmitting ? (
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
@@ -19,28 +18,61 @@ interface OrderStatusProps {
|
|||||||
function getStatusConfig(order: PublicOrderStatusSnapshot, locale: Locale) {
|
function getStatusConfig(order: PublicOrderStatusSnapshot, locale: Locale) {
|
||||||
if (order.rechargeSuccess) {
|
if (order.rechargeSuccess) {
|
||||||
return locale === 'en'
|
return locale === 'en'
|
||||||
? { label: 'Recharge Successful', color: 'text-green-600', icon: '✓', message: 'Your balance has been credited. Thank you for your payment.' }
|
? {
|
||||||
|
label: 'Recharge Successful',
|
||||||
|
color: 'text-green-600',
|
||||||
|
icon: '✓',
|
||||||
|
message: 'Your balance has been credited. Thank you for your payment.',
|
||||||
|
}
|
||||||
: { label: '充值成功', color: 'text-green-600', icon: '✓', message: '余额已到账,感谢您的充值!' };
|
: { label: '充值成功', color: 'text-green-600', icon: '✓', message: '余额已到账,感谢您的充值!' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.paymentSuccess) {
|
if (order.paymentSuccess) {
|
||||||
if (order.rechargeStatus === 'paid_pending' || order.rechargeStatus === 'recharging') {
|
if (order.rechargeStatus === 'paid_pending' || order.rechargeStatus === 'recharging') {
|
||||||
return locale === 'en'
|
return locale === 'en'
|
||||||
? { label: 'Recharging', color: 'text-blue-600', icon: '⟳', message: 'Payment received. Recharging your balance...' }
|
? {
|
||||||
|
label: 'Recharging',
|
||||||
|
color: 'text-blue-600',
|
||||||
|
icon: '⟳',
|
||||||
|
message: 'Payment received. Recharging your balance...',
|
||||||
|
}
|
||||||
: { label: '充值中', color: 'text-blue-600', icon: '⟳', message: '支付成功,正在充值余额中,请稍候...' };
|
: { label: '充值中', color: 'text-blue-600', icon: '⟳', message: '支付成功,正在充值余额中,请稍候...' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.rechargeStatus === 'failed') {
|
if (order.rechargeStatus === 'failed') {
|
||||||
return locale === 'en'
|
return locale === 'en'
|
||||||
? { label: 'Payment Successful', color: 'text-amber-600', icon: '!', message: 'Payment completed, but the balance top-up has not finished yet. The system may retry automatically. Please check the order list later or contact the administrator if it remains unresolved.' }
|
? {
|
||||||
: { label: '支付成功', color: 'text-amber-600', icon: '!', message: '支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。' };
|
label: 'Payment Successful',
|
||||||
|
color: 'text-amber-600',
|
||||||
|
icon: '!',
|
||||||
|
message:
|
||||||
|
'Payment completed, but the balance top-up has not finished yet. The system may retry automatically. Please check the order list later or contact the administrator if it remains unresolved.',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: '支付成功',
|
||||||
|
color: 'text-amber-600',
|
||||||
|
icon: '!',
|
||||||
|
message:
|
||||||
|
'支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.status === 'FAILED') {
|
if (order.status === 'FAILED') {
|
||||||
return locale === 'en'
|
return locale === 'en'
|
||||||
? { label: 'Payment Failed', color: 'text-red-600', icon: '✗', message: 'Payment was not completed. Please try again. If funds were deducted but not credited, contact the administrator.' }
|
? {
|
||||||
: { label: '支付失败', color: 'text-red-600', icon: '✗', message: '支付未完成,请重新发起支付;如已扣款未到账,请联系管理员处理。' };
|
label: 'Payment Failed',
|
||||||
|
color: 'text-red-600',
|
||||||
|
icon: '✗',
|
||||||
|
message:
|
||||||
|
'Payment was not completed. Please try again. If funds were deducted but not credited, contact the administrator.',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: '支付失败',
|
||||||
|
color: 'text-red-600',
|
||||||
|
icon: '✗',
|
||||||
|
message: '支付未完成,请重新发起支付;如已扣款未到账,请联系管理员处理。',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.status === 'PENDING') {
|
if (order.status === 'PENDING') {
|
||||||
@@ -51,7 +83,12 @@ function getStatusConfig(order: PublicOrderStatusSnapshot, locale: Locale) {
|
|||||||
|
|
||||||
if (order.status === 'EXPIRED') {
|
if (order.status === 'EXPIRED') {
|
||||||
return locale === 'en'
|
return locale === 'en'
|
||||||
? { label: 'Order Expired', color: 'text-gray-500', icon: '⏰', message: 'This order has expired. Please create a new one.' }
|
? {
|
||||||
|
label: 'Order Expired',
|
||||||
|
color: 'text-gray-500',
|
||||||
|
icon: '⏰',
|
||||||
|
message: 'This order has expired. Please create a new one.',
|
||||||
|
}
|
||||||
: { label: '订单超时', color: 'text-gray-500', icon: '⏰', message: '订单已超时,请重新创建订单。' };
|
: { label: '订单超时', color: 'text-gray-500', icon: '⏰', message: '订单已超时,请重新创建订单。' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +99,12 @@ function getStatusConfig(order: PublicOrderStatusSnapshot, locale: Locale) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return locale === 'en'
|
return locale === 'en'
|
||||||
? { label: 'Payment Error', color: 'text-red-600', icon: '✗', message: 'Payment status is abnormal. Please contact the administrator.' }
|
? {
|
||||||
|
label: 'Payment Error',
|
||||||
|
color: 'text-red-600',
|
||||||
|
icon: '✗',
|
||||||
|
message: 'Payment status is abnormal. Please contact the administrator.',
|
||||||
|
}
|
||||||
: { label: '支付异常', color: 'text-red-600', icon: '✗', message: '支付状态异常,请联系管理员处理。' };
|
: { label: '支付异常', color: 'text-red-600', icon: '✗', message: '支付状态异常,请联系管理员处理。' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,8 +142,7 @@ export default function OrderStatus({
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setCurrentOrder(nextOrder);
|
setCurrentOrder(nextOrder);
|
||||||
onStateChangeRef.current?.(nextOrder);
|
onStateChangeRef.current?.(nextOrder);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
refreshOrder();
|
refreshOrder();
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { Locale } from '@/lib/locale';
|
import type { Locale } from '@/lib/locale';
|
||||||
import { formatStatus, formatCreatedAt, getStatusBadgeClass, getPaymentDisplayInfo, type MyOrder } from '@/lib/pay-utils';
|
import {
|
||||||
|
formatStatus,
|
||||||
|
formatCreatedAt,
|
||||||
|
getStatusBadgeClass,
|
||||||
|
getPaymentDisplayInfo,
|
||||||
|
type MyOrder,
|
||||||
|
} from '@/lib/pay-utils';
|
||||||
|
|
||||||
interface OrderTableProps {
|
interface OrderTableProps {
|
||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
@@ -98,7 +104,9 @@ export default function OrderTable({ isDark, locale, loading, error, orders }: O
|
|||||||
{formatStatus(order.status, locale)}
|
{formatStatus(order.status, locale)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>{formatCreatedAt(order.createdAt, locale)}</div>
|
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>
|
||||||
|
{formatCreatedAt(order.createdAt, locale)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -223,12 +223,15 @@ export default function PaymentForm({
|
|||||||
!isValid &&
|
!isValid &&
|
||||||
(() => {
|
(() => {
|
||||||
const num = parseFloat(customAmount);
|
const num = parseFloat(customAmount);
|
||||||
let msg = locale === 'en'
|
let msg =
|
||||||
? 'Amount must be within range and support up to 2 decimal places'
|
locale === 'en'
|
||||||
: '金额需在范围内,且最多支持 2 位小数(精确到分)';
|
? 'Amount must be within range and support up to 2 decimal places'
|
||||||
|
: '金额需在范围内,且最多支持 2 位小数(精确到分)';
|
||||||
if (!isNaN(num)) {
|
if (!isNaN(num)) {
|
||||||
if (num < minAmount) msg = locale === 'en' ? `Minimum per transaction: ¥${minAmount}` : `单笔最低充值 ¥${minAmount}`;
|
if (num < minAmount)
|
||||||
else if (num > effectiveMax) msg = locale === 'en' ? `Maximum per transaction: ¥${effectiveMax}` : `单笔最高充值 ¥${effectiveMax}`;
|
msg = locale === 'en' ? `Minimum per transaction: ¥${minAmount}` : `单笔最低充值 ¥${minAmount}`;
|
||||||
|
else if (num > effectiveMax)
|
||||||
|
msg = locale === 'en' ? `Maximum per transaction: ¥${effectiveMax}` : `单笔最高充值 ¥${effectiveMax}`;
|
||||||
}
|
}
|
||||||
return <div className={['text-xs', dark ? 'text-amber-300' : 'text-amber-700'].join(' ')}>{msg}</div>;
|
return <div className={['text-xs', dark ? 'text-amber-300' : 'text-amber-700'].join(' ')}>{msg}</div>;
|
||||||
})()}
|
})()}
|
||||||
@@ -252,7 +255,13 @@ export default function PaymentForm({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={isUnavailable}
|
disabled={isUnavailable}
|
||||||
onClick={() => !isUnavailable && setPaymentType(type)}
|
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={[
|
className={[
|
||||||
'relative flex h-[58px] flex-col items-center justify-center rounded-lg border px-3 transition-all sm:flex-1',
|
'relative flex h-[58px] flex-col items-center justify-center rounded-lg border px-3 transition-all sm:flex-1',
|
||||||
isUnavailable
|
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-slate-700 bg-slate-800/50 opacity-50'
|
||||||
: 'cursor-not-allowed border-gray-200 bg-gray-50 opacity-50'
|
: 'cursor-not-allowed border-gray-200 bg-gray-50 opacity-50'
|
||||||
: isSelected
|
: 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
|
: dark
|
||||||
? 'border-slate-700 bg-slate-900 text-slate-200 hover:border-slate-500'
|
? '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',
|
: 'border-gray-300 bg-white text-slate-700 hover:border-gray-400',
|
||||||
@@ -271,7 +280,9 @@ export default function PaymentForm({
|
|||||||
<span className="flex flex-col items-start leading-none">
|
<span className="flex flex-col items-start leading-none">
|
||||||
<span className="text-xl font-semibold tracking-tight">{displayInfo.channel || type}</span>
|
<span className="text-xl font-semibold tracking-tight">{displayInfo.channel || type}</span>
|
||||||
{isUnavailable ? (
|
{isUnavailable ? (
|
||||||
<span className="text-[10px] tracking-wide text-red-400">{locale === 'en' ? 'Daily limit reached' : '今日额度已满'}</span>
|
<span className="text-[10px] tracking-wide text-red-400">
|
||||||
|
{locale === 'en' ? 'Daily limit reached' : '今日额度已满'}
|
||||||
|
</span>
|
||||||
) : displayInfo.sublabel ? (
|
) : displayInfo.sublabel ? (
|
||||||
<span
|
<span
|
||||||
className={`text-[10px] tracking-wide ${dark ? (isSelected ? 'text-slate-300' : 'text-slate-400') : 'text-slate-600'}`}
|
className={`text-[10px] tracking-wide ${dark ? (isSelected ? 'text-slate-300' : 'text-slate-400') : 'text-slate-600'}`}
|
||||||
@@ -292,7 +303,7 @@ export default function PaymentForm({
|
|||||||
return (
|
return (
|
||||||
<p className={['mt-2 text-xs', dark ? 'text-amber-300' : 'text-amber-600'].join(' ')}>
|
<p className={['mt-2 text-xs', dark ? 'text-amber-300' : 'text-amber-600'].join(' ')}>
|
||||||
{locale === 'en'
|
{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."
|
||||||
: '所选支付方式今日额度已满,请切换到其他支付方式'}
|
: '所选支付方式今日额度已满,请切换到其他支付方式'}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
@@ -331,9 +342,7 @@ export default function PaymentForm({
|
|||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
'rounded-lg border p-3 text-sm',
|
'rounded-lg border p-3 text-sm',
|
||||||
dark
|
dark ? 'border-amber-700 bg-amber-900/30 text-amber-300' : 'border-amber-200 bg-amber-50 text-amber-700',
|
||||||
? 'border-amber-700 bg-amber-900/30 text-amber-300'
|
|
||||||
: 'border-amber-200 bg-amber-50 text-amber-700',
|
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{locale === 'en'
|
{locale === 'en'
|
||||||
|
|||||||
@@ -4,12 +4,7 @@ import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
|||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import type { Locale } from '@/lib/locale';
|
import type { Locale } from '@/lib/locale';
|
||||||
import type { PublicOrderStatusSnapshot } from '@/lib/order/status';
|
import type { PublicOrderStatusSnapshot } from '@/lib/order/status';
|
||||||
import {
|
import { isStripeType, getPaymentMeta, getPaymentIconSrc, getPaymentChannelLabel } from '@/lib/pay-utils';
|
||||||
isStripeType,
|
|
||||||
getPaymentMeta,
|
|
||||||
getPaymentIconSrc,
|
|
||||||
getPaymentChannelLabel,
|
|
||||||
} from '@/lib/pay-utils';
|
|
||||||
import { buildOrderStatusUrl } from '@/lib/order/status-url';
|
import { buildOrderStatusUrl } from '@/lib/order/status-url';
|
||||||
import { TERMINAL_STATUSES } from '@/lib/constants';
|
import { TERMINAL_STATUSES } from '@/lib/constants';
|
||||||
|
|
||||||
@@ -84,24 +79,38 @@ export default function PaymentQRCode({
|
|||||||
scanPay: locale === 'en' ? 'Please scan with your payment app' : '请使用支付应用扫码支付',
|
scanPay: locale === 'en' ? 'Please scan with your payment app' : '请使用支付应用扫码支付',
|
||||||
back: locale === 'en' ? 'Back' : '返回',
|
back: locale === 'en' ? 'Back' : '返回',
|
||||||
cancelOrder: locale === 'en' ? 'Cancel Order' : '取消订单',
|
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' : '订单已支付',
|
paid: locale === 'en' ? 'Order Paid' : '订单已支付',
|
||||||
paidCancelBlocked:
|
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' : '返回充值',
|
backToRecharge: locale === 'en' ? 'Back to Recharge' : '返回充值',
|
||||||
credited: locale === 'en' ? 'Credited ¥' : '到账 ¥',
|
credited: locale === 'en' ? 'Credited ¥' : '到账 ¥',
|
||||||
stripeLoadFailed: locale === 'en' ? 'Failed to load payment component. Please refresh and try again.' : '支付组件加载失败,请刷新页面重试',
|
stripeLoadFailed:
|
||||||
initFailed: locale === 'en' ? 'Payment initialization failed. Please go back and try again.' : '支付初始化失败,请返回重试',
|
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...' : '正在加载支付表单...',
|
loadingForm: locale === 'en' ? 'Loading payment form...' : '正在加载支付表单...',
|
||||||
payFailed: locale === 'en' ? 'Payment failed. Please try again.' : '支付失败,请重试',
|
payFailed: locale === 'en' ? 'Payment failed. Please try again.' : '支付失败,请重试',
|
||||||
successProcessing: locale === 'en' ? 'Payment successful, processing your order...' : '支付成功,正在处理订单...',
|
successProcessing: locale === 'en' ? 'Payment successful, processing your order...' : '支付成功,正在处理订单...',
|
||||||
processing: locale === 'en' ? 'Processing...' : '处理中...',
|
processing: locale === 'en' ? 'Processing...' : '处理中...',
|
||||||
payNow: locale === 'en' ? 'Pay' : '支付',
|
payNow: locale === 'en' ? 'Pay' : '支付',
|
||||||
popupBlocked:
|
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 ' : '正在跳转到',
|
redirectingPrefix: locale === 'en' ? 'Redirecting to ' : '正在跳转到',
|
||||||
redirectingSuffix: locale === 'en' ? '...' : '...',
|
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 ' : '未跳转?点击前往',
|
notRedirectedPrefix: locale === 'en' ? 'Not redirected? Open ' : '未跳转?点击前往',
|
||||||
goPaySuffix: locale === 'en' ? '' : '',
|
goPaySuffix: locale === 'en' ? '' : '',
|
||||||
gotoPrefix: locale === 'en' ? 'Open ' : '前往',
|
gotoPrefix: locale === 'en' ? 'Open ' : '前往',
|
||||||
@@ -327,8 +336,7 @@ export default function PaymentQRCode({
|
|||||||
onStatusChange(data);
|
onStatusChange(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
}, [orderId, onStatusChange, statusAccessToken]);
|
}, [orderId, onStatusChange, statusAccessToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -372,8 +380,7 @@ export default function PaymentQRCode({
|
|||||||
} else {
|
} else {
|
||||||
await pollStatus();
|
await pollStatus();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const meta = getPaymentMeta(paymentType || 'alipay');
|
const meta = getPaymentMeta(paymentType || 'alipay');
|
||||||
@@ -412,7 +419,9 @@ export default function PaymentQRCode({
|
|||||||
{amount.toFixed(2)}
|
{amount.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={`mt-1 text-sm ${expired ? 'text-red-500' : !expired && timeLeftSeconds <= 60 ? 'text-red-500 animate-pulse' : dark ? 'text-slate-400' : 'text-gray-500'}`}>
|
<div
|
||||||
|
className={`mt-1 text-sm ${expired ? 'text-red-500' : !expired && timeLeftSeconds <= 60 ? 'text-red-500 animate-pulse' : dark ? 'text-slate-400' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
{expired ? t.expired : `${t.remaining}: ${timeLeft}`}
|
{expired ? t.expired : `${t.remaining}: ${timeLeft}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,9 +437,7 @@ export default function PaymentQRCode({
|
|||||||
dark ? 'border-slate-700' : 'border-gray-300',
|
dark ? 'border-slate-700' : 'border-gray-300',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<p className={['text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
|
<p className={['text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>{t.initFailed}</p>
|
||||||
{t.initFailed}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : !stripeLoaded ? (
|
) : !stripeLoaded ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
@@ -440,10 +447,14 @@ export default function PaymentQRCode({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : stripeError && !stripeLib ? (
|
) : stripeError && !stripeLib ? (
|
||||||
<div className={[
|
<div
|
||||||
'rounded-lg border p-3 text-sm',
|
className={[
|
||||||
dark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600',
|
'rounded-lg border p-3 text-sm',
|
||||||
].join(' ')}>{stripeError}</div>
|
dark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{stripeError}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -472,9 +483,7 @@ export default function PaymentQRCode({
|
|||||||
onClick={handleStripeSubmit}
|
onClick={handleStripeSubmit}
|
||||||
className={[
|
className={[
|
||||||
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
|
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
|
||||||
stripeSubmitting
|
stripeSubmitting ? 'cursor-not-allowed bg-gray-400' : meta.buttonClass,
|
||||||
? 'cursor-not-allowed bg-gray-400'
|
|
||||||
: meta.buttonClass,
|
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{stripeSubmitting ? (
|
{stripeSubmitting ? (
|
||||||
@@ -505,7 +514,10 @@ export default function PaymentQRCode({
|
|||||||
) : shouldAutoRedirect ? (
|
) : shouldAutoRedirect ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-center py-6">
|
<div className="flex items-center justify-center py-6">
|
||||||
<div className={`h-8 w-8 animate-spin rounded-full border-2 border-t-transparent`} style={{ borderColor: meta.color, borderTopColor: 'transparent' }} />
|
<div
|
||||||
|
className={`h-8 w-8 animate-spin rounded-full border-2 border-t-transparent`}
|
||||||
|
style={{ borderColor: meta.color, borderTopColor: 'transparent' }}
|
||||||
|
/>
|
||||||
<span className={['ml-3 text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
|
<span className={['ml-3 text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
|
||||||
{`${t.redirectingPrefix}${channelLabel}${t.redirectingSuffix}`}
|
{`${t.redirectingPrefix}${channelLabel}${t.redirectingSuffix}`}
|
||||||
</span>
|
</span>
|
||||||
@@ -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}`}
|
className={`flex w-full items-center justify-center gap-2 rounded-lg py-3 font-medium text-white shadow-md ${meta.buttonClass}`}
|
||||||
>
|
>
|
||||||
{iconSrc && <img src={iconSrc} alt={channelLabel} className="h-5 w-5 brightness-0 invert" />}
|
{iconSrc && <img src={iconSrc} alt={channelLabel} className="h-5 w-5 brightness-0 invert" />}
|
||||||
{redirected ? `${t.notRedirectedPrefix}${channelLabel}` : `${t.gotoPrefix}${channelLabel}${t.gotoSuffix}`}
|
{redirected
|
||||||
|
? `${t.notRedirectedPrefix}${channelLabel}`
|
||||||
|
: `${t.gotoPrefix}${channelLabel}${t.gotoSuffix}`}
|
||||||
</a>
|
</a>
|
||||||
<p className={['text-center text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
|
<p className={['text-center text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>{t.h5Hint}</p>
|
||||||
{t.h5Hint}
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -584,9 +596,7 @@ export default function PaymentQRCode({
|
|||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
className={[
|
className={[
|
||||||
'flex-1 rounded-lg border py-2 text-sm',
|
'flex-1 rounded-lg border py-2 text-sm',
|
||||||
dark
|
dark ? 'border-red-700 text-red-400 hover:bg-red-900/30' : 'border-red-300 text-red-600 hover:bg-red-50',
|
||||||
? 'border-red-700 text-red-400 hover:bg-red-900/30'
|
|
||||||
: 'border-red-300 text-red-600 hover:bg-red-50',
|
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{t.cancelOrder}
|
{t.cancelOrder}
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ export default function DailyChart({ data, dark, locale = 'zh' }: DailyChartProp
|
|||||||
<h3 className={['mb-4 text-sm font-semibold', dark ? 'text-slate-200' : 'text-slate-800'].join(' ')}>
|
<h3 className={['mb-4 text-sm font-semibold', dark ? 'text-slate-200' : 'text-slate-800'].join(' ')}>
|
||||||
{chartTitle}
|
{chartTitle}
|
||||||
</h3>
|
</h3>
|
||||||
<p className={['text-center text-sm py-16', dark ? 'text-slate-500' : 'text-gray-400'].join(' ')}>{emptyText}</p>
|
<p className={['text-center text-sm py-16', dark ? 'text-slate-500' : 'text-gray-400'].join(' ')}>
|
||||||
|
{emptyText}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -121,7 +123,11 @@ export default function DailyChart({ data, dark, locale = 'zh' }: DailyChartProp
|
|||||||
tickLine={false}
|
tickLine={false}
|
||||||
width={60}
|
width={60}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={<CustomTooltip dark={dark} currency={currency} amountLabel={amountLabel} countLabel={countLabel} />} />
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<CustomTooltip dark={dark} currency={currency} amountLabel={amountLabel} countLabel={countLabel} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="amount"
|
dataKey="amount"
|
||||||
|
|||||||
@@ -18,9 +18,20 @@ interface DashboardStatsProps {
|
|||||||
export default function DashboardStats({ summary, dark, locale = 'zh' }: DashboardStatsProps) {
|
export default function DashboardStats({ summary, dark, locale = 'zh' }: DashboardStatsProps) {
|
||||||
const currency = locale === 'en' ? '$' : '¥';
|
const currency = locale === 'en' ? '$' : '¥';
|
||||||
const cards = [
|
const cards = [
|
||||||
{ label: locale === 'en' ? 'Today Recharge' : '今日充值', value: `${currency}${summary.today.amount.toLocaleString()}`, accent: true },
|
{
|
||||||
{ label: locale === 'en' ? 'Today Orders' : '今日订单', value: `${summary.today.paidCount}/${summary.today.orderCount}` },
|
label: locale === 'en' ? 'Today Recharge' : '今日充值',
|
||||||
{ label: locale === 'en' ? 'Total Recharge' : '累计充值', value: `${currency}${summary.total.amount.toLocaleString()}`, accent: true },
|
value: `${currency}${summary.today.amount.toLocaleString()}`,
|
||||||
|
accent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: locale === 'en' ? 'Today Orders' : '今日订单',
|
||||||
|
value: `${summary.today.paidCount}/${summary.today.orderCount}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: locale === 'en' ? 'Total Recharge' : '累计充值',
|
||||||
|
value: `${currency}${summary.total.amount.toLocaleString()}`,
|
||||||
|
accent: true,
|
||||||
|
},
|
||||||
{ label: locale === 'en' ? 'Paid Orders' : '累计订单', value: String(summary.total.paidCount) },
|
{ label: locale === 'en' ? 'Paid Orders' : '累计订单', value: String(summary.total.paidCount) },
|
||||||
{ label: locale === 'en' ? 'Success Rate' : '成功率', value: `${summary.successRate}%` },
|
{ label: locale === 'en' ? 'Success Rate' : '成功率', value: `${summary.successRate}%` },
|
||||||
{ label: locale === 'en' ? 'Average Amount' : '平均充值', value: `${currency}${summary.avgAmount.toFixed(2)}` },
|
{ label: locale === 'en' ? 'Average Amount' : '平均充值', value: `${currency}${summary.avgAmount.toFixed(2)}` },
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ export default function Leaderboard({ data, dark, locale = 'zh' }: LeaderboardPr
|
|||||||
<td
|
<td
|
||||||
className={`whitespace-nowrap px-4 py-3 text-sm font-medium ${dark ? 'text-slate-200' : 'text-slate-900'}`}
|
className={`whitespace-nowrap px-4 py-3 text-sm font-medium ${dark ? 'text-slate-200' : 'text-slate-900'}`}
|
||||||
>
|
>
|
||||||
{currency}{entry.totalAmount.toLocaleString()}
|
{currency}
|
||||||
|
{entry.totalAmount.toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
<td className={tdMuted}>{entry.orderCount}</td>
|
<td className={tdMuted}>{entry.orderCount}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -49,77 +49,78 @@ interface OrderDetailProps {
|
|||||||
|
|
||||||
export default function OrderDetail({ order, onClose, dark, locale = 'zh' }: OrderDetailProps) {
|
export default function OrderDetail({ order, onClose, dark, locale = 'zh' }: OrderDetailProps) {
|
||||||
const currency = locale === 'en' ? '$' : '¥';
|
const currency = locale === 'en' ? '$' : '¥';
|
||||||
const text = locale === 'en'
|
const text =
|
||||||
? {
|
locale === 'en'
|
||||||
title: 'Order Details',
|
? {
|
||||||
auditLogs: 'Audit Logs',
|
title: 'Order Details',
|
||||||
operator: 'Operator',
|
auditLogs: 'Audit Logs',
|
||||||
emptyLogs: 'No logs',
|
operator: 'Operator',
|
||||||
close: 'Close',
|
emptyLogs: 'No logs',
|
||||||
yes: 'Yes',
|
close: 'Close',
|
||||||
no: 'No',
|
yes: 'Yes',
|
||||||
orderId: 'Order ID',
|
no: 'No',
|
||||||
userId: 'User ID',
|
orderId: 'Order ID',
|
||||||
userName: 'Username',
|
userId: 'User ID',
|
||||||
email: 'Email',
|
userName: 'Username',
|
||||||
amount: 'Amount',
|
email: 'Email',
|
||||||
status: 'Status',
|
amount: 'Amount',
|
||||||
paymentSuccess: 'Payment Success',
|
status: 'Status',
|
||||||
rechargeSuccess: 'Recharge Success',
|
paymentSuccess: 'Payment Success',
|
||||||
rechargeStatus: 'Recharge Status',
|
rechargeSuccess: 'Recharge Success',
|
||||||
paymentChannel: 'Payment Channel',
|
rechargeStatus: 'Recharge Status',
|
||||||
provider: 'Provider',
|
paymentChannel: 'Payment Channel',
|
||||||
rechargeCode: 'Recharge Code',
|
provider: 'Provider',
|
||||||
paymentTradeNo: 'Payment Trade No.',
|
rechargeCode: 'Recharge Code',
|
||||||
clientIp: 'Client IP',
|
paymentTradeNo: 'Payment Trade No.',
|
||||||
sourceHost: 'Source Host',
|
clientIp: 'Client IP',
|
||||||
sourcePage: 'Source Page',
|
sourceHost: 'Source Host',
|
||||||
createdAt: 'Created At',
|
sourcePage: 'Source Page',
|
||||||
expiresAt: 'Expires At',
|
createdAt: 'Created At',
|
||||||
paidAt: 'Paid At',
|
expiresAt: 'Expires At',
|
||||||
completedAt: 'Completed At',
|
paidAt: 'Paid At',
|
||||||
failedAt: 'Failed At',
|
completedAt: 'Completed At',
|
||||||
failedReason: 'Failure Reason',
|
failedAt: 'Failed At',
|
||||||
refundAmount: 'Refund Amount',
|
failedReason: 'Failure Reason',
|
||||||
refundReason: 'Refund Reason',
|
refundAmount: 'Refund Amount',
|
||||||
refundAt: 'Refunded At',
|
refundReason: 'Refund Reason',
|
||||||
forceRefund: 'Force Refund',
|
refundAt: 'Refunded At',
|
||||||
}
|
forceRefund: 'Force Refund',
|
||||||
: {
|
}
|
||||||
title: '订单详情',
|
: {
|
||||||
auditLogs: '审计日志',
|
title: '订单详情',
|
||||||
operator: '操作者',
|
auditLogs: '审计日志',
|
||||||
emptyLogs: '暂无日志',
|
operator: '操作者',
|
||||||
close: '关闭',
|
emptyLogs: '暂无日志',
|
||||||
yes: '是',
|
close: '关闭',
|
||||||
no: '否',
|
yes: '是',
|
||||||
orderId: '订单号',
|
no: '否',
|
||||||
userId: '用户ID',
|
orderId: '订单号',
|
||||||
userName: '用户名',
|
userId: '用户ID',
|
||||||
email: '邮箱',
|
userName: '用户名',
|
||||||
amount: '金额',
|
email: '邮箱',
|
||||||
status: '状态',
|
amount: '金额',
|
||||||
paymentSuccess: '支付成功',
|
status: '状态',
|
||||||
rechargeSuccess: '充值成功',
|
paymentSuccess: '支付成功',
|
||||||
rechargeStatus: '充值状态',
|
rechargeSuccess: '充值成功',
|
||||||
paymentChannel: '支付渠道',
|
rechargeStatus: '充值状态',
|
||||||
provider: '提供商',
|
paymentChannel: '支付渠道',
|
||||||
rechargeCode: '充值码',
|
provider: '提供商',
|
||||||
paymentTradeNo: '支付单号',
|
rechargeCode: '充值码',
|
||||||
clientIp: '客户端IP',
|
paymentTradeNo: '支付单号',
|
||||||
sourceHost: '来源域名',
|
clientIp: '客户端IP',
|
||||||
sourcePage: '来源页面',
|
sourceHost: '来源域名',
|
||||||
createdAt: '创建时间',
|
sourcePage: '来源页面',
|
||||||
expiresAt: '过期时间',
|
createdAt: '创建时间',
|
||||||
paidAt: '支付时间',
|
expiresAt: '过期时间',
|
||||||
completedAt: '完成时间',
|
paidAt: '支付时间',
|
||||||
failedAt: '失败时间',
|
completedAt: '完成时间',
|
||||||
failedReason: '失败原因',
|
failedAt: '失败时间',
|
||||||
refundAmount: '退款金额',
|
failedReason: '失败原因',
|
||||||
refundReason: '退款原因',
|
refundAmount: '退款金额',
|
||||||
refundAt: '退款时间',
|
refundReason: '退款原因',
|
||||||
forceRefund: '强制退款',
|
refundAt: '退款时间',
|
||||||
};
|
forceRefund: '强制退款',
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -32,37 +32,38 @@ interface OrderTableProps {
|
|||||||
|
|
||||||
export default function OrderTable({ orders, onRetry, onCancel, onViewDetail, dark, locale = 'zh' }: OrderTableProps) {
|
export default function OrderTable({ orders, onRetry, onCancel, onViewDetail, dark, locale = 'zh' }: OrderTableProps) {
|
||||||
const currency = locale === 'en' ? '$' : '¥';
|
const currency = locale === 'en' ? '$' : '¥';
|
||||||
const text = locale === 'en'
|
const text =
|
||||||
? {
|
locale === 'en'
|
||||||
orderId: 'Order ID',
|
? {
|
||||||
userName: 'Username',
|
orderId: 'Order ID',
|
||||||
email: 'Email',
|
userName: 'Username',
|
||||||
notes: 'Notes',
|
email: 'Email',
|
||||||
amount: 'Amount',
|
notes: 'Notes',
|
||||||
status: 'Status',
|
amount: 'Amount',
|
||||||
paymentMethod: 'Payment',
|
status: 'Status',
|
||||||
source: 'Source',
|
paymentMethod: 'Payment',
|
||||||
createdAt: 'Created At',
|
source: 'Source',
|
||||||
actions: 'Actions',
|
createdAt: 'Created At',
|
||||||
retry: 'Retry',
|
actions: 'Actions',
|
||||||
cancel: 'Cancel',
|
retry: 'Retry',
|
||||||
empty: 'No orders',
|
cancel: 'Cancel',
|
||||||
}
|
empty: 'No orders',
|
||||||
: {
|
}
|
||||||
orderId: '订单号',
|
: {
|
||||||
userName: '用户名',
|
orderId: '订单号',
|
||||||
email: '邮箱',
|
userName: '用户名',
|
||||||
notes: '备注',
|
email: '邮箱',
|
||||||
amount: '金额',
|
notes: '备注',
|
||||||
status: '状态',
|
amount: '金额',
|
||||||
paymentMethod: '支付方式',
|
status: '状态',
|
||||||
source: '来源',
|
paymentMethod: '支付方式',
|
||||||
createdAt: '创建时间',
|
source: '来源',
|
||||||
actions: '操作',
|
createdAt: '创建时间',
|
||||||
retry: '重试',
|
actions: '操作',
|
||||||
cancel: '取消',
|
retry: '重试',
|
||||||
empty: '暂无订单',
|
cancel: '取消',
|
||||||
};
|
empty: '暂无订单',
|
||||||
|
};
|
||||||
|
|
||||||
const thCls = `px-4 py-3 text-left text-xs font-medium uppercase ${dark ? 'text-slate-400' : 'text-gray-500'}`;
|
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'}`;
|
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
|
|||||||
<td className={tdMuted}>{order.userEmail || '-'}</td>
|
<td className={tdMuted}>{order.userEmail || '-'}</td>
|
||||||
<td className={tdMuted}>{order.userNotes || '-'}</td>
|
<td className={tdMuted}>{order.userNotes || '-'}</td>
|
||||||
<td className={`whitespace-nowrap px-4 py-3 text-sm font-medium ${dark ? 'text-slate-200' : ''}`}>
|
<td className={`whitespace-nowrap px-4 py-3 text-sm font-medium ${dark ? 'text-slate-200' : ''}`}>
|
||||||
{currency}{order.amount.toFixed(2)}
|
{currency}
|
||||||
|
{order.amount.toFixed(2)}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-4 py-3 text-sm">
|
<td className="whitespace-nowrap px-4 py-3 text-sm">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ export default function PaymentMethodChart({ data, dark, locale = 'zh' }: Paymen
|
|||||||
dark ? 'border-slate-700 bg-slate-800/60' : 'border-slate-200 bg-white shadow-sm',
|
dark ? 'border-slate-700 bg-slate-800/60' : 'border-slate-200 bg-white shadow-sm',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<h3 className={['mb-4 text-sm font-semibold', dark ? 'text-slate-200' : 'text-slate-800'].join(' ')}>
|
<h3 className={['mb-4 text-sm font-semibold', dark ? 'text-slate-200' : 'text-slate-800'].join(' ')}>{title}</h3>
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{data.map((method) => {
|
{data.map((method) => {
|
||||||
const meta = getPaymentMeta(method.paymentType);
|
const meta = getPaymentMeta(method.paymentType);
|
||||||
@@ -56,7 +54,8 @@ export default function PaymentMethodChart({ data, dark, locale = 'zh' }: Paymen
|
|||||||
<div className="mb-1.5 flex items-center justify-between text-sm">
|
<div className="mb-1.5 flex items-center justify-between text-sm">
|
||||||
<span className={dark ? 'text-slate-300' : 'text-slate-700'}>{label}</span>
|
<span className={dark ? 'text-slate-300' : 'text-slate-700'}>{label}</span>
|
||||||
<span className={dark ? 'text-slate-400' : 'text-slate-500'}>
|
<span className={dark ? 'text-slate-400' : 'text-slate-500'}>
|
||||||
{currency}{method.amount.toLocaleString()} · {method.percentage}%
|
{currency}
|
||||||
|
{method.amount.toLocaleString()} · {method.percentage}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -65,7 +64,10 @@ export default function PaymentMethodChart({ data, dark, locale = 'zh' }: Paymen
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={['h-full rounded-full transition-all', dark ? meta.chartBar.dark : meta.chartBar.light].join(' ')}
|
className={[
|
||||||
|
'h-full rounded-full transition-all',
|
||||||
|
dark ? meta.chartBar.dark : meta.chartBar.light,
|
||||||
|
].join(' ')}
|
||||||
style={{ width: `${method.percentage}%` }}
|
style={{ width: `${method.percentage}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,10 +74,7 @@ export default function RefundDialog({
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onCancel}>
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onCancel}>
|
||||||
<div
|
<div
|
||||||
className={[
|
className={['w-full max-w-md rounded-xl p-6 shadow-xl', dark ? 'bg-slate-900' : 'bg-white'].join(' ')}
|
||||||
'w-full max-w-md rounded-xl p-6 shadow-xl',
|
|
||||||
dark ? 'bg-slate-900' : 'bg-white',
|
|
||||||
].join(' ')}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<h3 className={['text-lg font-bold', dark ? 'text-slate-100' : 'text-gray-900'].join(' ')}>{text.title}</h3>
|
<h3 className={['text-lg font-bold', dark ? 'text-slate-100' : 'text-gray-900'].join(' ')}>{text.title}</h3>
|
||||||
@@ -90,7 +87,10 @@ export default function RefundDialog({
|
|||||||
|
|
||||||
<div className={['rounded-lg p-3', dark ? 'bg-slate-800' : 'bg-gray-50'].join(' ')}>
|
<div className={['rounded-lg p-3', dark ? 'bg-slate-800' : 'bg-gray-50'].join(' ')}>
|
||||||
<div className={['text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>{text.amount}</div>
|
<div className={['text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>{text.amount}</div>
|
||||||
<div className="text-lg font-bold text-red-600">{currency}{amount.toFixed(2)}</div>
|
<div className="text-lg font-bold text-red-600">
|
||||||
|
{currency}
|
||||||
|
{amount.toFixed(2)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{warning && (
|
{warning && (
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ const BODY_CHARSET_RE = /(?:^|&)charset=([^&]+)/i;
|
|||||||
function normalizeCharset(charset: string | null | undefined): string | null {
|
function normalizeCharset(charset: string | null | undefined): string | null {
|
||||||
if (!charset) return null;
|
if (!charset) return null;
|
||||||
|
|
||||||
const normalized = charset.trim().replace(/^['"]|['"]$/g, '').toLowerCase();
|
const normalized = charset
|
||||||
|
.trim()
|
||||||
|
.replace(/^['"]|['"]$/g, '')
|
||||||
|
.toLowerCase();
|
||||||
if (!normalized) return null;
|
if (!normalized) return null;
|
||||||
|
|
||||||
switch (normalized) {
|
switch (normalized) {
|
||||||
@@ -67,9 +70,7 @@ export function decodeAlipayPayload(rawBody: string | Buffer, headers: Record<st
|
|||||||
return fallbackDecoded;
|
return fallbackDecoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(`Failed to decode Alipay payload${lastError instanceof Error ? `: ${lastError.message}` : ''}`);
|
||||||
`Failed to decode Alipay payload${lastError instanceof Error ? `: ${lastError.message}` : ''}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeAlipaySignature(sign: string): string {
|
export function normalizeAlipaySignature(sign: string): string {
|
||||||
|
|||||||
@@ -11,11 +11,7 @@ import type {
|
|||||||
import { pageExecute, execute } from './client';
|
import { pageExecute, execute } from './client';
|
||||||
import { verifySign } from './sign';
|
import { verifySign } from './sign';
|
||||||
import { getEnv } from '@/lib/config';
|
import { getEnv } from '@/lib/config';
|
||||||
import type {
|
import type { AlipayTradeQueryResponse, AlipayTradeRefundResponse, AlipayTradeCloseResponse } from './types';
|
||||||
AlipayTradeQueryResponse,
|
|
||||||
AlipayTradeRefundResponse,
|
|
||||||
AlipayTradeCloseResponse,
|
|
||||||
} from './types';
|
|
||||||
import { parseAlipayNotificationParams } from './codec';
|
import { parseAlipayNotificationParams } from './codec';
|
||||||
|
|
||||||
export interface BuildAlipayPaymentUrlInput {
|
export interface BuildAlipayPaymentUrlInput {
|
||||||
@@ -165,8 +161,7 @@ export class AlipayProvider implements PaymentProvider {
|
|||||||
tradeNo,
|
tradeNo,
|
||||||
orderId,
|
orderId,
|
||||||
amount: Math.round(amount * 100) / 100,
|
amount: Math.round(amount * 100) / 100,
|
||||||
status:
|
status: tradeStatus === 'TRADE_SUCCESS' || tradeStatus === 'TRADE_FINISHED' ? 'success' : 'failed',
|
||||||
tradeStatus === 'TRADE_SUCCESS' || tradeStatus === 'TRADE_FINISHED' ? 'success' : 'failed',
|
|
||||||
rawData: params,
|
rawData: params,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ function wrapBase64(b64: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizePemLikeValue(key: string): string {
|
function normalizePemLikeValue(key: string): string {
|
||||||
return key.trim().replace(/\r\n/g, '\n').replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n');
|
return key
|
||||||
|
.trim()
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.replace(/\\r\\n/g, '\n')
|
||||||
|
.replace(/\\n/g, '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldLogVerifyDebug(): boolean {
|
function shouldLogVerifyDebug(): boolean {
|
||||||
@@ -42,7 +46,9 @@ export function generateSign(params: Record<string, string>, privateKey: string)
|
|||||||
/** 用支付宝公钥验证签名(回调验签:排除 sign 和 sign_type) */
|
/** 用支付宝公钥验证签名(回调验签:排除 sign 和 sign_type) */
|
||||||
export function verifySign(params: Record<string, string>, alipayPublicKey: string, sign: string): boolean {
|
export function verifySign(params: Record<string, string>, alipayPublicKey: string, sign: string): boolean {
|
||||||
const filtered = Object.entries(params)
|
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));
|
.sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
|
||||||
const signStr = filtered.map(([key, value]) => `${key}=${value}`).join('&');
|
const signStr = filtered.map(([key, value]) => `${key}=${value}`).join('&');
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
|
|||||||
if (pendingCount >= MAX_PENDING_ORDERS) {
|
if (pendingCount >= MAX_PENDING_ORDERS) {
|
||||||
throw new OrderError(
|
throw new OrderError(
|
||||||
'TOO_MANY_PENDING',
|
'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,
|
429,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -228,13 +232,21 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
|
|||||||
if (msg.includes('environment variables') || msg.includes('not configured') || msg.includes('not found')) {
|
if (msg.includes('environment variables') || msg.includes('not configured') || msg.includes('not found')) {
|
||||||
throw new OrderError(
|
throw new OrderError(
|
||||||
'PAYMENT_GATEWAY_ERROR',
|
'PAYMENT_GATEWAY_ERROR',
|
||||||
message(locale, `支付渠道(${input.paymentType})暂未配置,请联系管理员`, `Payment method (${input.paymentType}) is not configured. Please contact the administrator`),
|
message(
|
||||||
|
locale,
|
||||||
|
`支付渠道(${input.paymentType})暂未配置,请联系管理员`,
|
||||||
|
`Payment method (${input.paymentType}) is not configured. Please contact the administrator`,
|
||||||
|
),
|
||||||
503,
|
503,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new OrderError(
|
throw new OrderError(
|
||||||
'PAYMENT_GATEWAY_ERROR',
|
'PAYMENT_GATEWAY_ERROR',
|
||||||
message(locale, '支付渠道暂时不可用,请稍后重试或更换支付方式', 'Payment method is temporarily unavailable. Please try again later or use another payment method'),
|
message(
|
||||||
|
locale,
|
||||||
|
'支付渠道暂时不可用,请稍后重试或更换支付方式',
|
||||||
|
'Payment method is temporarily unavailable. Please try again later or use another payment method',
|
||||||
|
),
|
||||||
502,
|
502,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -413,10 +425,7 @@ export async function confirmPayment(input: {
|
|||||||
const result = await prisma.order.updateMany({
|
const result = await prisma.order.updateMany({
|
||||||
where: {
|
where: {
|
||||||
id: order.id,
|
id: order.id,
|
||||||
OR: [
|
OR: [{ status: ORDER_STATUS.PENDING }, { status: ORDER_STATUS.EXPIRED, updatedAt: { gte: graceDeadline } }],
|
||||||
{ status: ORDER_STATUS.PENDING },
|
|
||||||
{ status: ORDER_STATUS.EXPIRED, updatedAt: { gte: graceDeadline } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
status: ORDER_STATUS.PAID,
|
status: ORDER_STATUS.PAID,
|
||||||
@@ -574,11 +583,19 @@ export async function executeRecharge(orderId: string): Promise<void> {
|
|||||||
|
|
||||||
function assertRetryAllowed(order: { status: string; paidAt: Date | null }, locale: Locale): void {
|
function assertRetryAllowed(order: { status: string; paidAt: Date | null }, locale: Locale): void {
|
||||||
if (!order.paidAt) {
|
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)) {
|
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) {
|
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) {
|
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) {
|
if (order.status === ORDER_STATUS.COMPLETED) {
|
||||||
throw new OrderError('INVALID_STATUS', message(locale, '订单已完成', 'Order already completed'), 400);
|
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<void> {
|
export async function retryRecharge(orderId: string, locale: Locale = 'zh'): Promise<void> {
|
||||||
@@ -638,7 +663,11 @@ export async function retryRecharge(orderId: string, locale: Locale = 'zh'): Pro
|
|||||||
|
|
||||||
const derived = deriveOrderState(latest);
|
const derived = deriveOrderState(latest);
|
||||||
if (derived.rechargeStatus === 'recharging' || latest.status === ORDER_STATUS.PAID) {
|
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') {
|
if (derived.rechargeStatus === 'success') {
|
||||||
@@ -646,10 +675,18 @@ export async function retryRecharge(orderId: string, locale: Locale = 'zh'): Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRefundStatus(latest.status)) {
|
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({
|
await prisma.auditLog.create({
|
||||||
@@ -682,7 +719,11 @@ export async function processRefund(input: RefundInput): Promise<RefundResult> {
|
|||||||
const order = await prisma.order.findUnique({ where: { id: input.orderId } });
|
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) throw new OrderError('NOT_FOUND', message(locale, '订单不存在', 'Order not found'), 404);
|
||||||
if (order.status !== ORDER_STATUS.COMPLETED) {
|
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);
|
const rechargeAmount = Number(order.amount);
|
||||||
@@ -716,7 +757,11 @@ export async function processRefund(input: RefundInput): Promise<RefundResult> {
|
|||||||
data: { status: ORDER_STATUS.REFUNDING },
|
data: { status: ORDER_STATUS.REFUNDING },
|
||||||
});
|
});
|
||||||
if (lockResult.count === 0) {
|
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 {
|
try {
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ export function getOrderDisplayState(
|
|||||||
label: '支付成功',
|
label: '支付成功',
|
||||||
color: 'text-amber-600',
|
color: 'text-amber-600',
|
||||||
icon: '!',
|
icon: '!',
|
||||||
message: '支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。',
|
message:
|
||||||
|
'支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { ORDER_STATUS, PAYMENT_TYPE, PAYMENT_PREFIX, REDIRECT_PAYMENT_TYPES } from './constants';
|
||||||
ORDER_STATUS,
|
|
||||||
PAYMENT_TYPE,
|
|
||||||
PAYMENT_PREFIX,
|
|
||||||
REDIRECT_PAYMENT_TYPES,
|
|
||||||
} from './constants';
|
|
||||||
import type { Locale } from './locale';
|
import type { Locale } from './locale';
|
||||||
|
|
||||||
export interface UserInfo {
|
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})`;
|
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);
|
const meta = getPaymentText(type, locale);
|
||||||
return { channel: meta.label, provider: meta.provider, sublabel: meta.sublabel };
|
return { channel: meta.label, provider: meta.provider, sublabel: meta.sublabel };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,12 +125,7 @@ export async function subtractBalance(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addBalance(
|
export async function addBalance(userId: number, amount: number, notes: string, idempotencyKey: string): Promise<void> {
|
||||||
userId: number,
|
|
||||||
amount: number,
|
|
||||||
notes: string,
|
|
||||||
idempotencyKey: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const env = getEnv();
|
const env = getEnv();
|
||||||
const response = await fetch(`${env.SUB2API_BASE_URL}/api/v1/admin/users/${userId}/balance`, {
|
const response = await fetch(`${env.SUB2API_BASE_URL}/api/v1/admin/users/${userId}/balance`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
Reference in New Issue
Block a user