feat(payment): support placeholder orders for GMPay and EPay

This commit is contained in:
line-6000
2026-06-16 17:57:20 +08:00
parent 04c2b9b0b6
commit 3ab3659fe4
27 changed files with 1755 additions and 134 deletions
+11 -3
View File
@@ -112,7 +112,7 @@ func (c *BaseAdminController) CloseOrder(ctx echo.Context) error {
if order.ID == 0 {
return c.FailJson(ctx, constant.OrderNotExists)
}
if order.Status != mdb.StatusWaitPay {
if order.Status != mdb.StatusWaitPay && order.Status != mdb.StatusWaitSelect {
return c.FailJson(ctx, constant.OrderNotWaitPay)
}
ok, err := data.CloseOrderManually(tradeID)
@@ -122,8 +122,16 @@ func (c *BaseAdminController) CloseOrder(ctx echo.Context) error {
if !ok {
return c.FailJson(ctx, constant.OrderStatusConflict)
}
// Release the transaction lock so the amount slot becomes reusable.
_ = data.UnLockTransaction(order.Network, order.ReceiveAddress, order.Token, order.ActualAmount)
// Release only concrete on-chain locks. Placeholder orders and provider-only
// orders do not reserve a local wallet amount.
if order.Status == mdb.StatusWaitPay &&
(strings.TrimSpace(order.PayProvider) == "" || order.PayProvider == mdb.PaymentProviderOnChain) &&
strings.TrimSpace(order.Network) != "" &&
strings.TrimSpace(order.ReceiveAddress) != "" &&
strings.TrimSpace(order.Token) != "" &&
order.ActualAmount > 0 {
_ = data.UnLockTransaction(order.Network, order.ReceiveAddress, order.Token, order.ActualAmount)
}
return c.SucJson(ctx, nil)
}
+4 -4
View File
@@ -26,9 +26,9 @@ import (
// rate.okx_c2c_enabled (bool) — use OKX C2C rate feed
//
// - group=epay:
// epay.default_token (string) — token for EPAY orders, e.g. "usdt" (default)
// epay.default_currency (string) — fiat currency for EPAY orders, e.g. "cny" (default)
// epay.default_network (string) — blockchain network for EPAY orders, e.g. "tron" or "ton" (default "tron")
// epay.default_token (string) — default token for EPAY submit.php, e.g. "usdt"; empty allows status=4 placeholders when request token/network are also absent
// epay.default_currency (string) — default fiat currency for EPAY submit.php, e.g. "cny"; empty falls back to cny
// epay.default_network (string) — default network for EPAY submit.php, e.g. "tron" or "ton"; empty allows status=4 placeholders when request token/network are also absent
//
// - group=okpay:
// okpay.enabled (bool) — enable OkPay as a switch-network payment option
@@ -95,7 +95,7 @@ func (c *BaseAdminController) ListSettings(ctx echo.Context) error {
// @Summary Upsert settings
// @Description Batch insert/update settings. Returns per-key status; failed items include error_code for frontend i18n.
// @Description Supported groups: brand, rate, system, epay, okpay.
// @Description epay group keys: epay.default_token (e.g. "usdt" or "ton"), epay.default_currency (e.g. "cny"), epay.default_network (e.g. "tron" or "ton").
// @Description epay group keys: epay.default_token (e.g. "usdt" or "ton", empty allows status=4 placeholders), epay.default_currency (e.g. "cny", empty falls back to cny), epay.default_network (e.g. "tron" or "ton", empty allows status=4 placeholders).
// @Description okpay group keys: okpay.enabled, okpay.shop_id, okpay.shop_token, okpay.api_url, okpay.callback_url, okpay.return_url, okpay.timeout_seconds, okpay.allow_tokens.
// @Description rate group keys: rate.forced_rate_list (JSON map, e.g. {"cny":{"usdt":0.14635,"ton":0.5}}; base/coin keys are normalized to lowercase), rate.api_url, rate.adjust_percent, rate.okx_c2c_enabled.
// @Description brand group keys: brand.checkout_name, brand.logo_url, brand.site_title, brand.success_copy, brand.support_url, brand.background_color, brand.background_image_url. Legacy aliases brand.site_name, brand.page_title and brand.pay_success_text are also supported.
+11 -9
View File
@@ -25,6 +25,8 @@ func apiKeyFromContext(ctx echo.Context) *mdb.ApiKey {
// CreateTransaction 创建交易
// @Summary Create transaction
// @Description Create a payment transaction order. Accepts JSON body (application/json) or form-encoded body (application/x-www-form-urlencoded).
// @Description GMPay may omit both token and network to create a status=4 placeholder order; EPay submit.php can also create one when neither request parameters nor database defaults provide token/network. Supplying only one of token/network is invalid.
// @Description payment_type is optional for GMPay. If it is sent, it is a normal signed parameter and must be included when calculating signature.
// @Tags Payment
// @Accept json
// @Accept x-www-form-urlencoded
@@ -32,14 +34,14 @@ func apiKeyFromContext(ctx echo.Context) *mdb.ApiKey {
// @Param request body request.CreateTransactionRequest false "Transaction payload (JSON)"
// @Param order_id formData string false "Merchant order ID"
// @Param currency formData string false "Fiat currency (e.g. cny)"
// @Param token formData string false "Crypto token (e.g. TON, USDT)"
// @Param network formData string false "Network (e.g. ton, tron)"
// @Param token formData string false "Crypto token (e.g. TON, USDT); omit together with network to create a placeholder where supported"
// @Param network formData string false "Network (e.g. ton, tron); omit together with token to create a placeholder where supported"
// @Param amount formData number false "Amount"
// @Param notify_url formData string false "Callback URL"
// @Param signature formData string false "MD5 signature"
// @Param redirect_url formData string false "Redirect URL"
// @Param name formData string false "Order name"
// @Param payment_type formData string false "Payment type"
// @Param payment_type formData string false "Optional GMPay compatibility flag; include in signature when sent"
// @Success 200 {object} response.ApiResponse{data=response.CreateTransactionResponse}
// @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10009 invalid params, 10041 invalid notify_url, 10004 invalid amount, 10014 chain disabled, 10016 unsupported asset, 10003 no wallet, 10005 no amount channel"
// @Router /payments/gmpay/v1/order/create-transaction [post]
@@ -58,17 +60,17 @@ func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
return c.SucJson(ctx, resp)
}
// SwitchNetwork 切换支付网络,创建返回子订单
// SwitchNetwork 切换支付网络,补全占位父单或创建/返回子订单
// @Summary Switch payment network
// @Description Switch to a different payment target, creating or returning a sub-order.
// @Description Normal values such as ton/tron/solana/ethereum create on-chain child orders.
// @Description The special value okpay creates or reuses an OkPay-hosted child order and returns its payment_url.
// @Description Switch to a different payment target. A status=4 placeholder is completed in place and returns the same parent trade_id with is_selected=false; an already concrete status=1 parent creates or returns the only sub-order when switching to a different target.
// @Description Normal values such as ton/tron/solana/ethereum select on-chain payment; the special value okpay selects OkPay hosted payment.
// @Description For status=4 placeholders from GMPay or EPay submit.php, both on-chain targets and okpay complete the parent in place without creating a child order. Sub-orders cannot be switched again.
// @Tags Payment
// @Accept json
// @Produce json
// @Param request body request.SwitchNetworkRequest true "Switch network payload"
// @Success 200 {object} response.ApiResponse{data=response.CheckoutCounterResponse}
// @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10008 order not found, 10012 cannot switch sub-order, 10013 not waiting payment, 10016 unsupported asset, 10017/10018/10019 provider errors, 10042 provider order creation failed"
// @Failure 400 {object} response.ApiResponse "Stable errno in status_code: 10008 order not found, 10011 sub-order limit, 10012 cannot switch sub-order, 10013 not waiting payment, 10016 unsupported asset, 10017/10018/10019 provider errors, 10042 provider order creation failed"
// @Router /pay/switch-network [post]
func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
req := new(request.SwitchNetworkRequest)
@@ -98,7 +100,7 @@ func (c *BaseCommController) SwitchNetwork(ctx echo.Context) (err error) {
// POST (form) per the legacy EPAY protocol; swagger documents POST as
// the canonical form — the GET variant is identical save the transport.
// @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. Optional request token/network/currency override database defaults and must be included in the EPay signature when sent. The server injects internal payment_type=Epay after EPay signature verification; merchants do not send GMPay payment_type to this endpoint.
// @Tags Payment
// @Accept x-www-form-urlencoded
// @Produce html
+3 -2
View File
@@ -12,7 +12,8 @@ import (
// CheckoutCounter 收银台
// @Summary Checkout counter page
// @Description Return checkout initialization data when the order exists. This endpoint only confirms order existence and returns base order data; call /pay/check-status/{trade_id} for the current order status (1=waiting payment, 2=paid, 3=expired).
// @Description Return checkout initialization data when the order exists. This endpoint only confirms order existence and returns base order data; call /pay/check-status/{trade_id} for the current order status (1=waiting payment, 2=paid, 3=expired, 4=waiting token/network selection).
// @Description When status=4, actual_amount is 0 and token/network/receive_address are empty; this state is produced by GMPay placeholders or EPay submit.php when no token/network request values or database defaults exist. The cashier should guide the payer to choose an on-chain token/network or OkPay and then call /pay/switch-network.
// @Tags Payment
// @Produce json
// @Param trade_id path string true "Trade ID"
@@ -31,7 +32,7 @@ func (c *BaseCommController) CheckoutCounter(ctx echo.Context) (err error) {
// CheckStatus 支付状态检测
// @Summary Check payment status
// @Description Return the current order status by trade ID. Status: 1=waiting payment, 2=paid, 3=expired.
// @Description Return the current order status by trade ID. Status: 1=waiting payment, 2=paid, 3=expired, 4=waiting token/network selection.
// @Tags Payment
// @Produce json
// @Param trade_id path string true "Trade ID"
@@ -109,9 +109,9 @@ func (c *BaseCommController) GetPublicConfig(ctx echo.Context) error {
BackgroundImageURL: data.GetBrandBackgroundImageURL(),
},
Epay: response.EpayPublicConfig{
DefaultToken: data.GetSettingString(mdb.SettingKeyEpayDefaultToken, "usdt"),
DefaultToken: data.GetSettingString(mdb.SettingKeyEpayDefaultToken, ""),
DefaultCurrency: data.GetSettingString(mdb.SettingKeyEpayDefaultCurrency, "cny"),
DefaultNetwork: data.GetSettingString(mdb.SettingKeyEpayDefaultNetwork, "tron"),
DefaultNetwork: data.GetSettingString(mdb.SettingKeyEpayDefaultNetwork, ""),
},
OkPay: okpay,
Version: config.GetAppVersion(),