diff --git a/src/model/mdb/orders_mdb.go b/src/model/mdb/orders_mdb.go index 52ab52c..c54f55e 100644 --- a/src/model/mdb/orders_mdb.go +++ b/src/model/mdb/orders_mdb.go @@ -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..."` diff --git a/src/model/mdb/provider_order.go b/src/model/mdb/provider_order.go index 593d2ce..74b0de6 100644 --- a/src/model/mdb/provider_order.go +++ b/src/model/mdb/provider_order.go @@ -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 diff --git a/src/model/request/order_request.go b/src/model/request/order_request.go index 5d1a9ba..0486cdd 100644 --- a/src/model/request/order_request.go +++ b/src/model/request/order_request.go @@ -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"` } diff --git a/src/model/response/order_response.go b/src/model/response/order_response.go index 1666937..2a65808 100644 --- a/src/model/response/order_response.go +++ b/src/model/response/order_response.go @@ -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"` // 订单状态 } diff --git a/src/model/response/pay_response.go b/src/model/response/pay_response.go index b0177c5..1b81357 100644 --- a/src/model/response/pay_response.go +++ b/src/model/response/pay_response.go @@ -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..."` // 已验证并入账的交易哈希 } diff --git a/src/model/service/order_service.go b/src/model/service/order_service.go index 1570be9..aa3363a 100644 --- a/src/model/service/order_service.go +++ b/src/model/service/order_service.go @@ -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. diff --git a/src/model/service/order_service_test.go b/src/model/service/order_service_test.go index 5ced0d0..4792c12 100644 --- a/src/model/service/order_service_test.go +++ b/src/model/service/order_service_test.go @@ -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()