mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
feat: add epay return relay and simplify multi-arch builds
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
package response
|
||||
|
||||
type CheckoutCounterResponse struct {
|
||||
TradeId string `json:"trade_id" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"` // epusdt订单号
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,按 system.amount_precision 保留小数 法币金额
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额;status=4 占位订单返回 0
|
||||
Token string `json:"token" example:"USDT"` // 所属币种;status=4 占位订单为空
|
||||
Currency string `json:"currency" example:"CNY"` // 法币币种 CNY USD ...
|
||||
ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址;status=4 占位订单为空
|
||||
Network string `json:"network" example:"tron"` // 网络 TRON ETH;status=4 占位订单为空
|
||||
Status int `json:"status" enums:"1,2,3,4" example:"1"` // 订单状态 1=等待支付 2=支付成功 3=已过期 4=等待选择支付网络/币种;status=4 时前端应引导选择链上 token/network 或 OkPay
|
||||
PaymentType string `json:"payment_type" enums:"gmpay,epay" example:"gmpay"` // 支付接入类型;底层 Epay/Gmpay 转为小写 epay/gmpay 返回
|
||||
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
|
||||
RedirectUrl string `json:"redirect_url" example:"https://example.com/success"`
|
||||
TradeId string `json:"trade_id" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"` // epusdt订单号
|
||||
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,按 system.amount_precision 保留小数 法币金额
|
||||
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额;status=4 占位订单返回 0
|
||||
Token string `json:"token" example:"USDT"` // 所属币种;status=4 占位订单为空
|
||||
Currency string `json:"currency" example:"CNY"` // 法币币种 CNY USD ...
|
||||
ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址;status=4 占位订单为空
|
||||
Network string `json:"network" example:"tron"` // 网络 TRON ETH;status=4 占位订单为空
|
||||
Status int `json:"status" enums:"1,2,3,4" example:"1"` // 订单状态 1=等待支付 2=支付成功 3=已过期 4=等待选择支付网络/币种;status=4 时前端应引导选择链上 token/network 或 OkPay
|
||||
PaymentType string `json:"payment_type" enums:"gmpay,epay" example:"gmpay"` // 支付接入类型;底层 Epay/Gmpay 转为小写 epay/gmpay 返回
|
||||
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
|
||||
RedirectUrl string `json:"redirect_url" example:"https://example.com/success"` // 非 EPay 时为商户原始回跳地址;EPay 时为内部中转地址 /pay/return/{trade_id}
|
||||
PaymentUrl string `json:"payment_url" example:"https://pay.example.com/checkout/3nQ9pL2xV7sK1mR8cT4yB_aZ"` // 支付链接;链上订单为空,OkPay 订单为第三方 payLink
|
||||
CreatedAt int64 `json:"created_at" example:"1713264000"` // 订单创建时间 时间戳
|
||||
IsSelected bool `json:"is_selected" example:"false"` // 是否已选择当前支付方式;status=4 占位订单和刚补全的占位父单为 false
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,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 {
|
||||
|
||||
Reference in New Issue
Block a user