diff --git a/controller/channel.go b/controller/channel.go
index 480d5b4f..5d075f3c 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -8,6 +8,7 @@ import (
"one-api/constant"
"one-api/dto"
"one-api/model"
+ "one-api/service"
"strconv"
"strings"
@@ -633,6 +634,7 @@ func AddChannel(c *gin.Context) {
common.ApiError(c, err)
return
}
+ service.ResetProxyClientCache()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -894,6 +896,7 @@ func UpdateChannel(c *gin.Context) {
return
}
model.InitChannelCache()
+ service.ResetProxyClientCache()
channel.Key = ""
clearChannelInfo(&channel.Channel)
c.JSON(http.StatusOK, gin.H{
diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go
index ccde91db..9a568d85 100644
--- a/controller/topup_stripe.go
+++ b/controller/topup_stripe.go
@@ -225,7 +225,8 @@ func genStripeLink(referenceId string, customerId string, email string, amount i
Quantity: stripe.Int64(amount),
},
},
- Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
+ Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
+ AllowPromotionCodes: stripe.Bool(setting.StripePromotionCodesEnabled),
}
if "" == customerId {
diff --git a/dto/gemini.go b/dto/gemini.go
index bc05c6aa..80552aad 100644
--- a/dto/gemini.go
+++ b/dto/gemini.go
@@ -251,6 +251,7 @@ type GeminiChatTool struct {
GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"`
CodeExecution any `json:"codeExecution,omitempty"`
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
+ URLContext any `json:"urlContext,omitempty"`
}
type GeminiChatGenerationConfig struct {
diff --git a/main.go b/main.go
index b1421f9e..e12dddc5 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "bytes"
"embed"
"fmt"
"log"
@@ -16,6 +17,7 @@ import (
"one-api/setting/ratio_setting"
"os"
"strconv"
+ "strings"
"time"
"github.com/bytedance/gopkg/util/gopool"
@@ -147,6 +149,22 @@ func main() {
})
server.Use(sessions.Sessions("session", store))
+ analyticsInjectBuilder := &strings.Builder{}
+ if os.Getenv("UMAMI_WEBSITE_ID") != "" {
+ umamiSiteID := os.Getenv("UMAMI_WEBSITE_ID")
+ umamiScriptURL := os.Getenv("UMAMI_SCRIPT_URL")
+ if umamiScriptURL == "" {
+ umamiScriptURL = "https://analytics.umami.is/script.js"
+ }
+ analyticsInjectBuilder.WriteString("")
+ }
+ analyticsInject := analyticsInjectBuilder.String()
+ indexPage = bytes.ReplaceAll(indexPage, []byte("\n"), []byte(analyticsInject))
+
router.SetRouter(server, buildFS, indexPage)
var port = os.Getenv("PORT")
if port == "" {
diff --git a/model/option.go b/model/option.go
index ceecff65..9ace8fec 100644
--- a/model/option.go
+++ b/model/option.go
@@ -82,6 +82,7 @@ func InitOptionMap() {
common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret
common.OptionMap["StripePriceId"] = setting.StripePriceId
common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64)
+ common.OptionMap["StripePromotionCodesEnabled"] = strconv.FormatBool(setting.StripePromotionCodesEnabled)
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["Chats"] = setting.Chats2JsonString()
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
@@ -330,6 +331,8 @@ func updateOptionMap(key string, value string) (err error) {
setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64)
case "StripeMinTopUp":
setting.StripeMinTopUp, _ = strconv.Atoi(value)
+ case "StripePromotionCodesEnabled":
+ setting.StripePromotionCodesEnabled = value == "true"
case "TopupGroupRatio":
err = common.UpdateTopupGroupRatioByJSONString(value)
case "GitHubClientId":
diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go
index 199c8466..c8e9c757 100644
--- a/relay/channel/gemini/relay-gemini.go
+++ b/relay/channel/gemini/relay-gemini.go
@@ -245,6 +245,7 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
functions := make([]dto.FunctionRequest, 0, len(textRequest.Tools))
googleSearch := false
codeExecution := false
+ urlContext := false
for _, tool := range textRequest.Tools {
if tool.Function.Name == "googleSearch" {
googleSearch = true
@@ -254,6 +255,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
codeExecution = true
continue
}
+ if tool.Function.Name == "urlContext" {
+ urlContext = true
+ continue
+ }
if tool.Function.Parameters != nil {
params, ok := tool.Function.Parameters.(map[string]interface{})
@@ -281,6 +286,11 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
GoogleSearch: make(map[string]string),
})
}
+ if urlContext {
+ geminiTools = append(geminiTools, dto.GeminiChatTool{
+ URLContext: make(map[string]string),
+ })
+ }
if len(functions) > 0 {
geminiTools = append(geminiTools, dto.GeminiChatTool{
FunctionDeclarations: functions,
diff --git a/service/http_client.go b/service/http_client.go
index b191ddd7..d8fcfae0 100644
--- a/service/http_client.go
+++ b/service/http_client.go
@@ -7,12 +7,17 @@ import (
"net/http"
"net/url"
"one-api/common"
+ "sync"
"time"
"golang.org/x/net/proxy"
)
-var httpClient *http.Client
+var (
+ httpClient *http.Client
+ proxyClientLock sync.Mutex
+ proxyClients = make(map[string]*http.Client)
+)
func InitHttpClient() {
if common.RelayTimeout == 0 {
@@ -28,12 +33,31 @@ func GetHttpClient() *http.Client {
return httpClient
}
+// ResetProxyClientCache 清空代理客户端缓存,确保下次使用时重新初始化
+func ResetProxyClientCache() {
+ proxyClientLock.Lock()
+ defer proxyClientLock.Unlock()
+ for _, client := range proxyClients {
+ if transport, ok := client.Transport.(*http.Transport); ok && transport != nil {
+ transport.CloseIdleConnections()
+ }
+ }
+ proxyClients = make(map[string]*http.Client)
+}
+
// NewProxyHttpClient 创建支持代理的 HTTP 客户端
func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
if proxyURL == "" {
return http.DefaultClient, nil
}
+ proxyClientLock.Lock()
+ if client, ok := proxyClients[proxyURL]; ok {
+ proxyClientLock.Unlock()
+ return client, nil
+ }
+ proxyClientLock.Unlock()
+
parsedURL, err := url.Parse(proxyURL)
if err != nil {
return nil, err
@@ -41,11 +65,16 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
switch parsedURL.Scheme {
case "http", "https":
- return &http.Client{
+ client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(parsedURL),
},
- }, nil
+ }
+ client.Timeout = time.Duration(common.RelayTimeout) * time.Second
+ proxyClientLock.Lock()
+ proxyClients[proxyURL] = client
+ proxyClientLock.Unlock()
+ return client, nil
case "socks5", "socks5h":
// 获取认证信息
@@ -67,13 +96,18 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
return nil, err
}
- return &http.Client{
+ client := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
},
- }, nil
+ }
+ client.Timeout = time.Duration(common.RelayTimeout) * time.Second
+ proxyClientLock.Lock()
+ proxyClients[proxyURL] = client
+ proxyClientLock.Unlock()
+ return client, nil
default:
return nil, fmt.Errorf("unsupported proxy scheme: %s", parsedURL.Scheme)
diff --git a/setting/payment_stripe.go b/setting/payment_stripe.go
index 80d877df..d97120c8 100644
--- a/setting/payment_stripe.go
+++ b/setting/payment_stripe.go
@@ -5,3 +5,4 @@ var StripeWebhookSecret = ""
var StripePriceId = ""
var StripeUnitPrice = 8.0
var StripeMinTopUp = 1
+var StripePromotionCodesEnabled = false
diff --git a/web/index.html b/web/index.html
index 09d87ae1..df6b0e39 100644
--- a/web/index.html
+++ b/web/index.html
@@ -10,6 +10,7 @@
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
/>
New API
+
diff --git a/web/src/components/settings/PaymentSetting.jsx b/web/src/components/settings/PaymentSetting.jsx
index faaa9561..220c8664 100644
--- a/web/src/components/settings/PaymentSetting.jsx
+++ b/web/src/components/settings/PaymentSetting.jsx
@@ -45,6 +45,7 @@ const PaymentSetting = () => {
StripePriceId: '',
StripeUnitPrice: 8.0,
StripeMinTopUp: 1,
+ StripePromotionCodesEnabled: false,
});
let [loading, setLoading] = useState(false);
diff --git a/web/src/components/settings/PersonalSetting.jsx b/web/src/components/settings/PersonalSetting.jsx
index 3ba8dcfd..15dfbd97 100644
--- a/web/src/components/settings/PersonalSetting.jsx
+++ b/web/src/components/settings/PersonalSetting.jsx
@@ -19,7 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import React, { useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import { API, copy, showError, showInfo, showSuccess } from '../../helpers';
+import {
+ API,
+ copy,
+ showError,
+ showInfo,
+ showSuccess,
+ setStatusData,
+} from '../../helpers';
import { UserContext } from '../../context/User';
import { Modal } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
@@ -71,18 +78,40 @@ const PersonalSetting = () => {
});
useEffect(() => {
- let status = localStorage.getItem('status');
- if (status) {
- status = JSON.parse(status);
- setStatus(status);
- if (status.turnstile_check) {
+ let saved = localStorage.getItem('status');
+ if (saved) {
+ const parsed = JSON.parse(saved);
+ setStatus(parsed);
+ if (parsed.turnstile_check) {
setTurnstileEnabled(true);
- setTurnstileSiteKey(status.turnstile_site_key);
+ setTurnstileSiteKey(parsed.turnstile_site_key);
+ } else {
+ setTurnstileEnabled(false);
+ setTurnstileSiteKey('');
}
}
- getUserData().then((res) => {
- console.log(userState);
- });
+ // Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth)
+ (async () => {
+ try {
+ const res = await API.get('/api/status');
+ const { success, data } = res.data;
+ if (success && data) {
+ setStatus(data);
+ setStatusData(data);
+ if (data.turnstile_check) {
+ setTurnstileEnabled(true);
+ setTurnstileSiteKey(data.turnstile_site_key);
+ } else {
+ setTurnstileEnabled(false);
+ setTurnstileSiteKey('');
+ }
+ }
+ } catch (e) {
+ // ignore and keep local status
+ }
+ })();
+
+ getUserData();
}, []);
useEffect(() => {
diff --git a/web/src/components/settings/personal/cards/AccountManagement.jsx b/web/src/components/settings/personal/cards/AccountManagement.jsx
index 515a5c19..017e7c1e 100644
--- a/web/src/components/settings/personal/cards/AccountManagement.jsx
+++ b/web/src/components/settings/personal/cards/AccountManagement.jsx
@@ -28,6 +28,7 @@ import {
Tabs,
TabPane,
Popover,
+ Modal,
} from '@douyinfe/semi-ui';
import {
IconMail,
@@ -83,6 +84,9 @@ const AccountManagement = ({
);
};
+ const isBound = (accountId) => Boolean(accountId);
+ const [showTelegramBindModal, setShowTelegramBindModal] = React.useState(false);
+
return (
{/* 卡片头部 */}
@@ -142,7 +146,7 @@ const AccountManagement = ({
size='small'
onClick={() => setShowEmailBindModal(true)}
>
- {userState.user && userState.user.email !== ''
+ {isBound(userState.user?.email)
? t('修改绑定')
: t('绑定')}
@@ -165,9 +169,11 @@ const AccountManagement = ({
{t('微信')}
- {userState.user && userState.user.wechat_id !== ''
- ? t('已绑定')
- : t('未绑定')}
+ {!status.wechat_login
+ ? t('未启用')
+ : isBound(userState.user?.wechat_id)
+ ? t('已绑定')
+ : t('未绑定')}
@@ -179,7 +185,7 @@ const AccountManagement = ({
disabled={!status.wechat_login}
onClick={() => setShowWeChatBindModal(true)}
>
- {userState.user && userState.user.wechat_id !== ''
+ {isBound(userState.user?.wechat_id)
? t('修改绑定')
: status.wechat_login
? t('绑定')
@@ -220,8 +226,7 @@ const AccountManagement = ({
onGitHubOAuthClicked(status.github_client_id)
}
disabled={
- (userState.user && userState.user.github_id !== '') ||
- !status.github_oauth
+ isBound(userState.user?.github_id) || !status.github_oauth
}
>
{status.github_oauth ? t('绑定') : t('未启用')}
@@ -264,8 +269,7 @@ const AccountManagement = ({
)
}
disabled={
- (userState.user && userState.user.oidc_id !== '') ||
- !status.oidc_enabled
+ isBound(userState.user?.oidc_id) || !status.oidc_enabled
}
>
{status.oidc_enabled ? t('绑定') : t('未启用')}
@@ -298,26 +302,56 @@ const AccountManagement = ({
{status.telegram_oauth ? (
- userState.user.telegram_id !== '' ? (
-
+ setShowTelegramBindModal(false)}
+ footer={null}
+ >
+
+ {t('点击下方按钮通过 Telegram 完成绑定')}
+
+
+
{/* LinuxDO绑定 */}
@@ -350,8 +384,7 @@ const AccountManagement = ({
onLinuxDOOAuthClicked(status.linuxdo_client_id)
}
disabled={
- (userState.user && userState.user.linux_do_id !== '') ||
- !status.linuxdo_oauth
+ isBound(userState.user?.linux_do_id) || !status.linuxdo_oauth
}
>
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx
index 25ef68c6..2eb480e7 100644
--- a/web/src/components/table/channels/modals/EditChannelModal.jsx
+++ b/web/src/components/table/channels/modals/EditChannelModal.jsx
@@ -455,6 +455,14 @@ const EditChannelModal = (props) => {
data.is_enterprise_account = false;
}
+ if (
+ data.type === 45 &&
+ (!data.base_url ||
+ (typeof data.base_url === 'string' && data.base_url.trim() === ''))
+ ) {
+ data.base_url = 'https://ark.cn-beijing.volces.com';
+ }
+
setInputs(data);
if (formApiRef.current) {
formApiRef.current.setValues(data);
@@ -837,7 +845,9 @@ const EditChannelModal = (props) => {
delete localInputs.key;
}
} else {
- localInputs.key = batch ? JSON.stringify(keys) : JSON.stringify(keys[0]);
+ localInputs.key = batch
+ ? JSON.stringify(keys)
+ : JSON.stringify(keys[0]);
}
}
}
@@ -954,6 +964,56 @@ const EditChannelModal = (props) => {
}
};
+ // 密钥去重函数
+ const deduplicateKeys = () => {
+ const currentKey = formApiRef.current?.getValue('key') || inputs.key || '';
+
+ if (!currentKey.trim()) {
+ showInfo(t('请先输入密钥'));
+ return;
+ }
+
+ // 按行分割密钥
+ const keyLines = currentKey.split('\n');
+ const beforeCount = keyLines.length;
+
+ // 使用哈希表去重,保持原有顺序
+ const keySet = new Set();
+ const deduplicatedKeys = [];
+
+ keyLines.forEach((line) => {
+ const trimmedLine = line.trim();
+ if (trimmedLine && !keySet.has(trimmedLine)) {
+ keySet.add(trimmedLine);
+ deduplicatedKeys.push(trimmedLine);
+ }
+ });
+
+ const afterCount = deduplicatedKeys.length;
+ const deduplicatedKeyText = deduplicatedKeys.join('\n');
+
+ // 更新表单和状态
+ if (formApiRef.current) {
+ formApiRef.current.setValue('key', deduplicatedKeyText);
+ }
+ handleInputChange('key', deduplicatedKeyText);
+
+ // 显示去重结果
+ const message = t(
+ '去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥',
+ {
+ before: beforeCount,
+ after: afterCount,
+ },
+ );
+
+ if (beforeCount === afterCount) {
+ showInfo(t('未发现重复密钥'));
+ } else {
+ showSuccess(message);
+ }
+ };
+
const addCustomModels = () => {
if (customModel.trim() === '') return;
const modelArray = customModel.split(',').map((model) => model.trim());
@@ -1049,24 +1109,41 @@ const EditChannelModal = (props) => {
)}
{batch && (
- {
- setMultiToSingle((prev) => !prev);
- setInputs((prev) => {
- const newInputs = { ...prev };
- if (!multiToSingle) {
- newInputs.multi_key_mode = multiKeyMode;
- } else {
- delete newInputs.multi_key_mode;
- }
- return newInputs;
- });
- }}
- >
- {t('密钥聚合模式')}
-
+ <>
+ {
+ setMultiToSingle((prev) => {
+ const nextValue = !prev;
+ setInputs((prevInputs) => {
+ const newInputs = { ...prevInputs };
+ if (nextValue) {
+ newInputs.multi_key_mode = multiKeyMode;
+ } else {
+ delete newInputs.multi_key_mode;
+ }
+ return newInputs;
+ });
+ return nextValue;
+ });
+ }}
+ >
+ {t('密钥聚合模式')}
+
+
+ {inputs.type !== 41 && (
+
+ {t('密钥去重')}
+
+ )}
+ >
)}
) : null;
@@ -1268,7 +1345,10 @@ const EditChannelModal = (props) => {
value={inputs.vertex_key_type || 'json'}
onChange={(value) => {
// 更新设置中的 vertex_key_type
- handleChannelOtherSettingsChange('vertex_key_type', value);
+ handleChannelOtherSettingsChange(
+ 'vertex_key_type',
+ value,
+ );
// 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件
if (value === 'api_key') {
setBatch(false);
@@ -1288,7 +1368,8 @@ const EditChannelModal = (props) => {
/>
)}
{batch ? (
- inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? (
+ inputs.type === 41 &&
+ (inputs.vertex_key_type || 'json') === 'json' ? (
{
autoComplete='new-password'
onChange={(value) => handleInputChange('key', value)}
extraText={
-
+
{isEdit &&
isMultiKeyChannel &&
keyMode === 'append' && (
@@ -1352,7 +1433,8 @@ const EditChannelModal = (props) => {
)
) : (
<>
- {inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? (
+ {inputs.type === 41 &&
+ (inputs.vertex_key_type || 'json') === 'json' ? (
<>
{!batch && (
diff --git a/web/src/components/table/task-logs/modals/ContentModal.jsx b/web/src/components/table/task-logs/modals/ContentModal.jsx
index 3d747b77..3bfba37b 100644
--- a/web/src/components/table/task-logs/modals/ContentModal.jsx
+++ b/web/src/components/table/task-logs/modals/ContentModal.jsx
@@ -17,8 +17,11 @@ along with this program. If not, see
.
For commercial licensing, please contact support@quantumnous.com
*/
-import React from 'react';
-import { Modal } from '@douyinfe/semi-ui';
+import React, { useState, useEffect } from 'react';
+import { Modal, Button, Typography, Spin } from '@douyinfe/semi-ui';
+import { IconExternalOpen, IconCopy } from '@douyinfe/semi-icons';
+
+const { Text } = Typography;
const ContentModal = ({
isModalOpen,
@@ -26,17 +29,120 @@ const ContentModal = ({
modalContent,
isVideo,
}) => {
+ const [videoError, setVideoError] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (isModalOpen && isVideo) {
+ setVideoError(false);
+ setIsLoading(true);
+ }
+ }, [isModalOpen, isVideo]);
+
+ const handleVideoError = () => {
+ setVideoError(true);
+ setIsLoading(false);
+ };
+
+ const handleVideoLoaded = () => {
+ setIsLoading(false);
+ };
+
+ const handleCopyUrl = () => {
+ navigator.clipboard.writeText(modalContent);
+ };
+
+ const handleOpenInNewTab = () => {
+ window.open(modalContent, '_blank');
+ };
+
+ const renderVideoContent = () => {
+ if (videoError) {
+ return (
+
+
+ 视频无法在当前浏览器中播放,这可能是由于:
+
+
+ • 视频服务商的跨域限制
+
+
+ • 需要特定的请求头或认证
+
+
+ • 防盗链保护机制
+
+
+
+ }
+ onClick={handleOpenInNewTab}
+ style={{ marginRight: '8px' }}
+ >
+ 在新标签页中打开
+
+ }
+ onClick={handleCopyUrl}
+ >
+ 复制链接
+
+
+
+
+
+ {modalContent}
+
+
+
+ );
+ }
+
+ return (
+
+ {isLoading && (
+
+
+
+ )}
+
+ );
+ };
+
return (
setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
closable={null}
- bodyStyle={{ height: '400px', overflow: 'auto' }}
+ bodyStyle={{
+ height: isVideo ? '450px' : '400px',
+ overflow: 'auto',
+ padding: isVideo && videoError ? '0' : '24px'
+ }}
width={800}
>
{isVideo ? (
-
+ renderVideoContent()
) : (
{modalContent}
)}
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index e18ea85a..8a9d32f2 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -837,6 +837,7 @@
"确定要充值 $": "Confirm to top up $",
"微信/支付宝 实付金额:": "WeChat/Alipay actual payment amount:",
"Stripe 实付金额:": "Stripe actual payment amount:",
+ "允许在 Stripe 支付中输入促销码": "Allow entering promotion codes during Stripe checkout",
"支付中...": "Paying",
"支付宝": "Alipay",
"收益统计": "Income statistics",
diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json
index b423a795..f0b9e501 100644
--- a/web/src/i18n/locales/zh.json
+++ b/web/src/i18n/locales/zh.json
@@ -35,5 +35,6 @@
"域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。",
"common": {
"changeLanguage": "切换语言"
- }
+ },
+ "允许在 Stripe 支付中输入促销码": "允许在 Stripe 支付中输入促销码"
}
diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx b/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
index 2f4ea210..e4ddea11 100644
--- a/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
+++ b/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
@@ -45,6 +45,7 @@ export default function SettingsPaymentGateway(props) {
StripePriceId: '',
StripeUnitPrice: 8.0,
StripeMinTopUp: 1,
+ StripePromotionCodesEnabled: false,
});
const [originInputs, setOriginInputs] = useState({});
const formApiRef = useRef(null);
@@ -63,6 +64,10 @@ export default function SettingsPaymentGateway(props) {
props.options.StripeMinTopUp !== undefined
? parseFloat(props.options.StripeMinTopUp)
: 1,
+ StripePromotionCodesEnabled:
+ props.options.StripePromotionCodesEnabled !== undefined
+ ? props.options.StripePromotionCodesEnabled
+ : false,
};
setInputs(currentInputs);
setOriginInputs({ ...currentInputs });
@@ -114,6 +119,16 @@ export default function SettingsPaymentGateway(props) {
value: inputs.StripeMinTopUp.toString(),
});
}
+ if (
+ originInputs['StripePromotionCodesEnabled'] !==
+ inputs.StripePromotionCodesEnabled &&
+ inputs.StripePromotionCodesEnabled !== undefined
+ ) {
+ options.push({
+ key: 'StripePromotionCodesEnabled',
+ value: inputs.StripePromotionCodesEnabled ? 'true' : 'false',
+ });
+ }
// 发送请求
const requestQueue = options.map((opt) =>
@@ -225,6 +240,15 @@ export default function SettingsPaymentGateway(props) {
placeholder={t('例如:2,就是最低充值2$')}
/>
+
+
+
{t('更新 Stripe 设置')}