Przeglądaj źródła

Merge pull request #3293 from zhongyuanzhao-alt/ft-waffo-payment-zzy20260317

feat(waffo): Waffo payment gateway integration
Seefs 3 tygodni temu
rodzic
commit
04f5dd0206

+ 1 - 0
common/constants.go

@@ -212,5 +212,6 @@ const (
 const (
 	TopUpStatusPending = "pending"
 	TopUpStatusSuccess = "success"
+	TopUpStatusFailed  = "failed"
 	TopUpStatusExpired = "expired"
 )

+ 16 - 0
constant/waffo_pay_method.go

@@ -0,0 +1,16 @@
+package constant
+
+// WaffoPayMethod defines the display and API parameter mapping for Waffo payment methods.
+type WaffoPayMethod struct {
+	Name          string `json:"name"`            // Frontend display name
+	Icon          string `json:"icon"`            // Frontend icon identifier: credit-card, apple, google
+	PayMethodType string `json:"payMethodType"` // Waffo API PayMethodType, can be comma-separated
+	PayMethodName string `json:"payMethodName"` // Waffo API PayMethodName, empty means auto-select by Waffo checkout
+}
+
+// DefaultWaffoPayMethods is the default list of supported payment methods.
+var DefaultWaffoPayMethods = []WaffoPayMethod{
+	{Name: "Card", Icon: "/pay-card.png", PayMethodType: "CREDITCARD,DEBITCARD", PayMethodName: ""},
+	{Name: "Apple Pay", Icon: "/pay-apple.png", PayMethodType: "APPLEPAY", PayMethodName: "APPLEPAY"},
+	{Name: "Google Pay", Icon: "/pay-google.png", PayMethodType: "GOOGLEPAY", PayMethodName: "GOOGLEPAY"},
+}

+ 68 - 14
controller/topup.go

@@ -48,14 +48,52 @@ func GetTopUpInfo(c *gin.Context) {
 		}
 	}
 
+	// 如果启用了 Waffo 支付,添加到支付方法列表
+	enableWaffo := setting.WaffoEnabled &&
+		((!setting.WaffoSandbox &&
+			setting.WaffoApiKey != "" &&
+			setting.WaffoPrivateKey != "" &&
+			setting.WaffoPublicCert != "") ||
+			(setting.WaffoSandbox &&
+				setting.WaffoSandboxApiKey != "" &&
+				setting.WaffoSandboxPrivateKey != "" &&
+				setting.WaffoSandboxPublicCert != ""))
+	if enableWaffo {
+		hasWaffo := false
+		for _, method := range payMethods {
+			if method["type"] == "waffo" {
+				hasWaffo = true
+				break
+			}
+		}
+
+		if !hasWaffo {
+			waffoMethod := map[string]string{
+				"name":      "Waffo (Global Payment)",
+				"type":      "waffo",
+				"color":     "rgba(var(--semi-blue-5), 1)",
+				"min_topup": strconv.Itoa(setting.WaffoMinTopUp),
+			}
+			payMethods = append(payMethods, waffoMethod)
+		}
+	}
+
 	data := gin.H{
 		"enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "",
 		"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
 		"enable_creem_topup":  setting.CreemApiKey != "" && setting.CreemProducts != "[]",
-		"creem_products":      setting.CreemProducts,
+		"enable_waffo_topup": enableWaffo,
+		"waffo_pay_methods": func() interface{} {
+			if enableWaffo {
+				return setting.GetWaffoPayMethods()
+			}
+			return nil
+		}(),
+		"creem_products": setting.CreemProducts,
 		"pay_methods":         payMethods,
 		"min_topup":           operation_setting.MinTopUp,
 		"stripe_min_topup":    setting.StripeMinTopUp,
+		"waffo_min_topup":     setting.WaffoMinTopUp,
 		"amount_options":      operation_setting.GetPaymentSetting().AmountOptions,
 		"discount":            operation_setting.GetPaymentSetting().AmountDiscount,
 	}
