package controller import ( "context" "encoding/base64" "fmt" "log" "net/http" "strconv" "sync" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting" "github.com/gin-gonic/gin" "github.com/go-pay/gopay" "github.com/go-pay/gopay/alipay" "github.com/thanhpk/randstr" ) const ( PaymentMethodAlipay = "alipay" ) var alipayClientMu sync.Mutex var alipayClient *alipay.Client // ResetAlipayClient 重置支付宝客户端(配置变更时调用) func ResetAlipayClient() { alipayClientMu.Lock() alipayClient = nil alipayClientMu.Unlock() } func init() { setting.OnAlipayConfigChanged = ResetAlipayClient } // getAlipayClient 获取或创建支付宝客户端 func getAlipayClient() (*alipay.Client, error) { alipayClientMu.Lock() defer alipayClientMu.Unlock() if alipayClient != nil { return alipayClient, nil } if !setting.IsAlipayConfigured() { return nil, fmt.Errorf("支付宝未配置") } client, err := alipay.NewClient(setting.AlipayAppID, setting.AlipayPrivateKey, true) if err != nil { return nil, fmt.Errorf("创建支付宝客户端失败: %w", err) } client.SetCharset("utf-8"). SetSignType(alipay.RSA2). SetNotifyUrl(setting.AlipayNotifyURL) // 设置支付宝公钥(用于回调验签) pubKeyBytes, err := base64.StdEncoding.DecodeString(setting.AlipayPublicKey) if err != nil { pubKeyBytes = []byte(setting.AlipayPublicKey) } client.AutoVerifySign([]byte(wrapAsPEM(pubKeyBytes, "PUBLIC KEY"))) alipayClient = client return alipayClient, nil } // AlipayPayRequest 支付宝支付请求参数 type AlipayPayRequest struct { Amount int64 `json:"amount"` } // createAlipayPrecreateOrder 调用支付宝当面付预下单 API,返回二维码内容 func createAlipayPrecreateOrder(client *alipay.Client, subject, tradeNo string, totalAmount string) (string, error) { bm := make(gopay.BodyMap) bm.Set("subject", subject). Set("out_trade_no", tradeNo). Set("total_amount", totalAmount). Set("product_code", "FACE_TO_FACE_PAYMENT") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() rsp, err := client.TradePrecreate(ctx, bm) if err != nil { return "", fmt.Errorf("支付宝当面付下单失败: %w", err) } if rsp.Response.Code != "10000" { return "", fmt.Errorf("支付宝错误: %s - %s", rsp.Response.Code, rsp.Response.Msg) } return rsp.Response.QrCode, nil } // RequestAlipayPayAmount 计算支付宝应付金额 func RequestAlipayPayAmount(c *gin.Context) { var req AlipayPayRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) return } minTopup := calcMinTopup(setting.AlipayMinTopUp) if req.Amount < minTopup { c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)}) return } id := c.GetInt("id") group, err := model.GetUserGroup(id, true) if err != nil { c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"}) return } payMoney := calcPayMoney(float64(req.Amount), group, setting.AlipayUnitPrice) if payMoney <= 0.01 { c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"}) return } c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)}) } // RequestAlipayPay 创建支付宝支付订单,返回二维码 URL func RequestAlipayPay(c *gin.Context) { var req AlipayPayRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) return } minTopup := calcMinTopup(setting.AlipayMinTopUp) if req.Amount < minTopup { c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)}) return } if req.Amount > 10000 { c.JSON(200, gin.H{"message": "error", "data": "充值数量不能大于 10000"}) return } if !setting.IsAlipayConfigured() { c.JSON(200, gin.H{"message": "error", "data": "支付宝未配置"}) return } client, err := getAlipayClient() if err != nil { log.Println("获取支付宝客户端失败:", err) c.JSON(200, gin.H{"message": "error", "data": "支付宝配置错误"}) return } id := c.GetInt("id") group, err := model.GetUserGroup(id, true) if err != nil { c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"}) return } topupGroupRatio := common.GetTopupGroupRatio(group) if topupGroupRatio == 0 { topupGroupRatio = 1 } chargedMoney := float64(req.Amount) * topupGroupRatio tradeNo := fmt.Sprintf("ali%d%s", time.Now().UnixMilli(), randstr.String(8)) payMoney := calcPayMoney(float64(req.Amount), group, setting.AlipayUnitPrice) totalAmount := strconv.FormatFloat(payMoney, 'f', 2, 64) qrCode, err := createAlipayPrecreateOrder(client, fmt.Sprintf("充值%d", req.Amount), tradeNo, totalAmount) if err != nil { log.Println(err) c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"}) return } topUp := &model.TopUp{ UserId: id, Amount: req.Amount, Money: chargedMoney, TradeNo: tradeNo, PaymentMethod: PaymentMethodAlipay, CreateTime: time.Now().Unix(), Status: common.TopUpStatusPending, } if err := topUp.Insert(); err != nil { c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"}) return } c.JSON(200, gin.H{ "message": "success", "data": gin.H{ "trade_no": tradeNo, "qr_code_url": qrCode, }, }) } // AlipayPayStatus 轮询支付宝支付订单状态 func AlipayPayStatus(c *gin.Context) { tradeNo := c.Query("trade_no") if tradeNo == "" { c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) return } topUp := model.GetTopUpByTradeNo(tradeNo) if topUp == nil { c.JSON(200, gin.H{"message": "error", "data": "订单不存在"}) return } userId := c.GetInt("id") if topUp.UserId != userId { c.JSON(200, gin.H{"message": "error", "data": "订单不存在"}) return } c.JSON(200, gin.H{ "message": "success", "data": gin.H{ "status": topUp.Status, "amount": topUp.Amount, }, }) } // AlipayPayWebhook 处理支付宝异步回调通知 func AlipayPayWebhook(c *gin.Context) { notifyReq, err := alipay.ParseNotifyToBodyMap(c.Request) if err != nil { log.Printf("解析支付宝回调失败: %v", err) c.String(http.StatusBadRequest, "fail") return } ok, err := alipay.VerifySign(setting.AlipayPublicKey, notifyReq) if err != nil { log.Printf("支付宝回调验签失败: %v", err) c.String(http.StatusBadRequest, "fail") return } if !ok { log.Printf("支付宝回调验签不通过") c.String(http.StatusBadRequest, "fail") return } tradeStatus := notifyReq.Get("trade_status") if tradeStatus != "TRADE_SUCCESS" { c.String(http.StatusOK, "success") return } tradeNo := notifyReq.Get("out_trade_no") LockOrder(tradeNo) defer UnlockOrder(tradeNo) if err := model.RechargeAlipay(tradeNo); err != nil { log.Printf("支付宝充值失败: %s, err: %s", tradeNo, err.Error()) c.String(http.StatusInternalServerError, "fail") return } log.Printf("支付宝充值成功: %s", tradeNo) c.String(http.StatusOK, "success") }