Merge pull request #2975 from wucm667/fix/easypay-queryorder-trade-status
fix(payment): EasyPay 查单以 trade_status 判定支付成功,避免未付订单误判到账
This commit is contained in:
commit
e080d3f966
@ -213,22 +213,59 @@ func (e *EasyPay) QueryOrder(ctx context.Context, tradeNo string) (*payment.Quer
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("easypay query: %w", err)
|
||||
}
|
||||
type easyPayQueryData struct {
|
||||
TradeStatus *string `json:"trade_status"`
|
||||
Status *int `json:"status"`
|
||||
Money *string `json:"money"`
|
||||
TradeNo *string `json:"trade_no"`
|
||||
}
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Status int `json:"status"`
|
||||
Money string `json:"money"`
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
TradeStatus *string `json:"trade_status"`
|
||||
Status *int `json:"status"`
|
||||
Money *string `json:"money"`
|
||||
TradeNo *string `json:"trade_no"`
|
||||
Data easyPayQueryData `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return nil, fmt.Errorf("easypay parse query: %w", err)
|
||||
}
|
||||
status := payment.ProviderStatusPending
|
||||
if resp.Status == easypayStatusPaid {
|
||||
if resp.TradeStatus != nil {
|
||||
if *resp.TradeStatus == tradeStatusSuccess {
|
||||
status = payment.ProviderStatusPaid
|
||||
}
|
||||
} else if resp.Data.TradeStatus != nil {
|
||||
if *resp.Data.TradeStatus == tradeStatusSuccess {
|
||||
status = payment.ProviderStatusPaid
|
||||
}
|
||||
} else if resp.Status != nil {
|
||||
if *resp.Status == easypayStatusPaid {
|
||||
status = payment.ProviderStatusPaid
|
||||
}
|
||||
} else if resp.Data.Status != nil && *resp.Data.Status == easypayStatusPaid {
|
||||
status = payment.ProviderStatusPaid
|
||||
}
|
||||
amount, _ := strconv.ParseFloat(resp.Money, 64)
|
||||
|
||||
money := ""
|
||||
if resp.Money != nil {
|
||||
money = *resp.Money
|
||||
} else if resp.Data.Money != nil {
|
||||
money = *resp.Data.Money
|
||||
}
|
||||
responseTradeNo := tradeNo
|
||||
if resp.TradeNo != nil {
|
||||
if *resp.TradeNo != "" {
|
||||
responseTradeNo = *resp.TradeNo
|
||||
}
|
||||
} else if resp.Data.TradeNo != nil && *resp.Data.TradeNo != "" {
|
||||
responseTradeNo = *resp.Data.TradeNo
|
||||
}
|
||||
|
||||
amount, _ := strconv.ParseFloat(money, 64)
|
||||
return &payment.QueryOrderResponse{
|
||||
TradeNo: tradeNo,
|
||||
TradeNo: responseTradeNo,
|
||||
Status: status,
|
||||
Amount: amount,
|
||||
Metadata: e.MerchantIdentityMetadata(),
|
||||
|
||||
131
backend/internal/payment/provider/easypay_query_test.go
Normal file
131
backend/internal/payment/provider/easypay_query_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/payment"
|
||||
)
|
||||
|
||||
func TestEasyPayQueryOrderStatusMapping(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const orderID = "order-123"
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantStatus string
|
||||
wantTradeNo string
|
||||
wantAmount float64
|
||||
}{
|
||||
{
|
||||
name: "top level trade success is paid",
|
||||
body: `{"code":1,"trade_status":"TRADE_SUCCESS","status":0,"money":"12.34","trade_no":"gateway-123"}`,
|
||||
wantStatus: payment.ProviderStatusPaid,
|
||||
wantTradeNo: "gateway-123",
|
||||
wantAmount: 12.34,
|
||||
},
|
||||
{
|
||||
name: "waiting trade status with paid numeric status stays pending",
|
||||
body: `{"code":1,"trade_status":"WAITING","status":1,"money":"12.34","trade_no":"gateway-123"}`,
|
||||
wantStatus: payment.ProviderStatusPending,
|
||||
wantTradeNo: "gateway-123",
|
||||
wantAmount: 12.34,
|
||||
},
|
||||
{
|
||||
name: "empty trade status with paid numeric status stays pending",
|
||||
body: `{"code":1,"trade_status":"","status":1,"money":"12.34"}`,
|
||||
wantStatus: payment.ProviderStatusPending,
|
||||
wantTradeNo: orderID,
|
||||
wantAmount: 12.34,
|
||||
},
|
||||
{
|
||||
name: "nested data trade success is paid",
|
||||
body: `{"code":1,"data":{"trade_status":"TRADE_SUCCESS","status":0,"money":"9.99","trade_no":"data-456"}}`,
|
||||
wantStatus: payment.ProviderStatusPaid,
|
||||
wantTradeNo: "data-456",
|
||||
wantAmount: 9.99,
|
||||
},
|
||||
{
|
||||
name: "legacy numeric paid status remains compatible",
|
||||
body: `{"code":1,"status":1,"money":"3.21"}`,
|
||||
wantStatus: payment.ProviderStatusPaid,
|
||||
wantTradeNo: orderID,
|
||||
wantAmount: 3.21,
|
||||
},
|
||||
{
|
||||
name: "legacy numeric non paid status is pending",
|
||||
body: `{"code":1,"status":0,"money":"3.21"}`,
|
||||
wantStatus: payment.ProviderStatusPending,
|
||||
wantTradeNo: orderID,
|
||||
wantAmount: 3.21,
|
||||
},
|
||||
{
|
||||
name: "query failure with missing status is pending",
|
||||
body: `{"code":0,"msg":"订单不存在"}`,
|
||||
wantStatus: payment.ProviderStatusPending,
|
||||
wantTradeNo: orderID,
|
||||
},
|
||||
{
|
||||
name: "missing fields are pending",
|
||||
body: `{}`,
|
||||
wantStatus: payment.ProviderStatusPending,
|
||||
wantTradeNo: orderID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var gotForm url.Values
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("method = %q, want %q", r.Method, http.MethodPost)
|
||||
}
|
||||
if r.URL.Path != "/api.php" {
|
||||
t.Errorf("path = %q, want /api.php", r.URL.Path)
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
t.Errorf("ParseForm: %v", err)
|
||||
}
|
||||
gotForm = make(url.Values, len(r.PostForm))
|
||||
for key, values := range r.PostForm {
|
||||
gotForm[key] = append([]string(nil), values...)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(tt.body))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := newTestEasyPay(t, server.URL)
|
||||
resp, err := provider.QueryOrder(context.Background(), orderID)
|
||||
if err != nil {
|
||||
t.Fatalf("QueryOrder returned error: %v", err)
|
||||
}
|
||||
if resp.Status != tt.wantStatus {
|
||||
t.Fatalf("status = %q, want %q (response=%+v)", resp.Status, tt.wantStatus, resp)
|
||||
}
|
||||
if resp.TradeNo != tt.wantTradeNo {
|
||||
t.Fatalf("trade_no = %q, want %q", resp.TradeNo, tt.wantTradeNo)
|
||||
}
|
||||
if resp.Amount != tt.wantAmount {
|
||||
t.Fatalf("amount = %v, want %v", resp.Amount, tt.wantAmount)
|
||||
}
|
||||
for key, want := range map[string]string{
|
||||
"act": "order",
|
||||
"pid": "pid-1",
|
||||
"key": "pkey-1",
|
||||
"out_trade_no": orderID,
|
||||
} {
|
||||
if got := gotForm.Get(key); got != want {
|
||||
t.Fatalf("form[%s] = %q, want %q (form=%v)", key, got, want, gotForm)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user