package controller import ( "errors" "fmt" "io" "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 WxpayPaymentRequest struct { Amount int64 `json:"amount"` PromoCodeId int `json:"promo_code_id"` TradeType string `json:"trade_type"` OpenID string `json:"openid"` } func CreateWxpayPayment(c *gin.Context) { if !operation_setting.IsPaymentComplianceConfirmed() { common.ApiErrorI18n(c, i18n.MsgPaymentComplianceRequired) return } if !setting.Wxpay.Enabled { common.ApiErrorMsg(c, "微信支付未启用") return } var req WxpayPaymentRequest 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("WXPAY-%d-%d-%s", id, time.Now().UnixMilli(), common.GetRandomString(6)) topUp := &model.TopUp{ UserId: id, Amount: amount, Money: payMoney, TradeNo: tradeNo, PaymentMethod: model.PaymentMethodWxpay, PaymentProvider: model.PaymentProviderWxpay, 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 } wxpayProvider, err := provider.NewWxpay(setting.Wxpay) 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/wxpay" if setting.Wxpay.NotifyURL != "" { notifyURL = setting.Wxpay.NotifyURL } returnURL := callbackAddr + "/console/topup?show_history=true" if setting.Wxpay.ReturnURL != "" { returnURL = setting.Wxpay.ReturnURL } payAmount := decimal.NewFromFloat(payMoney).Mul(decimal.NewFromInt(100)).Round(0).IntPart() resp, err := wxpayProvider.CreatePayment(c.Request.Context(), &payment.PaymentRequest{ OrderID: tradeNo, Amount: int(payAmount), Currency: "CNY", Description: fmt.Sprintf("Top up %d credits", req.Amount), ReturnURL: returnURL, NotifyURL: notifyURL, ClientIP: c.ClientIP(), TradeType: req.TradeType, OpenID: req.OpenID, }) 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 WxpayNotify(c *gin.Context) { wxpayProvider, err := provider.NewWxpay(setting.Wxpay) 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())) wxpayNotifyFail(c, "支付配置错误") 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())) wxpayNotifyFail(c, "读取失败") return } status, err := wxpayProvider.VerifyNotification(c.Request.Context(), body, collectWxpayHeaders(c)) if err != nil { logger.LogWarn(c.Request.Context(), fmt.Sprintf("微信支付 异步通知验签或解密失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error())) wxpayNotifyFail(c, "验签失败") 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())) wxpayNotifySuccess(c) return } LockOrder(status.ProviderOrderID) defer UnlockOrder(status.ProviderOrderID) if err := model.ValidateTopUpPaymentAmount(status.ProviderOrderID, model.PaymentProviderWxpay, 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) { wxpayNotifySuccess(c) } else { wxpayNotifyFail(c, "金额校验失败") } return } if err := model.RechargeWxpay(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())) wxpayNotifyFail(c, "处理失败") return } logger.LogInfo(c.Request.Context(), fmt.Sprintf("微信支付 充值成功 trade_no=%s client_ip=%s", status.ProviderOrderID, c.ClientIP())) wxpayNotifySuccess(c) } func collectWxpayHeaders(c *gin.Context) map[string]string { headers := map[string]string{} for _, key := range []string{ "Wechatpay-Timestamp", "Wechatpay-Nonce", "Wechatpay-Signature", "Wechatpay-Serial", } { headers[key] = c.GetHeader(key) } return headers } func wxpayNotifySuccess(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"}) } func wxpayNotifyFail(c *gin.Context, message string) { c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": message}) }