package controller import ( "fmt" "io" "net/http" "strings" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/QuantumNous/new-api/setting/system_setting" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" "github.com/thanhpk/randstr" ) type WaffoPancakePayRequest struct { Amount int64 `json:"amount"` } func RequestWaffoPancakeAmount(c *gin.Context) { var req WaffoPancakePayRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"message": "error", "data": "参数错误"}) return } if req.Amount < int64(setting.WaffoPancakeMinTopUp) { c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", setting.WaffoPancakeMinTopUp)}) return } id := c.GetInt("id") group, err := model.GetUserGroup(id, true) if err != nil { c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"}) return } payMoney := getWaffoPancakePayMoney(req.Amount, group) if payMoney <= 0.01 { c.JSON(http.StatusOK, gin.H{"message": "error", "data": "充值金额过低"}) return } c.JSON(http.StatusOK, gin.H{"message": "success", "data": fmt.Sprintf("%.2f", payMoney)}) } func getWaffoPancakePayMoney(amount int64, group string) float64 { dAmount := decimal.NewFromInt(amount) if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { dAmount = dAmount.Div(decimal.NewFromFloat(common.QuotaPerUnit)) } topupGroupRatio := common.GetTopupGroupRatio(group) if topupGroupRatio == 0 { topupGroupRatio = 1 } discount := 1.0 if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(amount)]; ok && ds > 0 { discount = ds } payMoney := dAmount. Mul(decimal.NewFromFloat(setting.WaffoPancakeUnitPrice)). Mul(decimal.NewFromFloat(topupGroupRatio)). Mul(decimal.NewFromFloat(discount)) return payMoney.InexactFloat64() } func normalizeWaffoPancakeTopUpAmount(amount int64) int64 { if operation_setting.GetQuotaDisplayType() != operation_setting.QuotaDisplayTypeTokens { return amount } normalized := decimal.NewFromInt(amount). Div(decimal.NewFromFloat(common.QuotaPerUnit)). IntPart() if normalized < 1 { return 1 } return normalized } func formatWaffoPancakeAmount(payMoney float64) string { return decimal.NewFromFloat(payMoney).StringFixed(2) } func getWaffoPancakeBuyerEmail(user *model.User) string { if user != nil && strings.TrimSpace(user.Email) != "" { return user.Email } if user != nil { return fmt.Sprintf("%d@new-api.local", user.Id) } return "" } func getWaffoPancakeReturnURL() string { if strings.TrimSpace(setting.WaffoPancakeReturnURL) != "" { return setting.WaffoPancakeReturnURL } return strings.TrimRight(system_setting.ServerAddress, "/") + "/console/topup?show_history=true" } func RequestWaffoPancakePay(c *gin.Context) { if !setting.WaffoPancakeEnabled { c.JSON(http.StatusOK, gin.H{"message": "error", "data": "Waffo Pancake 支付未启用"}) return } currentWebhookKey := setting.WaffoPancakeWebhookPublicKey if setting.WaffoPancakeSandbox { currentWebhookKey = setting.WaffoPancakeWebhookTestKey } if strings.TrimSpace(setting.WaffoPancakeMerchantID) == "" || strings.TrimSpace(setting.WaffoPancakePrivateKey) == "" || strings.TrimSpace(currentWebhookKey) == "" || strings.TrimSpace(setting.WaffoPancakeStoreID) == "" || strings.TrimSpace(setting.WaffoPancakeProductID) == "" { c.JSON(http.StatusOK, gin.H{"message": "error", "data": "Waffo Pancake 配置不完整"}) return } var req WaffoPancakePayRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"message": "error", "data": "参数错误"}) return } if req.Amount < int64(setting.WaffoPancakeMinTopUp) { c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", setting.WaffoPancakeMinTopUp)}) return } id := c.GetInt("id") user, err := model.GetUserById(id, false) if err != nil || user == nil { c.JSON(http.StatusOK, gin.H{"message": "error", "data": "用户不存在"}) return } group, err := model.GetUserGroup(id, true) if err != nil { c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"}) return } payMoney := getWaffoPancakePayMoney(req.Amount, group) if payMoney < 0.01 { c.JSON(http.StatusOK, gin.H{"message": "error", "data": "充值金额过低"}) return } tradeNo := fmt.Sprintf("WAFFO_PANCAKE-%d-%d-%s", id, time.Now().UnixMilli(), randstr.String(6)) topUp := &model.TopUp{ UserId: id, Amount: normalizeWaffoPancakeTopUpAmount(req.Amount), Money: payMoney, TradeNo: tradeNo, PaymentMethod: model.PaymentMethodWaffoPancake, PaymentProvider: model.PaymentProviderWaffoPancake, CreateTime: time.Now().Unix(), Status: common.TopUpStatusPending, } if err := topUp.Insert(); err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("Waffo Pancake 创建充值订单失败 user_id=%d trade_no=%s amount=%d error=%q", id, tradeNo, req.Amount, err.Error())) c.JSON(http.StatusOK, gin.H{"message": "error", "data": "创建订单失败"}) return } expiresInSeconds := 45 * 60 session, err := service.CreateWaffoPancakeCheckoutSession(c.Request.Context(), &service.WaffoPancakeCreateSessionParams{ StoreID: setting.WaffoPancakeStoreID, ProductID: setting.WaffoPancakeProductID, ProductType: "onetime", Currency: strings.ToUpper(strings.TrimSpace(setting.WaffoPancakeCurrency)), PriceSnapshot: &service.WaffoPancakePriceSnapshot{ Amount: formatWaffoPancakeAmount(payMoney), TaxIncluded: false, TaxCategory: "saas", }, BuyerEmail: getWaffoPancakeBuyerEmail(user), SuccessURL: getWaffoPancakeReturnURL(), ExpiresInSeconds: &expiresInSeconds, }) if err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("Waffo Pancake 创建结账会话失败 user_id=%d trade_no=%s error=%q", id, tradeNo, err.Error())) topUp.Status = common.TopUpStatusFailed _ = topUp.Update() c.JSON(http.StatusOK, gin.H{"message": "error", "data": "拉起支付失败"}) return } logger.LogInfo(c.Request.Context(), fmt.Sprintf("Waffo Pancake 充值订单创建成功 user_id=%d trade_no=%s session_id=%s amount=%d money=%.2f", id, tradeNo, session.SessionID, req.Amount, payMoney)) c.JSON(http.StatusOK, gin.H{ "message": "success", "data": gin.H{ "checkout_url": session.CheckoutURL, "session_id": session.SessionID, "expires_at": session.ExpiresAt, "order_id": tradeNo, }, }) } func WaffoPancakeWebhook(c *gin.Context) { if !isWaffoPancakeWebhookEnabled() { logger.LogWarn(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 被拒绝 reason=webhook_disabled path=%q client_ip=%s", c.Request.RequestURI, c.ClientIP())) c.String(http.StatusForbidden, "webhook disabled") return } bodyBytes, err := io.ReadAll(c.Request.Body) if err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 读取请求体失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error())) c.String(http.StatusBadRequest, "bad request") return } signature := c.GetHeader("X-Waffo-Signature") logger.LogInfo(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 收到请求 path=%q client_ip=%s signature=%q body=%q", c.Request.RequestURI, c.ClientIP(), signature, string(bodyBytes))) event, err := service.VerifyConfiguredWaffoPancakeWebhook(string(bodyBytes), signature) if err != nil { logger.LogWarn(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 验签失败 path=%q client_ip=%s signature=%q body=%q error=%q", c.Request.RequestURI, c.ClientIP(), signature, string(bodyBytes), err.Error())) c.String(http.StatusUnauthorized, "invalid signature") return } logger.LogInfo(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 验签成功 event_type=%s event_id=%s order_id=%s client_ip=%s", event.NormalizedEventType(), event.ID, event.Data.OrderID, c.ClientIP())) if event.NormalizedEventType() != "order.completed" { c.String(http.StatusOK, "OK") return } tradeNo, err := service.ResolveWaffoPancakeTradeNo(event) if err != nil { logger.LogWarn(c.Request.Context(), fmt.Sprintf("Waffo Pancake webhook 订单号映射失败 event_id=%s order_id=%s error=%q", event.ID, event.Data.OrderID, err.Error())) c.String(http.StatusOK, "OK") return } LockOrder(tradeNo) defer UnlockOrder(tradeNo) if err := model.RechargeWaffoPancake(tradeNo); err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("Waffo Pancake 充值处理失败 trade_no=%s event_id=%s order_id=%s client_ip=%s error=%q", tradeNo, event.ID, event.Data.OrderID, c.ClientIP(), err.Error())) c.String(http.StatusInternalServerError, "retry") return } logger.LogInfo(c.Request.Context(), fmt.Sprintf("Waffo Pancake 充值成功 trade_no=%s event_id=%s order_id=%s client_ip=%s", tradeNo, event.ID, event.Data.OrderID, c.ClientIP())) c.String(http.StatusOK, "OK") }