Files
sale/backend/internal/api/handlers/payment.go
T
admin c64eb31719 fix: 正确获取BepUsdt钱包地址
BepUsdt API返回的钱包地址在token字段中,不是address字段

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 20:52:24 +08:00

348 lines
9.0 KiB
Go

package handlers
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sort"
"strconv"
"strings"
"time"
"sale/internal/models"
"sale/internal/utils"
"github.com/gin-gonic/gin"
)
type PaymentHandler struct{}
func NewPaymentHandler() *PaymentHandler {
return &PaymentHandler{}
}
type BepUsdtCreateRequest struct {
TradeType string `json:"trade_type,omitempty"`
OrderID string `json:"order_id"`
Amount float64 `json:"amount"`
NotifyURL string `json:"notify_url"`
RedirectURL string `json:"redirect_url,omitempty"`
Signature string `json:"signature"`
}
func (h *PaymentHandler) CreatePayment(c *gin.Context) {
userID := c.GetUint("user_id")
orderID, _ := strconv.Atoi(c.Param("id"))
var order models.Order
if err := utils.DB.Preload("OrderItems").Where("id = ? AND user_id = ?", orderID, userID).First(&order).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
if order.Status != models.OrderStatusPendingPayment {
c.JSON(http.StatusBadRequest, gin.H{"error": "Order is not pending payment"})
return
}
if order.PaymentExpiresAt != nil && time.Now().After(*order.PaymentExpiresAt) {
tx := utils.DB.Begin()
tx.Model(&order).Update("status", models.OrderStatusCancelled)
for _, item := range order.OrderItems {
var inventory models.Inventory
if err := tx.Where("product_id = ?", item.ProductID).First(&inventory).Error; err == nil {
tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity+item.Quantity)
}
}
tx.Commit()
c.JSON(http.StatusBadRequest, gin.H{"error": "支付已超时,订单已取消"})
return
}
var channels []models.PaymentChannel
utils.DB.Where("is_enabled = ? AND type = ?", true, "bepusdt").Order("sort_order ASC").Find(&channels)
if len(channels) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No available payment channel"})
return
}
channel := channels[0]
config := parseChannelConfig(channel.Config)
apiURL := strings.TrimSuffix(config["bepusdt_api_url"], "/")
apiToken := config["bepusdt_api_token"]
tradeType := config["bepusdt_trade_type"]
if tradeType == "" {
tradeType = "usdt.trc20"
}
scheme := "http"
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
scheme = "https"
}
host := c.Request.Host
baseURL := fmt.Sprintf("%s://%s", scheme, host)
notifyURL := fmt.Sprintf("%s/api/payment/bepusdt/notify", baseURL)
redirectURL := fmt.Sprintf("%s/orders/%d", baseURL, order.ID)
params := map[string]string{
"order_id": strconv.Itoa(int(order.ID)),
"amount": fmt.Sprint(order.TotalAmount),
"notify_url": notifyURL,
"redirect_url": redirectURL,
}
if tradeType != "" {
params["trade_type"] = tradeType
}
signature := signBepUsdt(params, apiToken)
log.Printf("[BepUsdt] signature params: %v", params)
log.Printf("[BepUsdt] signature: %s, token: %s", signature, apiToken)
reqBody := BepUsdtCreateRequest{
TradeType: tradeType,
OrderID: strconv.Itoa(int(order.ID)),
Amount: order.TotalAmount,
NotifyURL: notifyURL,
RedirectURL: redirectURL,
Signature: signature,
}
body, _ := json.Marshal(reqBody)
log.Printf("[BepUsdt] request body: %s", string(body))
req, err := http.NewRequest("POST", apiURL+"/api/v1/order/create-transaction", strings.NewReader(string(body)))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"})
return
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect payment gateway"})
return
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
log.Printf("[BepUsdt] response: %s", string(respBody))
var result map[string]interface{}
json.Unmarshal(respBody, &result)
if statusCode, ok := result["status_code"].(float64); !ok || int(statusCode) != 200 {
errMsg := "Payment creation failed"
if msg, ok := result["message"].(string); ok {
errMsg = msg
}
c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg})
return
}
data, ok := result["data"].(map[string]interface{})
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid response from payment gateway"})
return
}
tradeID, _ := data["trade_id"].(string)
token, _ := data["token"].(string)
paymentURL, _ := data["payment_url"].(string)
// 钱包地址在token字段中
walletAddress := token
log.Printf("[BepUsdt] payment data: trade_id=%s, token=%s, payment_url=%s, wallet_address=%s", tradeID, token, paymentURL, walletAddress)
var amount float64
if v, ok := data["amount"].(float64); ok {
amount = v
} else if v, ok := data["amount"].(string); ok {
amount, _ = strconv.ParseFloat(v, 64)
}
var actualAmount float64
if v, ok := data["actual_amount"].(float64); ok {
actualAmount = v
} else if v, ok := data["actual_amount"].(string); ok {
actualAmount, _ = strconv.ParseFloat(v, 64)
}
if actualAmount == 0 {
actualAmount = amount
}
var expiration int64
if v, ok := data["expiration_time"].(float64); ok {
expiration = int64(v)
}
if expiration > 1e12 {
expiration = expiration / 1000
}
var remainingSeconds int64
var expiresAt *time.Time
if expiration > 0 {
remainingSeconds = expiration - time.Now().Unix()
if remainingSeconds < 0 {
remainingSeconds = 0
}
if remainingSeconds > 0 {
t := time.Now().Add(time.Duration(remainingSeconds) * time.Second)
expiresAt = &t
}
}
utils.DB.Model(&order).Updates(map[string]interface{}{
"payment_method": "bepusdt",
"payment_expires_at": expiresAt,
})
paymentInfo := map[string]interface{}{
"trade_id": tradeID,
"order_id": order.ID,
"amount": amount,
"actual_amount": actualAmount,
"token": token,
"payment_url": paymentURL,
"wallet_address": walletAddress,
"expiration_time": remainingSeconds,
"trade_type": tradeType,
"channel_id": channel.ID,
}
c.JSON(http.StatusOK, gin.H{"data": paymentInfo})
}
func (h *PaymentHandler) BepUsdtNotify(c *gin.Context) {
body, _ := io.ReadAll(c.Request.Body)
var params map[string]interface{}
json.Unmarshal(body, &params)
orderIDStr, _ := params["order_id"].(string)
status, _ := params["status"].(float64)
signature, _ := params["signature"].(string)
orderID, err := strconv.Atoi(orderIDStr)
if err != nil {
c.String(http.StatusOK, "ok")
return
}
var order models.Order
if err := utils.DB.First(&order, orderID).Error; err != nil {
c.String(http.StatusOK, "ok")
return
}
var channels []models.PaymentChannel
utils.DB.Where("type = ?", "bepusdt").Find(&channels)
if len(channels) == 0 {
c.String(http.StatusOK, "ok")
return
}
channel := channels[0]
config := parseChannelConfig(channel.Config)
apiToken := config["bepusdt_api_token"]
stringParams := make(map[string]string)
for k, v := range params {
if k != "signature" {
stringParams[k] = fmt.Sprintf("%v", v)
}
}
expectedSign := signBepUsdt(stringParams, apiToken)
if signature != expectedSign {
c.String(http.StatusOK, "ok")
return
}
if int(status) == 2 {
utils.DB.Model(&order).Update("status", models.OrderStatusPendingConfirm)
} else if int(status) == 3 {
utils.DB.Model(&order).Update("status", models.OrderStatusCancelled)
}
c.String(http.StatusOK, "ok")
}
func (h *PaymentHandler) CheckPaymentStatus(c *gin.Context) {
userID := c.GetUint("user_id")
orderID, _ := strconv.Atoi(c.Param("id"))
var order models.Order
if err := utils.DB.Where("id = ? AND user_id = ?", orderID, userID).First(&order).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": gin.H{
"status": order.Status,
}})
}
func signBepUsdt(params map[string]string, token 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]))
}
str := strings.Join(parts, "&") + token
hash := md5.Sum([]byte(str))
return hex.EncodeToString(hash[:])
}
func parseChannelConfig(configStr string) map[string]string {
result := make(map[string]string)
if configStr == "" {
return result
}
var raw map[string]interface{}
if err := json.Unmarshal([]byte(configStr), &raw); err != nil {
var rawStr map[string]string
if err2 := json.Unmarshal([]byte(configStr), &rawStr); err2 == nil {
return rawStr
}
log.Printf("[BepUsdt] failed to parse channel config: %v", err)
return result
}
for k, v := range raw {
if v == nil {
continue
}
switch val := v.(type) {
case string:
result[k] = val
case float64:
if val == float64(int64(val)) {
result[k] = strconv.FormatInt(int64(val), 10)
} else {
result[k] = strconv.FormatFloat(val, 'f', -1, 64)
}
case bool:
result[k] = strconv.FormatBool(val)
default:
result[k] = fmt.Sprintf("%v", val)
}
}
return result
}