mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 10:16:15 +00:00
fix(epay): restrict submit type to alipay or supported selectors
This commit is contained in:
@@ -105,9 +105,9 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
|||||||
// the canonical form — the GET variant is identical save the transport.
|
// the canonical form — the GET variant is identical save the transport.
|
||||||
// @Summary Create transaction and redirect (EPAY compat)
|
// @Summary Create transaction and redirect (EPAY compat)
|
||||||
// @Description Legacy EPAY-style endpoint. Accepts GET (querystring) and POST (form). On success, 302 redirects to /pay/checkout-counter/{trade_id}. Signature uses MD5 of sorted params + secret_key of the api_keys row matching the submitted pid.
|
// @Description Legacy EPAY-style endpoint. Accepts GET (querystring) and POST (form). On success, 302 redirects to /pay/checkout-counter/{trade_id}. Signature uses MD5 of sorted params + secret_key of the api_keys row matching the submitted pid.
|
||||||
// @Description After signature verification, token/network resolution is: supported type=token.network selector (for example usdt.tron) first; otherwise request token/network; otherwise epay.default_token / epay.default_network. If token and network are still both empty, the order is created as status=4 placeholder. Supplying only one of token/network remains invalid.
|
// @Description After signature verification, type accepts only either alipay or a supported type=token.network selector (for example usdt.tron). Token/network resolution is: supported selector first; otherwise request token/network; otherwise epay.default_token / epay.default_network. If token and network are still both empty, the order is created as status=4 placeholder. Supplying only one of token/network remains invalid.
|
||||||
// @Description Currency resolution is unchanged: request currency -> epay.default_currency -> cny. Supported type selectors bypass only token/network defaults, not currency fallback.
|
// @Description Currency resolution is unchanged: request currency -> epay.default_currency -> cny. Supported type selectors bypass only token/network defaults, not currency fallback.
|
||||||
// @Description Success return/notify currently use the stored request type when present, and fall back to alipay only when type was missing. The server injects internal payment_type=Epay after EPay signature verification; merchants do not send GMPay payment_type to this endpoint.
|
// @Description Success return/notify reuse the stored request type. On this branch that means either alipay or a supported token.network selector; when the request omitted type, outbound fallback remains alipay. The server injects internal payment_type=Epay after EPay signature verification; merchants do not send GMPay payment_type to this endpoint.
|
||||||
// @Tags Payment
|
// @Tags Payment
|
||||||
// @Accept x-www-form-urlencoded
|
// @Accept x-www-form-urlencoded
|
||||||
// @Produce html
|
// @Produce html
|
||||||
@@ -117,7 +117,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
|||||||
// @Param notify_url query string false "Callback URL (GET query)"
|
// @Param notify_url query string false "Callback URL (GET query)"
|
||||||
// @Param return_url query string false "Redirect URL after payment (GET query)"
|
// @Param return_url query string false "Redirect URL after payment (GET query)"
|
||||||
// @Param name query string false "Order name (GET query)"
|
// @Param name query string false "Order name (GET query)"
|
||||||
// @Param type query string false "Opaque merchant type or supported token.network selector such as alipay or usdt.tron (GET query)"
|
// @Param type query string false "Either alipay or a supported token.network selector such as usdt.tron (GET query)"
|
||||||
// @Param sign query string false "MD5 signature (GET query)"
|
// @Param sign query string false "MD5 signature (GET query)"
|
||||||
// @Param sign_type query string false "Signature type (MD5, GET query)"
|
// @Param sign_type query string false "Signature type (MD5, GET query)"
|
||||||
// @Param pid formData integer true "API key PID"
|
// @Param pid formData integer true "API key PID"
|
||||||
@@ -126,7 +126,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
|
|||||||
// @Param notify_url formData string true "Callback URL"
|
// @Param notify_url formData string true "Callback URL"
|
||||||
// @Param return_url formData string false "Redirect URL after payment"
|
// @Param return_url formData string false "Redirect URL after payment"
|
||||||
// @Param name formData string false "Order name"
|
// @Param name formData string false "Order name"
|
||||||
// @Param type formData string false "Opaque merchant type or supported token.network selector such as alipay or usdt.tron"
|
// @Param type formData string false "Either alipay or a supported token.network selector such as usdt.tron"
|
||||||
// @Param sign formData string true "MD5 signature"
|
// @Param sign formData string true "MD5 signature"
|
||||||
// @Param sign_type formData string false "Signature type (MD5)"
|
// @Param sign_type formData string false "Signature type (MD5)"
|
||||||
// @Success 302 "Redirect to checkout counter"
|
// @Success 302 "Redirect to checkout counter"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
|
|||||||
// Non-EPay or not-yet-paid orders are sent back to the checkout counter.
|
// Non-EPay or not-yet-paid orders are sent back to the checkout counter.
|
||||||
// @Summary Return to merchant (EPAY compat)
|
// @Summary Return to merchant (EPAY compat)
|
||||||
// @Description Browser-facing success return hop for EPay orders. Paid EPay orders are redirected to the merchant return_url with signed legacy EPay query params. Orders that are not EPay or not yet paid are redirected back to the checkout counter.
|
// @Description Browser-facing success return hop for EPay orders. Paid EPay orders are redirected to the merchant return_url with signed legacy EPay query params. Orders that are not EPay or not yet paid are redirected back to the checkout counter.
|
||||||
// @Description The signed query params currently use the stored request type when present; otherwise type=alipay is returned for compatibility.
|
// @Description The signed query params reuse the stored request type. On this branch that means either alipay or a supported token.network selector; if the original request omitted type, type=alipay is returned for compatibility.
|
||||||
// @Description This route also returns explicit business errors when the merchant return_url is missing, the order API key is unavailable, or EPay signature construction fails.
|
// @Description This route also returns explicit business errors when the merchant return_url is missing, the order API key is unavailable, or EPay signature construction fails.
|
||||||
// @Tags Payment
|
// @Tags Payment
|
||||||
// @Produce html
|
// @Produce html
|
||||||
|
|||||||
+7
-21
@@ -5,7 +5,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -631,7 +630,7 @@ func TestSendOrderCallbackEpayUsesApiKeySecretByPid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendOrderCallbackEpayPreservesStoredTypeValues(t *testing.T) {
|
func TestSendOrderCallbackEpayPreservesStoredAlipayType(t *testing.T) {
|
||||||
cleanup := testutil.SetupTestDatabases(t)
|
cleanup := testutil.SetupTestDatabases(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@@ -645,17 +644,6 @@ func TestSendOrderCallbackEpayPreservesStoredTypeValues(t *testing.T) {
|
|||||||
t.Fatalf("create epay api key: %v", err)
|
t.Fatalf("create epay api key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
epayType string
|
|
||||||
}{
|
|
||||||
{name: "alipay", epayType: "alipay"},
|
|
||||||
{name: "non_selector", epayType: "usdt-tron"},
|
|
||||||
{name: "unsupported_selector", epayType: "usdc.tron"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for idx, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
formPayload := map[string]string{}
|
formPayload := map[string]string{}
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
@@ -672,8 +660,8 @@ func TestSendOrderCallbackEpayPreservesStoredTypeValues(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
order := &mdb.Orders{
|
order := &mdb.Orders{
|
||||||
TradeId: "trade_epay_type_case_" + strconv.Itoa(idx),
|
TradeId: "trade_epay_type_case_alipay",
|
||||||
OrderId: "order_epay_type_case_" + strconv.Itoa(idx),
|
OrderId: "order_epay_type_case_alipay",
|
||||||
Amount: 1,
|
Amount: 1,
|
||||||
Currency: "CNY",
|
Currency: "CNY",
|
||||||
ActualAmount: 1,
|
ActualAmount: 1,
|
||||||
@@ -682,8 +670,8 @@ func TestSendOrderCallbackEpayPreservesStoredTypeValues(t *testing.T) {
|
|||||||
Name: "VIP",
|
Name: "VIP",
|
||||||
Status: mdb.StatusPaySuccess,
|
Status: mdb.StatusPaySuccess,
|
||||||
NotifyUrl: server.URL,
|
NotifyUrl: server.URL,
|
||||||
BlockTransactionId: "block_epay_type_case_" + strconv.Itoa(idx),
|
BlockTransactionId: "block_epay_type_case_alipay",
|
||||||
EpayType: tc.epayType,
|
EpayType: "alipay",
|
||||||
PaymentType: "epay",
|
PaymentType: "epay",
|
||||||
ApiKeyID: key.ID,
|
ApiKeyID: key.ID,
|
||||||
}
|
}
|
||||||
@@ -691,8 +679,8 @@ func TestSendOrderCallbackEpayPreservesStoredTypeValues(t *testing.T) {
|
|||||||
if err := sendOrderCallback(order); err != nil {
|
if err := sendOrderCallback(order); err != nil {
|
||||||
t.Fatalf("send epay callback: %v", err)
|
t.Fatalf("send epay callback: %v", err)
|
||||||
}
|
}
|
||||||
if got := formPayload["type"]; got != tc.epayType {
|
if got := formPayload["type"]; got != "alipay" {
|
||||||
t.Fatalf("type = %q, want %q", got, tc.epayType)
|
t.Fatalf("type = %q, want alipay", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
signParams := map[string]interface{}{
|
signParams := map[string]interface{}{
|
||||||
@@ -711,8 +699,6 @@ func TestSendOrderCallbackEpayPreservesStoredTypeValues(t *testing.T) {
|
|||||||
if got := formPayload["sign"]; got != calcSig {
|
if got := formPayload["sign"]; got != calcSig {
|
||||||
t.Fatalf("sign = %q, want %q", got, calcSig)
|
t.Fatalf("sign = %q, want %q", got, calcSig)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDispatchPendingCallbacksEpayAcceptsSuccessAck(t *testing.T) {
|
func TestDispatchPendingCallbacksEpayAcceptsSuccessAck(t *testing.T) {
|
||||||
|
|||||||
+6
-2
@@ -23,8 +23,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// resolveEPayTypeSelector only claims the type as a selector when it maps to a
|
// resolveEPayTypeSelector only claims the type as a selector when it maps to a
|
||||||
// currently supported payment asset; otherwise the caller should keep the
|
// currently supported payment asset. Non-selector values are handled by the
|
||||||
// legacy token/network/default resolution and treat type as opaque.
|
// caller, which now only accepts empty type or alipay before falling back to
|
||||||
|
// request token/network/default resolution.
|
||||||
func resolveEPayTypeSelector(rawType string) (string, string, bool, error) {
|
func resolveEPayTypeSelector(rawType string) (string, string, bool, error) {
|
||||||
rawType = strings.TrimSpace(rawType)
|
rawType = strings.TrimSpace(rawType)
|
||||||
if rawType == "" || strings.Count(rawType, ".") != 1 {
|
if rawType == "" || strings.Count(rawType, ".") != 1 {
|
||||||
@@ -168,6 +169,9 @@ func RegisterRoute(e *echo.Echo) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return comm.Ctrl.FailJson(ctx, constant.SystemErr)
|
return comm.Ctrl.FailJson(ctx, constant.SystemErr)
|
||||||
}
|
}
|
||||||
|
if epayType != "" && !selectorMatched && !strings.EqualFold(epayType, "alipay") {
|
||||||
|
return comm.Ctrl.FailJson(ctx, constant.ParamsMarshalErr)
|
||||||
|
}
|
||||||
token := strings.TrimSpace(getString(params, "token"))
|
token := strings.TrimSpace(getString(params, "token"))
|
||||||
network := strings.TrimSpace(getString(params, "network"))
|
network := strings.TrimSpace(getString(params, "network"))
|
||||||
if selectorMatched {
|
if selectorMatched {
|
||||||
|
|||||||
+51
-86
@@ -1471,7 +1471,46 @@ func TestEpaySubmitPhpWithoutTokenNetworkDefaultsCreatesPlaceholder(t *testing.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEpaySubmitPhpNonSelectorTypeKeepsOldLogic(t *testing.T) {
|
func TestEpaySubmitPhpWithoutTypeKeepsCurrentBehavior(t *testing.T) {
|
||||||
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_token: %v", err)
|
||||||
|
}
|
||||||
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultNetwork, "tron", mdb.SettingTypeString); err != nil {
|
||||||
|
t.Fatalf("seed epay.default_network: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := signEpayValues(url.Values{
|
||||||
|
"pid": {"1"},
|
||||||
|
"name": {"epay-empty-type-001"},
|
||||||
|
"money": {"1.00"},
|
||||||
|
"out_trade_no": {"epay-empty-type-001"},
|
||||||
|
"notify_url": {"https://93.184.216.34/notify"},
|
||||||
|
"return_url": {"http://localhost/return"},
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/payments/epay/v1/order/create-transaction/submit.php?"+values.Encode(), nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusFound {
|
||||||
|
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
||||||
|
order, err := data.GetOrderInfoByTradeId(tradeID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reload epay empty-type order: %v", err)
|
||||||
|
}
|
||||||
|
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
||||||
|
t.Fatalf("empty-type order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
||||||
|
}
|
||||||
|
if order.EpayType != "" {
|
||||||
|
t.Fatalf("epay_type = %q, want empty", order.EpayType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEpaySubmitPhpRejectsNonSelectorType(t *testing.T) {
|
||||||
e := setupTestEnv(t)
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
||||||
@@ -1495,23 +1534,16 @@ func TestEpaySubmitPhpNonSelectorTypeKeepsOldLogic(t *testing.T) {
|
|||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
e.ServeHTTP(rec, req)
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
if rec.Code != http.StatusFound {
|
if rec.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String())
|
||||||
}
|
}
|
||||||
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
resp := parseResp(t, rec)
|
||||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
if got := int(resp["status_code"].(float64)); got != 10009 {
|
||||||
if err != nil {
|
t.Fatalf("status_code = %d, want 10009", got)
|
||||||
t.Fatalf("reload epay non-selector order: %v", err)
|
|
||||||
}
|
|
||||||
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
|
||||||
t.Fatalf("non-selector order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
|
||||||
}
|
|
||||||
if order.EpayType != "usdt-tron" {
|
|
||||||
t.Fatalf("epay_type = %q, want usdt-tron", order.EpayType)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEpaySubmitPhpUnsupportedSelectorFallsBackToOldLogic(t *testing.T) {
|
func TestEpaySubmitPhpRejectsUnsupportedSelectorType(t *testing.T) {
|
||||||
e := setupTestEnv(t)
|
e := setupTestEnv(t)
|
||||||
|
|
||||||
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
if err := data.SetSetting(mdb.SettingGroupEpay, mdb.SettingKeyEpayDefaultToken, "usdt", mdb.SettingTypeString); err != nil {
|
||||||
@@ -1535,19 +1567,12 @@ func TestEpaySubmitPhpUnsupportedSelectorFallsBackToOldLogic(t *testing.T) {
|
|||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
e.ServeHTTP(rec, req)
|
e.ServeHTTP(rec, req)
|
||||||
|
|
||||||
if rec.Code != http.StatusFound {
|
if rec.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("expected 302, got %d body=%s", rec.Code, rec.Body.String())
|
t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String())
|
||||||
}
|
}
|
||||||
tradeID := strings.TrimPrefix(rec.Header().Get("Location"), "/pay/checkout-counter/")
|
resp := parseResp(t, rec)
|
||||||
order, err := data.GetOrderInfoByTradeId(tradeID)
|
if got := int(resp["status_code"].(float64)); got != 10009 {
|
||||||
if err != nil {
|
t.Fatalf("status_code = %d, want 10009", got)
|
||||||
t.Fatalf("reload epay unsupported selector order: %v", err)
|
|
||||||
}
|
|
||||||
if order.Token != "USDT" || order.Network != mdb.NetworkTron || order.ReceiveAddress != "TTestTronAddress001" {
|
|
||||||
t.Fatalf("unsupported selector order fields = token %q network %q address %q", order.Token, order.Network, order.ReceiveAddress)
|
|
||||||
}
|
|
||||||
if order.EpayType != "usdc.tron" {
|
|
||||||
t.Fatalf("epay_type = %q, want usdc.tron", order.EpayType)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1897,66 +1922,6 @@ func TestPayReturn_PaidEpayRedirectsWithOriginalType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPayReturn_PaidEpayRedirectsWithNonSelectorType(t *testing.T) {
|
|
||||||
e := setupTestEnv(t)
|
|
||||||
tradeID := mustCreateEPayOrder(t, e, "epay-return-non-selector-001", "https://merchant.example/return", url.Values{
|
|
||||||
"type": {"usdt-tron"},
|
|
||||||
"token": {"usdt"},
|
|
||||||
"network": {"tron"},
|
|
||||||
"currency": {"cny"},
|
|
||||||
})
|
|
||||||
if err := dao.Mdb.Model(&mdb.Orders{}).
|
|
||||||
Where("trade_id = ?", tradeID).
|
|
||||||
Update("status", mdb.StatusPaySuccess).Error; err != nil {
|
|
||||||
t.Fatalf("mark order paid: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusFound {
|
|
||||||
t.Fatalf("expected 302, got %d: %s", rec.Code, rec.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
targetURL, err := url.Parse(rec.Header().Get("Location"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse redirect location: %v", err)
|
|
||||||
}
|
|
||||||
if got := targetURL.Query().Get("type"); got != "usdt-tron" {
|
|
||||||
t.Fatalf("type = %q, want usdt-tron", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPayReturn_PaidEpayRedirectsWithUnsupportedSelectorType(t *testing.T) {
|
|
||||||
e := setupTestEnv(t)
|
|
||||||
tradeID := mustCreateEPayOrder(t, e, "epay-return-unsel-001", "https://merchant.example/return", url.Values{
|
|
||||||
"type": {"usdc.tron"},
|
|
||||||
"token": {"usdt"},
|
|
||||||
"network": {"tron"},
|
|
||||||
"currency": {"cny"},
|
|
||||||
})
|
|
||||||
if err := dao.Mdb.Model(&mdb.Orders{}).
|
|
||||||
Where("trade_id = ?", tradeID).
|
|
||||||
Update("status", mdb.StatusPaySuccess).Error; err != nil {
|
|
||||||
t.Fatalf("mark order paid: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/pay/return/"+tradeID, nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusFound {
|
|
||||||
t.Fatalf("expected 302, got %d: %s", rec.Code, rec.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
targetURL, err := url.Parse(rec.Header().Get("Location"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse redirect location: %v", err)
|
|
||||||
}
|
|
||||||
if got := targetURL.Query().Get("type"); got != "usdc.tron" {
|
|
||||||
t.Fatalf("type = %q, want usdc.tron", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPayReturn_UnpaidEpayRedirectsBackToCheckout(t *testing.T) {
|
func TestPayReturn_UnpaidEpayRedirectsBackToCheckout(t *testing.T) {
|
||||||
e := setupTestEnv(t)
|
e := setupTestEnv(t)
|
||||||
tradeID := mustCreateEPayOrder(t, e, "epay-return-unpaid-001", "https://merchant.example/return", url.Values{
|
tradeID := mustCreateEPayOrder(t, e, "epay-return-unpaid-001", "https://merchant.example/return", url.Values{
|
||||||
|
|||||||
+14
-9
@@ -441,9 +441,9 @@ function epaySign(array $params, string $secretKey): string
|
|||||||
| `notify_url` | query/form | string | 是 | 异步回调地址。 |
|
| `notify_url` | query/form | string | 是 | 异步回调地址。 |
|
||||||
| `return_url` | query/form | string | 否 | 支付完成后的同步跳转地址。 |
|
| `return_url` | query/form | string | 否 | 支付完成后的同步跳转地址。 |
|
||||||
| `name` | query/form | string | 否 | 商品/订单名称。 |
|
| `name` | query/form | string | 否 | 商品/订单名称。 |
|
||||||
| `type` | query/form | string | 否 | 兼容字段,如 `alipay`。创建订单时不决定实际链上币种。 |
|
| `type` | query/form | string | 否 | 仅支持空值、`alipay`,或当前已启用并可收款的 `token.network` selector(如 `usdt.tron`)。推荐使用小写 `alipay`。 |
|
||||||
| `token` | query/form | string | 否 | 可选收款币种。优先级高于后台 `epay.default_token`;传了就必须参与 EPay 签名。 |
|
| `token` | query/form | string | 否 | 可选收款币种。仅在 `type` 不是命中的 selector 时参与解析;传了就必须参与 EPay 签名。 |
|
||||||
| `network` | query/form | string | 否 | 可选收款网络。优先级高于后台 `epay.default_network`;传了就必须参与 EPay 签名。 |
|
| `network` | query/form | string | 否 | 可选收款网络。仅在 `type` 不是命中的 selector 时参与解析;传了就必须参与 EPay 签名。 |
|
||||||
| `currency` | query/form | string | 否 | 可选法币币种。优先级高于后台 `epay.default_currency`;传了就必须参与 EPay 签名。 |
|
| `currency` | query/form | string | 否 | 可选法币币种。优先级高于后台 `epay.default_currency`;传了就必须参与 EPay 签名。 |
|
||||||
| `sign` | query/form | string | 是 | EPay 签名。 |
|
| `sign` | query/form | string | 是 | EPay 签名。 |
|
||||||
| `sign_type` | query/form | string | 否 | 通常为 `MD5`。 |
|
| `sign_type` | query/form | string | 否 | 通常为 `MD5`。 |
|
||||||
@@ -466,13 +466,16 @@ money=100&name=VIP¬ify_url=https://merchant.example/notify&out_trade_no=ORD20
|
|||||||
sign=b865b0acbb2b01554c35a1bd33351452
|
sign=b865b0acbb2b01554c35a1bd33351452
|
||||||
```
|
```
|
||||||
|
|
||||||
EPay 接口解析 `token/network/currency` 的优先级:
|
EPay 接口解析 `type/token/network/currency` 的规则:
|
||||||
|
|
||||||
- `token`:请求参数 `token` > 数据库 `epay.default_token` > 空。
|
- `type` 只接受三类输入:空值、`alipay`、命中的 `token.network` selector。
|
||||||
- `network`:请求参数 `network` > 数据库 `epay.default_network` > 空。
|
- `type=token.network` 且命中当前已启用支付资产时,会直接确定本次订单的 `token/network`,并覆盖请求参数里的 `token/network` 以及后台 `epay.default_token` / `epay.default_network`。
|
||||||
- `currency`:请求参数 `currency` > 数据库 `epay.default_currency` > `cny`。
|
- `type` 非空但既不是命中的 selector,也不是 `alipay` 时,直接返回 `10009 invalid params`。例如 `usdt-tron`、未启用的 `usdc.tron` 都会被拒绝。
|
||||||
- 最终 `token/network` 同时有值时,创建具体链上订单;同时为空时,创建状态 `4` 占位订单;只缺一个时返回参数错误。
|
- `type` 为空或为 `alipay` 时,`token/network` 继续走原有解析:先看请求参数,再分别用数据库 `epay.default_token` / `epay.default_network` 补齐。
|
||||||
- 服务端会在 EPay 签名校验通过后内部注入 `payment_type=Epay`,该字段不参与 EPay 入站签名;但请求里显式传入的 `token/network/currency` 属于原始 EPay 参数,必须参与签名。
|
- `currency` 解析不受 selector 影响:请求参数 `currency` > 数据库 `epay.default_currency` > `cny`。
|
||||||
|
- 最终解析结果里,`token/network` 同时有值时创建具体链上订单;同时为空时创建状态 `4` 占位订单;最终只缺一个时返回参数错误。
|
||||||
|
- 这意味着“请求里只传了一个值”不一定报错;如果另一个值能被 default 补齐,仍会成功。只有最终解析后仍然只剩一个值,才返回 `10009`。
|
||||||
|
- 服务端会在 EPay 签名校验通过后内部注入 `payment_type=Epay`,该字段不参与 EPay 入站签名;但请求里显式传入的 `type/token/network/currency` 仍属于原始 EPay 参数,必须参与签名。
|
||||||
|
|
||||||
后台默认配置可通过 `/payments/gmpay/v1/config` 的 `epay` 字段查看;新安装默认只预置 `epay.default_currency=cny`,`epay.default_token` 和 `epay.default_network` 为空,因此 EPay 未显式传 token/network 时会创建状态 `4` 占位订单。已有数据库的配置不会被 seed 覆盖,删除或置空 `epay.default_token` 和 `epay.default_network` 后,这两个字段会返回空字符串。
|
后台默认配置可通过 `/payments/gmpay/v1/config` 的 `epay` 字段查看;新安装默认只预置 `epay.default_currency=cny`,`epay.default_token` 和 `epay.default_network` 为空,因此 EPay 未显式传 token/network 时会创建状态 `4` 占位订单。已有数据库的配置不会被 seed 覆盖,删除或置空 `epay.default_token` 和 `epay.default_network` 后,这两个字段会返回空字符串。
|
||||||
|
|
||||||
@@ -519,6 +522,8 @@ GMPay 回调验签方式与创建订单一致,但排除 `signature` 字段。
|
|||||||
通过 EPay 兼容接口创建的订单,会使用 GET 请求回调 `notify_url`,参数如下:
|
通过 EPay 兼容接口创建的订单,会使用 GET 请求回调 `notify_url`,参数如下:
|
||||||
|
|
||||||
> EPay 回调会把 `pid` 输出为数字;使用 EPay 兼容接口或 `payment_type=Epay` 时,请确保 API Key 的 PID 是数字。
|
> EPay 回调会把 `pid` 输出为数字;使用 EPay 兼容接口或 `payment_type=Epay` 时,请确保 API Key 的 PID 是数字。
|
||||||
|
>
|
||||||
|
> `type` 出站时使用订单里保存的请求值。当前主分支正常入站能保存下来的只会是 `alipay` 或命中的 `token.network` selector;如果入站请求没传 `type`,出站才回退为 `alipay`。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
pid=1000
|
pid=1000
|
||||||
|
|||||||
Reference in New Issue
Block a user