Просмотр исходного кода

merge: feat/alipay-payment → master

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

Co-Authored-By: Claude <noreply@anthropic.com>
master
fengsilin 3 недель назад
Родитель
Сommit
156618fdad
53 измененных файлов: 22167 добавлений и 20862 удалений
  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 Просмотреть файл

@@ -72,17 +72,38 @@ func GetTopUpInfo(c *gin.Context) {
payMethods = append(payMethods, wechatMethod) 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{ data := gin.H{
"enable_online_topup": enableOnlineTopup, "enable_online_topup": enableOnlineTopup,
"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 != "[]",
"enable_wechat_topup": setting.IsWechatPayConfigured(), "enable_wechat_topup": setting.IsWechatPayConfigured(),
"enable_alipay_topup": setting.IsAlipayConfigured(),
"creem_products": setting.CreemProducts, "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,
"wechat_pay_min_topup": setting.WechatPayMinTopUp, "wechat_pay_min_topup": setting.WechatPayMinTopUp,
"alipay_pay_min_topup": setting.AlipayMinTopUp,
"amount_options": operation_setting.GetPaymentSetting().AmountOptions, "amount_options": operation_setting.GetPaymentSetting().AmountOptions,
"discount": operation_setting.GetPaymentSetting().AmountDiscount, "discount": operation_setting.GetPaymentSetting().AmountDiscount,
} }
@@ -143,15 +164,37 @@ func getPayMoney(amount int64, group string) float64 {
} }


