diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 62958642..10160d73 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -5441,6 +5441,8 @@ export default { result: { success: 'Payment Successful', subscriptionSuccess: 'Subscription Successful', + processing: 'Payment Processing', + processingHint: 'Payment confirmation is still pending. This page will refresh automatically.', failed: 'Payment Failed', backToRecharge: 'Back to Recharge', viewOrders: 'View Orders', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 7b7cdbb4..8676df4b 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -5629,6 +5629,8 @@ export default { result: { success: '支付成功', subscriptionSuccess: '订阅成功', + processing: '支付处理中', + processingHint: '支付结果仍在确认中,页面会自动刷新。', failed: '支付失败', backToRecharge: '返回充值', viewOrders: '查看订单', diff --git a/frontend/src/views/user/PaymentResultView.vue b/frontend/src/views/user/PaymentResultView.vue index 5c843d1f..d07bcc29 100644 --- a/frontend/src/views/user/PaymentResultView.vue +++ b/frontend/src/views/user/PaymentResultView.vue @@ -15,6 +15,10 @@ +
+
+
@@ -22,8 +26,11 @@

- {{ isSuccess ? t('payment.result.success') : t('payment.result.failed') }} + {{ statusTitle }}

+

+ {{ t('payment.result.processingHint') }} +

@@ -90,7 +97,7 @@ diff --git a/frontend/src/views/user/__tests__/PaymentResultView.spec.ts b/frontend/src/views/user/__tests__/PaymentResultView.spec.ts index d8199e3b..c4e38523 100644 --- a/frontend/src/views/user/__tests__/PaymentResultView.spec.ts +++ b/frontend/src/views/user/__tests__/PaymentResultView.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' const routeState = vi.hoisted(() => ({ @@ -73,7 +73,11 @@ describe('PaymentResultView', () => { window.localStorage.clear() }) - it('restores order id from a matching resume token and does not trust query success flags', async () => { + afterEach(() => { + vi.useRealTimers() + }) + + it('renders a pending state instead of a failure state when the restored order is still pending', async () => { routeState.query = { resume_token: 'resume-42', order_id: '999', @@ -107,8 +111,43 @@ describe('PaymentResultView', () => { expect(pollOrderStatus).toHaveBeenCalledWith(42) expect(verifyOrderPublic).not.toHaveBeenCalled() - expect(wrapper.text()).toContain('payment.result.failed') + expect(wrapper.text()).toContain('payment.result.processing') expect(wrapper.text()).not.toContain('payment.result.success') + expect(wrapper.text()).not.toContain('payment.result.failed') + }) + + it('refreshes a pending resume-token result until the order becomes paid', async () => { + vi.useFakeTimers() + routeState.query = { + resume_token: 'resume-77', + } + resolveOrderPublicByResumeToken + .mockResolvedValueOnce({ + data: orderFactory('PENDING'), + }) + .mockResolvedValueOnce({ + data: orderFactory('PAID'), + }) + + const wrapper = mount(PaymentResultView, { + global: { + stubs: { + OrderStatusBadge: true, + }, + }, + }) + + await flushPromises() + + expect(resolveOrderPublicByResumeToken).toHaveBeenCalledTimes(1) + expect(wrapper.text()).toContain('payment.result.processing') + + await vi.advanceTimersByTimeAsync(2000) + await flushPromises() + + expect(resolveOrderPublicByResumeToken).toHaveBeenCalledTimes(2) + expect(wrapper.text()).toContain('payment.result.success') + expect(wrapper.text()).not.toContain('payment.result.failed') }) it('does not fall back to public out_trade_no verification when resume_token recovery fails', async () => {