From 04deb819b0e549271d443040b083b2972cbc56a1 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Tue, 2 Jun 2026 14:59:18 +0800 Subject: [PATCH] fix(payment): use trade_status for EasyPay query --- backend/internal/payment/provider/easypay.go | 51 ++++++- .../payment/provider/easypay_query_test.go | 131 ++++++++++++++++++ 2 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 backend/internal/payment/provider/easypay_query_test.go diff --git a/backend/internal/payment/provider/easypay.go b/backend/internal/payment/provider/easypay.go index e7d8aab9..32d6b7be 100644 --- a/backend/internal/payment/provider/easypay.go +++ b/backend/internal/payment/provider/easypay.go @@ -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(), diff --git a/backend/internal/payment/provider/easypay_query_test.go b/backend/internal/payment/provider/easypay_query_test.go new file mode 100644 index 00000000..5042a94d --- /dev/null +++ b/backend/internal/payment/provider/easypay_query_test.go @@ -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) + } + } + }) + } +}