fix(payment): restore upgrade-safe payment flows
This commit is contained in:
@@ -291,6 +291,7 @@ onMounted(async () => {
|
||||
const routeOrderId = Number(readRouteQueryString('order_id')) || 0
|
||||
let outTradeNo = readRouteQueryString('out_trade_no')
|
||||
let orderId = 0
|
||||
let resumeTokenLookupFailed = false
|
||||
|
||||
const restored = restoreRecoverySnapshot({
|
||||
resumeToken,
|
||||
@@ -312,24 +313,17 @@ onMounted(async () => {
|
||||
orderId = resolvedOrder.id
|
||||
}
|
||||
} else if (routeOrderId > 0) {
|
||||
resumeTokenLookupFailed = true
|
||||
orderId = routeOrderId
|
||||
} else {
|
||||
resumeTokenLookupFailed = true
|
||||
}
|
||||
} else if (routeOrderId > 0) {
|
||||
orderId = routeOrderId
|
||||
}
|
||||
|
||||
const hasLegacyFallbackContext = readRouteQueryString('trade_status').trim() !== ''
|
||||
const shouldUsePublicOutTradeNo = !resumeToken && outTradeNo !== '' && (hasLegacyFallbackContext || routeOrderId > 0 || orderId > 0)
|
||||
|
||||
if (!order.value && shouldUsePublicOutTradeNo) {
|
||||
const legacyOrder = await resolveOrderFromOutTradeNo(outTradeNo)
|
||||
if (legacyOrder) {
|
||||
order.value = legacyOrder
|
||||
if (!orderId) {
|
||||
orderId = legacyOrder.id
|
||||
}
|
||||
}
|
||||
}
|
||||
const shouldUsePublicOutTradeNo = outTradeNo !== '' && (hasLegacyFallbackContext || routeOrderId > 0 || orderId > 0)
|
||||
|
||||
if (!order.value && orderId && (!resumeToken || routeOrderId > 0)) {
|
||||
try {
|
||||
@@ -339,7 +333,17 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!order.value && !resumeToken && !orderId && outTradeNo && hasLegacyFallbackContext) {
|
||||
if (!order.value && shouldUsePublicOutTradeNo && (!resumeToken || resumeTokenLookupFailed)) {
|
||||
const legacyOrder = await resolveOrderFromOutTradeNo(outTradeNo)
|
||||
if (legacyOrder) {
|
||||
order.value = legacyOrder
|
||||
if (!orderId) {
|
||||
orderId = legacyOrder.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!order.value && !orderId && outTradeNo && hasLegacyFallbackContext) {
|
||||
returnInfo.value = {
|
||||
outTradeNo,
|
||||
money: String(route.query.money || ''),
|
||||
@@ -350,17 +354,24 @@ onMounted(async () => {
|
||||
|
||||
const refreshOrder = async (): Promise<PaymentOrder | null> => {
|
||||
if (resumeToken) {
|
||||
return await resolveOrderFromResumeToken(resumeToken)
|
||||
const resolvedOrder = await resolveOrderFromResumeToken(resumeToken)
|
||||
if (resolvedOrder) {
|
||||
return resolvedOrder
|
||||
}
|
||||
}
|
||||
|
||||
if (orderId) {
|
||||
try {
|
||||
return await paymentStore.pollOrderStatus(orderId)
|
||||
} catch (_err: unknown) {
|
||||
// Fall through to legacy public verification when order polling is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUsePublicOutTradeNo) {
|
||||
return await resolveOrderFromOutTradeNo(outTradeNo)
|
||||
}
|
||||
|
||||
if (orderId) {
|
||||
return await paymentStore.pollOrderStatus(orderId)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -740,18 +740,23 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
|
||||
return
|
||||
}
|
||||
if (decision.kind === 'wechat_jsapi' && decision.jsapi) {
|
||||
const jsapiResult = await invokeWechatJsapiPayment(decision.jsapi as Record<string, unknown>)
|
||||
const errMsg = String(jsapiResult.err_msg || '').toLowerCase()
|
||||
if (errMsg.includes('cancel')) {
|
||||
appStore.showInfo(t('payment.qr.cancelled'))
|
||||
try {
|
||||
const jsapiResult = await invokeWechatJsapiPayment(decision.jsapi as Record<string, unknown>)
|
||||
const errMsg = String(jsapiResult.err_msg || '').toLowerCase()
|
||||
if (errMsg.includes('cancel')) {
|
||||
appStore.showInfo(t('payment.qr.cancelled'))
|
||||
resetPayment()
|
||||
} else if (errMsg && !errMsg.includes('ok')) {
|
||||
applyScenarioError({ reason: 'WECHAT_JSAPI_FAILED', message: errMsg }, visibleMethod)
|
||||
resetPayment()
|
||||
} else {
|
||||
const resultState = { ...decision.paymentState }
|
||||
resetPayment()
|
||||
await redirectToPaymentResult(resultState)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
resetPayment()
|
||||
} else if (errMsg && !errMsg.includes('ok')) {
|
||||
applyScenarioError({ reason: 'WECHAT_JSAPI_FAILED', message: errMsg }, visibleMethod)
|
||||
resetPayment()
|
||||
} else {
|
||||
const resultState = { ...decision.paymentState }
|
||||
resetPayment()
|
||||
await redirectToPaymentResult(resultState)
|
||||
throw err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -255,14 +255,21 @@ describe('PaymentResultView', () => {
|
||||
expect(window.localStorage.getItem(PAYMENT_RECOVERY_STORAGE_KEY)).toBeNull()
|
||||
})
|
||||
|
||||
it('does not fall back to public out_trade_no verification when resume_token recovery fails', async () => {
|
||||
it('falls back to public out_trade_no verification when resume_token recovery fails in legacy return flows', async () => {
|
||||
routeState.query = {
|
||||
resume_token: 'resume-fail',
|
||||
out_trade_no: 'legacy-should-not-run',
|
||||
trade_status: 'TRADE_SUCCESS',
|
||||
}
|
||||
resolveOrderPublicByResumeToken.mockRejectedValueOnce(new Error('resume failed'))
|
||||
mount(PaymentResultView, {
|
||||
verifyOrderPublic.mockResolvedValueOnce({
|
||||
data: {
|
||||
...orderFactory('PAID'),
|
||||
out_trade_no: 'legacy-should-not-run',
|
||||
},
|
||||
})
|
||||
|
||||
const wrapper = mount(PaymentResultView, {
|
||||
global: {
|
||||
stubs: {
|
||||
OrderStatusBadge: true,
|
||||
@@ -273,7 +280,9 @@ describe('PaymentResultView', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(resolveOrderPublicByResumeToken).toHaveBeenCalledWith('resume-fail')
|
||||
expect(verifyOrderPublic).not.toHaveBeenCalled()
|
||||
expect(verifyOrderPublic).toHaveBeenCalledWith('legacy-should-not-run')
|
||||
expect(pollOrderStatus).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('payment.result.success')
|
||||
})
|
||||
|
||||
it('ignores a stale global recovery snapshot when legacy return markers do not identify the order', async () => {
|
||||
|
||||
@@ -252,6 +252,33 @@ describe('PaymentView WeChat JSAPI flow', () => {
|
||||
expect(window.localStorage.getItem(PAYMENT_RECOVERY_STORAGE_KEY)).toBeNull()
|
||||
})
|
||||
|
||||
it('clears stale recovery state when JSAPI never becomes available', async () => {
|
||||
vi.useFakeTimers()
|
||||
createOrder.mockResolvedValue(jsapiOrderFixture('resume-token-missing-bridge'))
|
||||
;(window as Window & { WeixinJSBridge?: { invoke: typeof bridgeInvoke } }).WeixinJSBridge = undefined
|
||||
|
||||
const wrapper = shallowMount(PaymentView, {
|
||||
global: {
|
||||
stubs: {
|
||||
Teleport: true,
|
||||
Transition: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
await vi.advanceTimersByTimeAsync(4000)
|
||||
await flushPromises()
|
||||
await flushPromises()
|
||||
|
||||
expect(showError).toHaveBeenCalledWith(
|
||||
'payment.errors.wechatJsapiUnavailable payment.errors.wechatOpenInWeChatHint',
|
||||
)
|
||||
expect(routerPush).not.toHaveBeenCalled()
|
||||
expect(window.localStorage.getItem(PAYMENT_RECOVERY_STORAGE_KEY)).toBeNull()
|
||||
expect(wrapper.html()).not.toContain('payment-status-panel-stub')
|
||||
})
|
||||
|
||||
it('clears a stale recovery snapshot before handling wechat resume callback params', async () => {
|
||||
createOrder.mockRejectedValueOnce(new Error('resume failed'))
|
||||
window.localStorage.setItem(PAYMENT_RECOVERY_STORAGE_KEY, JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user