Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
58b2cc38
Commit
58b2cc38
authored
Apr 20, 2026
by
IanShaw027
Browse files
test: harden payment result resume flow
parent
b51bc7ee
Changes
2
Show whitespace changes
Inline
Side-by-side
frontend/src/views/user/PaymentResultView.vue
View file @
58b2cc38
...
@@ -94,6 +94,7 @@ import { ref, computed, onMounted } from 'vue'
...
@@ -94,6 +94,7 @@ import { ref, computed, onMounted } from 'vue'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
OrderStatusBadge
from
'
@/components/payment/OrderStatusBadge.vue
'
import
OrderStatusBadge
from
'
@/components/payment/OrderStatusBadge.vue
'
import
{
PAYMENT_RECOVERY_STORAGE_KEY
,
readPaymentRecoverySnapshot
}
from
'
@/components/payment/paymentFlow
'
import
{
usePaymentStore
}
from
'
@/stores/payment
'
import
{
usePaymentStore
}
from
'
@/stores/payment
'
import
{
paymentAPI
}
from
'
@/api/payment
'
import
{
paymentAPI
}
from
'
@/api/payment
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
...
@@ -129,46 +130,46 @@ const feeAmount = computed(() => {
...
@@ -129,46 +130,46 @@ const feeAmount = computed(() => {
})
})
const
isSuccess
=
computed
(()
=>
{
const
isSuccess
=
computed
(()
=>
{
// Always prioritize actual order status from backend
return
!!
order
.
value
&&
SUCCESS_STATUSES
.
has
(
order
.
value
.
status
)
if
(
order
.
value
)
{
return
SUCCESS_STATUSES
.
has
(
order
.
value
.
status
)
}
// Fallback only when order not loaded
if
(
route
.
query
.
status
===
'
success
'
)
return
true
if
(
route
.
query
.
trade_status
===
'
TRADE_SUCCESS
'
)
return
true
return
false
})
})
/** Extract numeric order ID from out_trade_no like "sub2_46" → 46 */
function
parseOutTradeNo
(
outTradeNo
:
string
):
number
{
const
match
=
outTradeNo
.
match
(
/_
(\d
+
)
$/
)
return
match
?
Number
(
match
[
1
])
:
0
}
onMounted
(
async
()
=>
{
onMounted
(
async
()
=>
{
// Try order_id first (internal navigation from QRCode/Stripe pages)
const
resumeToken
=
typeof
route
.
query
.
resume_token
===
'
string
'
?
route
.
query
.
resume_token
:
''
let
orderId
=
Number
(
route
.
query
.
order_id
)
||
0
let
orderId
=
Number
(
route
.
query
.
order_id
)
||
0
const
outTradeNo
=
String
(
route
.
query
.
out_trade_no
||
''
)
const
outTradeNo
=
String
(
route
.
query
.
out_trade_no
||
''
)
// Fallback: EasyPay return URL with out_trade_no
if
(
!
orderId
&&
resumeToken
&&
typeof
window
!==
'
undefined
'
)
{
if
(
!
orderId
&&
outTradeNo
)
{
const
restored
=
readPaymentRecoverySnapshot
(
orderId
=
parseOutTradeNo
(
outTradeNo
)
window
.
localStorage
.
getItem
(
PAYMENT_RECOVERY_STORAGE_KEY
),
// Store return info for display when order lookup fails
{
resumeToken
},
)
if
(
restored
?.
orderId
)
{
orderId
=
restored
.
orderId
}
}
if
(
orderId
)
{
try
{
order
.
value
=
await
paymentStore
.
pollOrderStatus
(
orderId
)
}
catch
(
_err
:
unknown
)
{
// Order lookup failed, will try legacy fallback below when possible.
}
}
if
(
!
order
.
value
&&
outTradeNo
)
{
returnInfo
.
value
=
{
returnInfo
.
value
=
{
outTradeNo
,
outTradeNo
,
money
:
String
(
route
.
query
.
money
||
''
),
money
:
String
(
route
.
query
.
money
||
''
),
type
:
String
(
route
.
query
.
type
||
''
),
type
:
String
(
route
.
query
.
type
||
''
),
tradeStatus
:
String
(
route
.
query
.
trade_status
||
''
),
tradeStatus
:
String
(
route
.
query
.
trade_status
||
''
),
}
}
}
// Verify payment via public endpoint (works without login)
if
(
outTradeNo
)
{
try
{
try
{
const
result
=
await
paymentAPI
.
verifyOrderPublic
(
outTradeNo
)
const
result
=
await
paymentAPI
.
verifyOrderPublic
(
outTradeNo
)
order
.
value
=
result
.
data
order
.
value
=
result
.
data
}
catch
(
_err
:
unknown
)
{
}
catch
(
_err
:
unknown
)
{
// Public verify failed, try authenticated endpoint if logged in
try
{
try
{
const
result
=
await
paymentAPI
.
verifyOrder
(
outTradeNo
)
const
result
=
await
paymentAPI
.
verifyOrder
(
outTradeNo
)
order
.
value
=
result
.
data
order
.
value
=
result
.
data
...
@@ -176,12 +177,11 @@ onMounted(async () => {
...
@@ -176,12 +177,11 @@ onMounted(async () => {
}
}
}
}
// Normal order lookup by ID (if verify didn't load the order)
if
(
!
order
.
value
&&
orderId
)
{
if
(
!
order
.
value
&&
orderId
)
{
try
{
try
{
order
.
value
=
await
paymentStore
.
pollOrderStatus
(
orderId
)
order
.
value
=
await
paymentStore
.
pollOrderStatus
(
orderId
)
}
catch
(
_err
:
unknown
)
{
}
catch
(
_err
:
unknown
)
{
// Order lookup failed, will show returnInfo fallback
// Order lookup failed, will show returnInfo fallback
.
}
}
}
}
loading
.
value
=
false
loading
.
value
=
false
...
...
frontend/src/views/user/__tests__/PaymentResultView.spec.ts
0 → 100644
View file @
58b2cc38
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
import
{
flushPromises
,
mount
}
from
'
@vue/test-utils
'
const
routeState
=
vi
.
hoisted
(()
=>
({
query
:
{}
as
Record
<
string
,
unknown
>
,
}))
const
routerPush
=
vi
.
hoisted
(()
=>
vi
.
fn
())
const
pollOrderStatus
=
vi
.
hoisted
(()
=>
vi
.
fn
())
const
verifyOrderPublic
=
vi
.
hoisted
(()
=>
vi
.
fn
())
const
verifyOrder
=
vi
.
hoisted
(()
=>
vi
.
fn
())
vi
.
mock
(
'
vue-router
'
,
async
()
=>
{
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
vue-router
'
)
>
(
'
vue-router
'
)
return
{
...
actual
,
useRoute
:
()
=>
routeState
,
useRouter
:
()
=>
({
push
:
routerPush
}),
}
})
vi
.
mock
(
'
vue-i18n
'
,
async
()
=>
{
const
actual
=
await
vi
.
importActual
<
typeof
import
(
'
vue-i18n
'
)
>
(
'
vue-i18n
'
)
return
{
...
actual
,
useI18n
:
()
=>
({
t
:
(
key
:
string
)
=>
key
,
}),
}
})
vi
.
mock
(
'
@/stores/payment
'
,
()
=>
({
usePaymentStore
:
()
=>
({
pollOrderStatus
,
}),
}))
vi
.
mock
(
'
@/api/payment
'
,
()
=>
({
paymentAPI
:
{
verifyOrderPublic
,
verifyOrder
,
},
}))
import
PaymentResultView
from
'
../PaymentResultView.vue
'
import
{
PAYMENT_RECOVERY_STORAGE_KEY
}
from
'
@/components/payment/paymentFlow
'
const
orderFactory
=
(
status
:
string
)
=>
({
id
:
42
,
user_id
:
9
,
amount
:
88
,
pay_amount
:
88
,
fee_rate
:
0
,
payment_type
:
'
alipay
'
,
out_trade_no
:
'
sub2_20260420abcd1234
'
,
status
,
order_type
:
'
balance
'
,
created_at
:
'
2026-04-20T12:00:00Z
'
,
expires_at
:
'
2026-04-20T12:30:00Z
'
,
refund_amount
:
0
,
})
describe
(
'
PaymentResultView
'
,
()
=>
{
beforeEach
(()
=>
{
routeState
.
query
=
{}
routerPush
.
mockReset
()
pollOrderStatus
.
mockReset
()
verifyOrderPublic
.
mockReset
()
verifyOrder
.
mockReset
()
window
.
localStorage
.
clear
()
})
it
(
'
restores order id from a matching resume token and does not trust query success flags
'
,
async
()
=>
{
routeState
.
query
=
{
resume_token
:
'
resume-42
'
,
status
:
'
success
'
,
}
window
.
localStorage
.
setItem
(
PAYMENT_RECOVERY_STORAGE_KEY
,
JSON
.
stringify
({
orderId
:
42
,
amount
:
88
,
qrCode
:
''
,
expiresAt
:
'
2099-01-01T00:10:00.000Z
'
,
paymentType
:
'
alipay
'
,
payUrl
:
'
https://pay.example.com/session/42
'
,
clientSecret
:
''
,
payAmount
:
88
,
orderType
:
'
balance
'
,
paymentMode
:
'
redirect
'
,
resumeToken
:
'
resume-42
'
,
createdAt
:
Date
.
UTC
(
2099
,
0
,
1
,
0
,
0
,
0
),
}))
pollOrderStatus
.
mockResolvedValue
(
orderFactory
(
'
PENDING
'
))
const
wrapper
=
mount
(
PaymentResultView
,
{
global
:
{
stubs
:
{
OrderStatusBadge
:
true
,
},
},
})
await
flushPromises
()
expect
(
pollOrderStatus
).
toHaveBeenCalledWith
(
42
)
expect
(
verifyOrderPublic
).
not
.
toHaveBeenCalled
()
expect
(
wrapper
.
text
()).
toContain
(
'
payment.result.failed
'
)
expect
(
wrapper
.
text
()).
not
.
toContain
(
'
payment.result.success
'
)
})
it
(
'
keeps legacy out_trade_no verification as a fallback when no order context is available
'
,
async
()
=>
{
routeState
.
query
=
{
out_trade_no
:
'
legacy-123
'
,
trade_status
:
'
TRADE_SUCCESS
'
,
}
verifyOrderPublic
.
mockResolvedValue
({
data
:
orderFactory
(
'
PAID
'
),
})
const
wrapper
=
mount
(
PaymentResultView
,
{
global
:
{
stubs
:
{
OrderStatusBadge
:
true
,
},
},
})
await
flushPromises
()
expect
(
verifyOrderPublic
).
toHaveBeenCalledWith
(
'
legacy-123
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
payment.result.success
'
)
})
})
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment