支付宝当面付扫码支付集成 + 代码重构 Co-Authored-By: Claude <noreply@anthropic.com>master
| @@ -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) | ||||
| @@ -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") | |||||
| } | |||||
| @@ -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) | |||||
| } | } | ||||
| @@ -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": | ||||
| @@ -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,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 | ||||
| @@ -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) | ||||
| @@ -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() | |||||
| @@ -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'); | ||||
| }; | }; | ||||
| @@ -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'> | ||||
| @@ -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 | ||||
| @@ -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' | ||||
| /> | /> | ||||
| )} | )} | ||||
| @@ -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]); | ||||
| @@ -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> | ||||
| @@ -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> | ||||
| ))} | ))} | ||||
| @@ -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> | ||||
| )} | )} | ||||
| @@ -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)); | |||||
| } | } | ||||
| }); | }); | ||||
| }; | }; | ||||
| @@ -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); | ||||
| @@ -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); | ||||
| @@ -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> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -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); | ||||
| } | } | ||||
| @@ -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); | ||||
| } | } | ||||
| @@ -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'], | ||||
| @@ -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> | ||||
| ); | ); | ||||
| @@ -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' | ||||
| @@ -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) => { | ||||
| @@ -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(); | ||||
| } | } | ||||
| @@ -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> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -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} | ||||
| @@ -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'> | ||||
| @@ -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} | ||||
| @@ -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) { | ||||
| @@ -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('未知错误'))); | |||||
| } | } | ||||
| } | } | ||||
| @@ -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, | ||||
| @@ -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) }) | |||||
| : '', | : '', | ||||
| }, | }, | ||||
| )} | )} | ||||
| @@ -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) { | ||||
| @@ -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 秒后跳转 | ||||
| @@ -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, | ||||
| }); | }); | ||||
| @@ -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 }, | ||||
| ]; | ]; | ||||
| @@ -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; | ||||
| } | } | ||||
| } | } | ||||
| @@ -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; | ||||
| } | } | ||||
| @@ -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; | ||||
| } | } | ||||
| @@ -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; | ||||
| } | } | ||||
| @@ -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> | |||||
| ); | |||||
| } | |||||
| @@ -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; | ||||
| @@ -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; | ||||
| }, | }, | ||||