Browse Source

merge: feat/alipay-payment → master

支付宝当面付扫码支付集成 + 代码重构

Co-Authored-By: Claude <noreply@anthropic.com>
master
fengsilin 3 weeks ago
parent
commit
156618fdad
53 changed files with 22167 additions and 20862 deletions
  1. +47
    -4
      controller/topup.go
  2. +280
    -0
      controller/topup_alipay.go
  3. +2
    -24
      controller/topup_wechat.go
  4. +28
    -0
      model/option.go
  5. +32
    -1
      model/topup.go
  6. +12
    -6
      model/topup_wechat.go
  7. +4
    -0
      router/api-router.go
  8. +18
    -0
      setting/payment_alipay.go
  9. +1
    -1
      web/src/components/auth/LoginForm.jsx
  10. +3
    -3
      web/src/components/auth/RegisterForm.jsx
  11. +3
    -2
      web/src/components/common/markdown/MarkdownRenderer.jsx
  12. +1
    -1
      web/src/components/common/ui/JSONEditor.jsx
  13. +1
    -1
      web/src/components/playground/CodeViewer.jsx
  14. +1
    -1
      web/src/components/playground/DebugPanel.jsx
  15. +2
    -2
      web/src/components/playground/MessageContent.jsx
  16. +2
    -2
      web/src/components/playground/ThinkingContent.jsx
  17. +5
    -4
      web/src/components/playground/configStorage.js
  18. +1
    -1
      web/src/components/settings/ModelDeploymentSetting.jsx
  19. +1
    -1
      web/src/components/settings/ModelSetting.jsx
  20. +6
    -0
      web/src/components/settings/PaymentSetting.jsx
  21. +1
    -1
      web/src/components/settings/RateLimitSetting.jsx
  22. +1
    -1
      web/src/components/settings/RatioSetting.jsx
  23. +1
    -1
      web/src/components/settings/personal/cards/NotificationSettings.jsx
  24. +1
    -1
      web/src/components/table/channels/ChannelsColumnDefs.jsx
  25. +1
    -1
      web/src/components/table/channels/ChannelsTabs.jsx
  26. +4
    -4
      web/src/components/table/channels/modals/EditChannelModal.jsx
  27. +2
    -2
      web/src/components/table/channels/modals/EditTagModal.jsx
  28. +1
    -1
      web/src/components/table/task-logs/TaskLogsColumnDefs.jsx
  29. +5
    -5
      web/src/components/topup/QRCodePayModal.jsx
  30. +8
    -5
      web/src/components/topup/RechargeCard.jsx
  31. +65
    -6
      web/src/components/topup/index.jsx
  32. +13
    -1
      web/src/components/topup/modals/TopupHistoryModal.jsx
  33. +7
    -6
      web/src/helpers/api.js
  34. +4
    -2
      web/src/helpers/passkey.js
  35. +1
    -1
      web/src/helpers/render.jsx
  36. +4
    -4
      web/src/hooks/channels/useChannelsData.jsx
  37. +2
    -1
      web/src/hooks/chat/useTokenKeys.js
  38. +1
    -1
      web/src/hooks/dashboard/useDashboardData.js
  39. +3082
    -2993
      web/src/i18n/locales/en.json
  40. +2917
    -2828
      web/src/i18n/locales/fr.json
  41. +2900
    -2811
      web/src/i18n/locales/ja.json
  42. +2930
    -2841
      web/src/i18n/locales/ru.json
  43. +3469
    -3383
      web/src/i18n/locales/vi.json
  44. +3059
    -2970
      web/src/i18n/locales/zh-CN.json
  45. +2985
    -2896
      web/src/i18n/locales/zh-TW.json
  46. +7
    -7
      web/src/pages/Home/components/PartnersSection.jsx
  47. +2
    -2
      web/src/pages/Playground/index.jsx
  48. +6
    -6
      web/src/pages/Setting/Dashboard/SettingsAPIInfo.jsx
  49. +6
    -6
      web/src/pages/Setting/Dashboard/SettingsAnnouncements.jsx
  50. +6
    -6
      web/src/pages/Setting/Dashboard/SettingsFAQ.jsx
  51. +210
    -0
      web/src/pages/Setting/Payment/SettingsPaymentGatewayAlipay.jsx
  52. +5
    -5
      web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.jsx
  53. +11
    -10
      web/src/services/secureVerification.js

+ 47
- 4
controller/topup.go View File

@@ -72,17 +72,38 @@ func GetTopUpInfo(c *gin.Context) {
payMethods = append(payMethods, wechatMethod)
}
}
// 如果启用了支付宝支付,添加到支付方法列表
if setting.IsAlipayConfigured() {
hasAlipay := false
for _, method := range payMethods {
if method["type"] == PaymentMethodAlipay {
hasAlipay = true
break
}
}
if !hasAlipay {
alipayMethod := map[string]string{
"name": "Alipay",
"type": PaymentMethodAlipay,
"color": "rgba(var(--semi-blue-5), 1)",
"min_topup": strconv.Itoa(setting.AlipayMinTopUp),
}
payMethods = append(payMethods, alipayMethod)
}
}

data := gin.H{
"enable_online_topup": enableOnlineTopup,
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
"enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]",
"enable_wechat_topup": setting.IsWechatPayConfigured(),
"enable_alipay_topup": setting.IsAlipayConfigured(),
"creem_products": setting.CreemProducts,
"pay_methods": payMethods,
"min_topup": operation_setting.MinTopUp,
"stripe_min_topup": setting.StripeMinTopUp,
"wechat_pay_min_topup": setting.WechatPayMinTopUp,
"alipay_pay_min_topup": setting.AlipayMinTopUp,
"amount_options": operation_setting.GetPaymentSetting().AmountOptions,
"discount": operation_setting.GetPaymentSetting().AmountDiscount,
}
@@ -143,15 +164,37 @@ func getPayMoney(amount int64, group string) float64 {
}

func getMinTopup() int64 {
minTopup := operation_setting.MinTopUp
return calcMinTopup(operation_setting.MinTopUp)
}

// calcMinTopup 计算最低充值数量(考虑 QuotaDisplayType 换算)
func calcMinTopup(baseMinTopup int) int64 {
minTopup := baseMinTopup
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
dMinTopup := decimal.NewFromInt(int64(minTopup))
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
minTopup = minTopup * int(common.QuotaPerUnit)
}
return int64(minTopup)
}

// calcPayMoney 计算应付金额(元),使用指定的单价和最低充值
func calcPayMoney(amount float64, group string, unitPrice float64) 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 * unitPrice * topupGroupRatio * discount
}

func RequestEpay(c *gin.Context) {
var req EpayRequest
err := c.ShouldBindJSON(&req)


+ 280
- 0
controller/topup_alipay.go View File

@@ -0,0 +1,280 @@
package controller

import (
"context"
"encoding/base64"
"fmt"
"log"
"net/http"
"strconv"
"sync"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"

"github.com/gin-gonic/gin"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay"
"github.com/thanhpk/randstr"
)

const (
PaymentMethodAlipay = "alipay"
)

var alipayClientMu sync.Mutex
var alipayClient *alipay.Client

// ResetAlipayClient 重置支付宝客户端(配置变更时调用)
func ResetAlipayClient() {
alipayClientMu.Lock()
alipayClient = nil
alipayClientMu.Unlock()
}

func init() {
setting.OnAlipayConfigChanged = ResetAlipayClient
}

// getAlipayClient 获取或创建支付宝客户端
func getAlipayClient() (*alipay.Client, error) {
alipayClientMu.Lock()
defer alipayClientMu.Unlock()

if alipayClient != nil {
return alipayClient, nil
}

if !setting.IsAlipayConfigured() {
return nil, fmt.Errorf("支付宝未配置")
}

client, err := alipay.NewClient(setting.AlipayAppID, setting.AlipayPrivateKey, true)
if err != nil {
return nil, fmt.Errorf("创建支付宝客户端失败: %w", err)
}

client.SetCharset("utf-8").
SetSignType(alipay.RSA2).
SetNotifyUrl(setting.AlipayNotifyURL)

// 设置支付宝公钥(用于回调验签)
pubKeyBytes, err := base64.StdEncoding.DecodeString(setting.AlipayPublicKey)
if err != nil {
pubKeyBytes = []byte(setting.AlipayPublicKey)
}
client.AutoVerifySign([]byte(wrapAsPEM(pubKeyBytes, "PUBLIC KEY")))

alipayClient = client
return alipayClient, nil
}

// AlipayPayRequest 支付宝支付请求参数
type AlipayPayRequest struct {
Amount int64 `json:"amount"`
}

// createAlipayPrecreateOrder 调用支付宝当面付预下单 API,返回二维码内容
func createAlipayPrecreateOrder(client *alipay.Client, subject, tradeNo string, totalAmount string) (string, error) {
bm := make(gopay.BodyMap)
bm.Set("subject", subject).
Set("out_trade_no", tradeNo).
Set("total_amount", totalAmount).
Set("product_code", "FACE_TO_FACE_PAYMENT")

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

rsp, err := client.TradePrecreate(ctx, bm)
if err != nil {
return "", fmt.Errorf("支付宝当面付下单失败: %w", err)
}

if rsp.Response.Code != "10000" {
return "", fmt.Errorf("支付宝错误: %s - %s", rsp.Response.Code, rsp.Response.Msg)
}

return rsp.Response.QrCode, nil
}

// RequestAlipayPayAmount 计算支付宝应付金额
func RequestAlipayPayAmount(c *gin.Context) {
var req AlipayPayRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
return
}

minTopup := calcMinTopup(setting.AlipayMinTopUp)
if req.Amount < minTopup {
c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)})
return
}

