Commit 202a433f by zhongyuan.zhao

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) <noreply@anthropic.com>
parent 620e066b
...@@ -211,5 +211,6 @@ const ( ...@@ -211,5 +211,6 @@ const (
const ( const (
TopUpStatusPending = "pending" TopUpStatusPending = "pending"
TopUpStatusSuccess = "success" TopUpStatusSuccess = "success"
TopUpStatusFailed = "failed"
TopUpStatusExpired = "expired" TopUpStatusExpired = "expired"
) )
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"},
}
...@@ -48,14 +48,52 @@ func GetTopUpInfo(c *gin.Context) { ...@@ -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{ data := gin.H{
"enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "", "enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "",
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "", "enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
"enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]", "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, "pay_methods": payMethods,
"min_topup": operation_setting.MinTopUp, "min_topup": operation_setting.MinTopUp,
"stripe_min_topup": setting.StripeMinTopUp, "stripe_min_topup": setting.StripeMinTopUp,
"waffo_min_topup": setting.WaffoMinTopUp,
"amount_options": operation_setting.GetPaymentSetting().AmountOptions, "amount_options": operation_setting.GetPaymentSetting().AmountOptions,
"discount": operation_setting.GetPaymentSetting().AmountDiscount, "discount": operation_setting.GetPaymentSetting().AmountDiscount,
} }
...@@ -204,27 +242,42 @@ func RequestEpay(c *gin.Context) { ...@@ -204,27 +242,42 @@ func RequestEpay(c *gin.Context) {
var orderLocks sync.Map var orderLocks sync.Map
var createLock sync.Mutex var createLock sync.Mutex
// refCountedMutex 带引用计数的互斥锁,确保最后一个使用者才从 map 中删除
type refCountedMutex struct {
mu sync.Mutex
refCount int
}
// LockOrder 尝试对给定订单号加锁 // LockOrder 尝试对给定订单号加锁
func LockOrder(tradeNo string) { func LockOrder(tradeNo string) {
lock, ok := orderLocks.Load(tradeNo) createLock.Lock()
if !ok { var rcm *refCountedMutex
createLock.Lock() if v, ok := orderLocks.Load(tradeNo); ok {
defer createLock.Unlock() rcm = v.(*refCountedMutex)
lock, ok = orderLocks.Load(tradeNo) } else {
if !ok { rcm = &refCountedMutex{}
lock = new(sync.Mutex) orderLocks.Store(tradeNo, rcm)
orderLocks.Store(tradeNo, lock)
}
} }
lock.(*sync.Mutex).Lock() rcm.refCount++
createLock.Unlock()
rcm.mu.Lock()
} }
// UnlockOrder 释放给定订单号的锁 // UnlockOrder 释放给定订单号的锁
func UnlockOrder(tradeNo string) { func UnlockOrder(tradeNo string) {
lock, ok := orderLocks.Load(tradeNo) v, ok := orderLocks.Load(tradeNo)
if ok { if !ok {
lock.(*sync.Mutex).Unlock() 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) { func EpayNotify(c *gin.Context) {
...@@ -410,3 +463,4 @@ func AdminCompleteTopUp(c *gin.Context) { ...@@ -410,3 +463,4 @@ func AdminCompleteTopUp(c *gin.Context) {
} }
common.ApiSuccess(c, nil) common.ApiSuccess(c, nil)
} }
...@@ -46,6 +46,7 @@ require ( ...@@ -46,6 +46,7 @@ require (
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5 github.com/tidwall/sjson v1.2.5
github.com/tiktoken-go/tokenizer v0.6.2 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 github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.45.0
golang.org/x/image v0.23.0 golang.org/x/image v0.23.0
...@@ -120,7 +121,6 @@ require ( ...@@ -120,7 +121,6 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/samber/go-singleflightx v0.3.2 // 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/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect
......
...@@ -89,6 +89,22 @@ func InitOptionMap() { ...@@ -89,6 +89,22 @@ func InitOptionMap() {
common.OptionMap["CreemProducts"] = setting.CreemProducts common.OptionMap["CreemProducts"] = setting.CreemProducts
common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode) common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode)
common.OptionMap["CreemWebhookSecret"] = setting.CreemWebhookSecret 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["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["Chats"] = setting.Chats2JsonString() common.OptionMap["Chats"] = setting.Chats2JsonString()
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString() common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
...@@ -358,6 +374,36 @@ func updateOptionMap(key string, value string) (err error) { ...@@ -358,6 +374,36 @@ func updateOptionMap(key string, value string) (err error) {
setting.CreemTestMode = value == "true" setting.CreemTestMode = value == "true"
case "CreemWebhookSecret": case "CreemWebhookSecret":
setting.CreemWebhookSecret = value 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": case "TopupGroupRatio":
err = common.UpdateTopupGroupRatioByJSONString(value) err = common.UpdateTopupGroupRatioByJSONString(value)
case "GitHubClientId": case "GitHubClientId":
...@@ -458,6 +504,10 @@ func updateOptionMap(key string, value string) (err error) { ...@@ -458,6 +504,10 @@ func updateOptionMap(key string, value string) (err error) {
setting.StreamCacheQueueLength, _ = strconv.Atoi(value) setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
case "PayMethods": case "PayMethods":
err = operation_setting.UpdatePayMethodsByJsonString(value) 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 return err
} }
......
...@@ -12,15 +12,15 @@ import ( ...@@ -12,15 +12,15 @@ import (
) )
type TopUp struct { type TopUp struct {
Id int `json:"id"` Id int `json:"id"`
UserId int `json:"user_id" gorm:"index"` UserId int `json:"user_id" gorm:"index"`
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Money float64 `json:"money"` Money float64 `json:"money"`
TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"` TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"` PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"`
CreateTime int64 `json:"create_time"` CreateTime int64 `json:"create_time"`
CompleteTime int64 `json:"complete_time"` CompleteTime int64 `json:"complete_time"`
Status string `json:"status"` Status string `json:"status"`
} }
func (topUp *TopUp) Insert() error { func (topUp *TopUp) Insert() error {
...@@ -376,3 +376,62 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string ...@@ -376,3 +376,62 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
return nil 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
}
...@@ -48,6 +48,7 @@ func SetApiRouter(router *gin.Engine) { ...@@ -48,6 +48,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.POST("/stripe/webhook", controller.StripeWebhook) apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
apiRouter.POST("/creem/webhook", controller.CreemWebhook) apiRouter.POST("/creem/webhook", controller.CreemWebhook)
apiRouter.POST("/waffo/webhook", controller.WaffoWebhook)
// Universal secure verification routes // Universal secure verification routes
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify) apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
...@@ -89,6 +90,7 @@ func SetApiRouter(router *gin.Engine) { ...@@ -89,6 +90,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay) selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay)
selfRoute.POST("/stripe/amount", controller.RequestStripeAmount) selfRoute.POST("/stripe/amount", controller.RequestStripeAmount)
selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay) selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay)
selfRoute.POST("/waffo/pay", middleware.CriticalRateLimit(), controller.RequestWaffoPay)
selfRoute.POST("/aff_transfer", controller.TransferAffQuota) selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
selfRoute.PUT("/setting", controller.UpdateUserSetting) selfRoute.PUT("/setting", controller.UpdateUserSetting)
......
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)
}
...@@ -23,6 +23,7 @@ import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralP ...@@ -23,6 +23,7 @@ import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralP
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway'; import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway';
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe'; import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe';
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem'; import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem';
import SettingsPaymentGatewayWaffo from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffo';
import { API, showError, toBoolean } from '../../helpers'; import { API, showError, toBoolean } from '../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
...@@ -66,7 +67,6 @@ const PaymentSetting = () => { ...@@ -66,7 +67,6 @@ const PaymentSetting = () => {
2, 2,
); );
} catch (error) { } catch (error) {
console.error('解析TopupGroupRatio出错:', error);
newInputs[item.key] = item.value; newInputs[item.key] = item.value;
} }
break; break;
...@@ -78,7 +78,6 @@ const PaymentSetting = () => { ...@@ -78,7 +78,6 @@ const PaymentSetting = () => {
2, 2,
); );
} catch (error) { } catch (error) {
console.error('解析AmountOptions出错:', error);
newInputs['AmountOptions'] = item.value; newInputs['AmountOptions'] = item.value;
} }
break; break;
...@@ -90,7 +89,6 @@ const PaymentSetting = () => { ...@@ -90,7 +89,6 @@ const PaymentSetting = () => {
2, 2,
); );
} catch (error) { } catch (error) {
console.error('解析AmountDiscount出错:', error);
newInputs['AmountDiscount'] = item.value; newInputs['AmountDiscount'] = item.value;
} }
break; break;
...@@ -146,6 +144,9 @@ const PaymentSetting = () => { ...@@ -146,6 +144,9 @@ const PaymentSetting = () => {
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} /> <SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} />
</Card> </Card>
<Card style={{ marginTop: '10px' }}>
<SettingsPaymentGatewayWaffo options={inputs} refresh={onRefresh} />
</Card>
</Spin> </Spin>
</> </>
); );
......
...@@ -87,6 +87,9 @@ const RechargeCard = ({ ...@@ -87,6 +87,9 @@ const RechargeCard = ({
statusLoading, statusLoading,
topupInfo, topupInfo,
onOpenHistory, onOpenHistory,
enableWaffoTopUp,
waffoTopUp,
waffoPayMethods,
subscriptionLoading = false, subscriptionLoading = false,
subscriptionPlans = [], subscriptionPlans = [],
billingPreference, billingPreference,
...@@ -224,19 +227,19 @@ const RechargeCard = ({ ...@@ -224,19 +227,19 @@ const RechargeCard = ({
<div className='py-8 flex justify-center'> <div className='py-8 flex justify-center'>
<Spin size='large' /> <Spin size='large' />
</div> </div>
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp ? ( ) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWaffoTopUp ? (
<Form <Form
getFormApi={(api) => (onlineFormApiRef.current = api)} getFormApi={(api) => (onlineFormApiRef.current = api)}
initValues={{ topUpCount: topUpCount }} initValues={{ topUpCount: topUpCount }}
> >
<div className='space-y-6'> <div className='space-y-6'>
{(enableOnlineTopUp || enableStripeTopUp) && ( {(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
<Row gutter={12}> <Row gutter={12}>
<Col xs={24} sm={24} md={24} lg={10} xl={10}> <Col xs={24} sm={24} md={24} lg={10} xl={10}>
<Form.InputNumber <Form.InputNumber
field='topUpCount' field='topUpCount'
label={t('充值数量')} label={t('充值数量')}
disabled={!enableOnlineTopUp && !enableStripeTopUp} disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWaffoTopUp}
placeholder={ placeholder={
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp) t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
} }
...@@ -362,7 +365,7 @@ const RechargeCard = ({ ...@@ -362,7 +365,7 @@ const RechargeCard = ({
</Row> </Row>
)} )}
{(enableOnlineTopUp || enableStripeTopUp) && ( {(enableOnlineTopUp || enableStripeTopUp || enableWaffoTopUp) && (
<Form.Slot <Form.Slot
label={ label={
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
...@@ -483,6 +486,46 @@ const RechargeCard = ({ ...@@ -483,6 +486,46 @@ const RechargeCard = ({
</Form.Slot> </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 充值区域 */} {/* Creem 充值区域 */}
{enableCreemTopUp && creemProducts.length > 0 && ( {enableCreemTopUp && creemProducts.length > 0 && (
<Form.Slot label={t('Creem 充值')}> <Form.Slot label={t('Creem 充值')}>
......
...@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import React, { useEffect, useState, useContext, useRef } from 'react'; import React, { useEffect, useState, useContext, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import { import {
API, API,
showError, showError,
...@@ -41,6 +42,7 @@ import TopupHistoryModal from './modals/TopupHistoryModal'; ...@@ -41,6 +42,7 @@ import TopupHistoryModal from './modals/TopupHistoryModal';
const TopUp = () => { const TopUp = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams();
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [statusState] = useContext(StatusContext); const [statusState] = useContext(StatusContext);
...@@ -69,6 +71,10 @@ const TopUp = () => { ...@@ -69,6 +71,10 @@ const TopUp = () => {
const [creemOpen, setCreemOpen] = useState(false); const [creemOpen, setCreemOpen] = useState(false);
const [selectedCreemProduct, setSelectedCreemProduct] = useState(null); const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
// Waffo 相关状态
const [enableWaffoTopUp, setEnableWaffoTopUp] = useState(false);
const [waffoPayMethods, setWaffoPayMethods] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [payWay, setPayWay] = useState(''); const [payWay, setPayWay] = useState('');
...@@ -256,7 +262,6 @@ const TopUp = () => { ...@@ -256,7 +262,6 @@ const TopUp = () => {
showError(res); showError(res);
} }
} catch (err) { } catch (err) {
console.log(err);
showError(t('支付请求失败')); showError(t('支付请求失败'));
} finally { } finally {
setOpen(false); setOpen(false);
...@@ -302,7 +307,6 @@ const TopUp = () => { ...@@ -302,7 +307,6 @@ const TopUp = () => {
showError(res); showError(res);
} }
} catch (err) { } catch (err) {
console.log(err);
showError(t('支付请求失败')); showError(t('支付请求失败'));
} finally { } finally {
setCreemOpen(false); setCreemOpen(false);
...@@ -310,6 +314,37 @@ const TopUp = () => { ...@@ -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) => { const processCreemCallback = (data) => {
// 与 Stripe 保持一致的实现方式 // 与 Stripe 保持一致的实现方式
window.open(data.checkout_url, '_blank'); window.open(data.checkout_url, '_blank');
...@@ -449,17 +484,20 @@ const TopUp = () => { ...@@ -449,17 +484,20 @@ const TopUp = () => {
? data.min_topup ? data.min_topup
: enableStripeTopUp : enableStripeTopUp
? data.stripe_min_topup ? data.stripe_min_topup
: 1; : data.enable_waffo_topup
? data.waffo_min_topup
: 1;
setEnableOnlineTopUp(enableOnlineTopUp); setEnableOnlineTopUp(enableOnlineTopUp);
setEnableStripeTopUp(enableStripeTopUp); setEnableStripeTopUp(enableStripeTopUp);
setEnableCreemTopUp(enableCreemTopUp); setEnableCreemTopUp(enableCreemTopUp);
const enableWaffoTopUp = data.enable_waffo_topup || false;
setEnableWaffoTopUp(enableWaffoTopUp);
setWaffoPayMethods(data.waffo_pay_methods || []);
setMinTopUp(minTopUpValue); setMinTopUp(minTopUpValue);
setTopUpCount(minTopUpValue); setTopUpCount(minTopUpValue);
// 设置 Creem 产品 // 设置 Creem 产品
try { try {
console.log(' data is ?', data);
console.log(' creem products is ?', data.creem_products);
const products = JSON.parse(data.creem_products || '[]'); const products = JSON.parse(data.creem_products || '[]');
setCreemProducts(products); setCreemProducts(products);
} catch (e) { } catch (e) {
...@@ -474,7 +512,6 @@ const TopUp = () => { ...@@ -474,7 +512,6 @@ const TopUp = () => {
// 初始化显示实付金额 // 初始化显示实付金额
getAmount(minTopUpValue); getAmount(minTopUpValue);
} catch (e) { } catch (e) {
console.log('解析支付方式失败:', e);
setPayMethods([]); setPayMethods([]);
} }
...@@ -487,10 +524,10 @@ const TopUp = () => { ...@@ -487,10 +524,10 @@ const TopUp = () => {
setPresetAmounts(customPresets); setPresetAmounts(customPresets);
} }
} else { } else {
console.error('获取充值配置失败:', data); showError(data || t('获取充值配置失败'));
} }
} catch (error) { } catch (error) {
console.error('获取充值配置异常:', error); showError(t('获取充值配置异常'));
} }
}; };
...@@ -531,6 +568,15 @@ const TopUp = () => { ...@@ -531,6 +568,15 @@ const TopUp = () => {
showSuccess(t('邀请链接已复制到剪切板')); showSuccess(t('邀请链接已复制到剪切板'));
}; };
// URL 参数自动打开账单弹窗(支付回跳时触发)
useEffect(() => {
if (searchParams.get('show_history') === 'true') {
setOpenHistory(true);
searchParams.delete('show_history');
setSearchParams(searchParams, { replace: true });
}
}, []);
useEffect(() => { useEffect(() => {
// 始终获取最新用户数据,确保余额等统计信息准确 // 始终获取最新用户数据,确保余额等统计信息准确
getUserQuota().then(); getUserQuota().then();
...@@ -587,7 +633,7 @@ const TopUp = () => { ...@@ -587,7 +633,7 @@ const TopUp = () => {
showError(res); showError(res);
} }
} catch (err) { } catch (err) {
console.log(err); // amount fetch failed silently
} }
setAmountLoading(false); setAmountLoading(false);
}; };
...@@ -613,7 +659,7 @@ const TopUp = () => { ...@@ -613,7 +659,7 @@ const TopUp = () => {
showError(res); showError(res);
} }
} catch (err) { } catch (err) {
console.log(err); // amount fetch failed silently
} finally { } finally {
setAmountLoading(false); setAmountLoading(false);
} }
...@@ -740,6 +786,9 @@ const TopUp = () => { ...@@ -740,6 +786,9 @@ const TopUp = () => {
enableCreemTopUp={enableCreemTopUp} enableCreemTopUp={enableCreemTopUp}
creemProducts={creemProducts} creemProducts={creemProducts}
creemPreTopUp={creemPreTopUp} creemPreTopUp={creemPreTopUp}
enableWaffoTopUp={enableWaffoTopUp}
waffoTopUp={waffoTopUp}
waffoPayMethods={waffoPayMethods}
presetAmounts={presetAmounts} presetAmounts={presetAmounts}
selectedPreset={selectedPreset} selectedPreset={selectedPreset}
selectPresetAmount={selectPresetAmount} selectPresetAmount={selectPresetAmount}
......
...@@ -37,7 +37,6 @@ import { IconSearch } from '@douyinfe/semi-icons'; ...@@ -37,7 +37,6 @@ import { IconSearch } from '@douyinfe/semi-icons';
import { API, timestamp2string } from '../../../helpers'; import { API, timestamp2string } from '../../../helpers';
import { isAdmin } from '../../../helpers/utils'; import { isAdmin } from '../../../helpers/utils';
import { useIsMobile } from '../../../hooks/common/useIsMobile'; import { useIsMobile } from '../../../hooks/common/useIsMobile';
const { Text } = Typography; const { Text } = Typography;
// 状态映射配置 // 状态映射配置
...@@ -51,6 +50,7 @@ const STATUS_CONFIG = { ...@@ -51,6 +50,7 @@ const STATUS_CONFIG = {
const PAYMENT_METHOD_MAP = { const PAYMENT_METHOD_MAP = {
stripe: 'Stripe', stripe: 'Stripe',
creem: 'Creem', creem: 'Creem',
waffo: 'Waffo',
alipay: '支付宝', alipay: '支付宝',
wxpay: '微信', wxpay: '微信',
}; };
...@@ -62,7 +62,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { ...@@ -62,7 +62,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const loadTopups = async (currentPage, currentPageSize) => { const loadTopups = async (currentPage, currentPageSize) => {
...@@ -82,7 +81,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { ...@@ -82,7 +81,6 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
Toast.error({ content: message || t('加载失败') }); Toast.error({ content: message || t('加载失败') });
} }
} catch (error) { } catch (error) {
console.error('Load topups error:', error);
Toast.error({ content: t('加载账单失败') }); Toast.error({ content: t('加载账单失败') });
} finally { } finally {
setLoading(false); setLoading(false);
...@@ -214,17 +212,21 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { ...@@ -214,17 +212,21 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
title: t('操作'), title: t('操作'),
key: 'action', key: 'action',
render: (_, record) => { render: (_, record) => {
if (record.status !== 'pending') return null; const actions = [];
return ( if (record.status === 'pending') {
<Button actions.push(
size='small' <Button
type='primary' key="complete"
theme='outline' size='small'
onClick={() => confirmAdminComplete(record.trade_no)} type='primary'
> theme='outline'
{t('补单')} onClick={() => confirmAdminComplete(record.trade_no)}
</Button> >
); {t('补单')}
</Button>
);
}
return actions.length > 0 ? <>{actions}</> : null;
}, },
}); });
} }
......
...@@ -3216,6 +3216,16 @@ ...@@ -3216,6 +3216,16 @@
"默认测试模型": "Default Test Model", "默认测试模型": "Default Test Model",
"默认用户消息": "Default User Message", "默认用户消息": "Default User Message",
"默认补全倍率": "Default completion ratio", "默认补全倍率": "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", "分组相关设置": "Group Related Settings",
"保存分组相关设置": "Save 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.", "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "This page only shows models without base pricing. After saving, configured models will be removed from this list automatically.",
......
...@@ -3160,6 +3160,16 @@ ...@@ -3160,6 +3160,16 @@
"默认测试模型": "Modèle de test par défaut", "默认测试模型": "Modèle de test par défaut",
"默认用户消息": "Bonjour", "默认用户消息": "Bonjour",
"默认补全倍率": "Taux de complétion par défaut", "默认补全倍率": "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", "分组相关设置": "Paramètres liés aux groupes",
"保存分组相关设置": "Enregistrer les 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.", "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "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.",
......
...@@ -3141,6 +3141,16 @@ ...@@ -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)",
"支付方式类型": "決済方法タイプ",
"支付方式名称": "決済方法名",
"获取充值配置失败": "チャージ設定の取得に失敗しました",
"获取充值配置异常": "チャージ設定エラー",
"分组相关设置": "グループ関連設定", "分组相关设置": "グループ関連設定",
"保存分组相关设置": "グループ関連設定を保存", "保存分组相关设置": "グループ関連設定を保存",
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "このページには価格または基本倍率が未設定のモデルのみ表示され、設定後は一覧から自動的に消えます。", "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "このページには価格または基本倍率が未設定のモデルのみ表示され、設定後は一覧から自動的に消えます。",
......
...@@ -3173,7 +3173,17 @@ ...@@ -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) песочницы",
"支付方式类型": "Тип метода оплаты",
"支付方式名称": "Название метода оплаты",
"获取充值配置失败": "Не удалось получить конфигурацию пополнения",
"获取充值配置异常": "Ошибка конфигурации пополнения",
"分组相关设置": "Настройки, связанные с группами", "分组相关设置": "Настройки, связанные с группами",
"保存分组相关设置": "Сохранить настройки, связанные с группами", "保存分组相关设置": "Сохранить настройки, связанные с группами",
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "На этой странице показаны только модели без цены или базового коэффициента. После сохранения они будут автоматически удалены из списка.", "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "На этой странице показаны только модели без цены или базового коэффициента. После сохранения они будут автоматически удалены из списка.",
......
...@@ -3712,6 +3712,16 @@ ...@@ -3712,6 +3712,16 @@
"默认测试模型": "Mô hình kiểm tra mặc định", "默认测试模型": "Mô hình kiểm tra mặc định",
"默认用户消息": "Xin chào", "默认用户消息": "Xin chào",
"默认补全倍率": "Tỷ lệ hoàn thành mặc định", "默认补全倍率": "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", "分组相关设置": "Cài đặt liên quan đến nhóm",
"保存分组相关设置": "Lưu 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.", "此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "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.",
......
...@@ -2900,6 +2900,16 @@ ...@@ -2900,6 +2900,16 @@
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h快取建立倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}", "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}}", "输出:{{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", "{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
"模型价格:{{symbol}}{{price}}": "模型價格:{{symbol}}{{price}}", "模型价格:{{symbol}}{{price}}": "模型價格:{{symbol}}{{price}}",
"模型价格 {{price}}": "模型價格 {{price}}", "模型价格 {{price}}": "模型價格 {{price}}",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment