| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- 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")
- }
|