@@ -204,27 +242,42 @@ func RequestEpay(c *gin.Context) {
 var orderLocks sync.Map
 var createLock sync.Mutex
 
+// refCountedMutex 带引用计数的互斥锁,确保最后一个使用者才从 map 中删除
+type refCountedMutex struct {
+	mu       sync.Mutex
+	refCount int
+}
+
 // LockOrder 尝试对给定订单号加锁
 func LockOrder(tradeNo string) {
-	lock, ok := orderLocks.Load(tradeNo)
-	if !ok {
-		createLock.Lock()
-		defer createLock.Unlock()
-		lock, ok = orderLocks.Load(tradeNo)
-		if !ok {
-			lock = new(sync.Mutex)
-			orderLocks.Store(tradeNo, lock)
-		}
+	createLock.Lock()
+	var rcm *refCountedMutex
+	if v, ok := orderLocks.Load(tradeNo); ok {
+		rcm = v.(*refCountedMutex)
+	} else {
+		rcm = &refCountedMutex{}
+		orderLocks.Store(tradeNo, rcm)
 	}
-	lock.(*sync.Mutex).Lock()
+	rcm.refCount++
+	createLock.Unlock()
+	rcm.mu.Lock()
 }
 
 // UnlockOrder 释放给定订单号的锁
 func UnlockOrder(tradeNo string) {
-	lock, ok := orderLocks.Load(tradeNo)
-	if ok {
-		lock.(*sync.Mutex).Unlock()
+	v, ok := orderLocks.Load(tradeNo)
+	if !ok {
+		return
 	}
+	rcm := v.(*refCountedMutex)
+	rcm.mu.Unlock()
+
+	createLock.Lock()
+	rcm.refCount--
+	if rcm.refCount == 0 {
+		orderLocks.Delete(tradeNo)
+	}
+	createLock.Unlock()
 }
 
 func EpayNotify(c *gin.Context) {
@@ -410,3 +463,4 @@ func AdminCompleteTopUp(c *gin.Context) {
 	}
 	common.ApiSuccess(c, nil)
 }
+

+ 380 - 0
controller/topup_waffo.go

@@ -0,0 +1,380 @@
+package controller
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"strconv"
+	"time"
+
+	"github.com/QuantumNous/new-api/common"
+	"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/thanhpk/randstr"
+	waffo "github.com/waffo-com/waffo-go"
+	"github.com/waffo-com/waffo-go/config"
+	"github.com/waffo-com/waffo-go/core"
+	"github.com/waffo-com/waffo-go/types/order"
+)
+
+func getWaffoSDK() (*waffo.Waffo, error) {
+	env := config.Sandbox
+	apiKey := setting.WaffoSandboxApiKey
+	privateKey := setting.WaffoSandboxPrivateKey
+	publicKey := setting.WaffoSandboxPublicCert
+	if !setting.WaffoSandbox {
+		env = config.Production
+		apiKey = setting.WaffoApiKey
+		privateKey = setting.WaffoPrivateKey
+		publicKey = setting.WaffoPublicCert
+	}
+	builder := config.NewConfigBuilder().
+		APIKey(apiKey).
+		PrivateKey(privateKey).
+		WaffoPublicKey(publicKey).
+		Environment(env)
+	if setting.WaffoMerchantId != "" {
+		builder = builder.MerchantID(setting.WaffoMerchantId)
+	}
+	cfg, err := builder.Build()
+	if err != nil {
+		return nil, err
+	}
+	return waffo.New(cfg), nil
+}
+
+func getWaffoUserEmail(user *model.User) string {
+	return fmt.Sprintf("%[email protected]", user.Id)
+}
+
+func getWaffoCurrency() string {
+	if setting.WaffoCurrency != "" {
+		return setting.WaffoCurrency
+	}
+	return "USD"
+}
+
+// zeroDecimalCurrencies 零小数位币种,金额不能带小数点
+var zeroDecimalCurrencies = map[string]bool{
+	"IDR": true, "JPY": true, "KRW": true, "VND": true,
+}
+
+func formatWaffoAmount(amount float64, currency string) string {
+	if zeroDecimalCurrencies[currency] {
+		return fmt.Sprintf("%.0f", amount)
+	}
+	return fmt.Sprintf("%.2f", amount)
+}
+
+// getWaffoPayMoney converts the user-facing amount to USD for Waffo payment.
+// Waffo only accepts USD, so this function handles the conversion from different
+// display types (USD/CNY/TOKENS) to the actual USD amount to charge.
+func getWaffoPayMoney(amount float64, group string) float64 {
+	originalAmount := amount
+	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
+		amount = amount / common.QuotaPerUnit
+	}
+	topupGroupRatio := common.GetTopupGroupRatio(group)
+	if topupGroupRatio == 0 {
+		topupGroupRatio = 1
+	}
+	discount := 1.0
+	if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok {
+		if ds > 0 {
+			discount = ds
+		}
+	}
+	return amount * setting.WaffoUnitPrice * topupGroupRatio * discount
+}
+
+type WaffoPayRequest struct {
+	Amount         int64  `json:"amount"`
+	PayMethodIndex *int   `json:"pay_method_index"` // 服务端支付方式列表的索引,nil 表示由 Waffo 自动选择
+	PayMethodType  string `json:"pay_method_type"`  // Deprecated: 兼容旧前端,优先使用 pay_method_index
+	PayMethodName  string `json:"pay_method_name"`  // Deprecated: 兼容旧前端,优先使用 pay_method_index
+}
+
+// RequestWaffoPay 创建 Waffo 支付订单
+func RequestWaffoPay(c *gin.Context) {
+	if !setting.WaffoEnabled {
+		c.JSON(200, gin.H{"message": "error", "data": "Waffo 支付未启用"})
+		return
+	}
+
+	var req WaffoPayRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
+		return
+	}
+	waffoMinTopup := int64(setting.WaffoMinTopUp)
+	if req.Amount < waffoMinTopup {
+		c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", waffoMinTopup)})
+		return
+	}
+
+	id := c.GetInt("id")
+	user, err := model.GetUserById(id, false)
+	if err != nil || user == nil {
+		c.JSON(200, gin.H{"message": "error", "data": "用户不存在"})
+		return
+	}
+
+	// 从服务端配置查找支付方式,客户端只传索引或旧字段
+	var resolvedPayMethodType, resolvedPayMethodName string
+	methods := setting.GetWaffoPayMethods()
+	if req.PayMethodIndex != nil {
+		// 新协议:按索引查找
+		idx := *req.PayMethodIndex
+		if idx < 0 || idx >= len(methods) {
+			log.Printf("Waffo 无效的支付方式索引: %d, UserId=%d, 可用范围: [0, %d)", idx, id, len(methods))
+			c.JSON(200, gin.H{"message": "error", "data": "不支持的支付方式"})
+			return
+		}
+		resolvedPayMethodType = methods[idx].PayMethodType
+		resolvedPayMethodName = methods[idx].PayMethodName
+	} else if req.PayMethodType != "" {
+		// 兼容旧前端:验证客户端传的值在服务端列表中
+		valid := false
+		for _, m := range methods {
+			if m.PayMethodType == req.PayMethodType && m.PayMethodName == req.PayMethodName {
+				valid = true
+				resolvedPayMethodType = m.PayMethodType
+				resolvedPayMethodName = m.PayMethodName
+				break
+			}
+		}
+		if !valid {
+			log.Printf("Waffo 无效的支付方式: PayMethodType=%s, PayMethodName=%s, UserId=%d", req.PayMethodType, req.PayMethodName, id)
+			c.JSON(200, gin.H{"message": "error", "data": "不支持的支付方式"})
+			return
+		}
+	}
+	// resolvedPayMethodType/Name 为空时,Waffo 自动选择支付方式
+
+	group, _ := model.GetUserGroup(id, true)
+	payMoney := getWaffoPayMoney(float64(req.Amount), group)
+	if payMoney < 0.01 {
+		c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
+		return
+	}
+
+	// 生成唯一订单号,paymentRequestId 与 merchantOrderId 保持一致,简化追踪
+	merchantOrderId := fmt.Sprintf("WAFFO-%d-%d-%s", id, time.Now().UnixMilli(), randstr.String(6))
+	paymentRequestId := merchantOrderId
+
+	// Token 模式下归一化 Amount(存等价美元/CNY 数量,避免 RechargeWaffo 双重放大)
+	amount := req.Amount
+	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
+		amount = int64(float64(req.Amount) / common.QuotaPerUnit)
+		if amount < 1 {
+			amount = 1
+		}
+	}
+
+	// 创建本地订单
+	topUp := &model.TopUp{
+		UserId:        id,
+		Amount:        amount,
+		Money:         payMoney,
+		TradeNo:       merchantOrderId,
+		PaymentMethod: "waffo",
+		CreateTime:    time.Now().Unix(),
+		Status:        common.TopUpStatusPending,
+	}
+	if err := topUp.Insert(); err != nil {
+		log.Printf("Waffo 创建本地订单失败: %v", err)
+		c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
+		return
+	}
+
+	sdk, err := getWaffoSDK()
+	if err != nil {
+		log.Printf("Waffo SDK 初始化失败: %v", err)
+		topUp.Status = common.TopUpStatusFailed
+		_ = topUp.Update()
+		c.JSON(200, gin.H{"message": "error", "data": "支付配置错误"})
+		return
+	}
+
+	callbackAddr := service.GetCallbackAddress()
+	notifyUrl := callbackAddr + "/api/waffo/webhook"
+	if setting.WaffoNotifyUrl != "" {
+		notifyUrl = setting.WaffoNotifyUrl
+	}
+	returnUrl := system_setting.ServerAddress + "/console/topup?show_history=true"
+	if setting.WaffoReturnUrl != "" {
+		returnUrl = setting.WaffoReturnUrl
+	}
+
+	currency := getWaffoCurrency()
+	createParams := &order.CreateOrderParams{
+		PaymentRequestID: paymentRequestId,
+		MerchantOrderID:  merchantOrderId,
+		OrderAmount:      formatWaffoAmount(payMoney, currency),
+		OrderCurrency:    currency,
+		OrderDescription: fmt.Sprintf("Recharge %d credits", req.Amount),
+		OrderRequestedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
+		NotifyURL:        notifyUrl,
+		MerchantInfo: &order.MerchantInfo{
+			MerchantID: setting.WaffoMerchantId,
+		},
+		UserInfo: &order.UserInfo{
+			UserID:       strconv.Itoa(user.Id),
+			UserEmail:    getWaffoUserEmail(user),
+			UserTerminal: "WEB",
+		},
+		PaymentInfo: &order.PaymentInfo{
+			ProductName:   "ONE_TIME_PAYMENT",
+			PayMethodType: resolvedPayMethodType,
+			PayMethodName: resolvedPayMethodName,
+		},
+		SuccessRedirectURL: returnUrl,
+		FailedRedirectURL:  returnUrl,
+	}
+	resp, err := sdk.Order().Create(c.Request.Context(), createParams, nil)
+	if err != nil {
+		log.Printf("Waffo 创建订单失败: %v", err)
+		topUp.Status = common.TopUpStatusFailed
+		_ = topUp.Update()
+		c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
+		return
+	}
+	if !resp.IsSuccess() {
+		log.Printf("Waffo 创建订单业务失败: [%s] %s, 完整响应: %+v", resp.Code, resp.Message, resp)
+		topUp.Status = common.TopUpStatusFailed
+		_ = topUp.Update()
+		c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
+		return
+	}
+
+	orderData := resp.GetData()
+	log.Printf("Waffo 订单创建成功 - 用户: %d, 订单: %s, 金额: %.2f", id, merchantOrderId, payMoney)
+
+	paymentUrl := orderData.FetchRedirectURL()
+	if paymentUrl == "" {
+		paymentUrl = orderData.OrderAction
+	}
+
+	c.JSON(200, gin.H{
+		"message": "success",
+		"data": gin.H{
+			"payment_url": paymentUrl,
+			"order_id":    merchantOrderId,
+		},
+	})
+}
+
+// webhookPayloadWithSubInfo 扩展 PAYMENT_NOTIFICATION,包含 SDK 未定义的 subscriptionInfo 字段
+type webhookPayloadWithSubInfo struct {
+	EventType string `json:"eventType"`
+	Result    struct {
+		core.PaymentNotificationResult
+		SubscriptionInfo *webhookSubscriptionInfo `json:"subscriptionInfo,omitempty"`
+	} `json:"result"`
+}
+
+type webhookSubscriptionInfo struct {
+	Period              string `json:"period,omitempty"`
+	MerchantRequest     string `json:"merchantRequest,omitempty"`
+	SubscriptionID      string `json:"subscriptionId,omitempty"`
+	SubscriptionRequest string `json:"subscriptionRequest,omitempty"`
+}
+
+// WaffoWebhook 处理 Waffo 回调通知(支付/退款/订阅)
+func WaffoWebhook(c *gin.Context) {
+	bodyBytes, err := io.ReadAll(c.Request.Body)
+	if err != nil {
+		log.Printf("Waffo Webhook 读取 body 失败: %v", err)
+		c.AbortWithStatus(http.StatusBadRequest)
+		return
+	}
+
+	sdk, err := getWaffoSDK()
+	if err != nil {
+		log.Printf("Waffo Webhook SDK 初始化失败: %v", err)
+		c.AbortWithStatus(http.StatusInternalServerError)
+		return
+	}
+
+	wh := sdk.Webhook()
+	bodyStr := string(bodyBytes)
+	signature := c.GetHeader("X-SIGNATURE")
+
+	// 验证请求签名
+	if !wh.VerifySignature(bodyStr, signature) {
+		log.Printf("Waffo webhook 签名验证失败")
+		c.AbortWithStatus(http.StatusBadRequest)
+		return
+	}
+
+	var event core.WebhookEvent
+	if err := common.Unmarshal(bodyBytes, &event); err != nil {
+		log.Printf("Waffo Webhook 解析失败: %v", err)
+		sendWaffoWebhookResponse(c, wh, false, "invalid payload")
+		return
+	}
+
+	switch event.EventType {
+	case core.EventPayment:
+		// 解析为扩展类型,区分普通支付和订阅支付
+		var payload webhookPayloadWithSubInfo
+		if err := common.Unmarshal(bodyBytes, &payload); err != nil {
+			sendWaffoWebhookResponse(c, wh, false, "invalid payment payload")
+			return
+		}
+		log.Printf("Waffo Webhook - EventType: %s, MerchantOrderId: %s, OrderStatus: %s",
+			event.EventType, payload.Result.MerchantOrderID, payload.Result.OrderStatus)
+		handleWaffoPayment(c, wh, &payload.Result.PaymentNotificationResult)
+	default:
+		log.Printf("Waffo Webhook 未知事件: %s", event.EventType)
+		sendWaffoWebhookResponse(c, wh, true, "")
+	}
+}
+
+// handleWaffoPayment 处理支付完成通知
+func handleWaffoPayment(c *gin.Context, wh *core.WebhookHandler, result *core.PaymentNotificationResult) {
+	if result.OrderStatus != "PAY_SUCCESS" {
+		log.Printf("Waffo 订单状态非成功: %s, 订单: %s", result.OrderStatus, result.MerchantOrderID)
+		// 终态失败订单标记为 failed,避免永远停在 pending
+		if result.MerchantOrderID != "" {
+			if topUp := model.GetTopUpByTradeNo(result.MerchantOrderID); topUp != nil &&
+				topUp.Status == common.TopUpStatusPending {
+				topUp.Status = common.TopUpStatusFailed
+				_ = topUp.Update()
+			}
+		}
+		sendWaffoWebhookResponse(c, wh, true, "")
+		return
+	}
+
+	merchantOrderId := result.MerchantOrderID
+
+	LockOrder(merchantOrderId)
+	defer UnlockOrder(merchantOrderId)
+
+	if err := model.RechargeWaffo(merchantOrderId); err != nil {
+		log.Printf("Waffo 充值处理失败: %v, 订单: %s", err, merchantOrderId)
+		sendWaffoWebhookResponse(c, wh, false, err.Error())
+		return
+	}
+
+	log.Printf("Waffo 充值成功 - 订单: %s", merchantOrderId)
+	sendWaffoWebhookResponse(c, wh, true, "")
+}
+
+// sendWaffoWebhookResponse 发送签名响应
+func sendWaffoWebhookResponse(c *gin.Context, wh *core.WebhookHandler, success bool, msg string) {
+	var body, sig string
+	if success {
+		body, sig = wh.BuildSuccessResponse()
+	} else {
+		body, sig = wh.BuildFailedResponse(msg)
+	}
+	c.Header("X-SIGNATURE", sig)
+	c.Data(http.StatusOK, "application/json", []byte(body))
+}

