package handlers import ( "crypto" "crypto/aes" "crypto/cipher" "crypto/md5" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" "encoding/pem" "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 } // 根据订单支付方式获取对应的支付通道 paymentMethod := order.PaymentMethod if paymentMethod == "" { paymentMethod = "usdt" // 默认 } // 先通过 code 查找支付通道 var channels []models.PaymentChannel utils.DB.Where("is_enabled = ? AND code = ?", true, paymentMethod).Order("sort_order ASC").Find(&channels) // 如果没找到,尝试通过 type 查找 if len(channels) == 0 { utils.DB.Where("is_enabled = ? AND type = ?", true, paymentMethod).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] // 如果是 hashpay,调用 HashPay 支付 if channel.Type == "hashpay" { h.CreateHashPayPayment(c) return } // BepUsdt 支付流程 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, ¶ms) 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("[Payment] 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 } // ===================== HashPay 支付 ===================== // HashPay 创建订单请求 type HashPayCreateRequest struct { MerchantNo string `json:"merchantNo"` Amount float64 `json:"amount"` Currency string `json:"currency,omitempty"` Description string `json:"description,omitempty"` ReturnURL string `json:"return_url,omitempty"` } // HashPay 创建订单响应 type HashPayCreateResponse struct { CheckoutURL string `json:"checkoutUrl"` Order struct { ID string `json:"id"` Amount float64 `json:"amount"` Currency string `json:"currency"` ExpiresAt int64 `json:"expiresAt"` Status string `json:"status"` } `json:"order"` Reused bool `json:"reused"` } // CreateHashPayPayment 创建 HashPay 支付订单 func (h *PaymentHandler) CreateHashPayPayment(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, "hashpay").Order("sort_order ASC").Find(&channels) if len(channels) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "No available HashPay payment channel"}) return } channel := channels[0] config := parseChannelConfig(channel.Config) baseURL := config["hashpay_base_url"] merchantID := config["hashpay_merchant_id"] privateKeyPEM := config["hashpay_private_key"] if baseURL == "" || merchantID == "" || privateKeyPEM == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "HashPay channel not configured properly"}) return } // 解析私钥 privateKey, err := parseRSAPrivateKey(privateKeyPEM) if err != nil { log.Printf("[HashPay] failed to parse private key: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid private key configuration"}) return } scheme := "http" if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" { scheme = "https" } host := c.Request.Host apiBaseURL := fmt.Sprintf("%s://%s", scheme, host) merchantNo := fmt.Sprintf("ORDER-%d", order.ID) returnURL := fmt.Sprintf("%s/orders/%d", apiBaseURL, order.ID) reqBody := HashPayCreateRequest{ MerchantNo: merchantNo, Amount: order.TotalAmount, Currency: "USD", Description: fmt.Sprintf("Order #%d", order.ID), ReturnURL: returnURL, } bodyBytes, _ := json.Marshal(reqBody) timestamp := fmt.Sprintf("%d", time.Now().Unix()) // 生成签名 signature, err := signHashPay("POST", "/api/merchant/new", timestamp, string(bodyBytes), privateKey) if err != nil { log.Printf("[HashPay] failed to sign request: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sign request"}) return } // 发送请求 req, err := http.NewRequest("POST", baseURL+"/api/merchant/new", strings.NewReader(string(bodyBytes))) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request"}) return } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Merchant-Id", merchantID) req.Header.Set("X-Timestamp", timestamp) req.Header.Set("X-Signature", signature) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { log.Printf("[HashPay] request failed: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect to HashPay"}) return } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) log.Printf("[HashPay] response: %s", string(respBody)) var result HashPayCreateResponse if err := json.Unmarshal(respBody, &result); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid response from HashPay"}) return } if result.CheckoutURL == "" { c.JSON(http.StatusInternalServerError, gin.H{"error": "No checkout URL returned"}) return } // 更新订单支付方式 utils.DB.Model(&order).Updates(map[string]interface{}{ "payment_method": "hashpay", }) // 计算过期时间 var expiresAt *time.Time if result.Order.ExpiresAt > 0 { t := time.Unix(result.Order.ExpiresAt, 0) expiresAt = &t } paymentInfo := map[string]interface{}{ "order_id": order.ID, "checkout_url": result.CheckoutURL, "hashpay_id": result.Order.ID, "amount": result.Order.Amount, "currency": result.Order.Currency, "expires_at": expiresAt, "channel_id": channel.ID, } c.JSON(http.StatusOK, gin.H{"data": paymentInfo}) } // HashPayNotify 处理 HashPay 回调通知 func (h *PaymentHandler) HashPayNotify(c *gin.Context) { body, _ := io.ReadAll(c.Request.Body) log.Printf("[HashPay] notify body: %s", string(body)) var envelope struct { Alg string `json:"alg"` Key string `json:"key"` IV string `json:"iv"` Data string `json:"data"` } if err := json.Unmarshal(body, &envelope); err != nil { c.String(http.StatusOK, "ok") return } // 获取商户私钥进行解密 var channels []models.PaymentChannel utils.DB.Where("type = ?", "hashpay").Find(&channels) if len(channels) == 0 { c.String(http.StatusOK, "ok") return } channel := channels[0] config := parseChannelConfig(channel.Config) privateKeyPEM := config["hashpay_private_key"] privateKey, err := parseRSAPrivateKey(privateKeyPEM) if err != nil { log.Printf("[HashPay] failed to parse private key for decryption: %v", err) c.String(http.StatusOK, "ok") return } // 解密回调数据 payload, err := decryptHashPayNotify(envelope.Key, envelope.IV, envelope.Data, privateKey) if err != nil { log.Printf("[HashPay] failed to decrypt notify: %v", err) c.String(http.StatusOK, "ok") return } log.Printf("[HashPay] decrypted payload: %+v", payload) // 解析订单号 merchantNo := payload.MerchantNo if merchantNo == "" { c.String(http.StatusOK, "ok") return } // 从 merchantNo 中提取订单ID (格式: ORDER-123) orderIDStr := strings.TrimPrefix(merchantNo, "ORDER-") 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 } // 更新订单状态 if payload.Status == "paid" { utils.DB.Model(&order).Update("status", models.OrderStatusPendingConfirm) } else if payload.Status == "expired" || payload.Status == "cancelled" { utils.DB.Model(&order).Update("status", models.OrderStatusCancelled) } c.String(http.StatusOK, "ok") } // HashPayNotifyPayload 回调解密后的数据结构 type HashPayNotifyPayload struct { Timestamp int64 `json:"timestamp"` MerchantNo string `json:"merchantNo"` OrderID string `json:"orderId"` Amount float64 `json:"amount"` Currency string `json:"currency"` Status string `json:"status"` } // parseRSAPrivateKey 解析 RSA 私钥 func parseRSAPrivateKey(pemStr string) (*rsa.PrivateKey, error) { block, _ := pem.Decode([]byte(pemStr)) if block == nil { return nil, fmt.Errorf("failed to parse PEM block") } // 尝试 PKCS1 if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { return key, nil } // 尝试 PKCS8 key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, err } rsaKey, ok := key.(*rsa.PrivateKey) if !ok { return nil, fmt.Errorf("not an RSA private key") } return rsaKey, nil } // signHashPay 生成 HashPay 请求签名 func signHashPay(method, path, timestamp, body string, privateKey *rsa.PrivateKey) (string, error) { // 签名原文: method + "\n" + path + "\n" + timestamp + "\n" + body message := fmt.Sprintf("%s\n%s\n%s\n%s", method, path, timestamp, body) hashed := sha256.Sum256([]byte(message)) signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:]) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(signature), nil } // decryptHashPayNotify 解密 HashPay 回调通知 func decryptHashPayNotify(encryptedKey, iv, encryptedData string, privateKey *rsa.PrivateKey) (*HashPayNotifyPayload, error) { // 解码 Base64 keyBytes, err := base64.StdEncoding.DecodeString(encryptedKey) if err != nil { return nil, fmt.Errorf("failed to decode key: %v", err) } ivBytes, err := base64.StdEncoding.DecodeString(iv) if err != nil { return nil, fmt.Errorf("failed to decode iv: %v", err) } dataBytes, err := base64.StdEncoding.DecodeString(encryptedData) if err != nil { return nil, fmt.Errorf("failed to decode data: %v", err) } // 使用 RSA-OAEP 解密 AES 密钥 aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, keyBytes, nil) if err != nil { return nil, fmt.Errorf("failed to decrypt AES key: %v", err) } // 使用 AES-256-GCM 解密数据 // Go 标准库的 AES-GCM 实现 block, err := aes.NewCipher(aesKey) if err != nil { return nil, fmt.Errorf("failed to create cipher: %v", err) } aesgcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("failed to create GCM: %v", err) } // GCM 的 nonce 就是 IV,密文最后是 tag // dataBytes = ciphertext + tag tagSize := aesgcm.Overhead() if len(dataBytes) < tagSize { return nil, fmt.Errorf("ciphertext too short") } plaintext, err := aesgcm.Open(nil, ivBytes, dataBytes, nil) if err != nil { return nil, fmt.Errorf("failed to decrypt data: %v", err) } var payload HashPayNotifyPayload if err := json.Unmarshal(plaintext, &payload); err != nil { return nil, fmt.Errorf("failed to unmarshal payload: %v", err) } return &payload, nil }