func getMinTopup() int64 { 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 { 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) 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) { func RequestEpay(c *gin.Context) {
var req EpayRequest var req EpayRequest
err := c.ShouldBindJSON(&req) err := c.ShouldBindJSON(&req)


+ 280
- 0
controller/topup_alipay.go Просмотреть файл

@@ -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 Просмотреть файл

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


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


// getWechatPayMoney 计算微信支付应付金额(元)
func getWechatPayMoney(amount float64, group string) float64 { 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 { 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 Просмотреть файл

@@ -101,6 +101,12 @@ func InitOptionMap() {
common.OptionMap["WechatPayPubKeyB64"] = setting.WechatPayPubKeyB64 common.OptionMap["WechatPayPubKeyB64"] = setting.WechatPayPubKeyB64
common.OptionMap["WechatPayMinTopUp"] = strconv.Itoa(setting.WechatPayMinTopUp) common.OptionMap["WechatPayMinTopUp"] = strconv.Itoa(setting.WechatPayMinTopUp)
common.OptionMap["WechatPayUnitPrice"] = strconv.FormatFloat(setting.WechatPayUnitPrice, 'f', -1, 64) 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["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["Chats"] = setting.Chats2JsonString() common.OptionMap["Chats"] = setting.Chats2JsonString()
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString() common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
@@ -178,6 +184,13 @@ func triggerWechatPayReset() {
} }
} }


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

func loadOptionsFromDatabase() { func loadOptionsFromDatabase() {
options, _ := AllOption() options, _ := AllOption()
for _, option := range options { for _, option := range options {
@@ -410,6 +423,21 @@ func updateOptionMap(key string, value string) (err error) {
setting.WechatPayMinTopUp, _ = strconv.Atoi(value) setting.WechatPayMinTopUp, _ = strconv.Atoi(value)
case "WechatPayUnitPrice": case "WechatPayUnitPrice":
setting.WechatPayUnitPrice, _ = strconv.ParseFloat(value, 64) 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": case "TopupGroupRatio":
err = common.UpdateTopupGroupRatioByJSONString(value) err = common.UpdateTopupGroupRatioByJSONString(value)
case "GitHubClientId": case "GitHubClientId":


+ 32
- 1
model/topup.go Просмотреть файл

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


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


"github.com/shopspring/decimal" "github.com/shopspring/decimal"
@@ -21,6 +22,32 @@ type TopUp struct {
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"`
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 { func (topUp *TopUp) Insert() error {
@@ -135,6 +162,7 @@ func GetUserTopUps(userId int, pageInfo *common.PageInfo) (topups []*TopUp, tota
return nil, 0, err return nil, 0, err
} }


fillTopUpEmails(topups)
return topups, total, nil return topups, total, nil
} }


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


fillTopUpEmails(topups)
return topups, total, nil 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 { if err = tx.Commit().Error; err != nil {
return nil, 0, err return nil, 0, err
} }
fillTopUpEmails(topups)
return topups, total, nil return topups, total, nil
} }


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


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


+ 12
- 6
model/topup_wechat.go Просмотреть файл

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


// RechargeWechat 微信支付充值完成(由回调触发) // RechargeWechat 微信支付充值完成(由回调触发)
// 与 Recharge/RechargeCreem 类似,使用事务+行锁保证幂等
func RechargeWechat(tradeNo string) error { 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 == "" { if tradeNo == "" {
return errors.New("未提供支付单号") return errors.New("未提供支付单号")
} }
@@ -34,7 +44,6 @@ func RechargeWechat(tradeNo string) error {
} }


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


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


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


if quotaToAdd > 0 { 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 return nil


+ 4
- 0
router/api-router.go Просмотреть файл

@@ -49,6 +49,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("/wechat/pay/webhook", controller.WechatPayWebhook) apiRouter.POST("/wechat/pay/webhook", controller.WechatPayWebhook)
apiRouter.POST("/alipay/pay/webhook", controller.AlipayPayWebhook)
// 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)


@@ -93,6 +94,9 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.POST("/wechat/pay/amount", controller.RequestWechatPayAmount) selfRoute.POST("/wechat/pay/amount", controller.RequestWechatPayAmount)
selfRoute.POST("/wechat/pay", controller.RequestWechatPay) selfRoute.POST("/wechat/pay", controller.RequestWechatPay)
selfRoute.GET("/wechat/pay/status", controller.WechatPayStatus) 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.POST("/aff_transfer", controller.TransferAffQuota)
selfRoute.PUT("/setting", controller.UpdateUserSetting) selfRoute.PUT("/setting", controller.UpdateUserSetting)




+ 18
- 0
setting/payment_alipay.go Просмотреть файл

@@ -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 Просмотреть файл

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




+ 3
- 3
web/src/components/auth/RegisterForm.jsx Просмотреть файл

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


const onSubmitWeChatVerificationCode = async () => { const onSubmitWeChatVerificationCode = async () => {
if (turnstileEnabled && turnstileToken === '') { if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
return; return;
} }
setWechatCodeSubmitLoading(true); setWechatCodeSubmitLoading(true);
@@ -257,7 +257,7 @@ const RegisterForm = () => {
const sendVerificationCode = async () => { const sendVerificationCode = async () => {
if (inputs.email === '') return; if (inputs.email === '') return;
if (turnstileEnabled && turnstileToken === '') { if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
return; return;
} }
setVerificationCodeLoading(true); setVerificationCodeLoading(true);
@@ -745,7 +745,7 @@ const RegisterForm = () => {
}} }}
> >
<div className='flex flex-col items-center'> <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>


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


+ 3
- 2
web/src/components/common/markdown/MarkdownRenderer.jsx Просмотреть файл

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


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


+ 1
- 1
web/src/components/common/ui/JSONEditor.jsx Просмотреть файл

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


+ 1
- 1
web/src/components/playground/CodeViewer.jsx Просмотреть файл

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




+ 1
- 1
web/src/components/playground/DebugPanel.jsx Просмотреть файл

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


+ 2
- 2
web/src/components/playground/MessageContent.jsx Просмотреть файл

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


+ 2
- 2
web/src/components/playground/ThinkingContent.jsx Просмотреть файл

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


+ 5
- 4
web/src/components/playground/configStorage.js Просмотреть файл

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


const MESSAGES_STORAGE_KEY = 'playground_messages'; const MESSAGES_STORAGE_KEY = 'playground_messages';


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


resolve(importedConfig); resolve(importedConfig);
} else { } else {
reject(new Error('配置文件格式无效'));
reject(new Error(i18next.t('配置文件格式无效')));
} }
} catch (parseError) { } 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); reader.readAsText(file);
} catch (error) { } catch (error) {
reject(new Error('导入配置失败: ' + error.message));
reject(new Error(i18next.t('导入配置失败: ') + error.message));
} }
}); });
}; };

+ 1
- 1
web/src/components/settings/ModelDeploymentSetting.jsx Просмотреть файл

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


+ 1
- 1
web/src/components/settings/ModelSetting.jsx Просмотреть файл

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


+ 6
- 0
web/src/components/settings/PaymentSetting.jsx Просмотреть файл

@@ -24,6 +24,7 @@ import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentG
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 SettingsPaymentGatewayWechat from '../../pages/Setting/Payment/SettingsPaymentGatewayWechat'; import SettingsPaymentGatewayWechat from '../../pages/Setting/Payment/SettingsPaymentGatewayWechat';
import SettingsPaymentGatewayAlipay from '../../pages/Setting/Payment/SettingsPaymentGatewayAlipay';
import { API, showError, toBoolean } from '../../helpers'; import { API, showError, toBoolean } from '../../helpers';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';


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


+ 1
- 1
web/src/components/settings/RateLimitSetting.jsx Просмотреть файл

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