+ 1 - 1
go.mod

@@ -46,6 +46,7 @@ require (
 	github.com/tidwall/gjson v1.18.0
 	github.com/tidwall/sjson v1.2.5
 	github.com/tiktoken-go/tokenizer v0.6.2
+	github.com/waffo-com/waffo-go v1.3.1
 	github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
 	golang.org/x/crypto v0.45.0
 	golang.org/x/image v0.23.0
@@ -120,7 +121,6 @@ require (
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/samber/go-singleflightx v0.3.2 // indirect
-	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/pretty v1.2.0 // indirect
 	github.com/tklauser/go-sysconf v0.3.12 // indirect

+ 14 - 33
go.sum

@@ -1,3 +1,5 @@
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
 github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
 github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g=
@@ -10,34 +12,18 @@ github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+Kc
 github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
 github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
 github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8=
-github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
-github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
 github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
 github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo=
 github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
 github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 h1:sPiRHLVUIIQcoVZTNwqQcdtjkqkPopyYmIX0M5ElRf4=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2/go.mod h1:ik86P3sgV+Bk7c1tBFCwI3VxMoSEwl4YkRB9xn1s340=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 h1:ZdzDAg075H6stMZtbD2o+PyB933M/f20e9WmCBC17wA=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2/go.mod h1:eE1IIzXG9sdZCB0pNNpMpsYTLl4YdOQD3njiVN1e/E4=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
-github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.33.0 h1:JzidOz4Hcn2RbP5fvIS1iAP+DcRv5VJtgixbEYDsI5g=
-github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.33.0/go.mod h1:9A4/PJYlWjvjEzzoOLGQjkLt4bYK9fRWi7uz1GSsAcA=
 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0 h1:TDKR8ACRw7G+GFaQlhoy6biu+8q6ZtSddQCy9avMdMI=
 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.0/go.mod h1:XlhOh5Ax/lesqN4aZCUgj9vVJed5VoXYHHFYGAlJEwU=
-github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
-github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
-github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
-github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
 github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
 github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -58,7 +44,6 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -132,12 +117,13 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
 github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
 github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -186,8 +172,6 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
-github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -245,7 +229,6 @@ github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -262,8 +245,9 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/samber/go-singleflightx v0.3.2 h1:jXbUU0fvis8Fdv4HGONboX5WdEZcYLoBEcKiE+ITCyQ=
 github.com/samber/go-singleflightx v0.3.2/go.mod h1:X2BR+oheHIYc73PvxRMlcASg6KYYTQyUYpdVU7t/ux4=
 github.com/samber/hot v0.11.0 h1:JhV9hk8SmZIqB0To8OyCzPubvszkuoSXWx/7FCEGO+Q=
@@ -320,6 +304,8 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/waffo-com/waffo-go v1.3.1 h1:NCYD3oQ59DTJj1bwS5T/659LI4h8PuAIW4Qj/w7fKPw=
+github.com/waffo-com/waffo-go v1.3.1/go.mod h1:IaXVYq6mmYtrLFFsLxPslNwuIZx0mIadWWjhe+eWb0g=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@@ -330,6 +316,8 @@ github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFi
 github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
 go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
 golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -339,14 +327,12 @@ golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/y
 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
 golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
 golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
-golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
-golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
+golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
 golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
 golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -367,19 +353,14 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
 golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
 golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
-golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
 golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
+golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
 google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
 google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

+ 50 - 0
model/option.go

@@ -89,6 +89,22 @@ func InitOptionMap() {
 	common.OptionMap["CreemProducts"] = setting.CreemProducts
 	common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode)
 	common.OptionMap["CreemWebhookSecret"] = setting.CreemWebhookSecret
+	common.OptionMap["WaffoEnabled"] = strconv.FormatBool(setting.WaffoEnabled)
+	common.OptionMap["WaffoApiKey"] = setting.WaffoApiKey
+	common.OptionMap["WaffoPrivateKey"] = setting.WaffoPrivateKey
+	common.OptionMap["WaffoPublicCert"] = setting.WaffoPublicCert
+	common.OptionMap["WaffoSandboxPublicCert"] = setting.WaffoSandboxPublicCert
+	common.OptionMap["WaffoSandboxApiKey"] = setting.WaffoSandboxApiKey
+	common.OptionMap["WaffoSandboxPrivateKey"] = setting.WaffoSandboxPrivateKey
+	common.OptionMap["WaffoSandbox"] = strconv.FormatBool(setting.WaffoSandbox)
+	common.OptionMap["WaffoMerchantId"] = setting.WaffoMerchantId
+	common.OptionMap["WaffoNotifyUrl"] = setting.WaffoNotifyUrl
+	common.OptionMap["WaffoReturnUrl"] = setting.WaffoReturnUrl
+	common.OptionMap["WaffoSubscriptionReturnUrl"] = setting.WaffoSubscriptionReturnUrl
+	common.OptionMap["WaffoCurrency"] = setting.WaffoCurrency
+	common.OptionMap["WaffoUnitPrice"] = strconv.FormatFloat(setting.WaffoUnitPrice, 'f', -1, 64)
+	common.OptionMap["WaffoMinTopUp"] = strconv.Itoa(setting.WaffoMinTopUp)
+	common.OptionMap["WaffoPayMethods"] = setting.WaffoPayMethods2JsonString()
 	common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
 	common.OptionMap["Chats"] = setting.Chats2JsonString()
 	common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
@@ -358,6 +374,36 @@ func updateOptionMap(key string, value string) (err error) {
 		setting.CreemTestMode = value == "true"
 	case "CreemWebhookSecret":
 		setting.CreemWebhookSecret = value
+	case "WaffoEnabled":
+		setting.WaffoEnabled = value == "true"
+	case "WaffoApiKey":
+		setting.WaffoApiKey = value
+	case "WaffoPrivateKey":
+		setting.WaffoPrivateKey = value
+	case "WaffoPublicCert":
+		setting.WaffoPublicCert = value
+	case "WaffoSandboxPublicCert":
+		setting.WaffoSandboxPublicCert = value
+	case "WaffoSandboxApiKey":
+		setting.WaffoSandboxApiKey = value
+	case "WaffoSandboxPrivateKey":
+		setting.WaffoSandboxPrivateKey = value
+	case "WaffoSandbox":
+		setting.WaffoSandbox = value == "true"
+	case "WaffoMerchantId":
+		setting.WaffoMerchantId = value
+	case "WaffoNotifyUrl":
+		setting.WaffoNotifyUrl = value
+	case "WaffoReturnUrl":
+		setting.WaffoReturnUrl = value
+	case "WaffoSubscriptionReturnUrl":
+		setting.WaffoSubscriptionReturnUrl = value
+	case "WaffoCurrency":
+		setting.WaffoCurrency = value
+	case "WaffoUnitPrice":
+		setting.WaffoUnitPrice, _ = strconv.ParseFloat(value, 64)
+	case "WaffoMinTopUp":
+		setting.WaffoMinTopUp, _ = strconv.Atoi(value)
 	case "TopupGroupRatio":
 		err = common.UpdateTopupGroupRatioByJSONString(value)
 	case "GitHubClientId":
@@ -458,6 +504,10 @@ func updateOptionMap(key string, value string) (err error) {
 		setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
 	case "PayMethods":
 		err = operation_setting.UpdatePayMethodsByJsonString(value)
+	case "WaffoPayMethods":
+		// WaffoPayMethods is read directly from OptionMap via setting.GetWaffoPayMethods().
+		// The value is already stored in OptionMap at the top of this function (line: common.OptionMap[key] = value).
+		// No additional in-memory variable to update.
 	}
 	return err
 }

+ 68 - 9
model/topup.go

@@ -12,15 +12,15 @@ import (
 )
 
 type TopUp struct {
-	Id            int     `json:"id"`
-	UserId        int     `json:"user_id" gorm:"index"`
-	Amount        int64   `json:"amount"`
-	Money         float64 `json:"money"`
-	TradeNo       string  `json:"trade_no" gorm:"unique;type:varchar(255);index"`
-	PaymentMethod string  `json:"payment_method" gorm:"type:varchar(50)"`
-	CreateTime    int64   `json:"create_time"`
-	CompleteTime  int64   `json:"complete_time"`
-	Status        string  `json:"status"`
+	Id               int     `json:"id"`
+	UserId           int     `json:"user_id" gorm:"index"`
+	Amount           int64   `json:"amount"`
+	Money            float64 `json:"money"`
+	TradeNo          string  `json:"trade_no" gorm:"unique;type:varchar(255);index"`
+	PaymentMethod    string  `json:"payment_method" gorm:"type:varchar(50)"`
+	CreateTime       int64   `json:"create_time"`
+	CompleteTime     int64   `json:"complete_time"`
+	Status           string  `json:"status"`
 }
 
 func (topUp *TopUp) Insert() error {
@@ -376,3 +376,62 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
 
 	return nil
 }
+
+func RechargeWaffo(tradeNo string) (err error) {
+	if tradeNo == "" {
+		return errors.New("未提供支付单号")
+	}
+
+	var quotaToAdd int
+	topUp := &TopUp{}
+
+	refCol := "`trade_no`"
+	if common.UsingPostgreSQL {
+		refCol = `"trade_no"`
+	}
+
+	err = DB.Transaction(func(tx *gorm.DB) error {
+		err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(topUp).Error
+		if err != nil {
+			return errors.New("充值订单不存在")
+		}
+
+		if topUp.Status == common.TopUpStatusSuccess {
+			return nil // 幂等:已成功直接返回
+		}
+
+		if topUp.Status != common.TopUpStatusPending {
+			return errors.New("充值订单状态错误")
+		}
+
+		dAmount := decimal.NewFromInt(topUp.Amount)
+		dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
+		quotaToAdd = int(dAmount.Mul(dQuotaPerUnit).IntPart())
+		if quotaToAdd <= 0 {
+			return errors.New("无效的充值额度")
+		}
+
+		topUp.CompleteTime = common.GetTimestamp()
+		topUp.Status = common.TopUpStatusSuccess
+		if err := tx.Save(topUp).Error; err != nil {
+			return err
+		}
+
+		if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil {
+			return err
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		common.SysError("waffo topup failed: " + err.Error())
+		return errors.New("充值失败,请稍后重试")
+	}
+
+	if quotaToAdd > 0 {
+		RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("Waffo充值成功,充值额度: %v,支付金额: %.2f", logger.FormatQuota(quotaToAdd), topUp.Money))
+	}
+
+	return nil
+}

+ 2 - 0
router/api-router.go

@@ -48,6 +48,7 @@ func SetApiRouter(router *gin.Engine) {
 
 		apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
 		apiRouter.POST("/creem/webhook", controller.CreemWebhook)
+		apiRouter.POST("/waffo/webhook", controller.WaffoWebhook)
 
 		// Universal secure verification routes
 		apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
@@ -89,6 +90,7 @@ func SetApiRouter(router *gin.Engine) {
 				selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay)
 				selfRoute.POST("/stripe/amount", controller.RequestStripeAmount)
 				selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay)
+				selfRoute.POST("/waffo/pay", middleware.CriticalRateLimit(), controller.RequestWaffoPay)
 				selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
 				selfRoute.PUT("/setting", controller.UpdateUserSetting)
 

+ 67 - 0
setting/payment_waffo.go

@@ -0,0 +1,67 @@
+package setting
+
+import (
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+)
+
+var (
+	WaffoEnabled           bool
+	WaffoApiKey            string
+	WaffoPrivateKey        string
+	WaffoPublicCert         string
+	WaffoSandboxPublicCert  string
+	WaffoSandboxApiKey     string
+	WaffoSandboxPrivateKey string
+	WaffoSandbox           bool
+	WaffoMerchantId        string
+	WaffoNotifyUrl             string
+	WaffoReturnUrl             string
+	WaffoSubscriptionReturnUrl string
+	WaffoCurrency          string
+	WaffoUnitPrice         float64 = 1.0
+	WaffoMinTopUp          int     = 1
+)
+
+// GetWaffoPayMethods 从 options 读取 Waffo 支付方式配置
+func GetWaffoPayMethods() []constant.WaffoPayMethod {
+	common.OptionMapRWMutex.RLock()
+	jsonStr := common.OptionMap["WaffoPayMethods"]
+	common.OptionMapRWMutex.RUnlock()
+
+	if jsonStr == "" {
+		return copyDefaultWaffoPayMethods()
+	}
+	var methods []constant.WaffoPayMethod
+	if err := common.UnmarshalJsonStr(jsonStr, &methods); err != nil {
+		return copyDefaultWaffoPayMethods()
+	}
+	return methods
+}
+
+// SetWaffoPayMethods 序列化 Waffo 支付方式配置并更新 OptionMap
+func SetWaffoPayMethods(methods []constant.WaffoPayMethod) error {
+	jsonBytes, err := common.Marshal(methods)
+	if err != nil {
+		return err
+	}
+	common.OptionMapRWMutex.Lock()
+	common.OptionMap["WaffoPayMethods"] = string(jsonBytes)
+	common.OptionMapRWMutex.Unlock()
+	return nil
+}
+
+func copyDefaultWaffoPayMethods() []constant.WaffoPayMethod {
+	cp := make([]constant.WaffoPayMethod, len(constant.DefaultWaffoPayMethods))
+	copy(cp, constant.DefaultWaffoPayMethods)
+	return cp
+}
+
+// WaffoPayMethods2JsonString 将默认 WaffoPayMethods 序列化为 JSON 字符串(供 InitOptionMap 使用)
+func WaffoPayMethods2JsonString() string {
+	jsonBytes, err := common.Marshal(constant.DefaultWaffoPayMethods)
+	if err != nil {
+		return "[]"
+	}
+	return string(jsonBytes)
+}

BIN
web/public/pay-apple.png


BIN
web/public/pay-card.png


BIN
web/public/pay-google.png


+ 4 - 3
web/src/components/settings/PaymentSetting.jsx

@@ -23,6 +23,7 @@ import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralP
 import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway';
 import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe';
 import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem';
+import SettingsPaymentGatewayWaffo from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffo';
 import { API, showError, toBoolean } from '../../helpers';
 import { useTranslation } from 'react-i18next';
 
@@ -66,7 +67,6 @@ const PaymentSetting = () => {
                 2,
               );
             } catch (error) {
-              console.error('解析TopupGroupRatio出错:', error);
               newInputs[item.key] = item.value;
             }
             break;
@@ -78,7 +78,6 @@ const PaymentSetting = () => {
                 2,
               );
             } catch (error) {
-              console.error('解析AmountOptions出错:', error);
               newInputs['AmountOptions'] = item.value;
             }
             break;
@@ -90,7 +89,6 @@ const PaymentSetting = () => {
                 2,
               );
             } catch (error) {
-              console.error('解析AmountDiscount出错:', error);
               newInputs['AmountDiscount'] = item.value;
             }
             break;
