feat: add epay return relay and simplify multi-arch builds

This commit is contained in:
line-6000
2026-06-17 18:01:09 +08:00
parent 75dfeced01
commit 0215b2787e
13 changed files with 697 additions and 79 deletions
+167
View File
@@ -0,0 +1,167 @@
package service
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/GMWalletApp/epusdt/config"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/response"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/sign"
)
type EPayReturnRedirect struct {
TargetURL string
IsMerchantRedirect bool
}
func isEPayOrder(order *mdb.Orders) bool {
return order != nil && strings.EqualFold(order.PaymentType, mdb.PaymentTypeEpay)
}
func buildCheckoutCounterPath(tradeID string) string {
return "/pay/checkout-counter/" + tradeID
}
func buildAbsoluteAppURL(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
base := strings.TrimRight(strings.TrimSpace(config.GetAppUri()), "/")
if base == "" {
return path
}
return base + path
}
func buildEPayReturnPath(tradeID string) string {
return "/pay/return/" + tradeID
}
func buildPublicRedirectURL(order *mdb.Orders) string {
if order == nil {
return ""
}
raw := strings.TrimSpace(order.RedirectUrl)
if raw == "" || !isEPayOrder(order) {
return raw
}
return buildAbsoluteAppURL(buildEPayReturnPath(order.TradeId))
}
func ResolveOrderApiKey(order *mdb.Orders) (*mdb.ApiKey, error) {
if order == nil || order.ApiKeyID == 0 {
return nil, constant.OrderApiKeyUnavailableErr
}
row, err := data.GetApiKeyByID(order.ApiKeyID)
if err != nil {
return nil, err
}
if row == nil || row.ID == 0 || row.Status != mdb.ApiKeyStatusEnable {
return nil, constant.OrderApiKeyUnavailableErr
}
return row, nil
}
func BuildEPayResultParams(order *mdb.Orders, apiKeyRow *mdb.ApiKey) (map[string]string, error) {
if order == nil || apiKeyRow == nil {
return nil, constant.EPayReturnSignatureErr
}
pidInt, err := strconv.Atoi(strings.TrimSpace(apiKeyRow.Pid))
if err != nil {
return nil, constant.EPayReturnSignatureErr
}
notifyData := response.OrderNotifyResponseEpay{
PID: pidInt,
TradeNo: order.TradeId,
OutTradeNo: order.OrderId,
Type: "alipay",
Name: order.Name,
Money: fmt.Sprintf("%.4f", order.Amount),
TradeStatus: "TRADE_SUCCESS",
}
signstr, err := sign.Get(notifyData, apiKeyRow.SecretKey)
if err != nil {
return nil, constant.EPayReturnSignatureErr
}
return map[string]string{
"pid": strconv.Itoa(pidInt),
"trade_no": notifyData.TradeNo,
"out_trade_no": notifyData.OutTradeNo,
"type": notifyData.Type,
"name": notifyData.Name,
"money": notifyData.Money,
"trade_status": notifyData.TradeStatus,
"sign": signstr,
"sign_type": "MD5",
}, nil
}
func appendQueryParams(rawURL string, params map[string]string) (string, error) {
rawURL = strings.TrimSpace(rawURL)
if rawURL == "" {
return "", constant.OrderRedirectURLErr
}
targetURL, err := url.Parse(rawURL)
if err != nil {
return "", constant.OrderRedirectURLErr
}
query := targetURL.Query()
for key, value := range params {
query.Set(key, value)
}
targetURL.RawQuery = query.Encode()
return targetURL.String(), nil
}
func ResolveEPayReturnRedirect(tradeID string) (*EPayReturnRedirect, error) {
order, err := GetOrderInfoByTradeId(tradeID)
if err != nil {
return nil, err
}
if !isEPayOrder(order) || order.Status != mdb.StatusPaySuccess {
return &EPayReturnRedirect{
TargetURL: buildCheckoutCounterPath(order.TradeId),
}, nil
}
rawMerchantURL := strings.TrimSpace(order.RedirectUrl)
if rawMerchantURL == "" {
return nil, constant.OrderRedirectURLErr
}
apiKeyRow, err := ResolveOrderApiKey(order)
if err != nil {
return nil, err
}
params, err := BuildEPayResultParams(order, apiKeyRow)
if err != nil {
return nil, err
}
targetURL, err := appendQueryParams(rawMerchantURL, params)
if err != nil {
return nil, err
}
return &EPayReturnRedirect{
TargetURL: targetURL,
IsMerchantRedirect: true,
}, nil
}
+155
View File
@@ -0,0 +1,155 @@
package service
import (
"net/url"
"testing"
"github.com/GMWalletApp/epusdt/config"
"github.com/GMWalletApp/epusdt/internal/testutil"
"github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/util/constant"
"github.com/GMWalletApp/epusdt/util/sign"
)
func TestBuildPublicRedirectURLRewritesOnlyEpayOrders(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
epayOrder := &mdb.Orders{
TradeId: "trade_epay_redirect",
PaymentType: mdb.PaymentTypeEpay,
RedirectUrl: "https://merchant.example/return",
}
wantRedirect := config.GetAppUri() + "/pay/return/trade_epay_redirect"
if got := buildPublicRedirectURL(epayOrder); got != wantRedirect {
t.Fatalf("epay redirect_url = %q, want internal return route", got)
}
gmpayOrder := &mdb.Orders{
TradeId: "trade_gmpay_redirect",
PaymentType: mdb.PaymentTypeGmpay,
RedirectUrl: "https://merchant.example/return",
}
if got := buildPublicRedirectURL(gmpayOrder); got != "https://merchant.example/return" {
t.Fatalf("gmpay redirect_url = %q, want merchant raw redirect_url", got)
}
if got := buildPublicRedirectURL(&mdb.Orders{TradeId: "trade_empty", PaymentType: mdb.PaymentTypeEpay}); got != "" {
t.Fatalf("empty redirect_url = %q, want empty", got)
}
}
func TestBuildEPayResultParamsMatchesLegacyFields(t *testing.T) {
params, err := BuildEPayResultParams(&mdb.Orders{
TradeId: "trade_epay_params",
OrderId: "order_epay_params",
Name: "VIP",
Amount: 1,
}, &mdb.ApiKey{
Pid: "1001",
SecretKey: "epay-secret",
})
if err != nil {
t.Fatalf("BuildEPayResultParams(): %v", err)
}
expected := map[string]string{
"pid": "1001",
"trade_no": "trade_epay_params",
"out_trade_no": "order_epay_params",
"type": "alipay",
"name": "VIP",
"money": "1.0000",
"trade_status": "TRADE_SUCCESS",
"sign_type": "MD5",
}
for key, want := range expected {
if got := params[key]; got != want {
t.Fatalf("%s = %q, want %q", key, got, want)
}
}
signParams := map[string]interface{}{
"pid": params["pid"],
"trade_no": params["trade_no"],
"out_trade_no": params["out_trade_no"],
"type": params["type"],
"name": params["name"],
"money": params["money"],
"trade_status": params["trade_status"],
}
wantSign, err := sign.Get(signParams, "epay-secret")
if err != nil {
t.Fatalf("sign.Get(): %v", err)
}
if got := params["sign"]; got != wantSign {
t.Fatalf("sign = %q, want %q", got, wantSign)
}
}
func TestBuildEPayResultParamsRejectsNonNumericPid(t *testing.T) {
_, err := BuildEPayResultParams(&mdb.Orders{
TradeId: "trade_bad_pid",
OrderId: "order_bad_pid",
Name: "VIP",
Amount: 1,
}, &mdb.ApiKey{
Pid: "not-a-number",
SecretKey: "epay-secret",
})
if err != constant.EPayReturnSignatureErr {
t.Fatalf("error = %v, want %v", err, constant.EPayReturnSignatureErr)
}
}
func TestResolveOrderApiKeyRejectsUnavailableRows(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if _, err := ResolveOrderApiKey(&mdb.Orders{TradeId: "missing_api_key_id"}); err != constant.OrderApiKeyUnavailableErr {
t.Fatalf("missing api_key_id error = %v, want %v", err, constant.OrderApiKeyUnavailableErr)
}
row := &mdb.ApiKey{
Name: "disabled",
Pid: "1201",
SecretKey: "disabled-secret",
Status: mdb.ApiKeyStatusDisable,
}
if err := dao.Mdb.Create(row).Error; err != nil {
t.Fatalf("create disabled api key: %v", err)
}
if _, err := ResolveOrderApiKey(&mdb.Orders{TradeId: "disabled_api_key", ApiKeyID: row.ID}); err != constant.OrderApiKeyUnavailableErr {
t.Fatalf("disabled api key error = %v, want %v", err, constant.OrderApiKeyUnavailableErr)
}
}
func TestAppendQueryParamsPreservesMerchantQuery(t *testing.T) {
target, err := appendQueryParams("https://merchant.example/return?from=merchant", map[string]string{
"pid": "1001",
"trade_no": "trade_epay_redirect",
"trade_status": "TRADE_SUCCESS",
})
if err != nil {
t.Fatalf("appendQueryParams(): %v", err)
}
parsed, err := url.Parse(target)
if err != nil {
t.Fatalf("url.Parse(): %v", err)
}
query := parsed.Query()
if got := query.Get("from"); got != "merchant" {
t.Fatalf("from = %q, want merchant", got)
}
if got := query.Get("pid"); got != "1001" {
t.Fatalf("pid = %q, want 1001", got)
}
if got := query.Get("trade_no"); got != "trade_epay_redirect" {
t.Fatalf("trade_no = %q, want trade_epay_redirect", got)
}
if got := query.Get("trade_status"); got != "TRADE_SUCCESS" {
t.Fatalf("trade_status = %q, want TRADE_SUCCESS", got)
}
}
+2 -2
View File
@@ -706,7 +706,7 @@ func completeWaitSelectOrder(parent *mdb.Orders, token string, network string) (
func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse {
paymentType := mdb.PaymentTypeGmpay
if strings.EqualFold(order.PaymentType, mdb.PaymentTypeEpay) {
if isEPayOrder(order) {
paymentType = mdb.PaymentTypeEpay
}
return &response.CheckoutCounterResponse{
@@ -720,7 +720,7 @@ func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse
Status: order.Status,
PaymentType: strings.ToLower(paymentType),
ExpirationTime: order.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
RedirectUrl: order.RedirectUrl,
RedirectUrl: buildPublicRedirectURL(order),
CreatedAt: order.CreatedAt.TimestampMilli(),
IsSelected: order.IsSelected,
}
+1 -23
View File
@@ -1,9 +1,6 @@
package service
import (
"strings"
"github.com/GMWalletApp/epusdt/config"
"github.com/GMWalletApp/epusdt/model/data"
"github.com/GMWalletApp/epusdt/model/mdb"
"github.com/GMWalletApp/epusdt/model/response"
@@ -23,26 +20,7 @@ func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterRespo
if orderInfo.ID <= 0 {
return nil, ErrOrder
}
paymentType := mdb.PaymentTypeGmpay
if strings.EqualFold(orderInfo.PaymentType, mdb.PaymentTypeEpay) {
paymentType = mdb.PaymentTypeEpay
}
resp := &response.CheckoutCounterResponse{
TradeId: orderInfo.TradeId,
Amount: orderInfo.Amount,
ActualAmount: orderInfo.ActualAmount,
Token: orderInfo.Token,
Currency: orderInfo.Currency,
ReceiveAddress: orderInfo.ReceiveAddress,
Network: orderInfo.Network,
Status: orderInfo.Status,
PaymentType: strings.ToLower(paymentType),
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
RedirectUrl: orderInfo.RedirectUrl,
CreatedAt: orderInfo.CreatedAt.TimestampMilli(),
IsSelected: orderInfo.IsSelected,
}
resp := buildCheckoutResponse(orderInfo)
if orderInfo.PayProvider == mdb.PaymentProviderOkPay {
providerRow, rowErr := data.GetProviderOrderByTradeIDAndProvider(orderInfo.TradeId, mdb.PaymentProviderOkPay)
if rowErr != nil {