id := c.GetInt("id")
group, err := model.GetUserGroup(id, true)
if err != nil {
c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
return
}

payMoney := calcPayMoney(float64(req.Amount), group, setting.AlipayUnitPrice)
if payMoney <= 0.01 {
c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
return
}

c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
}

// RequestAlipayPay 创建支付宝支付订单,返回二维码 URL
func RequestAlipayPay(c *gin.Context) {
var req AlipayPayRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
return
}

minTopup := calcMinTopup(setting.AlipayMinTopUp)
if req.Amount < minTopup {
c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)})
return
}
if req.Amount > 10000 {
c.JSON(200, gin.H{"message": "error", "data": "充值数量不能大于 10000"})
return
}

if !setting.IsAlipayConfigured() {
c.JSON(200, gin.H{"message": "error", "data": "支付宝未配置"})
return
}

client, err := getAlipayClient()
if err != nil {
log.Println("获取支付宝客户端失败:", err)
c.JSON(200, gin.H{"message": "error", "data": "支付宝配置错误"})
return
}

id := c.GetInt("id")

group, err := model.GetUserGroup(id, true)
if err != nil {
c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
return
}

topupGroupRatio := common.GetTopupGroupRatio(group)
if topupGroupRatio == 0 {
topupGroupRatio = 1
}
chargedMoney := float64(req.Amount) * topupGroupRatio

tradeNo := fmt.Sprintf("ali%d%s", time.Now().UnixMilli(), randstr.String(8))

payMoney := calcPayMoney(float64(req.Amount), group, setting.AlipayUnitPrice)
totalAmount := strconv.FormatFloat(payMoney, 'f', 2, 64)

qrCode, err := createAlipayPrecreateOrder(client, fmt.Sprintf("充值%d", req.Amount), tradeNo, totalAmount)
if err != nil {
log.Println(err)
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
return
}

topUp := &model.TopUp{
UserId: id,
Amount: req.Amount,
Money: chargedMoney,
TradeNo: tradeNo,
PaymentMethod: PaymentMethodAlipay,
CreateTime: time.Now().Unix(),
Status: common.TopUpStatusPending,
}
if err := topUp.Insert(); err != nil {
c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
return
}

c.JSON(200, gin.H{
"message": "success",
"data": gin.H{
"trade_no": tradeNo,
"qr_code_url": qrCode,
},
})
}

// AlipayPayStatus 轮询支付宝支付订单状态
func AlipayPayStatus(c *gin.Context) {
tradeNo := c.Query("trade_no")
if tradeNo == "" {
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
return
}

topUp := model.GetTopUpByTradeNo(tradeNo)
if topUp == nil {
c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
return
}

userId := c.GetInt("id")
if topUp.UserId != userId {
c.JSON(200, gin.H{"message": "error", "data": "订单不存在"})
return
}

c.JSON(200, gin.H{
"message": "success",
"data": gin.H{
"status": topUp.Status,
"amount": topUp.Amount,
},
})
}

// AlipayPayWebhook 处理支付宝异步回调通知
func AlipayPayWebhook(c *gin.Context) {
notifyReq, err := alipay.ParseNotifyToBodyMap(c.Request)
if err != nil {
log.Printf("解析支付宝回调失败: %v", err)
c.String(http.StatusBadRequest, "fail")
return
}

ok, err := alipay.VerifySign(setting.AlipayPublicKey, notifyReq)
if err != nil {
log.Printf("支付宝回调验签失败: %v", err)
c.String(http.StatusBadRequest, "fail")
return
}
if !ok {
log.Printf("支付宝回调验签不通过")
c.String(http.StatusBadRequest, "fail")
return
}

tradeStatus := notifyReq.Get("trade_status")
if tradeStatus != "TRADE_SUCCESS" {
c.String(http.StatusOK, "success")
return
}

tradeNo := notifyReq.Get("out_trade_no")

LockOrder(tradeNo)
defer UnlockOrder(tradeNo)

if err := model.RechargeAlipay(tradeNo); err != nil {
log.Printf("支付宝充值失败: %s, err: %s", tradeNo, err.Error())
c.String(http.StatusInternalServerError, "fail")
return
}

log.Printf("支付宝充值成功: %s", tradeNo)
c.String(http.StatusOK, "success")
}

+ 2
- 24
controller/topup_wechat.go View File

@@ -18,7 +18,6 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting"
"github.com/QuantumNous/new-api/setting/operation_setting"

"github.com/gin-gonic/gin"
"github.com/go-pay/gopay"
@@ -355,31 +354,10 @@ func WechatPayWebhook(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
}

// getWechatPayMoney 计算微信支付应付金额(元)
func getWechatPayMoney(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
}
}
payMoney := amount * setting.WechatPayUnitPrice * topupGroupRatio * discount
return payMoney
return calcPayMoney(amount, group, setting.WechatPayUnitPrice)
}

// getWechatMinTopup 获取微信支付最低充值数量
func getWechatMinTopup() int64 {
minTopup := setting.WechatPayMinTopUp
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
minTopup = minTopup * int(common.QuotaPerUnit)
}
return int64(minTopup)
return calcMinTopup(setting.WechatPayMinTopUp)
}

+ 28
- 0
model/option.go View File

@@ -101,6 +101,12 @@ func InitOptionMap() {
common.OptionMap["WechatPayPubKeyB64"] = setting.WechatPayPubKeyB64
common.OptionMap["WechatPayMinTopUp"] = strconv.Itoa(setting.WechatPayMinTopUp)
common.OptionMap["WechatPayUnitPrice"] = strconv.FormatFloat(setting.WechatPayUnitPrice, 'f', -1, 64)
common.OptionMap["AlipayAppID"] = setting.AlipayAppID
common.OptionMap["AlipayPrivateKey"] = setting.AlipayPrivateKey
common.OptionMap["AlipayPublicKey"] = setting.AlipayPublicKey
common.OptionMap["AlipayNotifyURL"] = setting.AlipayNotifyURL
common.OptionMap["AlipayMinTopUp"] = strconv.Itoa(setting.AlipayMinTopUp)
common.OptionMap["AlipayUnitPrice"] = strconv.FormatFloat(setting.AlipayUnitPrice, 'f', -1, 64)
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["Chats"] = setting.Chats2JsonString()
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
@@ -178,6 +184,13 @@ func triggerWechatPayReset() {
}
}

// triggerAlipayReset 安全触发支付宝客户端重置
func triggerAlipayReset() {
if setting.OnAlipayConfigChanged != nil {
setting.OnAlipayConfigChanged()
}
}