+ 1
- 1
web/src/components/settings/RatioSetting.jsx Просмотреть файл

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


+ 1
- 1
web/src/components/settings/personal/cards/NotificationSettings.jsx Просмотреть файл

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


+ 1
- 1
web/src/components/table/channels/ChannelsColumnDefs.jsx Просмотреть файл

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


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




+ 1
- 1
web/src/components/table/channels/ChannelsTabs.jsx Просмотреть файл

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


+ 4
- 4
web/src/components/table/channels/modals/EditChannelModal.jsx Просмотреть файл

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


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


const renderChannelOption = (renderProps) => { const renderChannelOption = (renderProps) => {


+ 2
- 2
web/src/components/table/channels/modals/EditTagModal.jsx Просмотреть файл

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


+ 1
- 1
web/src/components/table/task-logs/TaskLogsColumnDefs.jsx Просмотреть файл

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


web/src/components/topup/WechatPayQRCodeModal.jsx → web/src/components/topup/QRCodePayModal.jsx Просмотреть файл

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


const POLL_INTERVAL = 2000; 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 { t } = useTranslation();
const [status, setStatus] = useState('pending'); const [status, setStatus] = useState('pending');
const [countdown, setCountdown] = useState(300); const [countdown, setCountdown] = useState(300);
@@ -18,7 +18,7 @@ export default function WechatPayQRCodeModal({ visible, qrCodeUrl, tradeNo, onCl
const pollStatus = useCallback(async () => { const pollStatus = useCallback(async () => {
if (!tradeNo) return; if (!tradeNo) return;
try { 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; const { data } = res.data;
if (data?.status === 'success') { if (data?.status === 'success') {
setStatus('success'); setStatus('success');
@@ -35,7 +35,7 @@ export default function WechatPayQRCodeModal({ visible, qrCodeUrl, tradeNo, onCl
} catch (e) { } catch (e) {
// 忽略轮询错误 // 忽略轮询错误
} }
}, [tradeNo, onSuccess]);
}, [tradeNo, onSuccess, statusApiPath]);


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


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

+ 8
- 5
web/src/components/topup/RechargeCard.jsx Просмотреть файл

@@ -58,6 +58,7 @@ const RechargeCard = ({
enableStripeTopUp, enableStripeTopUp,
enableCreemTopUp, enableCreemTopUp,
enableWechatTopUp, enableWechatTopUp,
enableAlipayTopUp,
creemProducts, creemProducts,
creemPreTopUp, creemPreTopUp,
presetAmounts, presetAmounts,
@@ -225,19 +226,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 || enableWechatTopUp ? (
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp || enableWechatTopUp || enableAlipayTopUp ? (
<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 || enableWechatTopUp) && (
{(enableOnlineTopUp || enableStripeTopUp || enableWechatTopUp || enableAlipayTopUp) && (
<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 && !enableWechatTopUp}
disabled={!enableOnlineTopUp && !enableStripeTopUp && !enableWechatTopUp && !enableAlipayTopUp}
placeholder={ placeholder={
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp) t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
} }
@@ -297,11 +298,13 @@ const RechargeCard = ({
const minTopupVal = Number(payMethod.min_topup) || 0; const minTopupVal = Number(payMethod.min_topup) || 0;
const isStripe = payMethod.type === 'stripe'; const isStripe = payMethod.type === 'stripe';
const isWechatPay = payMethod.type === 'wechat_pay'; const isWechatPay = payMethod.type === 'wechat_pay';
const isEpay = !isStripe && !isWechatPay;
const isAlipay = payMethod.type === 'alipay';
const isEpay = !isStripe && !isWechatPay && !isAlipay;
const disabled = const disabled =
(!enableOnlineTopUp && isEpay) || (!enableOnlineTopUp && isEpay) ||
(!enableStripeTopUp && isStripe) || (!enableStripeTopUp && isStripe) ||
(!enableWechatTopUp && isWechatPay) || (!enableWechatTopUp && isWechatPay) ||
(!enableAlipayTopUp && isAlipay) ||
minTopupVal > Number(topUpCount || 0); minTopupVal > Number(topUpCount || 0);


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


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


+ 65
- 6
web/src/components/topup/index.jsx Просмотреть файл

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


const TopUp = () => { const TopUp = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -94,6 +94,12 @@ const TopUp = () => {
const [wechatPayTradeNo, setWechatPayTradeNo] = useState(''); const [wechatPayTradeNo, setWechatPayTradeNo] = useState('');
const [enableWechatTopUp, setEnableWechatTopUp] = useState(false); 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 [subscriptionPlans, setSubscriptionPlans] = useState([]);
const [subscriptionLoading, setSubscriptionLoading] = useState(true); const [subscriptionLoading, setSubscriptionLoading] = useState(true);
@@ -167,6 +173,11 @@ const TopUp = () => {
showError(t('管理员未开启微信支付充值!')); showError(t('管理员未开启微信支付充值!'));
return; return;
} }
} else if (payment === 'alipay') {
if (!enableAlipayTopUp) {
showError(t('管理员未开启支付宝充值!'));
return;
}
} else { } else {
if (!enableOnlineTopUp) { if (!enableOnlineTopUp) {
showError(t('管理员未开启在线充值!')); showError(t('管理员未开启在线充值!'));
@@ -200,6 +211,30 @@ const TopUp = () => {
return; 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); setPaymentLoading(true);
try { try {
if (payment === 'stripe') { if (payment === 'stripe') {
@@ -483,17 +518,21 @@ const TopUp = () => {
const enableOnlineTopUp = data.enable_online_topup || false; const enableOnlineTopUp = data.enable_online_topup || false;
const enableCreemTopUp = data.enable_creem_topup || false; const enableCreemTopUp = data.enable_creem_topup || false;
const enableWechatTopUpVal = data.enable_wechat_topup || false; const enableWechatTopUpVal = data.enable_wechat_topup || false;
const enableAlipayTopUpVal = data.enable_alipay_topup || false;
const minTopUpValue = enableOnlineTopUp const minTopUpValue = enableOnlineTopUp
? data.min_topup ? data.min_topup
: enableStripeTopUp : enableStripeTopUp
? data.stripe_min_topup ? data.stripe_min_topup
: enableWechatTopUpVal : enableWechatTopUpVal
? data.wechat_pay_min_topup || 1 ? data.wechat_pay_min_topup || 1
: 1;
: enableAlipayTopUpVal
? data.alipay_pay_min_topup || 1
: 1;
setEnableOnlineTopUp(enableOnlineTopUp); setEnableOnlineTopUp(enableOnlineTopUp);
setEnableStripeTopUp(enableStripeTopUp); setEnableStripeTopUp(enableStripeTopUp);
setEnableCreemTopUp(enableCreemTopUp); setEnableCreemTopUp(enableCreemTopUp);
setEnableWechatTopUp(enableWechatTopUpVal); setEnableWechatTopUp(enableWechatTopUpVal);
setEnableAlipayTopUp(enableAlipayTopUpVal);
setMinTopUp(minTopUpValue); setMinTopUp(minTopUpValue);
setTopUpCount(minTopUpValue); setTopUpCount(minTopUpValue);


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


{/* 微信支付二维码弹窗 */} {/* 微信支付二维码弹窗 */}
<WechatPayQRCodeModal
<QRCodePayModal
visible={wechatPayVisible} visible={wechatPayVisible}
qrCodeUrl={wechatPayQRCodeUrl} qrCodeUrl={wechatPayQRCodeUrl}
tradeNo={wechatPayTradeNo} tradeNo={wechatPayTradeNo}
@@ -753,8 +792,27 @@ const TopUp = () => {
onSuccess={() => { onSuccess={() => {
setWechatPayVisible(false); setWechatPayVisible(false);
showSuccess(t('充值成功!')); 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 充值确认模态框 */} {/* Creem 充值确认模态框 */}
@@ -793,6 +851,7 @@ const TopUp = () => {
enableStripeTopUp={enableStripeTopUp} enableStripeTopUp={enableStripeTopUp}
enableCreemTopUp={enableCreemTopUp} enableCreemTopUp={enableCreemTopUp}
enableWechatTopUp={enableWechatTopUp} enableWechatTopUp={enableWechatTopUp}
enableAlipayTopUp={enableAlipayTopUp}
creemProducts={creemProducts} creemProducts={creemProducts}
creemPreTopUp={creemPreTopUp} creemPreTopUp={creemPreTopUp}
presetAmounts={presetAmounts} presetAmounts={presetAmounts}


+ 13
- 1
web/src/components/topup/modals/TopupHistoryModal.jsx Просмотреть файл

@@ -168,6 +168,18 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
key: 'trade_no', key: 'trade_no',
render: (text) => <Text copyable>{text}</Text>, 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('支付方式'), title: t('支付方式'),
dataIndex: 'payment_method', dataIndex: 'payment_method',
@@ -206,7 +218,7 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
key: 'status', key: 'status',
render: renderStatusBadge, render: renderStatusBadge,
}, },
];
);


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


+ 7
- 6
web/src/helpers/api.js Просмотреть файл

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


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


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


return errorInfo; return errorInfo;
@@ -197,7 +198,7 @@ export const processGroupsData = (data, userGroup) => {
if (groupOptions.length === 0) { if (groupOptions.length === 0) {
groupOptions = [ groupOptions = [
{ {
label: '用户分组',
label: i18next.t('用户分组'),
value: '', value: '',
ratio: 1, ratio: 1,
}, },
@@ -325,7 +326,7 @@ export async function onCustomOAuthClicked(provider, options = {}) {
provider.authorization_endpoint, provider.authorization_endpoint,
); );
showError( showError(
'OAuth 配置错误:授权端点必须是完整的 URL(以 http:// 或 https:// 开头)',
i18next.t('OAuth 配置错误:授权端点必须是完整的 URL(以 http:// 或 https:// 开头)'),
); );
return; return;
} }
@@ -342,7 +343,7 @@ export async function onCustomOAuthClicked(provider, options = {}) {
window.open(authUrl.toString()); window.open(authUrl.toString());
} catch (error) { } catch (error) {
console.error('Failed to initiate custom OAuth:', 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 Просмотреть файл

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


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

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


+ 1
- 1
web/src/helpers/render.jsx Просмотреть файл

@@ -1373,7 +1373,7 @@ export function renderModelPrice(
symbol: symbol, symbol: symbol,
price: (inputRatioPrice * rate).toFixed(6), price: (inputRatioPrice * rate).toFixed(6),
audioPrice: audioInputSeperatePrice 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 Просмотреть файл

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


+ 2
- 1
web/src/hooks/chat/useTokenKeys.js Просмотреть файл

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


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


+ 1
- 1
web/src/hooks/dashboard/useDashboardData.js Просмотреть файл

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


+ 3082
- 2993
web/src/i18n/locales/en.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 2917
- 2828
web/src/i18n/locales/fr.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 2900
- 2811
web/src/i18n/locales/ja.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 2930
- 2841
web/src/i18n/locales/ru.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 3469
- 3383
web/src/i18n/locales/vi.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 3059
- 2970
web/src/i18n/locales/zh-CN.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 2985
- 2896
web/src/i18n/locales/zh-TW.json
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 7
- 7
web/src/pages/Home/components/PartnersSection.jsx Просмотреть файл

@@ -52,22 +52,22 @@ const PartnersSection = () => {
{ icon: Moonshot, name: 'Moonshot AI', size: 40 }, { icon: Moonshot, name: 'Moonshot AI', size: 40 },
{ icon: OpenAI, name: 'OpenAI', size: 40 }, { icon: OpenAI, name: 'OpenAI', size: 40 },
{ icon: XAI, name: 'Grok', 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: Cohere, name: 'Cohere', size: 40 },
{ icon: Claude, name: 'Claude', size: 40 }, { icon: Claude, name: 'Claude', size: 40 },
{ icon: Gemini, name: 'Gemini', size: 40 }, { icon: Gemini, name: 'Gemini', size: 40 },
{ icon: Suno, name: 'Suno', size: 40 }, { icon: Suno, name: 'Suno', size: 40 },
{ icon: Minimax, name: 'Minimax', 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: DeepSeek, name: 'DeepSeek', size: 40 },
{ icon: Qwen, name: '通义千问', size: 40 },
{ icon: Qwen, name: t('通义千问'), size: 40 },
{ icon: Midjourney, name: 'Midjourney', size: 40 }, { icon: Midjourney, name: 'Midjourney', size: 40 },
{ icon: Grok, name: 'Grok', size: 40 }, { icon: Grok, name: 'Grok', size: 40 },
{ icon: AzureAI, name: 'Azure AI', 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 }, { icon: Xinference, name: 'Xinference', size: 40 },
]; ];




+ 2
- 2
web/src/pages/Playground/index.jsx Просмотреть файл

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


+ 6
- 6
web/src/pages/Setting/Dashboard/SettingsAPIInfo.jsx Просмотреть файл

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


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


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


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




+ 6
- 6
web/src/pages/Setting/Dashboard/SettingsAnnouncements.jsx Просмотреть файл

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


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


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


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




+ 6
- 6
web/src/pages/Setting/Dashboard/SettingsFAQ.jsx Просмотреть файл

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


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


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


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




+ 210
- 0
web/src/pages/Setting/Payment/SettingsPaymentGatewayAlipay.jsx Просмотреть файл

@@ -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 Просмотреть файл

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


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


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


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


+ 11
- 10
web/src/services/secureVerification.js Просмотреть файл

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


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


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


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


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


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


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


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


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


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


Загрузка…
Отмена
Сохранить