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
55e8dd55
Commit
55e8dd55
authored
Apr 21, 2026
by
IanShaw027
Browse files
Tighten WeChat payment resume flow
parent
1521d503
Changes
15
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/auth_wechat_oauth.go
View file @
55e8dd55
...
@@ -435,24 +435,34 @@ func (h *AuthHandler) WeChatPaymentOAuthCallback(c *gin.Context) {
...
@@ -435,24 +435,34 @@ func (h *AuthHandler) WeChatPaymentOAuthCallback(c *gin.Context) {
scope
=
strings
.
TrimSpace
(
tokenResp
.
Scope
)
scope
=
strings
.
TrimSpace
(
tokenResp
.
Scope
)
}
}
fragment
:=
url
.
Values
{}
resumeToken
,
err
:=
h
.
wechatPaymentResumeService
()
.
CreateWeChatPaymentResumeToken
(
service
.
WeChatPaymentResumeClaims
{
fragment
.
Set
(
"openid"
,
openid
)
OpenID
:
openid
,
fragment
.
Set
(
"state"
,
state
)
PaymentType
:
paymentContext
.
PaymentType
,
fragment
.
Set
(
"scope"
,
scope
)
Amount
:
paymentContext
.
Amount
,
fragment
.
Set
(
"payment_type"
,
paymentContext
.
PaymentType
)
OrderType
:
paymentContext
.
OrderType
,
if
paymentContext
.
Amount
!=
""
{
PlanID
:
paymentContext
.
PlanID
,
fragment
.
Set
(
"amount"
,
paymentContext
.
Amount
)
RedirectTo
:
redirectTo
,
}
Scope
:
scope
,
if
paymentContext
.
OrderType
!=
""
{
})
fragment
.
Set
(
"order_type"
,
paymentContext
.
OrderType
)
if
err
!=
nil
{
}
redirectOAuthError
(
c
,
frontendCallback
,
"invalid_context"
,
"failed to encode payment resume context"
,
""
)
if
paymentContext
.
PlanID
>
0
{
return
fragment
.
Set
(
"plan_id"
,
strconv
.
FormatInt
(
paymentContext
.
PlanID
,
10
))
}
}
fragment
:=
url
.
Values
{}
fragment
.
Set
(
"wechat_resume_token"
,
resumeToken
)
fragment
.
Set
(
"redirect"
,
redirectTo
)
fragment
.
Set
(
"redirect"
,
redirectTo
)
redirectWithFragment
(
c
,
frontendCallback
,
fragment
)
redirectWithFragment
(
c
,
frontendCallback
,
fragment
)
}
}
func
(
h
*
AuthHandler
)
wechatPaymentResumeService
()
*
service
.
PaymentResumeService
{
key
,
err
:=
payment
.
ProvideEncryptionKey
(
h
.
cfg
)
if
err
!=
nil
{
return
service
.
NewPaymentResumeService
(
nil
)
}
return
service
.
NewPaymentResumeService
([]
byte
(
key
))
}
type
completeWeChatOAuthRequest
struct
{
type
completeWeChatOAuthRequest
struct
{
InvitationCode
string
`json:"invitation_code" binding:"required"`
InvitationCode
string
`json:"invitation_code" binding:"required"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
...
...
backend/internal/handler/auth_wechat_oauth_test.go
View file @
55e8dd55
...
@@ -21,6 +21,7 @@ import (
...
@@ -21,6 +21,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/pendingauthsession"
"github.com/Wei-Shaw/sub2api/ent/pendingauthsession"
dbuser
"github.com/Wei-Shaw/sub2api/ent/user"
dbuser
"github.com/Wei-Shaw/sub2api/ent/user"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/payment"
"github.com/Wei-Shaw/sub2api/internal/repository"
"github.com/Wei-Shaw/sub2api/internal/repository"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
...
@@ -175,6 +176,66 @@ func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
...
@@ -175,6 +176,66 @@ func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
require
.
Zero
(
t
,
count
)
require
.
Zero
(
t
,
count
)
}
}
func
TestWeChatPaymentOAuthCallbackRedirectsWithOpaqueResumeToken
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_ID"
,
"wx-mp-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_MP_APP_SECRET"
,
"wx-mp-secret"
)
originalAccessTokenURL
:=
wechatOAuthAccessTokenURL
t
.
Cleanup
(
func
()
{
wechatOAuthAccessTokenURL
=
originalAccessTokenURL
})
upstream
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
if
strings
.
Contains
(
r
.
URL
.
Path
,
"/sns/oauth2/access_token"
)
{
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
_
,
_
=
w
.
Write
([]
byte
(
`{"access_token":"wechat-access","openid":"openid-123","scope":"snsapi_base"}`
))
return
}
http
.
NotFound
(
w
,
r
)
}))
defer
upstream
.
Close
()
wechatOAuthAccessTokenURL
=
upstream
.
URL
+
"/sns/oauth2/access_token"
handler
,
client
:=
newWeChatOAuthTestHandler
(
t
,
false
)
defer
client
.
Close
()
handler
.
cfg
.
Totp
.
EncryptionKey
=
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
recorder
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
recorder
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/auth/oauth/wechat/payment/callback?code=wechat-code&state=state-123"
,
nil
)
req
.
Host
=
"api.example.com"
req
.
AddCookie
(
encodedCookie
(
wechatPaymentOAuthStateName
,
"state-123"
))
req
.
AddCookie
(
encodedCookie
(
wechatPaymentOAuthRedirect
,
"/purchase?from=wechat"
))
req
.
AddCookie
(
encodedCookie
(
wechatPaymentOAuthContextName
,
`{"payment_type":"wxpay","amount":"12.5","order_type":"subscription","plan_id":7}`
))
req
.
AddCookie
(
encodedCookie
(
wechatPaymentOAuthScope
,
"snsapi_base"
))
c
.
Request
=
req
handler
.
WeChatPaymentOAuthCallback
(
c
)
require
.
Equal
(
t
,
http
.
StatusFound
,
recorder
.
Code
)
location
:=
recorder
.
Header
()
.
Get
(
"Location"
)
parsed
,
err
:=
url
.
Parse
(
location
)
require
.
NoError
(
t
,
err
)
fragment
,
err
:=
url
.
ParseQuery
(
parsed
.
Fragment
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
"/purchase?from=wechat"
,
fragment
.
Get
(
"redirect"
))
require
.
NotEmpty
(
t
,
fragment
.
Get
(
"wechat_resume_token"
))
require
.
Empty
(
t
,
fragment
.
Get
(
"openid"
))
require
.
Empty
(
t
,
fragment
.
Get
(
"payment_type"
))
require
.
Empty
(
t
,
fragment
.
Get
(
"amount"
))
require
.
Empty
(
t
,
fragment
.
Get
(
"order_type"
))
require
.
Empty
(
t
,
fragment
.
Get
(
"plan_id"
))
claims
,
err
:=
handler
.
wechatPaymentResumeService
()
.
ParseWeChatPaymentResumeToken
(
fragment
.
Get
(
"wechat_resume_token"
))
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
"openid-123"
,
claims
.
OpenID
)
require
.
Equal
(
t
,
payment
.
TypeWxpay
,
claims
.
PaymentType
)
require
.
Equal
(
t
,
"12.5"
,
claims
.
Amount
)
require
.
Equal
(
t
,
payment
.
OrderTypeSubscription
,
claims
.
OrderType
)
require
.
EqualValues
(
t
,
7
,
claims
.
PlanID
)
require
.
Equal
(
t
,
"/purchase?from=wechat"
,
claims
.
RedirectTo
)
}
func
TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels
(
t
*
testing
.
T
)
{
func
TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels
(
t
*
testing
.
T
)
{
testCases
:=
[]
struct
{
testCases
:=
[]
struct
{
name
string
name
string
...
...
backend/internal/handler/payment_handler.go
View file @
55e8dd55
package
handler
package
handler
import
(
import
(
"fmt"
"strconv"
"strconv"
"strings"
"strings"
"github.com/Wei-Shaw/sub2api/internal/payment"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
middleware2
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
middleware2
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
...
@@ -205,6 +208,7 @@ type CreateOrderRequest struct {
...
@@ -205,6 +208,7 @@ type CreateOrderRequest struct {
Amount
float64
`json:"amount"`
Amount
float64
`json:"amount"`
PaymentType
string
`json:"payment_type" binding:"required"`
PaymentType
string
`json:"payment_type" binding:"required"`
OpenID
string
`json:"openid"`
OpenID
string
`json:"openid"`
WechatResumeToken
string
`json:"wechat_resume_token"`
ReturnURL
string
`json:"return_url"`
ReturnURL
string
`json:"return_url"`
PaymentSource
string
`json:"payment_source"`
PaymentSource
string
`json:"payment_source"`
OrderType
string
`json:"order_type"`
OrderType
string
`json:"order_type"`
...
@@ -225,6 +229,17 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) {
...
@@ -225,6 +229,17 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) {
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
return
}
}
if
strings
.
TrimSpace
(
req
.
WechatResumeToken
)
!=
""
{
claims
,
err
:=
h
.
paymentService
.
ParseWeChatPaymentResumeToken
(
req
.
WechatResumeToken
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
if
err
:=
applyWeChatPaymentResumeClaims
(
&
req
,
claims
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
}
mobile
:=
isMobile
(
c
)
mobile
:=
isMobile
(
c
)
if
req
.
IsMobile
!=
nil
{
if
req
.
IsMobile
!=
nil
{
...
@@ -253,6 +268,44 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) {
...
@@ -253,6 +268,44 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) {
response
.
Success
(
c
,
result
)
response
.
Success
(
c
,
result
)
}
}
func
applyWeChatPaymentResumeClaims
(
req
*
CreateOrderRequest
,
claims
*
service
.
WeChatPaymentResumeClaims
)
error
{
if
req
==
nil
||
claims
==
nil
{
return
infraerrors
.
BadRequest
(
"INVALID_WECHAT_PAYMENT_RESUME_TOKEN"
,
"wechat payment resume context is missing"
)
}
openid
:=
strings
.
TrimSpace
(
claims
.
OpenID
)
if
openid
==
""
{
return
infraerrors
.
BadRequest
(
"INVALID_WECHAT_PAYMENT_RESUME_TOKEN"
,
"wechat payment resume token missing openid"
)
}
paymentType
:=
service
.
NormalizeVisibleMethod
(
claims
.
PaymentType
)
if
paymentType
==
""
{
paymentType
=
payment
.
TypeWxpay
}
if
req
.
PaymentType
!=
""
{
requestPaymentType
:=
service
.
NormalizeVisibleMethod
(
req
.
PaymentType
)
if
requestPaymentType
!=
""
&&
requestPaymentType
!=
paymentType
{
return
infraerrors
.
BadRequest
(
"INVALID_WECHAT_PAYMENT_RESUME_TOKEN"
,
"wechat payment resume token payment type mismatch"
)
}
}
req
.
PaymentType
=
paymentType
req
.
OpenID
=
openid
if
strings
.
TrimSpace
(
claims
.
Amount
)
!=
""
{
amount
,
err
:=
strconv
.
ParseFloat
(
strings
.
TrimSpace
(
claims
.
Amount
),
64
)
if
err
!=
nil
||
amount
<=
0
{
return
infraerrors
.
BadRequest
(
"INVALID_WECHAT_PAYMENT_RESUME_TOKEN"
,
fmt
.
Sprintf
(
"invalid resume amount: %s"
,
claims
.
Amount
))
}
req
.
Amount
=
amount
}
if
claims
.
OrderType
!=
""
{
req
.
OrderType
=
claims
.
OrderType
}
if
claims
.
PlanID
>
0
{
req
.
PlanID
=
claims
.
PlanID
}
return
nil
}
// GetMyOrders returns the authenticated user's orders.
// GetMyOrders returns the authenticated user's orders.
// GET /api/v1/payment/orders/my
// GET /api/v1/payment/orders/my
func
(
h
*
PaymentHandler
)
GetMyOrders
(
c
*
gin
.
Context
)
{
func
(
h
*
PaymentHandler
)
GetMyOrders
(
c
*
gin
.
Context
)
{
...
...
backend/internal/handler/payment_handler_resume_test.go
0 → 100644
View file @
55e8dd55
//go:build unit
package
handler
import
(
"testing"
"github.com/Wei-Shaw/sub2api/internal/payment"
"github.com/Wei-Shaw/sub2api/internal/service"
)
func
TestApplyWeChatPaymentResumeClaims
(
t
*
testing
.
T
)
{
t
.
Parallel
()
req
:=
CreateOrderRequest
{
Amount
:
0
,
PaymentType
:
payment
.
TypeWxpay
,
OrderType
:
payment
.
OrderTypeBalance
,
}
err
:=
applyWeChatPaymentResumeClaims
(
&
req
,
&
service
.
WeChatPaymentResumeClaims
{
OpenID
:
"openid-123"
,
PaymentType
:
payment
.
TypeWxpay
,
Amount
:
"12.50"
,
OrderType
:
payment
.
OrderTypeSubscription
,
PlanID
:
7
,
})
if
err
!=
nil
{
t
.
Fatalf
(
"applyWeChatPaymentResumeClaims returned error: %v"
,
err
)
}
if
req
.
OpenID
!=
"openid-123"
{
t
.
Fatalf
(
"openid = %q, want %q"
,
req
.
OpenID
,
"openid-123"
)
}
if
req
.
Amount
!=
12.5
{
t
.
Fatalf
(
"amount = %v, want 12.5"
,
req
.
Amount
)
}
if
req
.
OrderType
!=
payment
.
OrderTypeSubscription
{
t
.
Fatalf
(
"order_type = %q, want %q"
,
req
.
OrderType
,
payment
.
OrderTypeSubscription
)
}
if
req
.
PlanID
!=
7
{
t
.
Fatalf
(
"plan_id = %d, want 7"
,
req
.
PlanID
)
}
}
func
TestApplyWeChatPaymentResumeClaimsRejectsPaymentTypeMismatch
(
t
*
testing
.
T
)
{
t
.
Parallel
()
req
:=
CreateOrderRequest
{
PaymentType
:
payment
.
TypeAlipay
,
}
err
:=
applyWeChatPaymentResumeClaims
(
&
req
,
&
service
.
WeChatPaymentResumeClaims
{
OpenID
:
"openid-123"
,
PaymentType
:
payment
.
TypeWxpay
,
Amount
:
"12.50"
,
OrderType
:
payment
.
OrderTypeBalance
,
})
if
err
==
nil
{
t
.
Fatal
(
"applyWeChatPaymentResumeClaims should reject mismatched payment types"
)
}
}
backend/internal/service/payment_resume_lookup.go
View file @
55e8dd55
...
@@ -33,3 +33,7 @@ func (s *PaymentService) GetPublicOrderByResumeToken(ctx context.Context, token
...
@@ -33,3 +33,7 @@ func (s *PaymentService) GetPublicOrderByResumeToken(ctx context.Context, token
return
order
,
nil
return
order
,
nil
}
}
func
(
s
*
PaymentService
)
ParseWeChatPaymentResumeToken
(
token
string
)
(
*
WeChatPaymentResumeClaims
,
error
)
{
return
s
.
paymentResume
()
.
ParseWeChatPaymentResumeToken
(
strings
.
TrimSpace
(
token
))
}
backend/internal/service/payment_resume_service.go
View file @
55e8dd55
...
@@ -31,6 +31,8 @@ const (
...
@@ -31,6 +31,8 @@ const (
VisibleMethodSourceEasyPayAlipay
=
"easypay_alipay"
VisibleMethodSourceEasyPayAlipay
=
"easypay_alipay"
VisibleMethodSourceOfficialWechat
=
"official_wxpay"
VisibleMethodSourceOfficialWechat
=
"official_wxpay"
VisibleMethodSourceEasyPayWechat
=
"easypay_wxpay"
VisibleMethodSourceEasyPayWechat
=
"easypay_wxpay"
wechatPaymentResumeTokenType
=
"wechat_payment_resume"
)
)
type
ResumeTokenClaims
struct
{
type
ResumeTokenClaims
struct
{
...
@@ -43,6 +45,18 @@ type ResumeTokenClaims struct {
...
@@ -43,6 +45,18 @@ type ResumeTokenClaims struct {
IssuedAt
int64
`json:"iat"`
IssuedAt
int64
`json:"iat"`
}
}
type
WeChatPaymentResumeClaims
struct
{
TokenType
string
`json:"tk,omitempty"`
OpenID
string
`json:"openid"`
PaymentType
string
`json:"pt,omitempty"`
Amount
string
`json:"amt,omitempty"`
OrderType
string
`json:"ot,omitempty"`
PlanID
int64
`json:"pid,omitempty"`
RedirectTo
string
`json:"rd,omitempty"`
Scope
string
`json:"scp,omitempty"`
IssuedAt
int64
`json:"iat"`
}
type
PaymentResumeService
struct
{
type
PaymentResumeService
struct
{
signingKey
[]
byte
signingKey
[]
byte
}
}
...
@@ -232,6 +246,66 @@ func (s *PaymentResumeService) CreateToken(claims ResumeTokenClaims) (string, er
...
@@ -232,6 +246,66 @@ func (s *PaymentResumeService) CreateToken(claims ResumeTokenClaims) (string, er
if
claims
.
IssuedAt
==
0
{
if
claims
.
IssuedAt
==
0
{
claims
.
IssuedAt
=
time
.
Now
()
.
Unix
()
claims
.
IssuedAt
=
time
.
Now
()
.
Unix
()
}
}
return
s
.
createSignedToken
(
claims
)
}
func
(
s
*
PaymentResumeService
)
ParseToken
(
token
string
)
(
*
ResumeTokenClaims
,
error
)
{
var
claims
ResumeTokenClaims
if
err
:=
s
.
parseSignedToken
(
token
,
&
claims
);
err
!=
nil
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token payload is invalid"
)
}
if
claims
.
OrderID
<=
0
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token missing order id"
)
}
return
&
claims
,
nil
}
func
(
s
*
PaymentResumeService
)
CreateWeChatPaymentResumeToken
(
claims
WeChatPaymentResumeClaims
)
(
string
,
error
)
{
claims
.
OpenID
=
strings
.
TrimSpace
(
claims
.
OpenID
)
if
claims
.
OpenID
==
""
{
return
""
,
fmt
.
Errorf
(
"wechat payment resume token requires openid"
)
}
if
claims
.
IssuedAt
==
0
{
claims
.
IssuedAt
=
time
.
Now
()
.
Unix
()
}
if
normalized
:=
NormalizeVisibleMethod
(
claims
.
PaymentType
);
normalized
!=
""
{
claims
.
PaymentType
=
normalized
}
if
claims
.
PaymentType
==
""
{
claims
.
PaymentType
=
payment
.
TypeWxpay
}
if
claims
.
OrderType
==
""
{
claims
.
OrderType
=
payment
.
OrderTypeBalance
}
claims
.
TokenType
=
wechatPaymentResumeTokenType
return
s
.
createSignedToken
(
claims
)
}
func
(
s
*
PaymentResumeService
)
ParseWeChatPaymentResumeToken
(
token
string
)
(
*
WeChatPaymentResumeClaims
,
error
)
{
var
claims
WeChatPaymentResumeClaims
if
err
:=
s
.
parseSignedToken
(
token
,
&
claims
);
err
!=
nil
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_WECHAT_PAYMENT_RESUME_TOKEN"
,
"wechat payment resume token payload is invalid"
)
}
if
claims
.
TokenType
!=
wechatPaymentResumeTokenType
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_WECHAT_PAYMENT_RESUME_TOKEN"
,
"wechat payment resume token type mismatch"
)
}
claims
.
OpenID
=
strings
.
TrimSpace
(
claims
.
OpenID
)
if
claims
.
OpenID
==
""
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_WECHAT_PAYMENT_RESUME_TOKEN"
,
"wechat payment resume token missing openid"
)
}
if
normalized
:=
NormalizeVisibleMethod
(
claims
.
PaymentType
);
normalized
!=
""
{
claims
.
PaymentType
=
normalized
}
if
claims
.
PaymentType
==
""
{
claims
.
PaymentType
=
payment
.
TypeWxpay
}
if
claims
.
OrderType
==
""
{
claims
.
OrderType
=
payment
.
OrderTypeBalance
}
return
&
claims
,
nil
}
func
(
s
*
PaymentResumeService
)
createSignedToken
(
claims
any
)
(
string
,
error
)
{
payload
,
err
:=
json
.
Marshal
(
claims
)
payload
,
err
:=
json
.
Marshal
(
claims
)
if
err
!=
nil
{
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"marshal resume claims: %w"
,
err
)
return
""
,
fmt
.
Errorf
(
"marshal resume claims: %w"
,
err
)
...
@@ -240,26 +314,19 @@ func (s *PaymentResumeService) CreateToken(claims ResumeTokenClaims) (string, er
...
@@ -240,26 +314,19 @@ func (s *PaymentResumeService) CreateToken(claims ResumeTokenClaims) (string, er
return
encodedPayload
+
"."
+
s
.
sign
(
encodedPayload
),
nil
return
encodedPayload
+
"."
+
s
.
sign
(
encodedPayload
),
nil
}
}
func
(
s
*
PaymentResumeService
)
P
arseToken
(
token
string
)
(
*
ResumeTokenClaims
,
error
)
{
func
(
s
*
PaymentResumeService
)
p
arse
Signed
Token
(
token
string
,
dest
any
)
error
{
parts
:=
strings
.
Split
(
token
,
"."
)
parts
:=
strings
.
Split
(
token
,
"."
)
if
len
(
parts
)
!=
2
||
parts
[
0
]
==
""
||
parts
[
1
]
==
""
{
if
len
(
parts
)
!=
2
||
parts
[
0
]
==
""
||
parts
[
1
]
==
""
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token is malformed"
)
return
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token is malformed"
)
}
}
if
!
hmac
.
Equal
([]
byte
(
parts
[
1
]),
[]
byte
(
s
.
sign
(
parts
[
0
])))
{
if
!
hmac
.
Equal
([]
byte
(
parts
[
1
]),
[]
byte
(
s
.
sign
(
parts
[
0
])))
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token signature mismatch"
)
return
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token signature mismatch"
)
}
}
payload
,
err
:=
base64
.
RawURLEncoding
.
DecodeString
(
parts
[
0
])
payload
,
err
:=
base64
.
RawURLEncoding
.
DecodeString
(
parts
[
0
])
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token payload is malformed"
)
return
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token payload is malformed"
)
}
var
claims
ResumeTokenClaims
if
err
:=
json
.
Unmarshal
(
payload
,
&
claims
);
err
!=
nil
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token payload is invalid"
)
}
}
if
claims
.
OrderID
<=
0
{
return
json
.
Unmarshal
(
payload
,
dest
)
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_RESUME_TOKEN"
,
"resume token missing order id"
)
}
return
&
claims
,
nil
}
}
func
(
s
*
PaymentResumeService
)
sign
(
payload
string
)
string
{
func
(
s
*
PaymentResumeService
)
sign
(
payload
string
)
string
{
...
...
backend/internal/service/payment_resume_service_test.go
View file @
55e8dd55
...
@@ -150,6 +150,39 @@ func TestPaymentResumeTokenRoundTrip(t *testing.T) {
...
@@ -150,6 +150,39 @@ func TestPaymentResumeTokenRoundTrip(t *testing.T) {
}
}
}
}
func
TestWeChatPaymentResumeTokenRoundTrip
(
t
*
testing
.
T
)
{
t
.
Parallel
()
svc
:=
NewPaymentResumeService
([]
byte
(
"0123456789abcdef0123456789abcdef"
))
token
,
err
:=
svc
.
CreateWeChatPaymentResumeToken
(
WeChatPaymentResumeClaims
{
OpenID
:
"openid-123"
,
PaymentType
:
payment
.
TypeWxpay
,
Amount
:
"12.50"
,
OrderType
:
payment
.
OrderTypeSubscription
,
PlanID
:
7
,
RedirectTo
:
"/purchase?from=wechat"
,
Scope
:
"snsapi_base"
,
IssuedAt
:
1234567890
,
})
if
err
!=
nil
{
t
.
Fatalf
(
"CreateWeChatPaymentResumeToken returned error: %v"
,
err
)
}
claims
,
err
:=
svc
.
ParseWeChatPaymentResumeToken
(
token
)
if
err
!=
nil
{
t
.
Fatalf
(
"ParseWeChatPaymentResumeToken returned error: %v"
,
err
)
}
if
claims
.
OpenID
!=
"openid-123"
||
claims
.
PaymentType
!=
payment
.
TypeWxpay
{
t
.
Fatalf
(
"claims mismatch: %+v"
,
claims
)
}
if
claims
.
Amount
!=
"12.50"
||
claims
.
OrderType
!=
payment
.
OrderTypeSubscription
||
claims
.
PlanID
!=
7
{
t
.
Fatalf
(
"claims payment context mismatch: %+v"
,
claims
)
}
if
claims
.
RedirectTo
!=
"/purchase?from=wechat"
||
claims
.
Scope
!=
"snsapi_base"
{
t
.
Fatalf
(
"claims redirect/scope mismatch: %+v"
,
claims
)
}
}
func
TestNormalizeVisibleMethodSource
(
t
*
testing
.
T
)
{
func
TestNormalizeVisibleMethodSource
(
t
*
testing
.
T
)
{
t
.
Parallel
()
t
.
Parallel
()
...
...
frontend/src/types/payment.ts
View file @
55e8dd55
...
@@ -157,6 +157,7 @@ export interface CreateOrderRequest {
...
@@ -157,6 +157,7 @@ export interface CreateOrderRequest {
return_url
?:
string
return_url
?:
string
payment_source
?:
string
payment_source
?:
string
openid
?:
string
openid
?:
string
wechat_resume_token
?:
string
is_mobile
?:
boolean
is_mobile
?:
boolean
}
}
...
...
frontend/src/views/auth/WechatPaymentCallbackView.vue
View file @
55e8dd55
...
@@ -114,23 +114,17 @@ onMounted(async () => {
...
@@ -114,23 +114,17 @@ onMounted(async () => {
return
return
}
}
const
openid
=
readParam
(
'
openid
'
)
const
resumeToken
=
readParam
(
'
wechat_resume_token
'
)
const
state
=
readParam
(
'
state
'
)
const
scope
=
readParam
(
'
scope
'
)
const
paymentType
=
readParam
(
'
payment_type
'
)
const
amount
=
readParam
(
'
amount
'
)
const
orderType
=
readParam
(
'
order_type
'
)
const
planId
=
readParam
(
'
plan_id
'
)
const
redirectURL
=
new
URL
(
const
redirectURL
=
new
URL
(
normalizeRedirectPath
(
readParam
(
'
redirect
'
)),
normalizeRedirectPath
(
readParam
(
'
redirect
'
)),
window
.
location
.
origin
,
window
.
location
.
origin
,
)
)
if
(
!
openid
)
{
if
(
!
resumeToken
)
{
errorMessage
.
value
=
textWithFallback
(
errorMessage
.
value
=
textWithFallback
(
'
auth.wechatPayment.callbackMissing
OpenId
'
,
'
auth.wechatPayment.callbackMissing
ResumeToken
'
,
'
微信支付回调缺少
openid
。
'
,
'
微信支付回调缺少
恢复令牌
。
'
,
'
The WeChat payment callback is missing the
openid
.
'
,
'
The WeChat payment callback is missing the
resume token
.
'
,
)
)
return
return
}
}
...
@@ -138,14 +132,8 @@ onMounted(async () => {
...
@@ -138,14 +132,8 @@ onMounted(async () => {
const
query
:
Record
<
string
,
string
>
=
{
const
query
:
Record
<
string
,
string
>
=
{
...
Object
.
fromEntries
(
redirectURL
.
searchParams
.
entries
()),
...
Object
.
fromEntries
(
redirectURL
.
searchParams
.
entries
()),
wechat_resume
:
'
1
'
,
wechat_resume
:
'
1
'
,
openid
,
wechat_resume_token
:
resumeToken
,
}
}
if
(
state
)
query
.
state
=
state
if
(
scope
)
query
.
scope
=
scope
if
(
paymentType
)
query
.
payment_type
=
paymentType
if
(
amount
)
query
.
amount
=
amount
if
(
orderType
)
query
.
order_type
=
orderType
if
(
planId
)
query
.
plan_id
=
planId
await
router
.
replace
({
await
router
.
replace
({
path
:
redirectURL
.
pathname
,
path
:
redirectURL
.
pathname
,
...
...
frontend/src/views/auth/__tests__/WechatPaymentCallbackView.spec.ts
View file @
55e8dd55
...
@@ -49,8 +49,8 @@ describe('WechatPaymentCallbackView', () => {
...
@@ -49,8 +49,8 @@ describe('WechatPaymentCallbackView', () => {
})
})
})
})
it
(
'
redirects back to purchase with
openid and payment context
from hash fragment
'
,
async
()
=>
{
it
(
'
redirects back to purchase with
an opaque resume token
from hash fragment
'
,
async
()
=>
{
locationState
.
current
.
hash
=
'
#
openid=openid-123&payment_type=wxpay&amount=12.5&order_type=balance
&redirect=%2Fpurchase%3Ffrom%3Dwechat
'
locationState
.
current
.
hash
=
'
#
wechat_resume_token=resume-token-123
&redirect=%2Fpurchase%3Ffrom%3Dwechat
'
mount
(
WechatPaymentCallbackView
)
mount
(
WechatPaymentCallbackView
)
await
flushPromises
()
await
flushPromises
()
...
@@ -60,21 +60,18 @@ describe('WechatPaymentCallbackView', () => {
...
@@ -60,21 +60,18 @@ describe('WechatPaymentCallbackView', () => {
query
:
{
query
:
{
from
:
'
wechat
'
,
from
:
'
wechat
'
,
wechat_resume
:
'
1
'
,
wechat_resume
:
'
1
'
,
openid
:
'
openid-123
'
,
wechat_resume_token
:
'
resume-token-123
'
,
payment_type
:
'
wxpay
'
,
amount
:
'
12.5
'
,
order_type
:
'
balance
'
,
},
},
})
})
})
})
it
(
'
shows an error when the callback payload is missing
openid
'
,
async
()
=>
{
it
(
'
shows an error when the callback payload is missing
the resume token
'
,
async
()
=>
{
locationState
.
current
.
hash
=
'
#payment_type=wxpay
'
locationState
.
current
.
hash
=
'
#payment_type=wxpay
'
const
wrapper
=
mount
(
WechatPaymentCallbackView
)
const
wrapper
=
mount
(
WechatPaymentCallbackView
)
await
flushPromises
()
await
flushPromises
()
expect
(
replaceMock
).
not
.
toHaveBeenCalled
()
expect
(
replaceMock
).
not
.
toHaveBeenCalled
()
expect
(
wrapper
.
text
()).
toContain
(
'
微信支付回调缺少
openid
。
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
微信支付回调缺少
恢复令牌
。
'
)
})
})
})
})
frontend/src/views/user/PaymentResultView.vue
View file @
55e8dd55
...
@@ -188,7 +188,8 @@ onMounted(async () => {
...
@@ -188,7 +188,8 @@ onMounted(async () => {
}
}
}
}
const
hasLegacyFallbackContext
=
Boolean
(
route
.
query
.
trade_status
||
route
.
query
.
money
||
route
.
query
.
type
)
const
hasLegacyFallbackContext
=
typeof
route
.
query
.
trade_status
===
'
string
'
&&
route
.
query
.
trade_status
.
trim
()
!==
''
if
(
!
order
.
value
&&
!
resumeToken
&&
!
orderId
&&
outTradeNo
&&
hasLegacyFallbackContext
)
{
if
(
!
order
.
value
&&
!
resumeToken
&&
!
orderId
&&
outTradeNo
&&
hasLegacyFallbackContext
)
{
returnInfo
.
value
=
{
returnInfo
.
value
=
{
outTradeNo
,
outTradeNo
,
...
...
frontend/src/views/user/PaymentView.vue
View file @
55e8dd55
...
@@ -284,6 +284,7 @@ import PaymentStatusPanel from '@/components/payment/PaymentStatusPanel.vue'
...
@@ -284,6 +284,7 @@ import PaymentStatusPanel from '@/components/payment/PaymentStatusPanel.vue'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
type
{
PaymentMethodOption
}
from
'
@/components/payment/PaymentMethodSelector.vue
'
import
type
{
PaymentMethodOption
}
from
'
@/components/payment/PaymentMethodSelector.vue
'
import
{
describePaymentScenarioError
}
from
'
./paymentUx
'
import
{
describePaymentScenarioError
}
from
'
./paymentUx
'
import
{
parseWechatResumeRoute
,
stripWechatResumeQuery
}
from
'
./paymentWechatResume
'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
route
=
useRoute
()
const
route
=
useRoute
()
...
@@ -315,6 +316,7 @@ const paymentPhase = ref<'select' | 'paying'>('select')
...
@@ -315,6 +316,7 @@ const paymentPhase = ref<'select' | 'paying'>('select')
interface
CreateOrderOptions
{
interface
CreateOrderOptions
{
openid
?:
string
openid
?:
string
wechatResumeToken
?:
string
paymentType
?:
string
paymentType
?:
string
isResume
?:
boolean
isResume
?:
boolean
}
}
...
@@ -344,13 +346,6 @@ function emptyPaymentState(): PaymentRecoverySnapshot {
...
@@ -344,13 +346,6 @@ function emptyPaymentState(): PaymentRecoverySnapshot {
}
}
}
}
function
readRouteQueryValue
(
value
:
unknown
):
string
{
if
(
Array
.
isArray
(
value
))
{
return
typeof
value
[
0
]
===
'
string
'
?
value
[
0
]
:
''
}
return
typeof
value
===
'
string
'
?
value
:
''
}
function
getWeixinJSBridge
():
WeixinJSBridgeLike
|
undefined
{
function
getWeixinJSBridge
():
WeixinJSBridgeLike
|
undefined
{
return
(
window
as
Window
&
{
WeixinJSBridge
?:
WeixinJSBridgeLike
}
).
WeixinJSBridge
return
(
window
as
Window
&
{
WeixinJSBridge
?:
WeixinJSBridgeLike
}
).
WeixinJSBridge
}
}
...
@@ -637,6 +632,9 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
...
@@ -637,6 +632,9 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
if
(
options
.
openid
)
{
if
(
options
.
openid
)
{
payload
.
openid
=
options
.
openid
payload
.
openid
=
options
.
openid
}
}
if
(
options
.
wechatResumeToken
)
{
payload
.
wechat_resume_token
=
options
.
wechatResumeToken
}
payload
.
is_mobile
=
isMobileDevice
()
payload
.
is_mobile
=
isMobileDevice
()
const
result
=
await
paymentStore
.
createOrder
(
payload
)
as
CreateOrderResult
&
{
resume_token
?:
string
}
const
result
=
await
paymentStore
.
createOrder
(
payload
)
as
CreateOrderResult
&
{
resume_token
?:
string
}
...
@@ -744,44 +742,34 @@ function applyScenarioError(err: unknown, paymentMethod: string) {
...
@@ -744,44 +742,34 @@ function applyScenarioError(err: unknown, paymentMethod: string) {
}
}
async
function
resumeWechatPaymentFromQuery
()
{
async
function
resumeWechatPaymentFromQuery
()
{
const
openid
=
readRouteQueryValue
(
route
.
query
.
openid
)
const
resume
=
parseWechatResumeRoute
(
route
.
query
,
checkout
.
value
.
plans
,
validAmount
.
value
)
if
(
readRouteQueryValue
(
route
.
query
.
wechat_resume
)
!==
'
1
'
||
!
openid
)
{
if
(
!
resume
)
{
return
}
selectedMethod
.
value
=
resume
.
paymentType
if
(
resume
.
orderType
===
'
balance
'
&&
resume
.
orderAmount
>
0
)
{
amount
.
value
=
resume
.
orderAmount
}
if
(
resume
.
orderType
===
'
subscription
'
&&
resume
.
planId
)
{
selectedPlan
.
value
=
checkout
.
value
.
plans
.
find
(
plan
=>
plan
.
id
===
resume
.
planId
)
??
null
}
await
router
.
replace
({
path
:
route
.
path
,
query
:
stripWechatResumeQuery
(
route
.
query
)
}
)
if
(
resume
.
wechatResumeToken
)
{
await
createOrder
(
0
,
resume
.
orderType
,
resume
.
planId
,
{
wechatResumeToken
:
resume
.
wechatResumeToken
,
paymentType
:
resume
.
paymentType
,
isResume
:
true
,
}
)
return
return
}
}
const
paymentType
=
normalizeVisibleMethod
(
readRouteQueryValue
(
route
.
query
.
payment_type
))
||
'
wxpay
'
if
(
resume
.
orderAmount
>
0
&&
resume
.
openid
)
{
const
orderType
=
readRouteQueryValue
(
route
.
query
.
order_type
)
===
'
subscription
'
?
'
subscription
'
:
'
balance
'
await
createOrder
(
resume
.
orderAmount
,
resume
.
orderType
,
resume
.
planId
,
{
const
planId
=
Number
.
parseInt
(
readRouteQueryValue
(
route
.
query
.
plan_id
),
10
)
openid
:
resume
.
openid
,
const
rawAmount
=
Number
.
parseFloat
(
readRouteQueryValue
(
route
.
query
.
amount
))
paymentType
:
resume
.
paymentType
,
const
orderAmount
=
Number
.
isFinite
(
rawAmount
)
&&
rawAmount
>
0
?
rawAmount
:
(
orderType
===
'
subscription
'
?
(
checkout
.
value
.
plans
.
find
(
plan
=>
plan
.
id
===
planId
)?.
price
??
0
)
:
validAmount
.
value
)
selectedMethod
.
value
=
paymentType
if
(
orderType
===
'
balance
'
&&
orderAmount
>
0
)
{
amount
.
value
=
orderAmount
}
if
(
orderType
===
'
subscription
'
&&
Number
.
isFinite
(
planId
)
&&
planId
>
0
)
{
selectedPlan
.
value
=
checkout
.
value
.
plans
.
find
(
plan
=>
plan
.
id
===
planId
)
??
null
}
const
nextQuery
=
{
...
route
.
query
}
delete
nextQuery
.
wechat_resume
delete
nextQuery
.
openid
delete
nextQuery
.
state
delete
nextQuery
.
scope
delete
nextQuery
.
payment_type
delete
nextQuery
.
amount
delete
nextQuery
.
order_type
delete
nextQuery
.
plan_id
await
router
.
replace
({
path
:
route
.
path
,
query
:
nextQuery
}
)
if
(
orderAmount
>
0
)
{
await
createOrder
(
orderAmount
,
orderType
,
Number
.
isFinite
(
planId
)
&&
planId
>
0
?
planId
:
undefined
,
{
openid
,
paymentType
,
isResume
:
true
,
isResume
:
true
,
}
)
}
)
}
}
...
...
frontend/src/views/user/__tests__/PaymentResultView.spec.ts
View file @
55e8dd55
...
@@ -157,6 +157,25 @@ describe('PaymentResultView', () => {
...
@@ -157,6 +157,25 @@ describe('PaymentResultView', () => {
expect
(
wrapper
.
text
()).
toContain
(
'
payment.result.success
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
payment.result.success
'
)
})
})
it
(
'
does not use public out_trade_no verification for bare order numbers without legacy return markers
'
,
async
()
=>
{
routeState
.
query
=
{
out_trade_no
:
'
legacy-bare
'
,
}
mount
(
PaymentResultView
,
{
global
:
{
stubs
:
{
OrderStatusBadge
:
true
,
},
},
})
await
flushPromises
()
expect
(
verifyOrderPublic
).
not
.
toHaveBeenCalled
()
expect
(
verifyOrder
).
not
.
toHaveBeenCalled
()
})
it
(
'
resolves order by resume token when local recovery snapshot is missing
'
,
async
()
=>
{
it
(
'
resolves order by resume token when local recovery snapshot is missing
'
,
async
()
=>
{
routeState
.
query
=
{
routeState
.
query
=
{
resume_token
:
'
resume-77
'
,
resume_token
:
'
resume-77
'
,
...
...
frontend/src/views/user/__tests__/paymentWechatResume.spec.ts
0 → 100644
View file @
55e8dd55
import
{
describe
,
expect
,
it
}
from
'
vitest
'
import
{
parseWechatResumeRoute
,
stripWechatResumeQuery
}
from
'
../paymentWechatResume
'
describe
(
'
parseWechatResumeRoute
'
,
()
=>
{
it
(
'
prefers the opaque resume token over legacy openid query params
'
,
()
=>
{
expect
(
parseWechatResumeRoute
({
wechat_resume
:
'
1
'
,
wechat_resume_token
:
'
resume-token-123
'
,
openid
:
'
openid-123
'
,
payment_type
:
'
wxpay
'
,
amount
:
'
12.5
'
,
order_type
:
'
subscription
'
,
plan_id
:
'
7
'
,
},
[],
88
)).
toEqual
({
wechatResumeToken
:
'
resume-token-123
'
,
paymentType
:
'
wxpay
'
,
orderType
:
'
balance
'
,
orderAmount
:
0
,
})
})
it
(
'
falls back to legacy openid-based resume when opaque token is absent
'
,
()
=>
{
expect
(
parseWechatResumeRoute
({
wechat_resume
:
'
1
'
,
openid
:
'
openid-123
'
,
payment_type
:
'
wxpay
'
,
amount
:
'
12.5
'
,
order_type
:
'
balance
'
,
},
[],
88
)).
toEqual
({
openid
:
'
openid-123
'
,
paymentType
:
'
wxpay
'
,
orderType
:
'
balance
'
,
orderAmount
:
12.5
,
planId
:
undefined
,
})
})
})
describe
(
'
stripWechatResumeQuery
'
,
()
=>
{
it
(
'
removes both opaque-token and legacy resume params from the route query
'
,
()
=>
{
expect
(
stripWechatResumeQuery
({
foo
:
'
bar
'
,
wechat_resume
:
'
1
'
,
wechat_resume_token
:
'
resume-token-123
'
,
openid
:
'
openid-123
'
,
payment_type
:
'
wxpay
'
,
amount
:
'
12.5
'
,
order_type
:
'
subscription
'
,
plan_id
:
'
7
'
,
state
:
'
state-123
'
,
scope
:
'
snsapi_base
'
,
})).
toEqual
({
foo
:
'
bar
'
,
})
})
})
frontend/src/views/user/paymentWechatResume.ts
0 → 100644
View file @
55e8dd55
import
type
{
LocationQuery
,
LocationQueryRaw
}
from
'
vue-router
'
import
type
{
SubscriptionPlan
}
from
'
@/types/payment
'
import
{
normalizeVisibleMethod
}
from
'
@/components/payment/paymentFlow
'
export
interface
ParsedWechatResumeRoute
{
orderAmount
:
number
orderType
:
'
balance
'
|
'
subscription
'
paymentType
:
string
planId
?:
number
openid
?:
string
wechatResumeToken
?:
string
}
function
readQueryString
(
query
:
LocationQuery
,
key
:
string
):
string
{
const
value
=
query
[
key
]
if
(
Array
.
isArray
(
value
))
{
return
typeof
value
[
0
]
===
'
string
'
?
value
[
0
]
:
''
}
return
typeof
value
===
'
string
'
?
value
:
''
}
export
function
parseWechatResumeRoute
(
query
:
LocationQuery
,
plans
:
SubscriptionPlan
[],
fallbackBalanceAmount
:
number
,
):
ParsedWechatResumeRoute
|
null
{
if
(
readQueryString
(
query
,
'
wechat_resume
'
)
!==
'
1
'
)
{
return
null
}
const
wechatResumeToken
=
readQueryString
(
query
,
'
wechat_resume_token
'
)
if
(
wechatResumeToken
)
{
return
{
wechatResumeToken
,
paymentType
:
'
wxpay
'
,
orderType
:
'
balance
'
,
orderAmount
:
0
,
}
}
const
openid
=
readQueryString
(
query
,
'
openid
'
)
if
(
!
openid
)
{
return
null
}
const
paymentType
=
normalizeVisibleMethod
(
readQueryString
(
query
,
'
payment_type
'
))
||
'
wxpay
'
const
orderType
=
readQueryString
(
query
,
'
order_type
'
)
===
'
subscription
'
?
'
subscription
'
:
'
balance
'
const
planId
=
Number
.
parseInt
(
readQueryString
(
query
,
'
plan_id
'
),
10
)
const
rawAmount
=
Number
.
parseFloat
(
readQueryString
(
query
,
'
amount
'
))
const
orderAmount
=
Number
.
isFinite
(
rawAmount
)
&&
rawAmount
>
0
?
rawAmount
:
(
orderType
===
'
subscription
'
?
(
plans
.
find
(
plan
=>
plan
.
id
===
planId
)?.
price
??
0
)
:
fallbackBalanceAmount
)
return
{
openid
,
paymentType
,
orderType
,
orderAmount
,
planId
:
Number
.
isFinite
(
planId
)
&&
planId
>
0
?
planId
:
undefined
,
}
}
export
function
stripWechatResumeQuery
(
query
:
LocationQuery
):
LocationQueryRaw
{
const
nextQuery
:
LocationQueryRaw
=
{
...
query
}
delete
nextQuery
.
wechat_resume
delete
nextQuery
.
wechat_resume_token
delete
nextQuery
.
openid
delete
nextQuery
.
state
delete
nextQuery
.
scope
delete
nextQuery
.
payment_type
delete
nextQuery
.
amount
delete
nextQuery
.
order_type
delete
nextQuery
.
plan_id
return
nextQuery
}
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