package controller import ( "errors" "fmt" "io" "math" "net/http" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/payment" "github.com/QuantumNous/new-api/payment/provider" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" ) type AlipayPaymentRequest struct { Amount int64 `json:"amount"` PromoCodeId int `json:"promo_code_id"` TradeType string `json:"trade_type"` } func CreateAlipayPayment(c *gin.Context) { if !operation_setting.IsPaymentComplianceConfirmed() { common.ApiErrorI18n(c, i18n.MsgPaymentComplianceRequired) return } if !setting.Alipay.Enabled { common.ApiErrorMsg(c, "支付宝支付未启用") return } var req AlipayPaymentRequest if err := c.ShouldBindJSON(&req); err != nil { common.ApiErrorMsg(c, "参数错误") return } if req.Amount < getMinTopup() { common.ApiErrorMsg(c, fmt.Sprintf("充值数量不能小于 %d", getMinTopup())) return } id := c.GetInt("id") group, err := model.GetUserGroup(id, true) if err != nil { common.ApiErrorMsg(c, "获取用户分组失败") return } payMoney := getPayMoney(req.Amount, group) if payMoney < 0.01 { common.ApiErrorMsg(c, "充值金额过低") return } amount := req.Amount if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { dAmount := decimal.NewFromInt(req.Amount) dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) amount = dAmount.Div(dQuotaPerUnit).IntPart() if amount < 1 { amount = 1 } } if !validateTopUpPromoCodeForOrder(c, id, req.PromoCodeId, amount) { return } tradeNo := fmt.Sprintf("ALIPAY-%d-%d-%s", id, time.Now().UnixMilli(), common.GetRandomString(6)) topUp := &model.TopUp{ UserId: id, Amount: amount, Money: payMoney, TradeNo: tradeNo, PaymentMethod: model.PaymentMethodAlipay, PaymentProvider: model.PaymentProviderAlipay, PromoCodeId: req.PromoCodeId, CreateTime: time.Now().Unix(), Status: common.TopUpStatusPending, } if err := topUp.Insert(); err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 创建充值订单失败 user_id=%d trade_no=%s amount=%d error=%q", id, tradeNo, req.Amount, err.Error())) common.ApiErrorMsg(c, "创建订单失败") return } alipayProvider, err := provider.NewAlipay(setting.Alipay) if err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 provider 初始化失败 user_id=%d trade_no=%s error=%q", id, tradeNo, err.Error())) topUp.Status = common.TopUpStatusFailed _ = topUp.Update() common.ApiErrorMsg(c, "支付配置错误") return } callbackAddr := service.GetCallbackAddress() notifyURL := callbackAddr + "/api/payment/webhook/alipay" if setting.Alipay.NotifyURL != "" { notifyURL = setting.Alipay.NotifyURL } returnURL := callbackAddr + "/api/payment/alipay/return" if setting.Alipay.ReturnURL != "" { returnURL = setting.Alipay.ReturnURL } resp, err := alipayProvider.CreatePayment(c.Request.Context(), &payment.PaymentRequest{ OrderID: tradeNo, Amount: int(math.Round(payMoney * 100)), Currency: "CNY", Description: fmt.Sprintf("Top up %d credits", req.Amount), ReturnURL: returnURL, NotifyURL: notifyURL, TradeType: req.TradeType, }) if err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 拉起支付失败 user_id=%d trade_no=%s error=%q", id, tradeNo, err.Error())) topUp.Status = common.TopUpStatusFailed _ = topUp.Update() common.ApiErrorMsg(c, "拉起支付失败") return } logger.LogInfo(c.Request.Context(), fmt.Sprintf("支付宝 充值订单创建成功 user_id=%d trade_no=%s amount=%d money=%.2f trade_type=%s", id, tradeNo, req.Amount, payMoney, req.TradeType)) common.ApiSuccess(c, gin.H{ "payment_url": resp.PaymentURL, "qrcode": resp.QRCode, "order_id": tradeNo, }) } func AlipayReturn(c *gin.Context) { alipayProvider, err := provider.NewAlipay(setting.Alipay) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"message": "error", "data": "支付配置错误"}) return } if _, err := alipayProvider.VerifyNotification(c.Request.Context(), []byte(c.Request.URL.Query().Encode()), nil); err != nil { logger.LogWarn(c.Request.Context(), fmt.Sprintf("支付宝 同步回调验签失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error())) c.JSON(http.StatusBadRequest, gin.H{"message": "error", "data": "验签失败"}) return } redirectURL := setting.Alipay.ReturnURL if redirectURL == "" { redirectURL = paymentReturnPath("/console/topup?show_history=true") } c.Redirect(http.StatusFound, redirectURL) } func AlipayNotify(c *gin.Context) { alipayProvider, err := provider.NewAlipay(setting.Alipay) if err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 provider 初始化失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error())) c.String(http.StatusOK, "fail") return } body, err := io.ReadAll(c.Request.Body) if err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 异步通知读取失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error())) c.String(http.StatusOK, "fail") return } status, err := alipayProvider.VerifyNotification(c.Request.Context(), body, nil) if err != nil { logger.LogWarn(c.Request.Context(), fmt.Sprintf("支付宝 异步通知验签失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error())) c.String(http.StatusOK, "fail") return } if status.Status != payment.OrderStatusSuccess { logger.LogInfo(c.Request.Context(), fmt.Sprintf("支付宝 异步通知忽略非成功状态 trade_no=%s status=%s client_ip=%s", status.ProviderOrderID, status.Status, c.ClientIP())) c.String(http.StatusOK, "success") return } LockOrder(status.ProviderOrderID) defer UnlockOrder(status.ProviderOrderID) if err := model.ValidateTopUpPaymentAmount(status.ProviderOrderID, model.PaymentProviderAlipay, status.Amount, status.Currency); err != nil { logger.LogWarn(c.Request.Context(), fmt.Sprintf("支付宝 异步通知金额校验失败 trade_no=%s amount=%d currency=%s client_ip=%s error=%q", status.ProviderOrderID, status.Amount, status.Currency, c.ClientIP(), err.Error())) if errors.Is(err, model.ErrTopUpNotFound) || errors.Is(err, model.ErrTopUpAmountMismatch) || errors.Is(err, model.ErrTopUpCurrencyMismatch) || errors.Is(err, model.ErrPaymentMethodMismatch) { c.String(http.StatusOK, "success") } else { c.String(http.StatusOK, "fail") } return } if err := model.RechargeAlipay(status.ProviderOrderID, c.ClientIP()); err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 充值处理失败 trade_no=%s client_ip=%s error=%q", status.ProviderOrderID, c.ClientIP(), err.Error())) c.String(http.StatusOK, "fail") return } logger.LogInfo(c.Request.Context(), fmt.Sprintf("支付宝 充值成功 trade_no=%s client_ip=%s", status.ProviderOrderID, c.ClientIP())) c.String(http.StatusOK, "success") }