Initial commit: 网络验证平台
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BEpusdtConfig struct {
|
||||
ApiURL string
|
||||
ApiToken string
|
||||
TradeType string
|
||||
Fiat string
|
||||
Timeout int
|
||||
Rate string
|
||||
}
|
||||
|
||||
type BEpusdtClient struct {
|
||||
config BEpusdtConfig
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type CreateTransactionRequest struct {
|
||||
OrderID string `json:"order_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
Signature string `json:"signature"`
|
||||
TradeType string `json:"trade_type,omitempty"`
|
||||
Fiat string `json:"fiat,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
Rate string `json:"rate,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
type CreateTransactionResponse struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Fiat string `json:"fiat"`
|
||||
TradeID string `json:"trade_id"`
|
||||
OrderID string `json:"order_id"`
|
||||
Amount interface{} `json:"amount"`
|
||||
ActualAmount interface{} `json:"actual_amount"`
|
||||
Status int `json:"status"`
|
||||
Token string `json:"token"`
|
||||
ExpirationTime int `json:"expiration_time"`
|
||||
PaymentURL string `json:"payment_url"`
|
||||
} `json:"data"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type CallbackData struct {
|
||||
TradeID string `json:"trade_id"`
|
||||
OrderID string `json:"order_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
ActualAmount float64 `json:"actual_amount"`
|
||||
Token string `json:"token"`
|
||||
BlockTransactionID string `json:"block_transaction_id"`
|
||||
Signature string `json:"signature"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func NewBEpusdtClient(config BEpusdtConfig) *BEpusdtClient {
|
||||
if config.Fiat == "" {
|
||||
config.Fiat = "CNY"
|
||||
}
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 600
|
||||
}
|
||||
|
||||
return &BEpusdtClient{
|
||||
config: config,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *BEpusdtClient) GenerateSignature(params map[string]string) string {
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
if params[k] != "" && k != "signature" && k != "sign_type" {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var parts []string
|
||||
for _, k := range keys {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
|
||||
}
|
||||
signStr := strings.Join(parts, "&") + c.config.ApiToken
|
||||
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(signStr))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (c *BEpusdtClient) CreateTransaction(orderID string, amount float64, notifyURL, redirectURL, name string) (*CreateTransactionResponse, error) {
|
||||
params := map[string]string{
|
||||
"order_id": orderID,
|
||||
"amount": fmt.Sprintf("%.2f", amount),
|
||||
"notify_url": notifyURL,
|
||||
"redirect_url": redirectURL,
|
||||
}
|
||||
|
||||
if c.config.TradeType != "" {
|
||||
params["trade_type"] = c.config.TradeType
|
||||
}
|
||||
if c.config.Fiat != "" {
|
||||
params["fiat"] = c.config.Fiat
|
||||
}
|
||||
if name != "" {
|
||||
params["name"] = name
|
||||
}
|
||||
if c.config.Timeout > 0 {
|
||||
params["timeout"] = fmt.Sprintf("%d", c.config.Timeout)
|
||||
}
|
||||
if c.config.Rate != "" {
|
||||
params["rate"] = c.config.Rate
|
||||
}
|
||||
|
||||
params["signature"] = c.GenerateSignature(params)
|
||||
|
||||
jsonData, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
apiURL := strings.TrimRight(c.config.ApiURL, "/")
|
||||
reqURL := apiURL + "/api/v1/order/create-transaction"
|
||||
|
||||
req, err := http.NewRequest("POST", reqURL, strings.NewReader(string(jsonData)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var result CreateTransactionResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if result.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("API error: %s (code: %d)", result.Message, result.StatusCode)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *BEpusdtClient) VerifyCallback(data CallbackData) bool {
|
||||
params := map[string]string{
|
||||
"trade_id": data.TradeID,
|
||||
"order_id": data.OrderID,
|
||||
"amount": fmt.Sprintf("%.2f", data.Amount),
|
||||
"actual_amount": fmt.Sprintf("%.2f", data.ActualAmount),
|
||||
"token": data.Token,
|
||||
"block_transaction_id": data.BlockTransactionID,
|
||||
"status": fmt.Sprintf("%d", data.Status),
|
||||
}
|
||||
|
||||
expectedSign := c.GenerateSignature(params)
|
||||
return strings.EqualFold(expectedSign, data.Signature)
|
||||
}
|
||||
|
||||
func ParseCallbackFromQuery(query url.Values) CallbackData {
|
||||
var data CallbackData
|
||||
data.TradeID = query.Get("trade_id")
|
||||
data.OrderID = query.Get("order_id")
|
||||
data.Token = query.Get("token")
|
||||
data.BlockTransactionID = query.Get("block_transaction_id")
|
||||
data.Signature = query.Get("signature")
|
||||
|
||||
if amount := query.Get("amount"); amount != "" {
|
||||
fmt.Sscanf(amount, "%f", &data.Amount)
|
||||
}
|
||||
if actualAmount := query.Get("actual_amount"); actualAmount != "" {
|
||||
fmt.Sscanf(actualAmount, "%f", &data.ActualAmount)
|
||||
}
|
||||
if status := query.Get("status"); status != "" {
|
||||
fmt.Sscanf(status, "%d", &data.Status)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func ParseCallbackFromJSON(body []byte) (CallbackData, error) {
|
||||
var data CallbackData
|
||||
err := json.Unmarshal(body, &data)
|
||||
return data, err
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"verification-platform-backend/internal/model"
|
||||
)
|
||||
|
||||
type PaymentResult struct {
|
||||
OrderID string
|
||||
TradeID string
|
||||
PaymentURL string
|
||||
ActualAmount string
|
||||
Token string
|
||||
ExpirationTime int
|
||||
}
|
||||
|
||||
type PaymentService interface {
|
||||
CreateOrder(orderID string, amount float64, notifyURL, redirectURL, name string) (*PaymentResult, error)
|
||||
VerifyCallback(data interface{}) bool
|
||||
}
|
||||
|
||||
func GetPaymentService(channel model.PaymentChannel, callbackBaseURL string) (PaymentService, error) {
|
||||
switch channel.Type {
|
||||
case "bepusdt":
|
||||
config, err := parseBEpusdtConfig(channel.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse bepusdt config: %w", err)
|
||||
}
|
||||
return NewBEpusdtAdapter(config, callbackBaseURL), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported payment type: %s", channel.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func parseBEpusdtConfig(configStr string) (BEpusdtConfig, error) {
|
||||
var config BEpusdtConfig
|
||||
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
||||
return config, fmt.Errorf("invalid config format: %w", err)
|
||||
}
|
||||
|
||||
if config.ApiURL == "" {
|
||||
return config, fmt.Errorf("api_url is required")
|
||||
}
|
||||
if config.ApiToken == "" {
|
||||
return config, fmt.Errorf("api_token is required")
|
||||
}
|
||||
if config.TradeType == "" {
|
||||
config.TradeType = "usdt.trc20"
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func interfaceToString(v interface{}) string {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case float64:
|
||||
return strconv.FormatFloat(val, 'f', -1, 64)
|
||||
case int:
|
||||
return strconv.Itoa(val)
|
||||
default:
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
|
||||
type BEpusdtAdapter struct {
|
||||
client *BEpusdtClient
|
||||
callbackBaseURL string
|
||||
}
|
||||
|
||||
func NewBEpusdtAdapter(config BEpusdtConfig, callbackBaseURL string) *BEpusdtAdapter {
|
||||
return &BEpusdtAdapter{
|
||||
client: NewBEpusdtClient(config),
|
||||
callbackBaseURL: callbackBaseURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *BEpusdtAdapter) CreateOrder(orderID string, amount float64, notifyURL, redirectURL, name string) (*PaymentResult, error) {
|
||||
resp, err := a.client.CreateTransaction(orderID, amount, notifyURL, redirectURL, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PaymentResult{
|
||||
OrderID: resp.Data.OrderID,
|
||||
TradeID: resp.Data.TradeID,
|
||||
PaymentURL: resp.Data.PaymentURL,
|
||||
ActualAmount: interfaceToString(resp.Data.ActualAmount),
|
||||
Token: resp.Data.Token,
|
||||
ExpirationTime: resp.Data.ExpirationTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *BEpusdtAdapter) VerifyCallback(data interface{}) bool {
|
||||
switch v := data.(type) {
|
||||
case CallbackData:
|
||||
return a.client.VerifyCallback(v)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user