From 202a433f86afde4e5b2291cee5aea666a8b3c6a7 Mon Sep 17 00:00:00 2001 From: "zhongyuan.zhao" Date: Tue, 17 Mar 2026 18:04:58 +0800 Subject: [PATCH 1/7] feat(waffo): Waffo payment gateway integration with configurable methods - Add Waffo payment SDK integration (waffo-go v1.3.1) - Backend: webhook handler, pay endpoint, order lock race-condition fix - Settings: full Waffo config (API keys, sandbox/prod, currency, pay methods) - Frontend: Waffo payment buttons in topup page, admin settings panel - i18n: Waffo-related translations for en/fr/ja/ru/vi/zh-TW Co-Authored-By: Claude Opus 4.6 (1M context) --- common/constants.go | 1 + constant/waffo_pay_method.go | 16 + controller/topup.go | 82 ++- controller/topup_waffo.go | 391 +++++++++++ go.mod | 2 +- go.sum | 47 +- model/option.go | 50 ++ model/topup.go | 77 ++- router/api-router.go | 2 + setting/payment_waffo.go | 67 ++ web/public/pay-apple.png | Bin 0 -> 1597 bytes web/public/pay-card.png | Bin 0 -> 3685 bytes web/public/pay-google.png | Bin 0 -> 4644 bytes .../components/settings/PaymentSetting.jsx | 7 +- web/src/components/topup/RechargeCard.jsx | 51 +- web/src/components/topup/index.jsx | 69 +- .../topup/modals/TopupHistoryModal.jsx | 30 +- web/src/i18n/locales/en.json | 10 + web/src/i18n/locales/fr.json | 10 + web/src/i18n/locales/ja.json | 10 + web/src/i18n/locales/ru.json | 12 +- web/src/i18n/locales/vi.json | 10 + web/src/i18n/locales/zh-TW.json | 10 + .../Payment/SettingsPaymentGatewayWaffo.jsx | 607 ++++++++++++++++++ 24 files changed, 1472 insertions(+), 89 deletions(-) create mode 100644 constant/waffo_pay_method.go create mode 100644 controller/topup_waffo.go create mode 100644 setting/payment_waffo.go create mode 100644 web/public/pay-apple.png create mode 100644 web/public/pay-card.png create mode 100644 web/public/pay-google.png create mode 100644 web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx diff --git a/common/constants.go b/common/constants.go index 6823b2c8..6a3ddfa4 100644 --- a/common/constants.go +++ b/common/constants.go @@ -211,5 +211,6 @@ const ( const ( TopUpStatusPending = "pending" TopUpStatusSuccess = "success" + TopUpStatusFailed = "failed" TopUpStatusExpired = "expired" ) diff --git a/constant/waffo_pay_method.go b/constant/waffo_pay_method.go new file mode 100644 index 00000000..0cee72a8 --- /dev/null +++ b/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"}, +} diff --git a/controller/topup.go b/controller/topup.go index a810eba7..e7a392a4 100644 --- a/controller/topup.go +++ b/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) } + diff --git a/controller/topup_waffo.go b/controller/topup_waffo.go new file mode 100644 index 00000000..d78bb314 --- /dev/null +++ b/controller/topup_waffo.go @@ -0,0 +1,391 @@ +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("%d@examples.com", 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) + + // 存储 gatewayOrderId,退款时直接使用;保存失败则中止,避免付款后无法退款 + if orderData.AcquiringOrderID != "" { + if err := topUp.Update(); err != nil { + log.Printf("Waffo 保存 gatewayOrderId 失败: %v, 订单: %s", err, merchantOrderId) + topUp.Status = common.TopUpStatusFailed + _ = topUp.Update() + c.JSON(200, gin.H{"message": "error", "data": "创建订单失败,请重试"}) + return + } + } + + 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)) +} diff --git a/go.mod b/go.mod index 2f28f781..f7aaaa9a 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 74298929..4adbe980 100644 --- a/go.sum +++ b/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= diff --git a/model/option.go b/model/option.go index 697e77df..967fa0aa 100644 --- a/model/option.go +++ b/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 } diff --git a/model/topup.go b/model/topup.go index 655d9b77..d8c92bfe 100644 --- a/model/topup.go +++ b/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 +} diff --git a/router/api-router.go b/router/api-router.go index 9836083d..91013fca 100644 --- a/router/api-router.go +++ b/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) diff --git a/setting/payment_waffo.go b/setting/payment_waffo.go new file mode 100644 index 00000000..c27ca6f2 --- /dev/null +++ b/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) +} diff --git a/web/public/pay-apple.png b/web/public/pay-apple.png new file mode 100644 index 0000000000000000000000000000000000000000..b040f6e89f864ccd15d8d8cd115e8362d2defc46 GIT binary patch literal 1597 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 literal 0 HcmV?d00001 diff --git a/web/public/pay-google.png b/web/public/pay-google.png new file mode 100644 index 0000000000000000000000000000000000000000..bd31bb92fad84e5c278342188c1fd99adc1d5dd8 GIT binary patch literal 4644 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 V;D=a$j~n{{G}U0Lbx_OD{{c6z(bxb0 literal 0 HcmV?d00001 diff --git a/web/src/components/settings/PaymentSetting.jsx b/web/src/components/settings/PaymentSetting.jsx index 28cbf13b..928d58a7 100644 --- a/web/src/components/settings/PaymentSetting.jsx +++ b/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 = () => { + + + ); diff --git a/web/src/components/topup/RechargeCard.jsx b/web/src/components/topup/RechargeCard.jsx index 1ce02309..3fc14722 100644 --- a/web/src/components/topup/RechargeCard.jsx +++ b/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 = ({
- ) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp ? ( + ) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWaffoTopUp ? (
(onlineFormApiRef.current = api)} initValues={{ topUpCount: topUpCount }} >
- {(enableOnlineTopUp || enableStripeTopUp) && ( + {(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && ( )} - {(enableOnlineTopUp || enableStripeTopUp) && ( + {(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && ( @@ -483,6 +486,46 @@ const RechargeCard = ({ )} + {/* Waffo 充值区域 */} + {enableWaffoTopUp && + waffoPayMethods && + waffoPayMethods.length > 0 && ( + + + {waffoPayMethods.map((method, index) => ( + + ))} + + + )} + {/* Creem 充值区域 */} {enableCreemTopUp && creemProducts.length > 0 && ( diff --git a/web/src/components/topup/index.jsx b/web/src/components/topup/index.jsx index 55c7a077..b50f6764 100644 --- a/web/src/components/topup/index.jsx +++ b/web/src/components/topup/index.jsx @@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ 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,10 @@ const TopUp = () => { const [creemOpen, setCreemOpen] = useState(false); const [selectedCreemProduct, setSelectedCreemProduct] = useState(null); + // Waffo 相关状态 + const [enableWaffoTopUp, setEnableWaffoTopUp] = useState(false); + const [waffoPayMethods, setWaffoPayMethods] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); const [open, setOpen] = useState(false); const [payWay, setPayWay] = useState(''); @@ -256,7 +262,6 @@ const TopUp = () => { showError(res); } } catch (err) { - console.log(err); showError(t('支付请求失败')); } finally { setOpen(false); @@ -302,7 +307,6 @@ const TopUp = () => { showError(res); } } catch (err) { - console.log(err); showError(t('支付请求失败')); } finally { setCreemOpen(false); @@ -310,6 +314,37 @@ const TopUp = () => { } }; + const waffoTopUp = async (payMethodIndex) => { + try { + if (topUpCount < minTopUp) { + showError(t('充值数量不能小于') + minTopUp); + 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 +484,20 @@ 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 || []); 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 +512,6 @@ const TopUp = () => { // 初始化显示实付金额 getAmount(minTopUpValue); } catch (e) { - console.log('解析支付方式失败:', e); setPayMethods([]); } @@ -487,10 +524,10 @@ const TopUp = () => { setPresetAmounts(customPresets); } } else { - console.error('获取充值配置失败:', data); + showError(data || t('获取充值配置失败')); } } catch (error) { - console.error('获取充值配置异常:', error); + showError(t('获取充值配置异常')); } }; @@ -531,6 +568,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 +633,7 @@ const TopUp = () => { showError(res); } } catch (err) { - console.log(err); + // amount fetch failed silently } setAmountLoading(false); }; @@ -613,7 +659,7 @@ const TopUp = () => { showError(res); } } catch (err) { - console.log(err); + // amount fetch failed silently } finally { setAmountLoading(false); } @@ -740,6 +786,9 @@ const TopUp = () => { enableCreemTopUp={enableCreemTopUp} creemProducts={creemProducts} creemPreTopUp={creemPreTopUp} + enableWaffoTopUp={enableWaffoTopUp} + waffoTopUp={waffoTopUp} + waffoPayMethods={waffoPayMethods} presetAmounts={presetAmounts} selectedPreset={selectedPreset} selectPresetAmount={selectPresetAmount} diff --git a/web/src/components/topup/modals/TopupHistoryModal.jsx b/web/src/components/topup/modals/TopupHistoryModal.jsx index ae562342..95f4e878 100644 --- a/web/src/components/topup/modals/TopupHistoryModal.jsx +++ b/web/src/components/topup/modals/TopupHistoryModal.jsx @@ -37,7 +37,6 @@ 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; // 状态映射配置 @@ -51,6 +50,7 @@ const STATUS_CONFIG = { const PAYMENT_METHOD_MAP = { stripe: 'Stripe', creem: 'Creem', + waffo: 'Waffo', alipay: '支付宝', wxpay: '微信', }; @@ -62,7 +62,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 +81,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 +212,21 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { title: t('操作'), key: 'action', render: (_, record) => { - if (record.status !== 'pending') return null; - return ( - - ); + const actions = []; + if (record.status === 'pending') { + actions.push( + + ); + } + return actions.length > 0 ? <>{actions} : null; }, }); } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index e7213cd3..b45e0279 100644 --- a/web/src/i18n/locales/en.json +++ b/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.", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 54ecd673..45c476fe 100644 --- a/web/src/i18n/locales/fr.json +++ b/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.", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index e7e1ff46..51171cb6 100644 --- a/web/src/i18n/locales/ja.json +++ b/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)", + "支付方式类型": "決済方法タイプ", + "支付方式名称": "決済方法名", + "获取充值配置失败": "チャージ設定の取得に失敗しました", + "获取充值配置异常": "チャージ設定エラー", "分组相关设置": "グループ関連設定", "保存分组相关设置": "グループ関連設定を保存", "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "このページには価格または基本倍率が未設定のモデルのみ表示され、設定後は一覧から自動的に消えます。", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 447ad3bb..7b95c8b8 100644 --- a/web/src/i18n/locales/ru.json +++ b/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) песочницы", + "支付方式类型": "Тип метода оплаты", + "支付方式名称": "Название метода оплаты", + "获取充值配置失败": "Не удалось получить конфигурацию пополнения", + "获取充值配置异常": "Ошибка конфигурации пополнения", "分组相关设置": "Настройки, связанные с группами", "保存分组相关设置": "Сохранить настройки, связанные с группами", "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "На этой странице показаны только модели без цены или базового коэффициента. После сохранения они будут автоматически удалены из списка.", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index e533237a..8933f196 100644 --- a/web/src/i18n/locales/vi.json +++ b/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.", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index 6ffa630a..37da6b71 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/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}}", diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx b/web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx new file mode 100644 index 00000000..a0e0bc2d --- /dev/null +++ b/web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx @@ -0,0 +1,607 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useEffect, useState, useRef } from 'react'; +import { + Banner, + Button, + Form, + Row, + Col, + Typography, + Spin, + Table, + Modal, + Input, + Space, +} from '@douyinfe/semi-ui'; +const { Text } = Typography; +import { API, showError, showSuccess } from '../../../helpers'; +import { useTranslation } from 'react-i18next'; + +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 ? ( + icon + ) : ( + + ), + }, + { + title: t('支付方式类型'), + dataIndex: 'payMethodType', + render: (text) => text || , + }, + { + title: t('支付方式名称'), + dataIndex: 'payMethodName', + render: (text) => text || , + }, + { + title: t('操作'), + key: 'action', + render: (_, record, index) => ( + + + + + ), + }, + ]; + + return ( + + (formApiRef.current = api)} + > + + + {t('Waffo 是一个支付聚合平台,支持多种支付方式。')} + + Waffo Official Site + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + {/* 支付方式配置区块(独立于 Form,使用独立状态管理) */} +
+ {t('支付方式')} + + {t('配置 Waffo 充值时可用的支付方式,保存后在充值页面展示给用户。')} + +
+ +
+ index} + pagination={false} + size='small' + empty={{t('暂无支付方式,点击上方按钮新增')}} + /> + + + + {/* 新增/编辑支付方式弹窗 */} + setPayMethodModalVisible(false)} + okText={t('确定')} + cancelText={t('取消')} + > +
+
+
+ {t('显示名称')} + * +
+ setPayMethodForm({ ...payMethodForm, name: val })} + placeholder={t('例如:Credit Card')} + /> + {t('用户在充值页面看到的支付方式名称,例如:Credit Card')} +
+
+
+ {t('图标')} +
+ + {payMethodForm.icon && ( + preview + )} + + + {payMethodForm.icon && ( + + )} + +
+ {t('上传 PNG/JPG/SVG 图片,建议尺寸 ≤ 128×128px')} +
+
+
+
+ {t('Pay Method Type')} +
+ setPayMethodForm({ ...payMethodForm, payMethodType: val })} + placeholder='CREDITCARD,DEBITCARD' + maxLength={64} + /> + {t('Waffo API 参数,可空,例如:CREDITCARD,DEBITCARD(最多64位)')} +
+
+
+ {t('Pay Method Name')} +
+ setPayMethodForm({ ...payMethodForm, payMethodName: val })} + placeholder={t('可空')} + maxLength={64} + /> + {t('Waffo API 参数,可空(最多64位)')} +
+
+
+ + ); +} From 2270f63c006abe9f9774c2472419c65eafefbf6e Mon Sep 17 00:00:00 2001 From: "zhongyuan.zhao" Date: Wed, 18 Mar 2026 15:55:09 +0800 Subject: [PATCH 2/7] fix(topup): add 'failed' status badge mapping in TopupHistoryModal The backend defines TopUpStatusFailed = "failed" but the frontend STATUS_CONFIG was missing this status, causing raw text display instead of a styled danger badge. Co-Authored-By: Claude Opus 4.6 (1M context) --- web/src/components/topup/modals/TopupHistoryModal.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/topup/modals/TopupHistoryModal.jsx b/web/src/components/topup/modals/TopupHistoryModal.jsx index 95f4e878..9659e929 100644 --- a/web/src/components/topup/modals/TopupHistoryModal.jsx +++ b/web/src/components/topup/modals/TopupHistoryModal.jsx @@ -43,6 +43,7 @@ const { Text } = Typography; const STATUS_CONFIG = { success: { type: 'success', key: '成功' }, pending: { type: 'warning', key: '待支付' }, + failed: { type: 'danger', key: '失败' }, expired: { type: 'danger', key: '已过期' }, }; From d595ef499070cc4156444a12d6d864e509598ec7 Mon Sep 17 00:00:00 2001 From: "zhongyuan.zhao" Date: Wed, 18 Mar 2026 15:57:56 +0800 Subject: [PATCH 3/7] fix(waffo): remove dead gatewayOrderId code that never persisted The code read orderData.AcquiringOrderID but never assigned it to any TopUp field before calling Update(), making the block a no-op. Removed since GatewayOrderId storage is not needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- controller/topup_waffo.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/controller/topup_waffo.go b/controller/topup_waffo.go index d78bb314..fce37642 100644 --- a/controller/topup_waffo.go +++ b/controller/topup_waffo.go @@ -255,17 +255,6 @@ func RequestWaffoPay(c *gin.Context) { orderData := resp.GetData() log.Printf("Waffo 订单创建成功 - 用户: %d, 订单: %s, 金额: %.2f", id, merchantOrderId, payMoney) - // 存储 gatewayOrderId,退款时直接使用;保存失败则中止,避免付款后无法退款 - if orderData.AcquiringOrderID != "" { - if err := topUp.Update(); err != nil { - log.Printf("Waffo 保存 gatewayOrderId 失败: %v, 订单: %s", err, merchantOrderId) - topUp.Status = common.TopUpStatusFailed - _ = topUp.Update() - c.JSON(200, gin.H{"message": "error", "data": "创建订单失败,请重试"}) - return - } - } - paymentUrl := orderData.FetchRedirectURL() if paymentUrl == "" { paymentUrl = orderData.OrderAction From bd09b47ef4e8c9e7f5b58e9c79f0995471dae293 Mon Sep 17 00:00:00 2001 From: "zhongyuan.zhao" Date: Wed, 18 Mar 2026 16:01:47 +0800 Subject: [PATCH 4/7] fix(waffo): use dedicated waffoMinTopUp for client-side validation The waffoTopUp function was validating against the shared minTopUp which could be set by epay/stripe when multiple gateways are enabled, causing mismatch with backend's WaffoMinTopUp check. Co-Authored-By: Claude Opus 4.6 (1M context) --- web/src/components/topup/index.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/src/components/topup/index.jsx b/web/src/components/topup/index.jsx index b50f6764..0348e3c8 100644 --- a/web/src/components/topup/index.jsx +++ b/web/src/components/topup/index.jsx @@ -74,6 +74,7 @@ const TopUp = () => { // 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); @@ -316,8 +317,8 @@ const TopUp = () => { const waffoTopUp = async (payMethodIndex) => { try { - if (topUpCount < minTopUp) { - showError(t('充值数量不能小于') + minTopUp); + if (topUpCount < waffoMinTopUp) { + showError(t('充值数量不能小于') + waffoMinTopUp); return; } setPaymentLoading(true); @@ -493,6 +494,7 @@ const TopUp = () => { const enableWaffoTopUp = data.enable_waffo_topup || false; setEnableWaffoTopUp(enableWaffoTopUp); setWaffoPayMethods(data.waffo_pay_methods || []); + setWaffoMinTopUp(data.waffo_min_topup || 1); setMinTopUp(minTopUpValue); setTopUpCount(minTopUpValue); From e70bfa2d573d571a2a1680fb5f6b953c263ab1a2 Mon Sep 17 00:00:00 2001 From: "zhongyuan.zhao" Date: Wed, 18 Mar 2026 16:12:47 +0800 Subject: [PATCH 5/7] =?UTF-8?q?fix(i18n):=20use=20consistent=20zh-TW=20ter?= =?UTF-8?q?m=20=E7=AE=A1=E9=81=93=E7=AE=A1=E7=90=86=20instead=20of=20?= =?UTF-8?q?=E9=A0=BB=E9=81=93=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- web/src/i18n/locales/zh-TW.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index 37da6b71..61980d94 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -2900,7 +2900,7 @@ "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)", From 1daeac42ef0710353cf240f9a07ec69fa2f21cc0 Mon Sep 17 00:00:00 2001 From: "zhongyuan.zhao" Date: Wed, 18 Mar 2026 16:15:06 +0800 Subject: [PATCH 6/7] fix(waffo): move Typography destructuring after all imports ESM requires all import statements before other code. Co-Authored-By: Claude Opus 4.6 (1M context) --- web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx b/web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx index a0e0bc2d..29c8cdfa 100644 --- a/web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx +++ b/web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffo.jsx @@ -31,10 +31,11 @@ import { Input, Space, } from '@douyinfe/semi-ui'; -const { Text } = Typography; 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); From 669e596ff7c7fb64cdf2a80a3d80f685c87727f3 Mon Sep 17 00:00:00 2001 From: "zhongyuan.zhao" Date: Wed, 18 Mar 2026 16:18:13 +0800 Subject: [PATCH 7/7] fix(waffo): filter waffo from generic payment selector, avoid duplicate buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When only Waffo was enabled, the generic payment method list showed a "Waffo (Global Payment)" button calling preTopUp (epay flow) instead of waffoTopUp, while the dedicated "Waffo 充值" section had the correct buttons. Fix: filter waffo entries from generic list and hide the "选择支付方式" column when no non-waffo methods exist. Co-Authored-By: Claude Opus 4.6 (1M context) --- web/src/components/topup/RechargeCard.jsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/web/src/components/topup/RechargeCard.jsx b/web/src/components/topup/RechargeCard.jsx index 3fc14722..f37d129b 100644 --- a/web/src/components/topup/RechargeCard.jsx +++ b/web/src/components/topup/RechargeCard.jsx @@ -291,11 +291,11 @@ const RechargeCard = ({ style={{ width: '100%' }} /> + {payMethods && payMethods.filter(m => m.type !== 'waffo').length > 0 && ( - {payMethods && payMethods.length > 0 ? ( - {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 = @@ -355,13 +355,9 @@ const RechargeCard = ({ ); })} - ) : ( -
- {t('暂无可用的支付方式,请联系管理员配置')} -
- )}
+ )} )}