func loadOptionsFromDatabase() {
options, _ := AllOption()
for _, option := range options {
@@ -410,6 +423,21 @@ func updateOptionMap(key string, value string) (err error) {
setting.WechatPayMinTopUp, _ = strconv.Atoi(value)
case "WechatPayUnitPrice":
setting.WechatPayUnitPrice, _ = strconv.ParseFloat(value, 64)
case "AlipayAppID":
setting.AlipayAppID = value
triggerAlipayReset()
case "AlipayPrivateKey":
setting.AlipayPrivateKey = value
triggerAlipayReset()
case "AlipayPublicKey":
setting.AlipayPublicKey = value
triggerAlipayReset()
case "AlipayNotifyURL":
setting.AlipayNotifyURL = value
case "AlipayMinTopUp":
setting.AlipayMinTopUp, _ = strconv.Atoi(value)
case "AlipayUnitPrice":
setting.AlipayUnitPrice, _ = strconv.ParseFloat(value, 64)
case "TopupGroupRatio":
err = common.UpdateTopupGroupRatioByJSONString(value)
case "GitHubClientId":


+ 32
- 1
model/topup.go View File

@@ -5,6 +5,7 @@ import (
"fmt"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/types"
"github.com/QuantumNous/new-api/logger"

"github.com/shopspring/decimal"
@@ -21,6 +22,32 @@ type TopUp struct {
CreateTime int64 `json:"create_time"`
CompleteTime int64 `json:"complete_time"`
Status string `json:"status"`
UserEmail string `json:"user_email" gorm:"-"` // Join 查询时填充,非数据库字段
}

// fillTopUpEmails 批量填充 topup 记录的用户邮箱
func fillTopUpEmails(topups []*TopUp) {
if len(topups) == 0 {
return
}
userIds := types.NewSet[int]()
for _, t := range topups {
userIds.Add(t.UserId)
}

var users []User
if err := DB.Select("id, email").Where("id IN ?", userIds.Items()).Find(&users).Error; err != nil {
common.SysError("fillTopUpEmails: " + err.Error())
return
}

emailMap := make(map[int]string, len(users))
for _, u := range users {
emailMap[u.Id] = u.Email
}
for _, t := range topups {
t.UserEmail = emailMap[t.UserId]
}
}

func (topUp *TopUp) Insert() error {
@@ -135,6 +162,7 @@ func GetUserTopUps(userId int, pageInfo *common.PageInfo) (topups []*TopUp, tota
return nil, 0, err
}

fillTopUpEmails(topups)
return topups, total, nil
}

@@ -164,6 +192,7 @@ func GetAllTopUps(pageInfo *common.PageInfo) (topups []*TopUp, total int64, err
return nil, 0, err
}

fillTopUpEmails(topups)
return topups, total, nil
}

@@ -198,6 +227,7 @@ func SearchUserTopUps(userId int, keyword string, pageInfo *common.PageInfo) (to
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
fillTopUpEmails(topups)
return topups, total, nil
}

@@ -232,6 +262,7 @@ func SearchAllTopUps(keyword string, pageInfo *common.PageInfo) (topups []*TopUp
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
fillTopUpEmails(topups)
return topups, total, nil
}

@@ -269,7 +300,7 @@ func ManualCompleteTopUp(tradeNo string) error {
// 计算应充值额度:
// - Stripe/微信支付订单:Money 代表经分组倍率换算后的数量,直接 * QuotaPerUnit
// - 其他订单(如易支付):Amount 为美元数量,* QuotaPerUnit
if topUp.PaymentMethod == "stripe" || topUp.PaymentMethod == "wechat_pay" {
if topUp.PaymentMethod == "stripe" || topUp.PaymentMethod == "wechat_pay" || topUp.PaymentMethod == "alipay" {
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
quotaToAdd = int(decimal.NewFromFloat(topUp.Money).Mul(dQuotaPerUnit).IntPart())
} else {


+ 12
- 6
model/topup_wechat.go View File

@@ -12,8 +12,18 @@ import (
)

// RechargeWechat 微信支付充值完成(由回调触发)
// 与 Recharge/RechargeCreem 类似,使用事务+行锁保证幂等
func RechargeWechat(tradeNo string) error {
return rechargeByQRCodePayment(tradeNo, "微信支付")
}

// RechargeAlipay 支付宝充值完成(由回调触发)
func RechargeAlipay(tradeNo string) error {
return rechargeByQRCodePayment(tradeNo, "支付宝")
}

// rechargeByQRCodePayment 扫码支付充值完成(微信/支付宝通用)
// 使用事务+行锁保证幂等
func rechargeByQRCodePayment(tradeNo string, paymentMethod string) error {
if tradeNo == "" {
return errors.New("未提供支付单号")
}
@@ -34,7 +44,6 @@ func RechargeWechat(tradeNo string) error {
}

if topUp.Status == common.TopUpStatusSuccess {
// 已处理,幂等返回
return nil
}

@@ -48,9 +57,6 @@ func RechargeWechat(tradeNo string) error {
return err
}

// 微信支付充值额度计算:
// topUp.Money = req.Amount * topUpGroupRatio(经分组倍率调整后的数量)
// 充值额度 = topUp.Money * QuotaPerUnit(与 Stripe 的 Recharge 逻辑一致)
dMoney := decimal.NewFromFloat(topUp.Money)
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
quotaToAdd = dMoney.Mul(dQuotaPerUnit).IntPart()
@@ -73,7 +79,7 @@ func RechargeWechat(tradeNo string) error {
}

if quotaToAdd > 0 {
RecordLog(userId, LogTypeTopup, fmt.Sprintf("使用微信支付充值成功,充值金额: %v,支付金额:%.2f", logger.FormatQuota(int(quotaToAdd)), payMoney))
RecordLog(userId, LogTypeTopup, fmt.Sprintf("使用%s充值成功,充值金额: %v,支付金额:%.2f", paymentMethod, logger.FormatQuota(int(quotaToAdd)), payMoney))
}

return nil


+ 4
- 0
router/api-router.go View File

@@ -49,6 +49,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
apiRouter.POST("/wechat/pay/webhook", controller.WechatPayWebhook)
apiRouter.POST("/alipay/pay/webhook", controller.AlipayPayWebhook)
// Universal secure verification routes
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)

@@ -93,6 +94,9 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.POST("/wechat/pay/amount", controller.RequestWechatPayAmount)
selfRoute.POST("/wechat/pay", controller.RequestWechatPay)
selfRoute.GET("/wechat/pay/status", controller.WechatPayStatus)
selfRoute.POST("/alipay/pay/amount", controller.RequestAlipayPayAmount)
selfRoute.POST("/alipay/pay", controller.RequestAlipayPay)
selfRoute.GET("/alipay/pay/status", controller.AlipayPayStatus)
selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
selfRoute.PUT("/setting", controller.UpdateUserSetting)



+ 18
- 0
setting/payment_alipay.go View File

@@ -0,0 +1,18 @@
package setting

var AlipayAppID = ""
var AlipayPrivateKey = "" // 应用私钥(RSA2)
var AlipayPublicKey = "" // 支付宝公钥(用于验签)
var AlipayNotifyURL = ""
var AlipayMinTopUp = 1
var AlipayUnitPrice = 7.0

// IsAlipayConfigured 检查支付宝核心配置是否完整
func IsAlipayConfigured() bool {
return AlipayAppID != "" &&
AlipayPrivateKey != "" &&
AlipayPublicKey != ""
}

// OnAlipayConfigChanged 配置变更时调用的回调函数(由 controller 包注册)
var OnAlipayConfigChanged func()

+ 1
- 1
web/src/components/auth/LoginForm.jsx View File

@@ -490,7 +490,7 @@ const LoginForm = () => {
userDispatch({ type: 'login', payload: data });
setUserData(data);
updateAPI();
showSuccess('登录成功!');
showSuccess(t('登录成功!'));
navigate('/console');
};



+ 3
- 3
web/src/components/auth/RegisterForm.jsx View File

@@ -184,7 +184,7 @@ const RegisterForm = () => {

const onSubmitWeChatVerificationCode = async () => {
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
return;
}
setWechatCodeSubmitLoading(true);
@@ -257,7 +257,7 @@ const RegisterForm = () => {
const sendVerificationCode = async () => {
if (inputs.email === '') return;
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
return;
}
setVerificationCodeLoading(true);
@@ -745,7 +745,7 @@ const RegisterForm = () => {
}}
>
<div className='flex flex-col items-center'>
<img src={status.wechat_qrcode} alt='微信二维码' className='mb-4' />
<img src={status.wechat_qrcode} alt={t('微信二维码')} className='mb-4' />
</div>

<div className='text-center mb-4'>


+ 3
- 2
web/src/components/common/markdown/MarkdownRenderer.jsx View File

@@ -268,7 +268,7 @@ export function PreCode(props) {
color: 'var(--semi-color-text-2)',
}}
>
HTML预览:
{t('HTML预览:')}
</div>
<SandboxedHtmlPreview code={htmlCode} />
</div>
@@ -635,6 +635,7 @@ function _MarkdownContent(props) {
export const MarkdownContent = React.memo(_MarkdownContent);

export function MarkdownRenderer(props) {
const { t } = useTranslation();
const {
content,
loading,
@@ -680,7 +681,7 @@ export function MarkdownRenderer(props) {
animation: 'spin 1s linear infinite',
}}
/>
正在渲染...
{t('正在渲染...')}
</div>
) : (
<MarkdownContent


+ 1
- 1
web/src/components/common/ui/JSONEditor.jsx View File

@@ -661,7 +661,7 @@ const JSONEditor = ({
{hasJsonError && (
<Banner
type='danger'
description={`JSON 格式错误: ${jsonError}`}
description={`${t('JSON 格式错误')}: ${jsonError}`}
className='mb-3'
/>
)}


+ 1
- 1
web/src/components/playground/CodeViewer.jsx View File

@@ -201,7 +201,7 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
}
return (
formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
'\n\n// ... 内容被截断以提升性能 ...'
'\n\n// ... ' + t('内容被截断以提升性能') + ' ...'
);
}, [formattedContent, contentMetrics.isLarge, isExpanded]);



+ 1
- 1
web/src/components/playground/DebugPanel.jsx View File

@@ -146,7 +146,7 @@ const DebugPanel = ({
{t('预览请求体')}
{customRequestMode && (
<span className='px-1.5 py-0.5 text-xs bg-orange-100 text-orange-600 rounded-full'>
自定义
{t('自定义')}
</span>
)}
</div>


+ 2
- 2
web/src/components/playground/MessageContent.jsx View File

@@ -272,7 +272,7 @@ const MessageContent = ({
<div key={index} className='max-w-sm'>
<img
src={imgItem.image_url.url}
alt={`用户上传的图片 ${index + 1}`}
alt={t('用户上传的图片', { index: index + 1 })}
className='rounded-lg max-w-full h-auto shadow-sm border'
style={{ maxHeight: '300px' }}
onError={(e) => {
@@ -284,7 +284,7 @@ const MessageContent = ({
className='text-red-500 text-sm p-2 bg-red-50 rounded-lg border border-red-200'
style={{ display: 'none' }}
>
图片加载失败: {imgItem.image_url.url}
{t('图片加载失败')}: {imgItem.image_url.url}
</div>
</div>
))}


+ 2
- 2
web/src/components/playground/ThinkingContent.jsx View File

@@ -105,7 +105,7 @@ const ThinkingContent = ({
style={{ color: 'white' }}
className='text-xs mt-0.5 opacity-80 hidden sm:block'
>
来源: {thinkingSource}
{t('来源')}: {thinkingSource}
</Typography.Text>
)}
</div>
@@ -122,7 +122,7 @@ const ThinkingContent = ({
style={{ color: 'white' }}
className='text-xs sm:text-sm font-medium opacity-90'
>
思考中
{t('思考中')}
</Typography.Text>
</div>
)}


+ 5
- 4
web/src/components/playground/configStorage.js View File

@@ -21,6 +21,7 @@ import {
STORAGE_KEYS,
DEFAULT_CONFIG,
} from '../../constants/playground.constants';
import i18next from 'i18next';

const MESSAGES_STORAGE_KEY = 'playground_messages';

@@ -215,16 +216,16 @@ export const importConfig = (file) => {

resolve(importedConfig);
} else {
reject(new Error('配置文件格式无效'));
reject(new Error(i18next.t('配置文件格式无效')));
}
} catch (parseError) {
reject(new Error('解析配置文件失败: ' + parseError.message));
reject(new Error(i18next.t('解析配置文件失败: ') + parseError.message));
}
};
reader.onerror = () => reject(new Error('读取文件失败'));
reader.onerror = () => reject(new Error(i18next.t('读取文件失败')));
reader.readAsText(file);
} catch (error) {
reject(new Error('导入配置失败: ' + error.message));
reject(new Error(i18next.t('导入配置失败: ') + error.message));
}
});
};

+ 1
- 1
web/src/components/settings/ModelDeploymentSetting.jsx View File

@@ -60,7 +60,7 @@ const ModelDeploymentSetting = () => {
setLoading(true);
await getOptions();
} catch (error) {
showError('刷新失败');
showError(t('刷新失败'));
console.error(error);
} finally {
setLoading(false);


+ 1
- 1
web/src/components/settings/ModelSetting.jsx View File

@@ -95,7 +95,7 @@ const ModelSetting = () => {
await getOptions();
// showSuccess('刷新成功');
} catch (error) {
showError('刷新失败');
showError(t('刷新失败'));
console.error(error);
} finally {
setLoading(false);


+ 6
- 0
web/src/components/settings/PaymentSetting.jsx View File

@@ -24,6 +24,7 @@ import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentG
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe';
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem';
import SettingsPaymentGatewayWechat from '../../pages/Setting/Payment/SettingsPaymentGatewayWechat';
import SettingsPaymentGatewayAlipay from '../../pages/Setting/Payment/SettingsPaymentGatewayAlipay';
import { API, showError, toBoolean } from '../../helpers';
import { useTranslation } from 'react-i18next';

@@ -101,6 +102,8 @@ const PaymentSetting = () => {
case 'StripeMinTopUp':
case 'WechatPayUnitPrice':
case 'WechatPayMinTopUp':
case 'AlipayUnitPrice':
case 'AlipayMinTopUp':
newInputs[item.key] = parseFloat(item.value);
break;
default:
@@ -152,6 +155,9 @@ const PaymentSetting = () => {
<Card style={{ marginTop: '10px' }}>
<SettingsPaymentGatewayWechat options={inputs} refresh={onRefresh} />
</Card>
<Card style={{ marginTop: '10px' }}>
<SettingsPaymentGatewayAlipay options={inputs} refresh={onRefresh} />
</Card>
</Spin>
</>
);


+ 1
- 1
web/src/components/settings/RateLimitSetting.jsx View File

@@ -64,7 +64,7 @@ const RateLimitSetting = () => {
await getOptions();
// showSuccess('刷新成功');
} catch (error) {
showError('刷新失败');
showError(t('刷新失败'));
} finally {
setLoading(false);
}


+ 1
- 1
web/src/components/settings/RatioSetting.jsx View File

@@ -83,7 +83,7 @@ const RatioSetting = () => {
setLoading(true);
await getOptions();
} catch (error) {
showError('刷新失败');
showError(t('刷新失败'));
} finally {
setLoading(false);
}


+ 1
- 1
web/src/components/settings/personal/cards/NotificationSettings.jsx View File

@@ -533,7 +533,7 @@ const NotificationSettings = ({
<CodeViewer
content={{
type: 'quota_exceed',
title: '额度预警通知',
title: t('额度预警通知'),
content:
'您的额度即将用尽,当前剩余额度为 {{value}}',
values: ['$0.99'],


+ 1
- 1
web/src/components/table/channels/ChannelsColumnDefs.jsx View File

@@ -74,7 +74,7 @@ const renderType = (type, record = {}, t) => {

const typeTag = (
<Tag color={type2label[type]?.color} shape='circle' prefixIcon={icon}>
{type2label[type]?.label}
{t(type2label[type]?.label)}
</Tag>
);



+ 1
- 1
web/src/components/table/channels/ChannelsTabs.jsx View File

@@ -78,7 +78,7 @@ const ChannelsTabs = ({
tab={
<span className='flex items-center gap-2'>
{getChannelIcon(option.value)}
{option.label}
{t(option.label)}
<Tag
color={activeTypeKey === key ? 'red' : 'grey'}
shape='circle'


+ 4
- 4
web/src/components/table/channels/modals/EditChannelModal.jsx View File

@@ -467,7 +467,7 @@ const EditChannelModal = (props) => {

if (name === 'base_url' && value.endsWith('/v1')) {
Modal.confirm({
title: '警告',
title: t('警告'),
content:
'不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
onOk: () => {
@@ -1725,10 +1725,10 @@ const EditChannelModal = (props) => {
() =>
CHANNEL_OPTIONS.map((opt) => ({
...opt,
// 保持 label 为纯文本以支持搜索
label: opt.label,
// 使用 t() 翻译 label,同时保留原始中文 key 以支持搜索
label: t(opt.label),
})),
[],
[t],
);

const renderChannelOption = (renderProps) => {


+ 2
- 2
web/src/components/table/channels/modals/EditTagModal.jsx View File

@@ -236,7 +236,7 @@ const EditTagModal = (props) => {
data.param_override === undefined &&
data.header_override === undefined
) {
showWarning('没有任何修改!');
showWarning(t('没有任何修改!'));
setLoading(false);
return;
}
@@ -248,7 +248,7 @@ const EditTagModal = (props) => {
try {
const res = await API.put('/api/channel/tag', data);
if (res?.data?.success) {
showSuccess('标签更新成功!');
showSuccess(t('标签更新成功!'));
refresh();
handleClose();
}


+ 1
- 1
web/src/components/table/task-logs/TaskLogsColumnDefs.jsx View File

@@ -150,7 +150,7 @@ const renderPlatform = (platform, t) => {
if (option) {
return (
<Tag color={option.color} shape='circle'>
{option.label}
{t(option.label)}
</Tag>
);
}


web/src/components/topup/WechatPayQRCodeModal.jsx → web/src/components/topup/QRCodePayModal.jsx View File

@@ -8,7 +8,7 @@ const { Text } = Typography;

const POLL_INTERVAL = 2000;

export default function WechatPayQRCodeModal({ visible, qrCodeUrl, tradeNo, onClose, onSuccess }) {
export default function QRCodePayModal({ visible, qrCodeUrl, tradeNo, onClose, onSuccess, title, subtitle, statusApiPath }) {
const { t } = useTranslation();
const [status, setStatus] = useState('pending');
const [countdown, setCountdown] = useState(300);
@@ -18,7 +18,7 @@ export default function WechatPayQRCodeModal({ visible, qrCodeUrl, tradeNo, onCl
const pollStatus = useCallback(async () => {
if (!tradeNo) return;
try {
const res = await API.get(`/api/user/wechat/pay/status?trade_no=${tradeNo}`);
const res = await API.get(`${statusApiPath}?trade_no=${tradeNo}`);
const { data } = res.data;
if (data?.status === 'success') {
setStatus('success');
@@ -35,7 +35,7 @@ export default function WechatPayQRCodeModal({ visible, qrCodeUrl, tradeNo, onCl
} catch (e) {
// 忽略轮询错误
}
}, [tradeNo, onSuccess]);
}, [tradeNo, onSuccess, statusApiPath]);

useEffect(() => {
if (visible && tradeNo && status === 'pending') {
@@ -108,7 +108,7 @@ export default function WechatPayQRCodeModal({ visible, qrCodeUrl, tradeNo, onCl
</div>
<div style={{ marginTop: 16 }}>
<Text type="secondary">
{t('请使用微信扫描二维码完成支付')}
{subtitle || t('请扫描二维码完成支付')}
</Text>
</div>
<div style={{ marginTop: 8 }}>
@@ -123,7 +123,7 @@ export default function WechatPayQRCodeModal({ visible, qrCodeUrl, tradeNo, onCl

return (
<Modal
title={t('微信支付')}
title={title || t('扫码支付')}
visible={visible}
onCancel={onClose}
footer={null}

+ 8
- 5
web/src/components/topup/RechargeCard.jsx View File

@@ -58,6 +58,7 @@ const RechargeCard = ({
enableStripeTopUp,
enableCreemTopUp,
enableWechatTopUp,
enableAlipayTopUp,
creemProducts,
creemPreTopUp,
presetAmounts,
@@ -225,19 +226,19 @@ const RechargeCard = ({
<div className='py-8 flex justify-center'>
<Spin size='large' />
</div>
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWechatTopUp ? (
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWechatTopUp || enableAlipayTopUp ? (
<Form
getFormApi={(api) => (onlineFormApiRef.current = api)}
initValues={{ topUpCount: topUpCount }}
>
<div className='space-y-6'>
{(enableOnlineTopUp || enableStripeTopUp || enableWechatTopUp) && (
{(enableOnlineTopUp || enableStripeTopUp || enableWechatTopUp || enableAlipayTopUp) && (
<Row gutter={12}>
<Col xs={24} sm={24} md={24} lg={10} xl={10}>
<Form.InputNumber
field='topUpCount'
label={t('充值数量')}
disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWechatTopUp}
disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWechatTopUp && !enableAlipayTopUp}
placeholder={
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
}
@@ -297,11 +298,13 @@ const RechargeCard = ({
const minTopupVal = Number(payMethod.min_topup) || 0;
const isStripe = payMethod.type === 'stripe';
const isWechatPay = payMethod.type === 'wechat_pay';
const isEpay = !isStripe && !isWechatPay;
const isAlipay = payMethod.type === 'alipay';
const isEpay = !isStripe && !isWechatPay && !isAlipay;
const disabled =
(!enableOnlineTopUp && isEpay) ||
(!enableStripeTopUp && isStripe) ||
(!enableWechatTopUp && isWechatPay) ||
(!enableAlipayTopUp && isAlipay) ||
minTopupVal > Number(topUpCount || 0);

const buttonEl = (
@@ -366,7 +369,7 @@ const RechargeCard = ({
</Row>
)}

{(enableOnlineTopUp || enableStripeTopUp || enableWechatTopUp) && (
{(enableOnlineTopUp || enableStripeTopUp || enableWechatTopUp || enableAlipayTopUp) && (
<Form.Slot
label={
<div className='flex items-center gap-2'>


+ 65
- 6
web/src/components/topup/index.jsx View File

@@ -38,7 +38,7 @@ import InvitationCard from './InvitationCard';
import TransferModal from './modals/TransferModal';
import PaymentConfirmModal from './modals/PaymentConfirmModal';
import TopupHistoryModal from './modals/TopupHistoryModal';
import WechatPayQRCodeModal from './WechatPayQRCodeModal';
import QRCodePayModal from './QRCodePayModal';

const TopUp = () => {
const { t } = useTranslation();
@@ -94,6 +94,12 @@ const TopUp = () => {
const [wechatPayTradeNo, setWechatPayTradeNo] = useState('');
const [enableWechatTopUp, setEnableWechatTopUp] = useState(false);

// 支付宝支付相关状态
const [alipayPayVisible, setAlipayPayVisible] = useState(false);
const [alipayPayQRCodeUrl, setAlipayPayQRCodeUrl] = useState('');
const [alipayPayTradeNo, setAlipayPayTradeNo] = useState('');
const [enableAlipayTopUp, setEnableAlipayTopUp] = useState(false);

// 订阅相关
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
const [subscriptionLoading, setSubscriptionLoading] = useState(true);
@@ -167,6 +173,11 @@ const TopUp = () => {
showError(t('管理员未开启微信支付充值!'));
return;
}
} else if (payment === 'alipay') {
if (!enableAlipayTopUp) {
showError(t('管理员未开启支付宝充值!'));
return;
}
} else {
if (!enableOnlineTopUp) {
showError(t('管理员未开启在线充值!'));
@@ -200,6 +211,30 @@ const TopUp = () => {
return;
}

// 支付宝支付直接创建订单并显示二维码
if (payment === 'alipay') {
setPaymentLoading(true);
try {
const res = await API.post('/api/user/alipay/pay', {
amount: parseInt(topUpCount),
});
const { message, data } = res.data;
if (message === 'success') {
setAlipayPayQRCodeUrl(data.qr_code_url);
setAlipayPayTradeNo(data.trade_no);
setAlipayPayVisible(true);
} else {
const errorMsg = typeof data === 'string' ? data : message || t('支付失败');
showError(errorMsg);
}
} catch (err) {
showError(t('支付请求失败'));
} finally {
setPaymentLoading(false);
}
return;
}

setPaymentLoading(true);
try {
if (payment === 'stripe') {
@@ -483,17 +518,21 @@ const TopUp = () => {
const enableOnlineTopUp = data.enable_online_topup || false;
const enableCreemTopUp = data.enable_creem_topup || false;
const enableWechatTopUpVal = data.enable_wechat_topup || false;
const enableAlipayTopUpVal = data.enable_alipay_topup || false;
const minTopUpValue = enableOnlineTopUp
? data.min_topup
: enableStripeTopUp
? data.stripe_min_topup
: enableWechatTopUpVal
? data.wechat_pay_min_topup || 1
: 1;
: enableAlipayTopUpVal
? data.alipay_pay_min_topup || 1
: 1;
setEnableOnlineTopUp(enableOnlineTopUp);
setEnableStripeTopUp(enableStripeTopUp);
setEnableCreemTopUp(enableCreemTopUp);
setEnableWechatTopUp(enableWechatTopUpVal);
setEnableAlipayTopUp(enableAlipayTopUpVal);
setMinTopUp(minTopUpValue);
setTopUpCount(minTopUpValue);

@@ -622,7 +661,7 @@ const TopUp = () => {
setAmount(parseFloat(data));
} else {
setAmount(0);
Toast.error({ content: '错误:' + data, id: 'getAmount' });
Toast.error({ content: t('错误:') + data, id: 'getAmount' });
}
} else {
showError(res);
@@ -648,7 +687,7 @@ const TopUp = () => {
setAmount(parseFloat(data));
} else {
setAmount(0);
Toast.error({ content: '错误:' + data, id: 'getAmount' });
Toast.error({ content: t('错误:') + data, id: 'getAmount' });
}
} else {
showError(res);
@@ -745,7 +784,7 @@ const TopUp = () => {
/>

{/* 微信支付二维码弹窗 */}
<WechatPayQRCodeModal
<QRCodePayModal
visible={wechatPayVisible}
qrCodeUrl={wechatPayQRCodeUrl}
tradeNo={wechatPayTradeNo}
@@ -753,8 +792,27 @@ const TopUp = () => {
onSuccess={() => {
setWechatPayVisible(false);
showSuccess(t('充值成功!'));
userDispatch({ type: 'refresh' });
getUserQuota();
}}
title={t('微信支付')}
subtitle={t('请使用微信扫描二维码完成支付')}
statusApiPath="/api/user/wechat/pay/status"
/>

{/* 支付宝二维码弹窗 */}
<QRCodePayModal
visible={alipayPayVisible}
qrCodeUrl={alipayPayQRCodeUrl}
tradeNo={alipayPayTradeNo}
onClose={() => setAlipayPayVisible(false)}
onSuccess={() => {
setAlipayPayVisible(false);
showSuccess(t('充值成功!'));
getUserQuota();
}}
title={t('支付宝')}
subtitle={t('请使用支付宝扫描二维码完成支付')}
statusApiPath="/api/user/alipay/pay/status"
/>

{/* Creem 充值确认模态框 */}
@@ -793,6 +851,7 @@ const TopUp = () => {
enableStripeTopUp={enableStripeTopUp}
enableCreemTopUp={enableCreemTopUp}
enableWechatTopUp={enableWechatTopUp}
enableAlipayTopUp={enableAlipayTopUp}
creemProducts={creemProducts}
creemPreTopUp={creemPreTopUp}
presetAmounts={presetAmounts}


+ 13
- 1
web/src/components/topup/modals/TopupHistoryModal.jsx View File

@@ -168,6 +168,18 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
key: 'trade_no',
render: (text) => <Text copyable>{text}</Text>,
},
];

if (userIsAdmin) {
baseColumns.push({
title: t('用户'),
dataIndex: 'user_email',
key: 'user_email',
render: (text) => <Text>{text || '-'}</Text>,
});
}

baseColumns.push(
{
title: t('支付方式'),
dataIndex: 'payment_method',
@@ -206,7 +218,7 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
key: 'status',
render: renderStatusBadge,
},
];
);

// 管理员才显示操作列
if (userIsAdmin) {


+ 7
- 6
web/src/helpers/api.js View File

@@ -24,6 +24,7 @@ import {
isValidMessage,
} from './utils';
import axios from 'axios';
import i18next from 'i18next';
import { MESSAGE_ROLES } from '../constants/playground.constants';

export let API = axios.create({
@@ -147,7 +148,7 @@ export const buildApiPayload = (
// 处理API错误响应
export const handleApiError = (error, response = null) => {
const errorInfo = {
error: error.message || '未知错误',
error: error.message || i18next.t('未知错误'),
timestamp: new Date().toISOString(),
stack: error.stack,
};
@@ -158,9 +159,9 @@ export const handleApiError = (error, response = null) => {
}

if (error.message.includes('HTTP error')) {
errorInfo.details = '服务器返回了错误状态码';
errorInfo.details = i18next.t('服务器返回了错误状态码');
} else if (error.message.includes('Failed to fetch')) {
errorInfo.details = '网络连接失败或服务器无响应';
errorInfo.details = i18next.t('网络连接失败或服务器无响应');
}

return errorInfo;
@@ -197,7 +198,7 @@ export const processGroupsData = (data, userGroup) => {
if (groupOptions.length === 0) {
groupOptions = [
{
label: '用户分组',
label: i18next.t('用户分组'),
value: '',
ratio: 1,
},
@@ -325,7 +326,7 @@ export async function onCustomOAuthClicked(provider, options = {}) {
provider.authorization_endpoint,
);
showError(
'OAuth 配置错误:授权端点必须是完整的 URL(以 http:// 或 https:// 开头)',
i18next.t('OAuth 配置错误:授权端点必须是完整的 URL(以 http:// 或 https:// 开头)'),
);
return;
}
@@ -342,7 +343,7 @@ export async function onCustomOAuthClicked(provider, options = {}) {
window.open(authUrl.toString());
} catch (error) {
console.error('Failed to initiate custom OAuth:', error);
showError('OAuth 登录失败:' + (error.message || '未知错误'));
showError(i18next.t('OAuth 登录失败:') + (error.message || i18next.t('未知错误')));
}
}



+ 4
- 2
web/src/helpers/passkey.js View File

@@ -16,6 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.

For commercial licensing, please contact support@quantumnous.com
*/
import i18next from 'i18next';

export function base64UrlToBuffer(base64url) {
if (!base64url) return new ArrayBuffer(0);
let padding = '='.repeat((4 - (base64url.length % 4)) % 4);
@@ -50,7 +52,7 @@ export function prepareCredentialCreationOptions(payload) {
payload?.response ||
payload?.Response;
if (!options) {
throw new Error('无法从服务端响应中解析 Passkey 注册参数');
throw new Error(i18next.t('无法从服务端响应中解析 Passkey 注册参数'));
}
const publicKey = {
...options,
@@ -85,7 +87,7 @@ export function prepareCredentialRequestOptions(payload) {
payload?.response ||
payload?.Response;
if (!options) {
throw new Error('无法从服务端响应中解析 Passkey 登录参数');
throw new Error(i18next.t('无法从服务端响应中解析 Passkey 登录参数'));
}
const publicKey = {
...options,


+ 1
- 1
web/src/helpers/render.jsx View File

@@ -1373,7 +1373,7 @@ export function renderModelPrice(
symbol: symbol,
price: (inputRatioPrice * rate).toFixed(6),
audioPrice: audioInputSeperatePrice
? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens`
? i18next.t(',音频 {{symbol}}{{price}} / 1M tokens', { symbol, price: (audioInputPrice * rate).toFixed(6) })
: '',
},
)}


+ 4
- 4
web/src/hooks/channels/useChannelsData.jsx View File

@@ -249,7 +249,7 @@ export const useChannelsData = () => {
key: tag,
id: tag,
tag: tag,
name: '标签:' + tag,
name: t('标签:') + tag,
group: '',
used_quota: 0,
response_time: 0,
@@ -613,7 +613,7 @@ export const useChannelsData = () => {
switch (type) {
case 'priority':
if (data.priority === undefined || data.priority === '') {
showInfo('优先级必须是整数!');
showInfo(t('优先级必须是整数!'));
return;
}
data.priority = parseInt(data.priority);
@@ -624,7 +624,7 @@ export const useChannelsData = () => {
data.weight < 0 ||
data.weight === ''
) {
showInfo('权重必须是非负整数!');
showInfo(t('权重必须是非负整数!'));
return;
}
data.weight = parseInt(data.weight);
@@ -634,7 +634,7 @@ export const useChannelsData = () => {
try {
const res = await API.put('/api/channel/tag', data);
if (res?.data?.success) {
showSuccess('更新成功!');
showSuccess(t('更新成功!'));
await refresh();
}
} catch (error) {


+ 2
- 1
web/src/hooks/chat/useTokenKeys.js View File

@@ -20,6 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
import { useEffect, useState } from 'react';
import { fetchTokenKeys, getServerAddress } from '../../helpers/token';
import { showError } from '../../helpers';
import i18next from 'i18next';

export function useTokenKeys(id) {
const [keys, setKeys] = useState([]);
@@ -30,7 +31,7 @@ export function useTokenKeys(id) {
const loadAllData = async () => {
const fetchedKeys = await fetchTokenKeys();
if (fetchedKeys.length === 0) {
showError('当前没有可用的启用令牌,请确认是否有令牌处于启用状态!');
showError(i18next.t('当前没有可用的启用令牌,请确认是否有令牌处于启用状态!'));
setTimeout(() => {
window.location.href = '/console/token';
}, 1500); // 延迟 1.5 秒后跳转


+ 1
- 1
web/src/hooks/dashboard/useDashboardData.js View File

@@ -177,7 +177,7 @@ export const useDashboardData = (userState, userDispatch, statusState) => {
if (data.length === 0) {
data.push({
count: 0,
model_name: '无数据',
model_name: t('无数据'),
quota: 0,
created_at: now.getTime() / 1000,
});


+ 3082
- 2993
web/src/i18n/locales/en.json
File diff suppressed because it is too large
View File


+ 2917
- 2828
web/src/i18n/locales/fr.json
File diff suppressed because it is too large
View File


+ 2900
- 2811
web/src/i18n/locales/ja.json
File diff suppressed because it is too large
View File


+ 2930
- 2841
web/src/i18n/locales/ru.json
File diff suppressed because it is too large
View File


+ 3469
- 3383
web/src/i18n/locales/vi.json
File diff suppressed because it is too large
View File


+ 3059
- 2970
web/src/i18n/locales/zh-CN.json
File diff suppressed because it is too large
View File


+ 2985
- 2896
web/src/i18n/locales/zh-TW.json
File diff suppressed because it is too large
View File


+ 7
- 7
web/src/pages/Home/components/PartnersSection.jsx View File

@@ -52,22 +52,22 @@ const PartnersSection = () => {
{ icon: Moonshot, name: 'Moonshot AI', size: 40 },
{ icon: OpenAI, name: 'OpenAI', size: 40 },
{ icon: XAI, name: 'Grok', size: 40 },
{ icon: Zhipu, name: '智谱', size: 40 },
{ icon: Volcengine, name: '火山引擎', size: 40 },
{ icon: Zhipu, name: t('智谱'), size: 40 },
{ icon: Volcengine, name: t('火山引擎'), size: 40 },
{ icon: Cohere, name: 'Cohere', size: 40 },
{ icon: Claude, name: 'Claude', size: 40 },
{ icon: Gemini, name: 'Gemini', size: 40 },
{ icon: Suno, name: 'Suno', size: 40 },
{ icon: Minimax, name: 'Minimax', size: 40 },
{ icon: Wenxin, name: '文心', size: 40 },
{ icon: Spark, name: '讯飞星火', size: 40 },
{ icon: Qingyan, name: '腾讯混元', size: 40 },
{ icon: Wenxin, name: t('文心'), size: 40 },
{ icon: Spark, name: t('讯飞星火'), size: 40 },
{ icon: Qingyan, name: t('腾讯混元'), size: 40 },
{ icon: DeepSeek, name: 'DeepSeek', size: 40 },
{ icon: Qwen, name: '通义千问', size: 40 },
{ icon: Qwen, name: t('通义千问'), size: 40 },
{ icon: Midjourney, name: 'Midjourney', size: 40 },
{ icon: Grok, name: 'Grok', size: 40 },
{ icon: AzureAI, name: 'Azure AI', size: 40 },
{ icon: Hunyuan, name: '腾讯混元', size: 40 },
{ icon: Hunyuan, name: t('腾讯混元'), size: 40 },
{ icon: Xinference, name: 'Xinference', size: 40 },
];



+ 2
- 2
web/src/pages/Playground/index.jsx View File

@@ -214,7 +214,7 @@ const Playground = () => {
(url) => url.trim() !== '',
);
if (validImageUrls.length > 0) {
const textContent = getTextContent(messages[i]) || '示例消息';
const textContent = getTextContent(messages[i]) || t('示例消息');
const content = buildMessageContent(
textContent,
validImageUrls,
@@ -262,7 +262,7 @@ const Playground = () => {
return;
} catch (error) {
console.error('自定义请求体JSON解析失败:', error);
Toast.error(ERROR_MESSAGES.JSON_PARSE_ERROR);
Toast.error(t(ERROR_MESSAGES.JSON_PARSE_ERROR));
return;
}
}


+ 6
- 6
web/src/pages/Setting/Dashboard/SettingsAPIInfo.jsx View File

@@ -91,7 +91,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
});
const { success, message } = res.data;
if (success) {
showSuccess('API信息已更新');
showSuccess(t('API信息已更新'));
if (refresh) refresh();
} else {
showError(message);
@@ -106,7 +106,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
setHasChanges(false);
} catch (error) {
console.error('API信息更新失败', error);
showError('API信息更新失败');
showError(t('API信息更新失败'));
} finally {
setLoading(false);
}
@@ -144,7 +144,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
const newList = apiInfoList.filter((api) => api.id !== deletingApi.id);
setApiInfoList(newList);
setHasChanges(true);
showSuccess('API信息已删除,请及时点击“保存设置”进行保存');
showSuccess(t('API信息已删除,请及时点击”保存设置”进行保存'));
}
setShowDeleteModal(false);
setDeletingApi(null);
@@ -152,7 +152,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {

const handleSaveApi = async () => {
if (!apiForm.url || !apiForm.route || !apiForm.description) {
showError('请填写完整的API信息');
showError(t('请填写完整的API信息'));
return;
}

@@ -182,7 +182,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
: 'API信息已添加,请及时点击“保存设置”进行保存',
);
} catch (error) {
showError('操作失败: ' + error.message);
showError(t('操作失败') + ': ' + error.message);
} finally {
setModalLoading(false);
}
@@ -299,7 +299,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {

const handleBatchDelete = () => {
if (selectedRowKeys.length === 0) {
showError('请先选择要删除的API信息');
showError(t('请先选择要删除的API信息'));
return;
}



+ 6
- 6
web/src/pages/Setting/Dashboard/SettingsAnnouncements.jsx View File

@@ -204,7 +204,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
});
const { success, message } = res.data;
if (success) {
showSuccess('系统公告已更新');
showSuccess(t('系统公告已更新'));
if (refresh) refresh();
} else {
showError(message);
@@ -219,7 +219,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
setHasChanges(false);
} catch (error) {
console.error('系统公告更新失败', error);
showError('系统公告更新失败');
showError(t('系统公告更新失败'));
} finally {
setLoading(false);
}
@@ -261,7 +261,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
);
setAnnouncementsList(newList);
setHasChanges(true);
showSuccess('公告已删除,请及时点击“保存设置”进行保存');
showSuccess(t('公告已删除,请及时点击”保存设置”进行保存'));
}
setShowDeleteModal(false);
setDeletingAnnouncement(null);
@@ -269,7 +269,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {

const handleSaveAnnouncement = async () => {
if (!announcementForm.content || !announcementForm.publishDate) {
showError('请填写完整的公告信息');
showError(t('请填写完整的公告信息'));
return;
}

@@ -306,7 +306,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {
: '公告已添加,请及时点击“保存设置”进行保存',
);
} catch (error) {
showError('操作失败: ' + error.message);
showError(t('操作失败') + ': ' + error.message);
} finally {
setModalLoading(false);
}
@@ -371,7 +371,7 @@ const SettingsAnnouncements = ({ options, refresh }) => {

const handleBatchDelete = () => {
if (selectedRowKeys.length === 0) {
showError('请先选择要删除的系统公告');
showError(t('请先选择要删除的系统公告'));
return;
}



+ 6
- 6
web/src/pages/Setting/Dashboard/SettingsFAQ.jsx View File

@@ -140,7 +140,7 @@ const SettingsFAQ = ({ options, refresh }) => {
});
const { success, message } = res.data;
if (success) {
showSuccess('常见问答已更新');
showSuccess(t('常见问答已更新'));
if (refresh) refresh();
} else {
showError(message);
@@ -155,7 +155,7 @@ const SettingsFAQ = ({ options, refresh }) => {
setHasChanges(false);
} catch (error) {
console.error('常见问答更新失败', error);
showError('常见问答更新失败');
showError(t('常见问答更新失败'));
} finally {
setLoading(false);
}
@@ -189,7 +189,7 @@ const SettingsFAQ = ({ options, refresh }) => {
const newList = faqList.filter((item) => item.id !== deletingFaq.id);
setFaqList(newList);
setHasChanges(true);
showSuccess('问答已删除,请及时点击“保存设置”进行保存');
showSuccess(t('问答已删除,请及时点击”保存设置”进行保存'));
}
setShowDeleteModal(false);
setDeletingFaq(null);
@@ -197,7 +197,7 @@ const SettingsFAQ = ({ options, refresh }) => {

const handleSaveFaq = async () => {
if (!faqForm.question || !faqForm.answer) {
showError('请填写完整的问答信息');
showError(t('请填写完整的问答信息'));
return;
}

@@ -227,7 +227,7 @@ const SettingsFAQ = ({ options, refresh }) => {
: '问答已添加,请及时点击“保存设置”进行保存',
);
} catch (error) {
showError('操作失败: ' + error.message);
showError(t('操作失败') + ': ' + error.message);
} finally {
setModalLoading(false);
}
@@ -290,7 +290,7 @@ const SettingsFAQ = ({ options, refresh }) => {

const handleBatchDelete = () => {
if (selectedRowKeys.length === 0) {
showError('请先选择要删除的常见问答');
showError(t('请先选择要删除的常见问答'));
return;
}



+ 210
- 0
web/src/pages/Setting/Payment/SettingsPaymentGatewayAlipay.jsx View File

@@ -0,0 +1,210 @@
import React, { useEffect, useState, useRef } from 'react';
import {
Banner,
Button,
Form,
Row,
Col,
Typography,
Spin,
} from '@douyinfe/semi-ui';
const { Text } = Typography;
import {
API,
removeTrailingSlash,
showError,
showSuccess,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';

export default function SettingsPaymentGatewayAlipay(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState({
AlipayAppID: '',
AlipayPrivateKey: '',
AlipayPublicKey: '',
AlipayNotifyURL: '',
AlipayMinTopUp: 1,
AlipayUnitPrice: 7.0,
});
const [originInputs, setOriginInputs] = useState({});
const formApiRef = useRef(null);

useEffect(() => {
if (props.options && formApiRef.current) {
const currentInputs = {
AlipayAppID: props.options.AlipayAppID || '',
AlipayPrivateKey: props.options.AlipayPrivateKey || '',
AlipayPublicKey: props.options.AlipayPublicKey || '',
AlipayNotifyURL: props.options.AlipayNotifyURL || '',
AlipayMinTopUp:
props.options.AlipayMinTopUp !== undefined
? parseFloat(props.options.AlipayMinTopUp)
: 1,
AlipayUnitPrice:
props.options.AlipayUnitPrice !== undefined
? parseFloat(props.options.AlipayUnitPrice)
: 7.0,
};
setInputs(currentInputs);
setOriginInputs({ ...currentInputs });
formApiRef.current.setValues(currentInputs);
}
}, [props.options]);

const handleFormChange = (values) => {
setInputs(values);
};

const submitAlipaySetting = async () => {
if (props.options.ServerAddress === '') {
showError(t('请先填写服务器地址'));
return;
}

setLoading(true);
try {
const fields = [
'AlipayAppID',
'AlipayPrivateKey',
'AlipayPublicKey',
'AlipayNotifyURL',
];

const options = [];

for (const key of fields) {
const value = inputs[key];
if (value !== undefined && value !== originInputs[key]) {
options.push({ key, value: value || '' });
}
}

if (
inputs.AlipayUnitPrice !== undefined &&
inputs.AlipayUnitPrice !== null &&
inputs.AlipayUnitPrice !== originInputs.AlipayUnitPrice
) {
options.push({
key: 'AlipayUnitPrice',
value: inputs.AlipayUnitPrice.toString(),
});
}
if (
inputs.AlipayMinTopUp !== undefined &&
inputs.AlipayMinTopUp !== null &&
inputs.AlipayMinTopUp !== originInputs.AlipayMinTopUp
) {
options.push({
key: 'AlipayMinTopUp',
value: inputs.AlipayMinTopUp.toString(),
});
}

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);
};

return (
<Spin spinning={loading}>
<Form
initValues={inputs}
onValueChange={handleFormChange}
getFormApi={(api) => (formApiRef.current = api)}
>
<Form.Section text={t('支付宝设置')}>
<Text>
支付宝当面付(扫码支付)设置,使用 RSA2 公钥模式。请前往
<a
href='https://open.alipay.com/'
target='_blank'
rel='noreferrer'
>
支付宝开放平台
</a>
获取相关参数。
</Text>
<Banner
type='info'
description={`Webhook 填:${props.options.ServerAddress ? removeTrailingSlash(props.options.ServerAddress) : t('网站地址')}/api/alipay/pay/webhook`}
/>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='AlipayAppID'
label={t('应用 AppID')}
placeholder={t('支付宝应用 AppID')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Input
field='AlipayNotifyURL'
label={t('回调通知 URL')}
placeholder={t('支付成功后的回调地址')}
/>
</Col>
</Row>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='AlipayPrivateKey'
label={t('应用私钥(RSA2)')}
placeholder={t('应用私钥,用于请求签名')}
type='password'
rows={3}
/>
</Col>
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
<Form.TextArea
field='AlipayPublicKey'
label={t('支付宝公钥')}
placeholder={t('支付宝公钥,用于回调验签')}
type='password'
rows={3}
/>
</Col>
</Row>
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.InputNumber
field='AlipayUnitPrice'
precision={2}
label={t('充值单价(元/配额单位)')}
placeholder={t('例如:7,就是7元/配额单位')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.InputNumber
field='AlipayMinTopUp'
label={t('最低充值数量')}
placeholder={t('例如:1')}
/>
</Col>
</Row>
<Button onClick={submitAlipaySetting}>{t('更新支付宝设置')}</Button>
</Form.Section>
</Form>
</Spin>
);
}

+ 5
- 5
web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.jsx View File

@@ -210,7 +210,7 @@ export default function ModelSettingsVisualEditor(props) {
if (results.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (results.includes(undefined)) {
return showError('部分保存失败,请重试');
return showError(t('部分保存失败,请重试'));
}
}

@@ -221,11 +221,11 @@ export default function ModelSettingsVisualEditor(props) {
}
}

showSuccess('保存成功');
showSuccess(t('保存成功'));
props.refresh();
} catch (error) {
console.error('保存失败:', error);
showError('保存失败,请重试');
showError(t('保存失败,请重试'));
} finally {
setLoading(false);
}
@@ -309,7 +309,7 @@ export default function ModelSettingsVisualEditor(props) {

const updateModel = (name, field, value) => {
if (isNaN(value)) {
showError('请输入数字');
showError(t('请输入数字'));
return;
}
setModels((prev) =>
@@ -337,7 +337,7 @@ export default function ModelSettingsVisualEditor(props) {
completionTokenPrice,
) => {
if (!modelTokenPrice || modelTokenPrice === '0') {
showError('模型价格不能为0');
showError(t('模型价格不能为0'));
return '';
}
return completionTokenPrice / modelTokenPrice;


+ 11
- 10
web/src/services/secureVerification.js View File

@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/

import { API, showError } from '../helpers';
import i18next from 'i18next';
import {
prepareCredentialRequestOptions,
buildAssertionResult,
@@ -94,7 +95,7 @@ export class SecureVerificationService {
*/
static async verify2FA(code) {
if (!code?.trim()) {
throw new Error('请输入验证码或备用码');
throw new Error(i18next.t('请输入验证码或备用码'));
}

// 调用通用验证 API,验证成功后后端会设置 session
@@ -104,7 +105,7 @@ export class SecureVerificationService {
});

if (!verifyResponse.data?.success) {
throw new Error(verifyResponse.data?.message || '验证失败');
throw new Error(verifyResponse.data?.message || i18next.t('验证失败'));
}

// 验证成功,session 已在后端设置
@@ -119,7 +120,7 @@ export class SecureVerificationService {
// 开始Passkey验证
const beginResponse = await API.post('/api/user/passkey/verify/begin');
if (!beginResponse.data?.success) {
throw new Error(beginResponse.data?.message || '开始验证失败');
throw new Error(beginResponse.data?.message || i18next.t('开始验证失败'));
}

// 准备WebAuthn选项
@@ -130,7 +131,7 @@ export class SecureVerificationService {
// 执行WebAuthn验证
const credential = await navigator.credentials.get({ publicKey });
if (!credential) {
throw new Error('Passkey 验证被取消');
throw new Error(i18next.t('Passkey 验证被取消'));
}

// 构建验证结果
@@ -142,7 +143,7 @@ export class SecureVerificationService {
assertionResult,
);
if (!finishResponse.data?.success) {
throw new Error(finishResponse.data?.message || '验证失败');
throw new Error(finishResponse.data?.message || i18next.t('验证失败'));
}

// 调用通用验证 API 设置 session(Passkey 验证已完成)
@@ -151,15 +152,15 @@ export class SecureVerificationService {
});

if (!verifyResponse.data?.success) {
throw new Error(verifyResponse.data?.message || '验证失败');
throw new Error(verifyResponse.data?.message || i18next.t('验证失败'));
}

// 验证成功,session 已在后端设置
} catch (error) {
if (error.name === 'NotAllowedError') {
throw new Error('Passkey 验证被取消或超时');
throw new Error(i18next.t('Passkey 验证被取消或超时'));
} else if (error.name === 'InvalidStateError') {
throw new Error('Passkey 验证状态无效');
throw new Error(i18next.t('Passkey 验证状态无效'));
} else {
throw error;
}
@@ -179,7 +180,7 @@ export class SecureVerificationService {
case 'passkey':
return await this.verifyPasskey();
default:
throw new Error(`不支持的验证方式: ${method}`);
throw new Error(i18next.t('不支持的验证方式: {{method}}', { method }));
}
}
}
@@ -225,7 +226,7 @@ export const createApiCalls = {
response = await API.delete(url, { data });
break;
default:
throw new Error(`不支持的HTTP方法: ${method}`);
throw new Error(i18next.t('不支持的HTTP方法: {{method}}', { method }));
}
return response.data;
},


Loading…
Cancel
Save