feat(order): use crypto-random url-safe trade IDs and update docs examples

This commit is contained in:
line-6000
2026-05-27 02:59:25 +08:00
parent c38f728e10
commit 13c81ee560
7 changed files with 60 additions and 32 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ const (
)
type Orders struct {
TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id" example:"T2026041612345678"`
TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"`
OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_id" example:"ORD20260416001"`
ParentTradeId string `gorm:"column:parent_trade_id;index:idx_orders_parent_trade_id;default:''" json:"parent_trade_id"`
BlockTransactionId string `gorm:"index:orders_block_transaction_id_index;column:block_transaction_id" json:"block_transaction_id" example:"0xabc123..."`
+1 -1
View File
@@ -23,7 +23,7 @@ const (
// The main orders table remains the source of truth for merchant-facing state,
// while this table keeps provider-specific identifiers and callback payloads.
type ProviderOrder struct {
TradeId string `gorm:"column:trade_id;size:32;not null;uniqueIndex:provider_orders_trade_id_provider_uindex,priority:1;index:provider_orders_trade_id_index" json:"trade_id" example:"T2026041612345678"`
TradeId string `gorm:"column:trade_id;size:32;not null;uniqueIndex:provider_orders_trade_id_provider_uindex,priority:1;index:provider_orders_trade_id_index" json:"trade_id" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"`
// Provider values should match the bound Orders.PayProvider value.
Provider string `gorm:"column:provider;size:32;not null;uniqueIndex:provider_orders_trade_id_provider_uindex,priority:2;index:provider_orders_provider_status_index,priority:1" json:"provider" example:"okpay"`
// ProviderOrderID is the downstream provider's own order identifier for the
+1 -1
View File
@@ -52,7 +52,7 @@ func (r ManualPaymentRequest) Translates() map[string]string {
// SwitchNetworkRequest 切换支付网络
type SwitchNetworkRequest struct {
TradeId string `json:"trade_id" validate:"required" example:"T2026041612345678"`
TradeId string `json:"trade_id" validate:"required" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"`
Token string `json:"token" validate:"required" example:"USDT"`
Network string `json:"network" validate:"required" example:"okpay,tron,solana,ethereum"`
}
+19 -19
View File
@@ -2,21 +2,21 @@ package response
// CreateTransactionResponse 创建订单成功返回
type CreateTransactionResponse struct {
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
OrderId string `json:"order_id" example:"ORD20260416001"` // 客户交易id
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,按 system.amount_precision 保留小数
Currency string `json:"currency" example:"CNY"` // 订单货币类型 CNY USD......
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,按 system.amount_precision 保留小数
ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址
Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT......
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
PaymentUrl string `json:"payment_url" example:"https://pay.example.com/checkout/T2026041612345678"` // 收银台地址
TradeId string `json:"trade_id" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"` // epusdt订单号
OrderId string `json:"order_id" example:"ORD20260416001"` // 客户交易id
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,按 system.amount_precision 保留小数
Currency string `json:"currency" example:"CNY"` // 订单货币类型 CNY USD......
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,按 system.amount_precision 保留小数
ReceiveAddress string `json:"receive_address" example:"TTestTronAddress001"` // 收款钱包地址
Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT......
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
PaymentUrl string `json:"payment_url" example:"https://pay.example.com/checkout/3nQ9pL2xV7sK1mR8cT4yB_aZ"` // 收银台地址
}
// OrderNotifyResponse 订单异步回调结构体
type OrderNotifyResponse struct {
Pid string `json:"pid" example:"1000"` // 签名使用的商户 PID,商户据此查本地 secret 验签
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
TradeId string `json:"trade_id" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"` // epusdt订单号
OrderId string `json:"order_id" example:"ORD20260416001"` // 客户交易id
Amount float64 `json:"amount" example:"100.0000"` // 订单金额,按 system.amount_precision 保留小数
ActualAmount float64 `json:"actual_amount" example:"14.2857"` // 订单实际需要支付的金额,按 system.amount_precision 保留小数
@@ -30,13 +30,13 @@ type OrderNotifyResponse struct {
// OrderNotifyResponseEpay epay订单异步回调结构体
type OrderNotifyResponseEpay struct {
PID int `json:"pid" example:"1001"` // 商户ID
TradeNo string `json:"trade_no" example:"T2026041612345678"` // 平台订单号
OutTradeNo string `json:"out_trade_no" example:"ORD20260416001"` // 商户订单号
Type string `json:"type" example:"usdt"` // 订单类型
Name string `json:"name" example:"VIP月卡"` // 商品名称
Money string `json:"money" example:"100.0000"` // 订单金额,保留4位小数
Sign string `json:"sign" example:"a1b2c3d4..."` // 签名
SignType string `json:"sign_type" example:"MD5"` // 签名类型
TradeStatus string `json:"trade_status" example:"TRADE_SUCCESS"` // 订单状态
PID int `json:"pid" example:"1001"` // 商户ID
TradeNo string `json:"trade_no" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"` // 平台订单号
OutTradeNo string `json:"out_trade_no" example:"ORD20260416001"` // 商户订单号
Type string `json:"type" example:"usdt"` // 订单类型
Name string `json:"name" example:"VIP月卡"` // 商品名称
Money string `json:"money" example:"100.0000"` // 订单金额,保留4位小数
Sign string `json:"sign" example:"a1b2c3d4..."` // 签名
SignType string `json:"sign_type" example:"MD5"` // 签名类型
TradeStatus string `json:"trade_status" example:"TRADE_SUCCESS"` // 订单状态
}
+5 -5
View File
@@ -1,7 +1,7 @@
package response
type CheckoutCounterResponse struct {
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
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"` // 订单实际需要支付的金额,按 system.amount_precision 保留小数 加密货币金额
Token string `json:"token" example:"USDT"` // 所属币种 TRX USDT......
@@ -10,19 +10,19 @@ type CheckoutCounterResponse struct {
Network string `json:"network" example:"tron"` // 网络 TRON ETH ...
ExpirationTime int64 `json:"expiration_time" example:"1713264600"` // 过期时间 时间戳
RedirectUrl string `json:"redirect_url" example:"https://example.com/success"`
PaymentUrl string `json:"payment_url" example:"https://pay.example.com/checkout/T2026041612345678"` // 支付链接;链上订单通常为本地收银台,OkPay 子订单为第三方 payLink
CreatedAt int64 `json:"created_at" example:"1713264000"` // 订单创建时间 时间戳
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"`
}
type CheckStatusResponse struct {
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
TradeId string `json:"trade_id" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"` // epusdt订单号
// 订单状态 1=等待支付 2=支付成功 3=已过期
Status int `json:"status" enums:"1,2,3" example:"1"`
}
type ManualPaymentResponse struct {
TradeId string `json:"trade_id" example:"T2026041612345678"` // epusdt订单号
TradeId string `json:"trade_id" example:"3nQ9pL2xV7sK1mR8cT4yB_aZ"` // epusdt订单号
Status int `json:"status" enums:"1,2,3" example:"2"` // 订单状态 1=等待支付 2=支付成功 3=已过期
BlockTransactionId string `json:"block_transaction_id" example:"0xabc123def456..."` // 已验证并入账的交易哈希
}
+7 -5
View File
@@ -1,12 +1,12 @@
package service
import (
cryptorand "crypto/rand"
"encoding/base64"
"errors"
"fmt"
"math/rand"
"strings"
"sync"
"time"
"github.com/GMWalletApp/epusdt/config"
"github.com/GMWalletApp/epusdt/model/dao"
@@ -322,9 +322,11 @@ func ReserveAvailableWalletAndAmount(tradeID string, network string, token strin
// GenerateCode creates a unique trade id.
func GenerateCode() string {
date := time.Now().Format("20060102")
r := rand.Intn(1000)
return fmt.Sprintf("%s%d%03d", date, time.Now().UnixNano()/1e6, r)
buf := make([]byte, 18)
if _, err := cryptorand.Read(buf); err != nil {
panic(fmt.Sprintf("generate trade id: crypto/rand failed: %v", err))
}
return base64.RawURLEncoding.EncodeToString(buf)
}
// GetOrderInfoByTradeId returns a validated order.
+26
View File
@@ -1,6 +1,7 @@
package service
import (
"encoding/base64"
"fmt"
"io"
"net/http"
@@ -36,6 +37,31 @@ func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
func TestGenerateCodeFormat(t *testing.T) {
tradeID := GenerateCode()
if len(tradeID) != 24 {
t.Fatalf("trade id length = %d, want 24", len(tradeID))
}
if strings.Contains(tradeID, "=") {
t.Fatalf("trade id = %q, contains padding '='", tradeID)
}
for _, ch := range tradeID {
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' {
continue
}
t.Fatalf("trade id = %q, contains non URL-safe character %q", tradeID, ch)
}
decoded, err := base64.RawURLEncoding.DecodeString(tradeID)
if err != nil {
t.Fatalf("decode trade id %q: %v", tradeID, err)
}
if len(decoded) != 18 {
t.Fatalf("decoded trade id length = %d, want 18", len(decoded))
}
}
func installMockHTTPClient(t *testing.T, handler roundTripFunc) {
t.Helper()