09f1ee4601
- 后端: payment.go 支付处理(创建订单/回调通知/状态查询/签名算法) - 后端: 新增公开支付通道API(PublicList,不暴露config) - 前端: Checkout.vue 收银台页面(倒计时/二维码/支付轮询) - 前端: Cart.vue 对接动态支付通道,选择bepusdt跳转收银台 - 前端: API添加paymentChannelApi和orderApi支付方法 - 路由: 注册checkout路由和支付相关API
260 lines
6.6 KiB
Go
260 lines
6.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"sale/internal/models"
|
|
"sale/internal/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type PaymentHandler struct{}
|
|
|
|
func NewPaymentHandler() *PaymentHandler {
|
|
return &PaymentHandler{}
|
|
}
|
|
|
|
type BepUsdtCreateRequest struct {
|
|
Address string `json:"address"`
|
|
TradeType string `json:"trade_type"`
|
|
OrderID string `json:"order_id"`
|
|
Amount float64 `json:"amount"`
|
|
Signature string `json:"signature"`
|
|
NotifyURL string `json:"notify_url"`
|
|
RedirectURL string `json:"redirect_url"`
|
|
Timeout int `json:"timeout"`
|
|
Rate string `json:"rate"`
|
|
}
|
|
|
|
type BepUsdtCreateResponse struct {
|
|
StatusCode int `json:"status_code"`
|
|
Message string `json:"message"`
|
|
TradeID string `json:"trade_id"`
|
|
OrderID string `json:"order_id"`
|
|
Amount string `json:"amount"`
|
|
ActualAmount string `json:"actual_amount"`
|
|
Token string `json:"token"`
|
|
Expiration int `json:"expiration_time"`
|
|
PaymentURL string `json:"payment_url"`
|
|
}
|
|
|
|
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.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
|
|
}
|
|
|
|
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]
|
|
var config map[string]string
|
|
json.Unmarshal([]byte(channel.Config), &config)
|
|
|
|
apiURL := config["bepusdt_api_url"]
|
|
apiToken := config["bepusdt_api_token"]
|
|
tradeType := config["bepusdt_trade_type"]
|
|
if tradeType == "" {
|
|
tradeType = "usdt.trc20"
|
|
}
|
|
timeout, _ := strconv.Atoi(config["bepusdt_timeout"])
|
|
if timeout < 60 {
|
|
timeout = 600
|
|
}
|
|
rate := config["bepusdt_rate"]
|
|
|
|
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.Sprintf("%.2f", order.TotalAmount),
|
|
"notify_url": notifyURL,
|
|
"redirect_url": redirectURL,
|
|
"trade_type": tradeType,
|
|
}
|
|
if timeout > 0 {
|
|
params["timeout"] = strconv.Itoa(timeout)
|
|
}
|
|
if rate != "" {
|
|
params["rate"] = rate
|
|
}
|
|
|
|
signature := signBepUsdt(params, apiToken)
|
|
|
|
reqBody := BepUsdtCreateRequest{
|
|
TradeType: tradeType,
|
|
OrderID: strconv.Itoa(int(order.ID)),
|
|
Amount: order.TotalAmount,
|
|
Signature: signature,
|
|
NotifyURL: notifyURL,
|
|
RedirectURL: redirectURL,
|
|
Timeout: timeout,
|
|
Rate: rate,
|
|
}
|
|
|
|
body, _ := json.Marshal(reqBody)
|
|
resp, err := http.Post(apiURL+"/api/v1/order/create-transaction", "application/json", strings.NewReader(string(body)))
|
|
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)
|
|
|
|
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 := result["data"].(map[string]interface{})
|
|
tradeID, _ := data["trade_id"].(string)
|
|
actualAmount, _ := data["actual_amount"].(string)
|
|
token, _ := data["token"].(string)
|
|
expiration, _ := data["expiration_time"].(float64)
|
|
|
|
utils.DB.Model(&order).Updates(map[string]interface{}{
|
|
"payment_method": "bepusdt",
|
|
})
|
|
|
|
paymentInfo := map[string]interface{}{
|
|
"trade_id": tradeID,
|
|
"order_id": order.ID,
|
|
"amount": data["amount"],
|
|
"actual_amount": actualAmount,
|
|
"token": token,
|
|
"expiration_time": int(expiration),
|
|
"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]
|
|
var config map[string]string
|
|
json.Unmarshal([]byte(channel.Config), &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] != "" {
|
|
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[:])
|
|
}
|