@@ -146,6 +144,9 @@ const PaymentSetting = () => {
         <Card style={{ marginTop: '10px' }}>
           <SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} />
         </Card>
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsPaymentGatewayWaffo options={inputs} refresh={onRefresh} />
+        </Card>
       </Spin>
     </>
   );

+ 50 - 11
web/src/components/topup/RechargeCard.jsx

@@ -87,6 +87,9 @@ const RechargeCard = ({
   statusLoading,
   topupInfo,
   onOpenHistory,
+  enableWaffoTopUp,
+  waffoTopUp,
+  waffoPayMethods,
   subscriptionLoading = false,
   subscriptionPlans = [],
   billingPreference,
@@ -224,19 +227,19 @@ const RechargeCard = ({
           <div className='py-8 flex justify-center'>
             <Spin size='large' />
           </div>
-        ) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp ? (
+        ) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWaffoTopUp ? (
           <Form
             getFormApi={(api) => (onlineFormApiRef.current = api)}
             initValues={{ topUpCount: topUpCount }}
           >
             <div className='space-y-6'>
-              {(enableOnlineTopUp || enableStripeTopUp) && (
+              {(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
                 <Row gutter={12}>
                   <Col xs={24} sm={24} md={24} lg={10} xl={10}>
                     <Form.InputNumber
                       field='topUpCount'
                       label={t('充值数量')}
-                      disabled={!enableOnlineTopUp && !enableStripeTopUp}
+                      disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWaffoTopUp}
                       placeholder={
                         t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
                       }
@@ -288,11 +291,11 @@ const RechargeCard = ({
                       style={{ width: '100%' }}
                     />
                   </Col>
+                  {payMethods && payMethods.filter(m => m.type !== 'waffo').length > 0 && (
                   <Col xs={24} sm={24} md={24} lg={14} xl={14}>
                     <Form.Slot label={t('选择支付方式')}>
-                      {payMethods && payMethods.length > 0 ? (
                         <Space wrap>
-                          {payMethods.map((payMethod) => {
+                          {payMethods.filter(m => m.type !== 'waffo').map((payMethod) => {
                             const minTopupVal = Number(payMethod.min_topup) || 0;
                             const isStripe = payMethod.type === 'stripe';
                             const disabled =
@@ -352,17 +355,13 @@ const RechargeCard = ({
                             );
                           })}
                         </Space>
-                      ) : (
-                        <div className='text-gray-500 text-sm p-3 bg-gray-50 rounded-lg border border-dashed border-gray-300'>
-                          {t('暂无可用的支付方式,请联系管理员配置')}
-                        </div>
-                      )}
                     </Form.Slot>
                   </Col>
+                  )}
                 </Row>
               )}
 
-              {(enableOnlineTopUp || enableStripeTopUp) && (
+              {(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
                 <Form.Slot
                   label={
                     <div className='flex items-center gap-2'>
@@ -483,6 +482,46 @@ const RechargeCard = ({
                 </Form.Slot>
               )}
 
+              {/* Waffo 充值区域 */}
+              {enableWaffoTopUp &&
+                waffoPayMethods &&
+                waffoPayMethods.length > 0 && (
+                  <Form.Slot label={t('Waffo 充值')}>
+                    <Space wrap>
+                      {waffoPayMethods.map((method, index) => (
+                        <Button
+                          key={index}
+                          theme='outline'
+                          type='tertiary'
+                          onClick={() => waffoTopUp(index)}
+                          loading={paymentLoading}
+                          icon={
+                            method.icon ? (
+                              <img
+                                src={method.icon}
+                                alt={method.name}
+                                style={{
+                                  width: 36,
+                                  height: 36,
+                                  objectFit: 'contain',
+                                }}
+                              />
+                            ) : (
+                              <CreditCard
+                                size={18}
+                                color='var(--semi-color-text-2)'
+                              />
+                            )
+                          }
+                          className='!rounded-lg !px-4 !py-2'
+                        >
+                          {method.name}
+                        </Button>
+                      ))}
+                    </Space>
+                  </Form.Slot>
+                )}
+
               {/* Creem 充值区域 */}
               {enableCreemTopUp && creemProducts.length > 0 && (
                 <Form.Slot label={t('Creem 充值')}>

+ 61 - 10
web/src/components/topup/index.jsx

@@ -18,6 +18,7 @@ For commercial licensing, please contact [email protected]
 */
 
 import React, { useEffect, useState, useContext, useRef } from 'react';
+import { useSearchParams } from 'react-router-dom';
 import {
   API,
   showError,
@@ -41,6 +42,7 @@ import TopupHistoryModal from './modals/TopupHistoryModal';
 
 const TopUp = () => {
   const { t } = useTranslation();
+  const [searchParams, setSearchParams] = useSearchParams();
   const [userState, userDispatch] = useContext(UserContext);
   const [statusState] = useContext(StatusContext);
 
@@ -69,6 +71,11 @@ const TopUp = () => {
   const [creemOpen, setCreemOpen] = useState(false);
   const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
 
+  // Waffo 相关状态
+  const [enableWaffoTopUp, setEnableWaffoTopUp] = useState(false);
+  const [waffoPayMethods, setWaffoPayMethods] = useState([]);
+  const [waffoMinTopUp, setWaffoMinTopUp] = useState(1);
+
   const [isSubmitting, setIsSubmitting] = useState(false);
   const [open, setOpen] = useState(false);
   const [payWay, setPayWay] = useState('');
@@ -256,7 +263,6 @@ const TopUp = () => {
         showError(res);
       }
     } catch (err) {
-      console.log(err);
       showError(t('支付请求失败'));
     } finally {
       setOpen(false);
@@ -302,7 +308,6 @@ const TopUp = () => {
         showError(res);
       }
     } catch (err) {
-      console.log(err);
       showError(t('支付请求失败'));
     } finally {
       setCreemOpen(false);
@@ -310,6 +315,37 @@ const TopUp = () => {
     }
   };
 
+  const waffoTopUp = async (payMethodIndex) => {
+    try {
+        if (topUpCount < waffoMinTopUp) {
+            showError(t('充值数量不能小于') + waffoMinTopUp);
+            return;
+        }
+        setPaymentLoading(true);
+        const requestBody = {
+            amount: parseInt(topUpCount),
+        };
+        if (payMethodIndex != null) {
+            requestBody.pay_method_index = payMethodIndex;
+        }
+        const res = await API.post('/api/user/waffo/pay', requestBody);
+        if (res !== undefined) {
+            const { message, data } = res.data;
+            if (message === 'success' && data?.payment_url) {
+                window.open(data.payment_url, '_blank');
+            } else {
+                showError(data || t('支付请求失败'));
+            }
+        } else {
+            showError(res);
+        }
+    } catch (e) {
+        showError(t('支付请求失败'));
+    } finally {
+        setPaymentLoading(false);
+    }
+  };
+
   const processCreemCallback = (data) => {
     // 与 Stripe 保持一致的实现方式
     window.open(data.checkout_url, '_blank');
@@ -449,17 +485,21 @@ const TopUp = () => {
             ? data.min_topup
             : enableStripeTopUp
               ? data.stripe_min_topup
-              : 1;
+              : data.enable_waffo_topup
+                ? data.waffo_min_topup
+                : 1;
           setEnableOnlineTopUp(enableOnlineTopUp);
           setEnableStripeTopUp(enableStripeTopUp);
           setEnableCreemTopUp(enableCreemTopUp);
+          const enableWaffoTopUp = data.enable_waffo_topup || false;
+          setEnableWaffoTopUp(enableWaffoTopUp);
+          setWaffoPayMethods(data.waffo_pay_methods || []);
+          setWaffoMinTopUp(data.waffo_min_topup || 1);
           setMinTopUp(minTopUpValue);
           setTopUpCount(minTopUpValue);
 
           // 设置 Creem 产品
           try {
-            console.log(' data is ?', data);
-            console.log(' creem products is ?', data.creem_products);
             const products = JSON.parse(data.creem_products || '[]');
             setCreemProducts(products);
           } catch (e) {
@@ -474,7 +514,6 @@ const TopUp = () => {
           // 初始化显示实付金额
           getAmount(minTopUpValue);
         } catch (e) {
-          console.log('解析支付方式失败:', e);
           setPayMethods([]);
         }
 
@@ -487,10 +526,10 @@ const TopUp = () => {
           setPresetAmounts(customPresets);
         }
       } else {
-        console.error('获取充值配置失败:', data);
+        showError(data || t('获取充值配置失败'));
       }
     } catch (error) {
-      console.error('获取充值配置异常:', error);
+      showError(t('获取充值配置异常'));
     }
   };
 
@@ -531,6 +570,15 @@ const TopUp = () => {
     showSuccess(t('邀请链接已复制到剪切板'));
   };
 
+  // URL 参数自动打开账单弹窗(支付回跳时触发)
+  useEffect(() => {
+    if (searchParams.get('show_history') === 'true') {
+      setOpenHistory(true);
+      searchParams.delete('show_history');
+      setSearchParams(searchParams, { replace: true });
+    }
+  }, []);
+
   useEffect(() => {
     // 始终获取最新用户数据,确保余额等统计信息准确
     getUserQuota().then();
@@ -587,7 +635,7 @@ const TopUp = () => {
         showError(res);
       }
     } catch (err) {
-      console.log(err);
+      // amount fetch failed silently
     }
     setAmountLoading(false);
   };
@@ -613,7 +661,7 @@ const TopUp = () => {
         showError(res);
       }
     } catch (err) {
-      console.log(err);
+      // amount fetch failed silently
     } finally {
       setAmountLoading(false);
     }
@@ -740,6 +788,9 @@ const TopUp = () => {
           enableCreemTopUp={enableCreemTopUp}
           creemProducts={creemProducts}
           creemPreTopUp={creemPreTopUp}
+          enableWaffoTopUp={enableWaffoTopUp}
+          waffoTopUp={waffoTopUp}
+          waffoPayMethods={waffoPayMethods}
           presetAmounts={presetAmounts}
           selectedPreset={selectedPreset}
           selectPresetAmount={selectPresetAmount}

+ 17 - 14
web/src/components/topup/modals/TopupHistoryModal.jsx

@@ -37,13 +37,13 @@ import { IconSearch } from '@douyinfe/semi-icons';
 import { API, timestamp2string } from '../../../helpers';
 import { isAdmin } from '../../../helpers/utils';
 import { useIsMobile } from '../../../hooks/common/useIsMobile';
-
 const { Text } = Typography;
 
 // 状态映射配置
 const STATUS_CONFIG = {
   success: { type: 'success', key: '成功' },
   pending: { type: 'warning', key: '待支付' },
+  failed: { type: 'danger', key: '失败' },
   expired: { type: 'danger', key: '已过期' },
 };
 
@@ -51,6 +51,7 @@ const STATUS_CONFIG = {
 const PAYMENT_METHOD_MAP = {
   stripe: 'Stripe',
   creem: 'Creem',
+  waffo: 'Waffo',
   alipay: '支付宝',
   wxpay: '微信',
 };
@@ -62,7 +63,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
   const [page, setPage] = useState(1);
   const [pageSize, setPageSize] = useState(10);
   const [keyword, setKeyword] = useState('');
-
   const isMobile = useIsMobile();
 
   const loadTopups = async (currentPage, currentPageSize) => {
@@ -82,7 +82,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
         Toast.error({ content: message || t('加载失败') });
       }
     } catch (error) {
-      console.error('Load topups error:', error);
       Toast.error({ content: t('加载账单失败') });
     } finally {
       setLoading(false);
@@ -214,17 +213,21 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
         title: t('操作'),
         key: 'action',
         render: (_, record) => {
-          if (record.status !== 'pending') return null;
-          return (
-            <Button
-              size='small'
-              type='primary'
-              theme='outline'
-              onClick={() => confirmAdminComplete(record.trade_no)}
-            >
-              {t('补单')}
-            </Button>
-          );
+          const actions = [];
+          if (record.status === 'pending') {
+            actions.push(
+              <Button
+                key="complete"
+                size='small'
+                type='primary'
+                theme='outline'
+                onClick={() => confirmAdminComplete(record.trade_no)}
+              >
+                {t('补单')}
+              </Button>
+            );
+          }
+          return actions.length > 0 ? <>{actions}</> : null;
         },
       });
     }

+ 10 - 0
web/src/i18n/locales/en.json

@@ -3216,6 +3216,16 @@
     "默认测试模型": "Default Test Model",
     "默认用户消息": "Default User Message",
     "默认补全倍率": "Default completion ratio",
+    "提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Notice: Endpoint mapping is for Model Marketplace display only and does not affect real model invocation. To configure real invocation, please go to Channel Management.",
+    "购买订阅获得模型额度/次数": "Purchase a subscription to get model quota/usage",
+    "生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "Production RSA private key Base64 (PKCS#8 DER)",
+    "沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Sandbox RSA private key Base64 (PKCS#8 DER)",
+    "生产环境 Waffo 公钥 Base64 (X.509 DER)": "Production Waffo public key Base64 (X.509 DER)",
+    "沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Sandbox Waffo public key Base64 (X.509 DER)",
+    "支付方式类型": "Pay Method Type",
+    "支付方式名称": "Pay Method Name",
+    "获取充值配置失败": "Failed to get topup configuration",
+    "获取充值配置异常": "Topup configuration error",
     "分组相关设置": "Group Related Settings",
     "保存分组相关设置": "Save Group Related Settings",
     "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "This page only shows models without base pricing. After saving, configured models will be removed from this list automatically.",

+ 10 - 0
web/src/i18n/locales/fr.json

@@ -3160,6 +3160,16 @@
     "默认测试模型": "Modèle de test par défaut",
     "默认用户消息": "Bonjour",
     "默认补全倍率": "Taux de complétion par défaut",
+    "提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Remarque : la correspondance des endpoints sert uniquement à l'affichage dans la place de marché des modèles et n'affecte pas l'invocation réelle. Pour configurer l'invocation réelle, veuillez aller dans « Gestion des canaux ».",
+    "购买订阅获得模型额度/次数": "Acheter un abonnement pour obtenir des quotas/usages de modèles",
+    "生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "Clé privée RSA Base64 (PKCS#8 DER) de production",
+    "沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Clé privée RSA Base64 (PKCS#8 DER) de sandbox",
+    "生产环境 Waffo 公钥 Base64 (X.509 DER)": "Clé publique Waffo Base64 (X.509 DER) de production",
+    "沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Clé publique Waffo Base64 (X.509 DER) de sandbox",
+    "支付方式类型": "Type de méthode de paiement",
+    "支付方式名称": "Nom de méthode de paiement",
+    "获取充值配置失败": "Échec de la récupération de la configuration de recharge",
+    "获取充值配置异常": "Erreur de configuration de recharge",
     "分组相关设置": "Paramètres liés aux groupes",
     "保存分组相关设置": "Enregistrer les paramètres liés aux groupes",
     "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "Cette page n'affiche que les modèles sans prix ou ratio de base. Après enregistrement, ils seront retirés automatiquement de cette liste.",

+ 10 - 0
web/src/i18n/locales/ja.json

@@ -3141,6 +3141,16 @@
     "默认测试模型": "デフォルトテストモデル",
     "默认用户消息": "こんにちは",
     "默认补全倍率": "デフォルト補完倍率",
+    "提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "注意: エンドポイントマッピングは「モデル広場」での表示専用で、実際の呼び出しには影響しません。実際の呼び出し設定は「チャネル管理」で行ってください。",
+    "购买订阅获得模型额度/次数": "サブスクリプション購入でモデルのクォータ/回数を取得",
+    "生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "本番環境 RSA 秘密鍵 Base64 (PKCS#8 DER)",
+    "沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "サンドボックス RSA 秘密鍵 Base64 (PKCS#8 DER)",
+    "生产环境 Waffo 公钥 Base64 (X.509 DER)": "本番環境 Waffo 公開鍵 Base64 (X.509 DER)",
+    "沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "サンドボックス Waffo 公開鍵 Base64 (X.509 DER)",
+    "支付方式类型": "決済方法タイプ",
+    "支付方式名称": "決済方法名",
+    "获取充值配置失败": "チャージ設定の取得に失敗しました",
+    "获取充值配置异常": "チャージ設定エラー",
     "分组相关设置": "グループ関連設定",
     "保存分组相关设置": "グループ関連設定を保存",
     "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "このページには価格または基本倍率が未設定のモデルのみ表示され、設定後は一覧から自動的に消えます。",

+ 11 - 1
web/src/i18n/locales/ru.json

@@ -3173,7 +3173,17 @@
     "默认折叠侧边栏": "Сворачивать боковую панель по умолчанию",
     "默认测试模型": "Модель для тестирования по умолчанию",
     "默认用户消息": "Здравствуйте",
-    "默认补全倍率": "Default completion ratio",
+    "默认补全倍率": "Коэффициент завершения по умолчанию",
+    "提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Примечание: сопоставление эндпоинтов используется только для отображения в «Маркетплейсе моделей» и не влияет на реальный вызов. Чтобы настроить реальное поведение вызовов, перейдите в «Управление каналами».",
+    "购买订阅获得模型额度/次数": "Купите подписку, чтобы получить лимит/количество использования моделей",
+    "生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "RSA закрытый ключ Base64 (PKCS#8 DER) производственной среды",
+    "沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "RSA закрытый ключ Base64 (PKCS#8 DER) песочницы",
+    "生产环境 Waffo 公钥 Base64 (X.509 DER)": "Открытый ключ Waffo Base64 (X.509 DER) производственной среды",
+    "沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Открытый ключ Waffo Base64 (X.509 DER) песочницы",
+    "支付方式类型": "Тип метода оплаты",
+    "支付方式名称": "Название метода оплаты",
+    "获取充值配置失败": "Не удалось получить конфигурацию пополнения",
+    "获取充值配置异常": "Ошибка конфигурации пополнения",
     "分组相关设置": "Настройки, связанные с группами",
     "保存分组相关设置": "Сохранить настройки, связанные с группами",
     "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "На этой странице показаны только модели без цены или базового коэффициента. После сохранения они будут автоматически удалены из списка.",

+ 10 - 0
web/src/i18n/locales/vi.json

@@ -3712,6 +3712,16 @@
     "默认测试模型": "Mô hình kiểm tra mặc định",
     "默认用户消息": "Xin chào",
     "默认补全倍率": "Tỷ lệ hoàn thành mặc định",
+    "提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Lưu ý: Ánh xạ endpoint chỉ dùng để hiển thị trong \"Chợ mô hình\" và không ảnh hưởng đến việc gọi thực tế. Để cấu hình gọi thực tế, vui lòng vào \"Quản lý kênh\".",
+    "购买订阅获得模型额度/次数": "Mua đăng ký để nhận hạn mức/lượt dùng mô hình",
+    "生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "Khóa riêng RSA Base64 (PKCS#8 DER) môi trường sản xuất",
+    "沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Khóa riêng RSA Base64 (PKCS#8 DER) môi trường sandbox",
+    "生产环境 Waffo 公钥 Base64 (X.509 DER)": "Khóa công khai Waffo Base64 (X.509 DER) môi trường sản xuất",
+    "沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Khóa công khai Waffo Base64 (X.509 DER) môi trường sandbox",
+    "支付方式类型": "Loại phương thức thanh toán",
+    "支付方式名称": "Tên phương thức thanh toán",
+    "获取充值配置失败": "Không thể lấy cấu hình nạp tiền",
+    "获取充值配置异常": "Lỗi cấu hình nạp tiền",
     "分组相关设置": "Cài đặt liên quan đến nhóm",
     "保存分组相关设置": "Lưu cài đặt liên quan đến nhóm",
     "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "Trang này chỉ hiển thị các mô hình chưa thiết lập giá hoặc tỷ lệ cơ bản. Sau khi lưu, chúng sẽ tự động biến mất khỏi danh sách.",

+ 10 - 0
web/src/i18n/locales/zh-TW.json

@@ -2900,6 +2900,16 @@
     "1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h快取建立倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
     "输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 輸出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
     "空": "空",
+    "提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "提示:端點映射僅用於模型廣場展示,不會影響模型真實呼叫。如需配置真實呼叫,請前往「管道管理」。",
+    "购买订阅获得模型额度/次数": "購買訂閱取得模型額度/次數",
+    "生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "正式環境 RSA 私鑰 Base64 (PKCS#8 DER)",
+    "沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "沙盒環境 RSA 私鑰 Base64 (PKCS#8 DER)",
+    "生产环境 Waffo 公钥 Base64 (X.509 DER)": "正式環境 Waffo 公鑰 Base64 (X.509 DER)",
+    "沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "沙盒環境 Waffo 公鑰 Base64 (X.509 DER)",
+    "支付方式类型": "付款方式類型",
+    "支付方式名称": "付款方式名稱",
+    "获取充值配置失败": "取得儲值設定失敗",
+    "获取充值配置异常": "儲值設定異常",
     "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
     "模型价格:{{symbol}}{{price}}": "模型價格:{{symbol}}{{price}}",
     "模型价格 {{price}}": "模型價格 {{price}}",

+ 608 - 0
web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx

@@ -0,0 +1,608 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact [email protected]
+*/
+
+import React, { useEffect, useState, useRef } from 'react';
+import {
+  Banner,
+  Button,
+  Form,
+  Row,
+  Col,
+  Typography,
+  Spin,
+  Table,
+  Modal,
+  Input,
+  Space,
+} from '@douyinfe/semi-ui';
+import { API, showError, showSuccess } from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+const { Text } = Typography;
+
+export default function SettingsPaymentGatewayWaffo(props) {
+  const { t } = useTranslation();
+  const [loading, setLoading] = useState(false);
+  const [inputs, setInputs] = useState({
+    WaffoEnabled: false,
+    WaffoApiKey: '',
+    WaffoPrivateKey: '',
+    WaffoPublicCert: '',
+    WaffoSandboxPublicCert: '',
+    WaffoSandboxApiKey: '',
+    WaffoSandboxPrivateKey: '',
+    WaffoSandbox: false,
+    WaffoMerchantId: '',
+    WaffoCurrency: 'USD',
+    WaffoUnitPrice: 1.0,
+    WaffoMinTopUp: 1,
+    WaffoNotifyUrl: '',
+    WaffoReturnUrl: '',
+  });
+  const [originInputs, setOriginInputs] = useState({});
+  const formApiRef = useRef(null);
+  const iconFileInputRef = useRef(null);
+
+  const handleIconFileChange = (e) => {
+    const file = e.target.files[0];
+    if (!file) return;
+    const MAX_ICON_SIZE = 100 * 1024; // 100 KB
+    if (file.size > MAX_ICON_SIZE) {
+      showError(t('图标文件不能超过 100KB,请压缩后重新上传'));
+      e.target.value = '';
+      return;
+    }
+    const reader = new FileReader();
+    reader.onload = (event) => {
+      setPayMethodForm((prev) => ({ ...prev, icon: event.target.result }));
+    };
+    reader.readAsDataURL(file);
+    e.target.value = '';
+  };
+
+  // 支付方式列表
+  const [waffoPayMethods, setWaffoPayMethods] = useState([]);
+  // 支付方式弹窗
+  const [payMethodModalVisible, setPayMethodModalVisible] = useState(false);
+  // 当前编辑的索引,-1 表示新增
+  const [editingPayMethodIndex, setEditingPayMethodIndex] = useState(-1);
+  // 弹窗内表单字段的临时状态
+  const [payMethodForm, setPayMethodForm] = useState({
+    name: '',
+    icon: '',
+    payMethodType: '',
+    payMethodName: '',
+  });
+
+  useEffect(() => {
+    if (props.options && formApiRef.current) {
+      const currentInputs = {
+        WaffoEnabled: props.options.WaffoEnabled === 'true' || props.options.WaffoEnabled === true,
+        WaffoApiKey: props.options.WaffoApiKey || '',
+        WaffoPrivateKey: props.options.WaffoPrivateKey || '',
+        WaffoPublicCert: props.options.WaffoPublicCert || '',
+        WaffoSandboxPublicCert: props.options.WaffoSandboxPublicCert || '',
+        WaffoSandboxApiKey: props.options.WaffoSandboxApiKey || '',
+        WaffoSandboxPrivateKey: props.options.WaffoSandboxPrivateKey || '',
+        WaffoSandbox: props.options.WaffoSandbox === 'true',
+        WaffoMerchantId: props.options.WaffoMerchantId || '',
+        WaffoCurrency: props.options.WaffoCurrency || 'USD',
+        WaffoUnitPrice: parseFloat(props.options.WaffoUnitPrice) || 1.0,
+        WaffoMinTopUp: parseInt(props.options.WaffoMinTopUp) || 1,
+        WaffoNotifyUrl: props.options.WaffoNotifyUrl || '',
+        WaffoReturnUrl: props.options.WaffoReturnUrl || '',
+      };
+      setInputs(currentInputs);
+      setOriginInputs({ ...currentInputs });
+      formApiRef.current.setValues(currentInputs);
+
+      // 解析支付方式列表
+      try {
+        const rawPayMethods = props.options.WaffoPayMethods;
+        if (rawPayMethods) {
+          const parsed = JSON.parse(rawPayMethods);
+          if (Array.isArray(parsed)) {
+            setWaffoPayMethods(parsed);
+          }
+        }
+      } catch {
+        setWaffoPayMethods([]);
+      }
+    }
+  }, [props.options]);
+
+  const handleFormChange = (values) => {
+    setInputs(values);
+  };
+
+  const submitWaffoSetting = async () => {
+    setLoading(true);
+    try {
+      const options = [];
+
+      options.push({
+        key: 'WaffoEnabled',
+        value: inputs.WaffoEnabled ? 'true' : 'false',
+      });
+
+      if (inputs.WaffoApiKey && inputs.WaffoApiKey !== '') {
+        options.push({ key: 'WaffoApiKey', value: inputs.WaffoApiKey });
+      }
+
+      if (inputs.WaffoPrivateKey && inputs.WaffoPrivateKey !== '') {
+        options.push({ key: 'WaffoPrivateKey', value: inputs.WaffoPrivateKey });
+      }
+
+      options.push({ key: 'WaffoPublicCert', value: inputs.WaffoPublicCert || '' });
+      options.push({ key: 'WaffoSandboxPublicCert', value: inputs.WaffoSandboxPublicCert || '' });
+
+      if (inputs.WaffoSandboxApiKey && inputs.WaffoSandboxApiKey !== '') {
+        options.push({ key: 'WaffoSandboxApiKey', value: inputs.WaffoSandboxApiKey });
+      }
+
+      if (inputs.WaffoSandboxPrivateKey && inputs.WaffoSandboxPrivateKey !== '') {
+        options.push({ key: 'WaffoSandboxPrivateKey', value: inputs.WaffoSandboxPrivateKey });
+      }
+
+      options.push({
+        key: 'WaffoSandbox',
+        value: inputs.WaffoSandbox ? 'true' : 'false',
+      });
+
+      options.push({ key: 'WaffoMerchantId', value: inputs.WaffoMerchantId || '' });
+      options.push({ key: 'WaffoCurrency', value: inputs.WaffoCurrency || '' });
+
+      options.push({
+        key: 'WaffoUnitPrice',
+        value: String(inputs.WaffoUnitPrice || 1.0),
+      });
+
+      options.push({
+        key: 'WaffoMinTopUp',
+        value: String(inputs.WaffoMinTopUp || 1),
+      });
+
+      options.push({ key: 'WaffoNotifyUrl', value: inputs.WaffoNotifyUrl || '' });
+      options.push({ key: 'WaffoReturnUrl', value: inputs.WaffoReturnUrl || '' });
+
+      // 保存支付方式列表
+      options.push({
+        key: 'WaffoPayMethods',
+        value: JSON.stringify(waffoPayMethods),
+      });
+
+      // 发送请求
+      const requestQueue = options.map((opt) =>
+        API.put('/api/option/', {
+          key: opt.key,
+          value: opt.value,
+        }),
+      );
+
+      const results = await Promise.all(requestQueue);
+
+      // 检查所有请求是否成功
+      const errorResults = results.filter((res) => !res.data.success);
+      if (errorResults.length > 0) {
+        errorResults.forEach((res) => {
+          showError(res.data.message);
+        });
+      } else {
+        showSuccess(t('更新成功'));
+        // 更新本地存储的原始值
+        setOriginInputs({ ...inputs });
+        props.refresh?.();
+      }
+    } catch (error) {
+      showError(t('更新失败'));
+    }
+    setLoading(false);
+  };
+
+  // 打开新增弹窗
+  const openAddPayMethodModal = () => {
+    setEditingPayMethodIndex(-1);
+    setPayMethodForm({ name: '', icon: '', payMethodType: '', payMethodName: '' });
+    setPayMethodModalVisible(true);
+  };
+
+  // 打开编辑弹窗
+  const openEditPayMethodModal = (record, index) => {
+    setEditingPayMethodIndex(index);
+    setPayMethodForm({
+      name: record.name || '',
+      icon: record.icon || '',
+      payMethodType: record.payMethodType || '',
+      payMethodName: record.payMethodName || '',
+    });
+    setPayMethodModalVisible(true);
+  };
+
+  // 确认保存弹窗(新增或编辑)
+  const handlePayMethodModalOk = () => {
+    if (!payMethodForm.name || payMethodForm.name.trim() === '') {
+      showError(t('支付方式名称不能为空'));
+      return;
+    }
+    const newMethod = {
+      name: payMethodForm.name.trim(),
+      icon: payMethodForm.icon.trim(),
+      payMethodType: payMethodForm.payMethodType.trim(),
+      payMethodName: payMethodForm.payMethodName.trim(),
+    };
+    if (editingPayMethodIndex === -1) {
+      // 新增
+      setWaffoPayMethods([...waffoPayMethods, newMethod]);
+    } else {
+      // 编辑
+      const updated = [...waffoPayMethods];
+      updated[editingPayMethodIndex] = newMethod;
+      setWaffoPayMethods(updated);
+    }
+    setPayMethodModalVisible(false);
+  };
+
+  // 删除支付方式
+  const handleDeletePayMethod = (index) => {
+    const updated = waffoPayMethods.filter((_, i) => i !== index);
+    setWaffoPayMethods(updated);
+  };
+
+  // 支付方式表格列定义
+  const payMethodColumns = [
+    {
+      title: t('显示名称'),
+      dataIndex: 'name',
+    },
+    {
+      title: t('图标'),
+      dataIndex: 'icon',
+      render: (text) =>
+        text ? (
+          <img
+            src={text}
+            alt='icon'
+            style={{ width: 24, height: 24, objectFit: 'contain' }}
+          />
+        ) : (
+          <Text type='tertiary'>—</Text>
+        ),
+    },
+    {
+      title: t('支付方式类型'),
+      dataIndex: 'payMethodType',
+      render: (text) => text || <Text type='tertiary'>—</Text>,
+    },
+    {
+      title: t('支付方式名称'),
+      dataIndex: 'payMethodName',
+      render: (text) => text || <Text type='tertiary'>—</Text>,
+    },
+    {
+      title: t('操作'),
+      key: 'action',
+      render: (_, record, index) => (
+        <Space>
+          <Button
+            size='small'
+            onClick={() => openEditPayMethodModal(record, index)}
+          >
+            {t('编辑')}
+          </Button>
+          <Button
+            size='small'
+            type='danger'
+            onClick={() => handleDeletePayMethod(index)}
+          >
+            {t('删除')}
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <Spin spinning={loading}>
+      <Form
+        initValues={inputs}
+        onValueChange={handleFormChange}
+        getFormApi={(api) => (formApiRef.current = api)}
+      >
+        <Form.Section text={t('Waffo 设置')}>
+          <Text>
+            {t('Waffo 是一个支付聚合平台,支持多种支付方式。')}
+            <a href='https://waffo.com' target='_blank' rel='noreferrer'>
+              Waffo Official Site
+            </a>
+            <br />
+          </Text>
+          <Banner
+            type='info'
+            description={t(
+              '请在 Waffo 后台获取 API 密钥、商户 ID 以及 RSA 密钥对,并配置回调地址。',
+            )}
+          />
+
+          <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Switch
+                field='WaffoEnabled'
+                label={t('启用 Waffo')}
+                size='default'
+                checkedText='|'
+                uncheckedText='〇'
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Switch
+                field='WaffoSandbox'
+                label={t('沙盒模式')}
+                size='default'
+                checkedText='|'
+                uncheckedText='〇'
+                extraText={t('启用后将使用 Waffo 沙盒环境')}
+              />
+            </Col>
+          </Row>
+
+          <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
+            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+              <Form.Input
+                field='WaffoApiKey'
+                label={t('API 密钥 (生产)')}
+                placeholder={t('生产环境 Waffo API 密钥')}
+                type='password'
+              />
+            </Col>
+            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+              <Form.Input
+                field='WaffoSandboxApiKey'
+                label={t('API 密钥 (沙盒)')}
+                placeholder={t('沙盒环境 Waffo API 密钥')}
+                type='password'
+              />
+            </Col>
+          </Row>
+
+          <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
+            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+              <Form.Input
+                field='WaffoMerchantId'
+                label={t('商户 ID')}
+                placeholder={t('Waffo 商户 ID')}
+              />
+            </Col>
+          </Row>
+
+          <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
+            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+              <Form.TextArea
+                field='WaffoPrivateKey'
+                label={t('RSA 私钥 (生产)')}
+                placeholder={t('生产环境 RSA 私钥 Base64 (PKCS#8 DER)')}
+                type='password'
+                autosize={{ minRows: 3, maxRows: 6 }}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+              <Form.TextArea
+                field='WaffoSandboxPrivateKey'
+                label={t('RSA 私钥 (沙盒)')}
+                placeholder={t('沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)')}
+                type='password'
+                autosize={{ minRows: 3, maxRows: 6 }}
+              />
+            </Col>
+          </Row>
+
+          <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
+            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+              <Form.TextArea
+                field='WaffoPublicCert'
+                label={t('Waffo 公钥 (生产)')}
+                placeholder={t('生产环境 Waffo 公钥 Base64 (X.509 DER)')}
+                type='password'
+                autosize={{ minRows: 3, maxRows: 6 }}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+              <Form.TextArea
+                field='WaffoSandboxPublicCert'
+                label={t('Waffo 公钥 (沙盒)')}
+                placeholder={t('沙盒环境 Waffo 公钥 Base64 (X.509 DER)')}
+                type='password'
+                autosize={{ minRows: 3, maxRows: 6 }}
+              />
+            </Col>
+          </Row>
+
+          <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.Input
+                field='WaffoCurrency'
+                label={t('货币')}
+                disabled
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.InputNumber
+                field='WaffoUnitPrice'
+                label={t('单价 (USD)')}
+                placeholder='1.0'
+                min={0}
+                step={0.1}
+                extraText={t('每个充值单位对应的 USD 金额,默认 1.0')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+              <Form.InputNumber
+                field='WaffoMinTopUp'
+                label={t('最低充值数量')}
+                placeholder='1'
+                min={1}
+                step={1}
+                extraText={t('Waffo 充值的最低数量,默认 1')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+              <Form.Input
+                field='WaffoNotifyUrl'
+                label={t('回调通知地址')}
+                placeholder={t('例如 https://example.com/api/waffo/webhook')}
+                extraText={t('留空则自动使用 服务器地址 + /api/waffo/webhook')}
+              />
+            </Col>
+            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+              <Form.Input
+                field='WaffoReturnUrl'
+                label={t('支付返回地址')}
+                placeholder={t('例如 https://example.com/console/topup')}
+                extraText={t('支付完成后用户跳转的页面,留空则自动使用 服务器地址 + /console/topup')}
+              />
+            </Col>
+          </Row>
+
+          <Button onClick={submitWaffoSetting} style={{ marginTop: 16 }}>
+            {t('更新 Waffo 设置')}
+          </Button>
+        </Form.Section>
+      </Form>
+
+      {/* 支付方式配置区块(独立于 Form,使用独立状态管理) */}
+      <div style={{ marginTop: 24 }}>
+        <Typography.Title heading={6} style={{ marginBottom: 8 }}>{t('支付方式')}</Typography.Title>
+        <Text type='secondary'>
+          {t('配置 Waffo 充值时可用的支付方式,保存后在充值页面展示给用户。')}
+        </Text>
+        <div style={{ marginTop: 12, marginBottom: 12 }}>
+          <Button onClick={openAddPayMethodModal}>
+            {t('新增支付方式')}
+          </Button>
+        </div>
+        <Table
+          columns={payMethodColumns}
+          dataSource={waffoPayMethods}
+          rowKey={(record, index) => index}
+          pagination={false}
+          size='small'
+          empty={<Text type='tertiary'>{t('暂无支付方式,点击上方按钮新增')}</Text>}
+        />
+        <Button onClick={submitWaffoSetting} style={{ marginTop: 16 }}>
+          {t('更新 Waffo 设置')}
+        </Button>
+      </div>
+
+      {/* 新增/编辑支付方式弹窗 */}
+      <Modal
+        title={editingPayMethodIndex === -1 ? t('新增支付方式') : t('编辑支付方式')}
+        visible={payMethodModalVisible}
+        onOk={handlePayMethodModalOk}
+        onCancel={() => setPayMethodModalVisible(false)}
+        okText={t('确定')}
+        cancelText={t('取消')}
+      >
+        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
+          <div>
+            <div style={{ marginBottom: 4 }}>
+              <Text strong>{t('显示名称')}</Text>
+              <span style={{ color: 'var(--semi-color-danger)', marginLeft: 4 }}>*</span>
+            </div>
+            <Input
+              value={payMethodForm.name}
+              onChange={(val) => setPayMethodForm({ ...payMethodForm, name: val })}
+              placeholder={t('例如:Credit Card')}
+            />
+            <Text type='tertiary' size='small'>{t('用户在充值页面看到的支付方式名称,例如:Credit Card')}</Text>
+          </div>
+          <div>
+            <div style={{ marginBottom: 4 }}>
+              <Text strong>{t('图标')}</Text>
+            </div>
+            <Space align='center'>
+              {payMethodForm.icon && (
+                <img
+                  src={payMethodForm.icon}
+                  alt='preview'
+                  style={{
+                    width: 32,
+                    height: 32,
+                    objectFit: 'contain',
+                    border: '1px solid var(--semi-color-border)',
+                    borderRadius: 4,
+                  }}
+                />
+              )}
+              <input
+                type='file'
+                accept='image/*'
+                ref={iconFileInputRef}
+                style={{ display: 'none' }}
+                onChange={handleIconFileChange}
+              />
+              <Button
+                size='small'
+                onClick={() => iconFileInputRef.current?.click()}
+              >
+                {payMethodForm.icon ? t('重新上传') : t('上传图片')}
+              </Button>
+              {payMethodForm.icon && (
+                <Button
+                  size='small'
+                  type='danger'
+                  onClick={() =>
+                    setPayMethodForm((prev) => ({ ...prev, icon: '' }))
+                  }
+                >
+                  {t('清除')}
+                </Button>
+              )}
+            </Space>
+            <div>
+              <Text type='tertiary' size='small'>{t('上传 PNG/JPG/SVG 图片,建议尺寸 ≤ 128×128px')}</Text>
+            </div>
+          </div>
+          <div>
+            <div style={{ marginBottom: 4 }}>
+              <Text strong>{t('Pay Method Type')}</Text>
+            </div>
+            <Input
+              value={payMethodForm.payMethodType}
+              onChange={(val) => setPayMethodForm({ ...payMethodForm, payMethodType: val })}
+              placeholder='CREDITCARD,DEBITCARD'
+              maxLength={64}
+            />
+            <Text type='tertiary' size='small'>{t('Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)')}</Text>
+          </div>
+          <div>
+            <div style={{ marginBottom: 4 }}>
+              <Text strong>{t('Pay Method Name')}</Text>
+            </div>
+            <Input
+              value={payMethodForm.payMethodName}
+              onChange={(val) => setPayMethodForm({ ...payMethodForm, payMethodName: val })}
+              placeholder={t('可空')}
+              maxLength={64}
+            />
+            <Text type='tertiary' size='small'>{t('Waffo API 参数,可空(最多64位)')}</Text>
+          </div>
+        </div>
+      </Modal>
+    </Spin>
+  );
+}