From fbb30768203b0c43abb0b682402958da5667febb Mon Sep 17 00:00:00 2001 From: wans10 Date: Fri, 1 May 2026 00:10:31 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(subscription):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E5=A5=97=E9=A4=90=E6=A8=A1=E5=9E=8B=E9=99=90?= =?UTF-8?q?=E5=88=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 SubscriptionPlan 模型中增加 model_limits 字段及相关校验逻辑 - 实现订阅预扣费时的模型权限检查,确保请求模型在套餐允许范围内 - 优化错误提示:区分“模型不受支持”与“订阅额度不足”两种异常情况 - 在管理后台(Classic UI)增加模型限制配置界面,支持带图标的模型选择 - 在订阅列表和选购卡片中展示可用模型限制信息 - 同步更新数据库迁移 DDL 及 8 种语言的 i18n 翻译 --- controller/subscription.go | 2 + model/main.go | 4 + model/subscription.go | 51 ++++++++++ .../subscriptions/SubscriptionsColumnDefs.jsx | 6 ++ .../modals/AddEditSubscriptionModal.jsx | 97 ++++++++++++++++++- .../topup/SubscriptionPlansCard.jsx | 13 +++ web/classic/src/i18n/locales/en.json | 4 + web/classic/src/i18n/locales/fr.json | 4 + web/classic/src/i18n/locales/ja.json | 4 + web/classic/src/i18n/locales/ru.json | 4 + web/classic/src/i18n/locales/vi.json | 3 + web/classic/src/i18n/locales/zh-CN.json | 4 + web/classic/src/i18n/locales/zh-TW.json | 4 + web/classic/src/i18n/locales/zh.json | 4 + 14 files changed, 203 insertions(+), 1 deletion(-) diff --git a/controller/subscription.go b/controller/subscription.go index c6095312b77..72eccdaec9b 100644 --- a/controller/subscription.go +++ b/controller/subscription.go @@ -239,6 +239,8 @@ func AdminUpdateSubscriptionPlan(c *gin.Context) { "upgrade_group": req.Plan.UpgradeGroup, "quota_reset_period": req.Plan.QuotaResetPeriod, "quota_reset_custom_seconds": req.Plan.QuotaResetCustomSeconds, + "model_limits_enabled": req.Plan.ModelLimitsEnabled, + "model_limits": req.Plan.ModelLimits, "updated_at": common.GetTimestamp(), } if err := tx.Model(&model.SubscriptionPlan{}).Where("id = ?", id).Updates(updateMap).Error; err != nil { diff --git a/model/main.go b/model/main.go index f37cb667cd4..d482eebd90e 100644 --- a/model/main.go +++ b/model/main.go @@ -402,6 +402,8 @@ func ensureSubscriptionPlanTableSQLite() error { ` + "`total_amount`" + ` bigint NOT NULL DEFAULT 0, ` + "`quota_reset_period`" + ` varchar(16) DEFAULT 'never', ` + "`quota_reset_custom_seconds`" + ` bigint DEFAULT 0, +` + "`model_limits_enabled`" + ` numeric DEFAULT 0, +` + "`model_limits`" + ` text DEFAULT '', ` + "`created_at`" + ` bigint, ` + "`updated_at`" + ` bigint, PRIMARY KEY (` + "`id`" + `) @@ -435,6 +437,8 @@ PRIMARY KEY (` + "`id`" + `) {Name: "total_amount", DDL: "`total_amount` bigint NOT NULL DEFAULT 0"}, {Name: "quota_reset_period", DDL: "`quota_reset_period` varchar(16) DEFAULT 'never'"}, {Name: "quota_reset_custom_seconds", DDL: "`quota_reset_custom_seconds` bigint DEFAULT 0"}, + {Name: "model_limits_enabled", DDL: "`model_limits_enabled` numeric DEFAULT 0"}, + {Name: "model_limits", DDL: "`model_limits` text DEFAULT ''"}, {Name: "created_at", DDL: "`created_at` bigint"}, {Name: "updated_at", DDL: "`updated_at` bigint"}, } diff --git a/model/subscription.go b/model/subscription.go index da8fdae9410..0c27f2b00cf 100644 --- a/model/subscription.go +++ b/model/subscription.go @@ -175,10 +175,52 @@ type SubscriptionPlan struct { QuotaResetPeriod string `json:"quota_reset_period" gorm:"type:varchar(16);default:'never'"` QuotaResetCustomSeconds int64 `json:"quota_reset_custom_seconds" gorm:"type:bigint;default:0"` + // Model limits: restrict which models can be used with this plan + ModelLimitsEnabled bool `json:"model_limits_enabled" gorm:"default:false"` + ModelLimits string `json:"model_limits" gorm:"type:text"` + CreatedAt int64 `json:"created_at" gorm:"bigint"` UpdatedAt int64 `json:"updated_at" gorm:"bigint"` } +// GetModelLimits returns the model limits as a slice. +func (p *SubscriptionPlan) GetModelLimits() []string { + if p.ModelLimits == "" { + return []string{} + } + parts := strings.Split(p.ModelLimits, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + +// GetModelLimitsMap returns the model limits as a map for fast lookup. +func (p *SubscriptionPlan) GetModelLimitsMap() map[string]bool { + limits := p.GetModelLimits() + limitsMap := make(map[string]bool, len(limits)) + for _, limit := range limits { + if limit != "" { + limitsMap[limit] = true + } + } + return limitsMap +} + +// IsModelAllowed checks if a model is allowed by this plan's model limits. +// Returns true if model limits are disabled or if the model is in the allowed list. +func (p *SubscriptionPlan) IsModelAllowed(modelName string) bool { + if !p.ModelLimitsEnabled || p.ModelLimits == "" { + return true + } + limitsMap := p.GetModelLimitsMap() + return limitsMap[modelName] +} + func (p *SubscriptionPlan) BeforeCreate(tx *gorm.DB) error { now := common.GetTimestamp() p.CreatedAt = now @@ -1013,12 +1055,18 @@ func PreConsumeUserSubscription(requestId string, userId int, modelName string, if len(subs) == 0 { return errors.New("no active subscription") } + modelNotAllowed := false for _, candidate := range subs { sub := candidate plan, err := getSubscriptionPlanByIdTx(tx, sub.PlanId) if err != nil { return err } + // Check model limits: skip subscriptions whose plan doesn't allow the requested model + if modelName != "" && !plan.IsModelAllowed(modelName) { + modelNotAllowed = true + continue + } if err := maybeResetUserSubscriptionWithPlanTx(tx, &sub, plan, now); err != nil { return err } @@ -1062,6 +1110,9 @@ func PreConsumeUserSubscription(requestId string, userId int, modelName string, returnValue.AmountUsedAfter = sub.AmountUsed return nil } + if modelNotAllowed { + return fmt.Errorf("no subscription allows model %s", modelName) + } return fmt.Errorf("subscription quota insufficient, need=%d", amount) }) if err != nil { diff --git a/web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx b/web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx index 222b05126ca..705b6919662 100644 --- a/web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx +++ b/web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx @@ -101,6 +101,12 @@ const renderPlanTitle = (text, record, t) => { {formatDuration(plan, t)} {t('重置')} {formatResetPeriod(plan, t)} + {t('模型限制')} + + {plan?.model_limits_enabled && plan?.model_limits + ? `${plan.model_limits.split(',').filter(Boolean).length} ${t('个模型')}` + : t('不限')} + ); diff --git a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx index 73aef7317ad..ef994ea5658 100644 --- a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx +++ b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx @@ -36,10 +36,17 @@ import { IconCalendarClock, IconClose, IconCreditCard, + IconLink, IconSave, } from '@douyinfe/semi-icons'; import { Clock, RefreshCw } from 'lucide-react'; -import { API, showError, showSuccess } from '../../../../helpers'; +import { + API, + showError, + showSuccess, + getModelCategories, + selectFilter, +} from '../../../../helpers'; import { quotaToDisplayAmount, displayAmountToQuota, @@ -75,6 +82,7 @@ const AddEditSubscriptionModal = ({ const [loading, setLoading] = useState(false); const [groupOptions, setGroupOptions] = useState([]); const [groupLoading, setGroupLoading] = useState(false); + const [models, setModels] = useState([]); const isMobile = useIsMobile(); const formApiRef = useRef(null); const isEdit = editingPlan?.plan?.id !== undefined; @@ -97,12 +105,17 @@ const AddEditSubscriptionModal = ({ upgrade_group: '', stripe_price_id: '', creem_product_id: '', + model_limits: [], }); const buildFormValues = () => { const base = getInitValues(); if (editingPlan?.plan?.id === undefined) return base; const p = editingPlan.plan || {}; + let modelLimits = []; + if (p.model_limits && p.model_limits !== '') { + modelLimits = p.model_limits.split(',').filter(Boolean); + } return { ...base, title: p.title || '', @@ -123,7 +136,42 @@ const AddEditSubscriptionModal = ({ upgrade_group: p.upgrade_group || '', stripe_price_id: p.stripe_price_id || '', creem_product_id: p.creem_product_id || '', + model_limits: modelLimits, + }; }; + + const loadModels = async () => { + try { + const res = await API.get('/api/user/models'); + const { success, data } = res.data; + if (success) { + const categories = getModelCategories(t); + const localModelOptions = data.map((model) => { + let icon = null; + for (const [key, category] of Object.entries(categories)) { + if (key !== 'all' && category.filter({ model_name: model })) { + icon = category.icon; + break; + } + } + return { + label: ( + + {icon} + {model} + + ), + value: model, + }; + }); + setModels(localModelOptions); + } else { + setModels([]); + } + } catch (error) { + console.error('Failed to load models:', error); + setModels([]); + } }; useEffect(() => { @@ -139,6 +187,7 @@ const AddEditSubscriptionModal = ({ }) .catch(() => setGroupOptions([])) .finally(() => setGroupLoading(false)); + loadModels(); }, [visible]); const submit = async (values) => { @@ -148,6 +197,7 @@ const AddEditSubscriptionModal = ({ } setLoading(true); try { + const modelLimitsStr = (values.model_limits || []).join(','); const payload = { plan: { ...values, @@ -164,6 +214,8 @@ const AddEditSubscriptionModal = ({ max_purchase_per_user: Number(values.max_purchase_per_user || 0), total_amount: displayAmountToQuota(values.total_amount), upgrade_group: values.upgrade_group || '', + model_limits_enabled: modelLimitsStr.length > 0, + model_limits: modelLimitsStr, }, }; if (editingPlan?.plan?.id) { @@ -501,6 +553,49 @@ const AddEditSubscriptionModal = ({ + {/* 模型限制 */} + +
+ + + +
+ + {t('模型限制')} + +
+ {t('限制该订阅套餐可使用的模型范围')} +
+
+
+ + + + + + +
+ {/* 第三方支付配置 */}
diff --git a/web/classic/src/components/topup/SubscriptionPlansCard.jsx b/web/classic/src/components/topup/SubscriptionPlansCard.jsx index 9c50828372b..3319794d312 100644 --- a/web/classic/src/components/topup/SubscriptionPlansCard.jsx +++ b/web/classic/src/components/topup/SubscriptionPlansCard.jsx @@ -510,6 +510,10 @@ const SubscriptionPlansCard = ({ formatSubscriptionResetPeriod(plan, t) === t('不重置') ? null : `${t('额度重置')}: ${formatSubscriptionResetPeriod(plan, t)}`; + const modelLimitsLabel = + plan?.model_limits_enabled && plan?.model_limits + ? `${t('可用模型')}: ${plan.model_limits.split(',').filter(Boolean).length} ${t('个模型')}` + : null; const planBenefits = [ { label: `${t('有效期')}: ${formatSubscriptionDuration(plan, t)}`, @@ -523,6 +527,15 @@ const SubscriptionPlansCard = ({ : { label: totalLabel }, limitLabel ? { label: limitLabel } : null, upgradeLabel ? { label: upgradeLabel } : null, + modelLimitsLabel + ? { + label: modelLimitsLabel, + tooltip: plan.model_limits + .split(',') + .filter(Boolean) + .join(', '), + } + : null, ].filter(Boolean); return ( diff --git a/web/classic/src/i18n/locales/en.json b/web/classic/src/i18n/locales/en.json index 829a9e4b2a3..39cb7fdd672 100644 --- a/web/classic/src/i18n/locales/en.json +++ b/web/classic/src/i18n/locales/en.json @@ -2151,6 +2151,7 @@ "模型配置": "Model Configuration", "模型重定向": "Model mapping", "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "The following models from the redirect have not been added to the “Models” list and requests will fail due to no available model:", + "模型限制": "Model Restrictions", "模型限制列表": "Model restrictions list", "模式": "Mode", "模板": "Template", @@ -3231,6 +3232,7 @@ "请选择至少一个部署位置": "Please select at least one deployment location", "请选择订阅套餐": "Please select a subscription plan", "请选择该令牌支持的模型,留空支持所有模型": "Select models supported by the token, leave blank to support all models", + "请选择该套餐支持的模型,留空支持所有模型": "Select models allowed by this plan, leave empty for all models", "请选择该渠道所支持的模型": "Please select the model supported by this channel", "请选择该渠道所支持的模型,留空则不更改": "Please select the models supported by the channel, leaving blank will not change", "请选择过期时间": "Please select expiration time", @@ -3388,6 +3390,7 @@ "选择同步来源": "Select sync source", "选择同步渠道": "Select synchronization channel", "选择同步语言": "Select sync language", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "When set, requests billed via this subscription can only use selected models; leave empty for no restriction", "选择容器": "Select Container", "选择您的首选界面语言,设置将自动保存并同步到所有设备": "Select your preferred interface language. Settings will be saved automatically and synced across all devices", "选择成功": "Selection successful", @@ -3563,6 +3566,7 @@ "降级": "Demote", "限制周期": "Limit period", "限制周期统一使用上方配置的“限制周期”值。": "The limit period uniformly uses the \"limit period\" value configured above.", + "限制该订阅套餐可使用的模型范围": "Restrict models available for this subscription plan", "限流": "Rate Limiting", "限购": "Limit", "隐私政策": "Privacy Policy", diff --git a/web/classic/src/i18n/locales/fr.json b/web/classic/src/i18n/locales/fr.json index e433dc8f72e..c4ca4f26ed8 100644 --- a/web/classic/src/i18n/locales/fr.json +++ b/web/classic/src/i18n/locales/fr.json @@ -2142,6 +2142,7 @@ "模型配置": "Configuration du modèle", "模型重定向": "Redirection de modèle", "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "Les modèles suivants provenant de la redirection n'ont pas été ajoutés à la liste « Modèles », l'appel échouera faute de modèle disponible :", + "模型限制": "Restrictions de modèle", "模型限制列表": "Liste des restrictions de modèle", "模式": "Mode", "模板": "Modèle", @@ -3216,6 +3217,7 @@ "请选择至少一个部署位置": "Please select at least one deployment location", "请选择订阅套餐": "Veuillez sélectionner un forfait d'abonnement", "请选择该令牌支持的模型,留空支持所有模型": "Sélectionnez les modèles pris en charge par le jeton, laissez vide pour prendre en charge tous les modèles", + "请选择该套餐支持的模型,留空支持所有模型": "Veuillez sélectionner les modèles pris en charge par ce plan, laissez vide pour tous les modèles", "请选择该渠道所支持的模型": "Veuillez sélectionner le modèle pris en charge par ce canal", "请选择该渠道所支持的模型,留空则不更改": "Veuillez sélectionner les modèles pris en charge par le canal, laisser vide ne changera rien", "请选择过期时间": "Veuillez sélectionner une date d'expiration", @@ -3368,6 +3370,7 @@ "选择同步来源": "Sélectionner la source de synchronisation", "选择同步渠道": "Sélectionner le canal de synchronisation", "选择同步语言": "Sélectionner la langue de synchronisation", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "Une fois sélectionné, les requêtes facturées via ce plan d'abonnement ne peuvent utiliser que les modèles sélectionnés ; laissez vide pour aucune restriction", "选择容器": "Select Container", "选择您的首选界面语言,设置将自动保存并同步到所有设备": "Sélectionnez votre langue d'interface préférée, les paramètres seront automatiquement enregistrés et synchronisés sur tous les appareils", "选择成功": "Sélection réussie", @@ -3541,6 +3544,7 @@ "降级": "Rétrograder", "限制周期": "Période de limite", "限制周期统一使用上方配置的“限制周期”值。": "La période de limite utilise uniformément la valeur \"période de limite\" configurée ci-dessus.", + "限制该订阅套餐可使用的模型范围": "Restreindre les modèles disponibles pour ce plan d'abonnement", "限流": "Limitation de débit", "限购": "Limite", "隐私政策": "Politique de confidentialité", diff --git a/web/classic/src/i18n/locales/ja.json b/web/classic/src/i18n/locales/ja.json index 5cfa0a2615f..c85e78dcaf6 100644 --- a/web/classic/src/i18n/locales/ja.json +++ b/web/classic/src/i18n/locales/ja.json @@ -2113,6 +2113,7 @@ "模型配置": "モデル設定", "模型重定向": "モデルマッピング", "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "The following models from the redirect have not been added to the “Models” list and requests will fail due to no available model:", + "模型限制": "モデルの制限", "模型限制列表": "モデル制限リスト", "模式": "モード", "模板": "テンプレート", @@ -3185,6 +3186,7 @@ "请选择至少一个部署位置": "Please select at least one deployment location", "请选择订阅套餐": "サブスクリプションプランを選択してください", "请选择该令牌支持的模型,留空支持所有模型": "対応モデルを選択してください。空欄の場合は全モデルに対応します。", + "请选择该套餐支持的模型,留空支持所有模型": "このプランでサポートされているモデルを選択してください。すべてのモデルをサポートする場合は空白のままにしてください", "请选择该渠道所支持的模型": "このチャネルでサポートされているモデルを選択してください", "请选择该渠道所支持的模型,留空则不更改": "このチャネルに対応しているモデルを選択してください。空欄の場合は変更されません", "请选择过期时间": "有効期限を選択してください", @@ -3337,6 +3339,7 @@ "选择同步来源": "同期ソースを選択", "选择同步渠道": "同期チャネルを選択", "选择同步语言": "同期する言語を選択", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "選択後、このサブスクリプションプランで課金されるリクエストは選択したモデルのみを使用できます。空白のままにすると制限はありません", "选择容器": "Select Container", "选择您的首选界面语言,设置将自动保存并同步到所有设备": "お好みのインターフェース言語を選択してください。設定は自動的に保存され、すべてのデバイスに同期されます", "选择成功": "選択に成功しました", @@ -3510,6 +3513,7 @@ "降级": "降格", "限制周期": "制限期間", "限制周期统一使用上方配置的“限制周期”值。": "制限期間は、一律で上記にて設定された「制限期間」の値を使用します。", + "限制该订阅套餐可使用的模型范围": "このサブスクリプションプランで利用可能なモデルを制限する", "限流": "レート制限", "限购": "購入制限", "隐私政策": "プライバシーポリシー", diff --git a/web/classic/src/i18n/locales/ru.json b/web/classic/src/i18n/locales/ru.json index f4950bfeed8..fe524d94643 100644 --- a/web/classic/src/i18n/locales/ru.json +++ b/web/classic/src/i18n/locales/ru.json @@ -2160,6 +2160,7 @@ "模型配置": "Конфигурация модели", "模型重定向": "Перенаправление модели", "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "Следующие модели из перенаправления ещё не добавлены в список «Модели», из-за отсутствия доступных моделей вызовы завершатся ошибкой:", + "模型限制": "Ограничения модели", "模型限制列表": "Список ограничений модели", "模式": "Режим", "模板": "Шаблон", @@ -3236,6 +3237,7 @@ "请选择至少一个部署位置": "Please select at least one deployment location", "请选择订阅套餐": "Выберите план подписки", "请选择该令牌支持的模型,留空支持所有模型": "Пожалуйста, выберите модели, поддерживаемые этим токеном, оставьте пустым для поддержки всех моделей", + "请选择该套餐支持的模型,留空支持所有模型": "Пожалуйста, выберите модели, поддерживаемые этим планом, оставьте пустым для всех моделей", "请选择该渠道所支持的模型": "Пожалуйста, выберите модели, поддерживаемые этим каналом", "请选择该渠道所支持的模型,留空则不更改": "Пожалуйста, выберите модели, поддерживаемые этим каналом, оставьте пустым для без изменений", "请选择过期时间": "Пожалуйста, выберите время истечения", @@ -3388,6 +3390,7 @@ "选择同步来源": "Выберите источник синхронизации", "选择同步渠道": "Выберите канал синхронизации", "选择同步语言": "Выберите язык синхронизации", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "После выбора запросы, оплачиваемые по этому плану подписки, могут использовать только выбранные модели; оставьте пустым для отсутствия ограничений", "选择容器": "Select Container", "选择您的首选界面语言,设置将自动保存并同步到所有设备": "Выберите предпочитаемый язык интерфейса, настройки будут автоматически сохранены и синхронизированы на всех устройствах", "选择成功": "Выбрано успешно", @@ -3561,6 +3564,7 @@ "降级": "Понизить версию", "限制周期": "Период ограничения", "限制周期统一使用上方配置的“限制周期”值。": "Период ограничения равномерно использует значение 'Период ограничения', настроенное выше.", + "限制该订阅套餐可使用的模型范围": "Ограничить модели, доступные для этого плана подписки", "限流": "Ограничение скорости", "限购": "Лимит", "隐私政策": "Политика конфиденциальности", diff --git a/web/classic/src/i18n/locales/vi.json b/web/classic/src/i18n/locales/vi.json index 1d83bf6b738..9c76709b720 100644 --- a/web/classic/src/i18n/locales/vi.json +++ b/web/classic/src/i18n/locales/vi.json @@ -3631,6 +3631,7 @@ "请选择要导出的数据": "Vui lòng chọn dữ liệu để xuất", "请选择订阅套餐": "Vui lòng chọn gói đăng ký", "请选择该令牌支持的模型,留空支持所有模型": "Vui lòng chọn các mô hình được mã thông báo này hỗ trợ, để trống để hỗ trợ tất cả các mô hình", + "请选择该套餐支持的模型,留空支持所有模型": "Vui lòng chọn các mô hình được gói này hỗ trợ, để trống cho tất cả các mô hình", "请选择该渠道所支持的模型": "Vui lòng chọn mô hình được kênh này hỗ trợ", "请选择该渠道所支持的模型,留空则不更改": "Vui lòng chọn mô hình được kênh này hỗ trợ, để trống sẽ không thay đổi", "请选择语言": "Vui lòng chọn ngôn ngữ", @@ -3826,6 +3827,7 @@ "选择同步来源": "Chọn nguồn đồng bộ", "选择同步渠道": "Chọn kênh đồng bộ", "选择同步语言": "Chọn ngôn ngữ đồng bộ", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "Khi được đặt, các yêu cầu thanh toán qua gói đăng ký này chỉ có thể sử dụng các mô hình đã chọn; để trống để không có giới hạn", "选择图片": "Chọn hình ảnh", "选择头像": "Chọn ảnh đại diện", "选择容器": "Select Container", @@ -4075,6 +4077,7 @@ "降级": "Hạ cấp", "限制周期": "Chu kỳ giới hạn", "限制周期统一使用上方配置的“限制周期”值。": "Chu kỳ giới hạn sử dụng thống nhất giá trị \"Chu kỳ giới hạn\" được cấu hình ở trên.", + "限制该订阅套餐可使用的模型范围": "Giới hạn các mô hình có sẵn cho gói đăng ký này", "限流": "Giới hạn tốc độ", "限购": "Giới hạn mua", "隐私政策": "Chính sách bảo mật", diff --git a/web/classic/src/i18n/locales/zh-CN.json b/web/classic/src/i18n/locales/zh-CN.json index 2156b46d8ee..57365a53523 100644 --- a/web/classic/src/i18n/locales/zh-CN.json +++ b/web/classic/src/i18n/locales/zh-CN.json @@ -2111,6 +2111,7 @@ "模型配置": "模型配置", "模型重定向": "模型重定向", "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:", + "模型限制": "模型限制", "模型限制列表": "模型限制列表", "模式": "模式", "模板": "模板", @@ -3187,6 +3188,7 @@ "请选择至少一个部署位置": "请选择至少一个部署位置", "请选择订阅套餐": "请选择订阅套餐", "请选择该令牌支持的模型,留空支持所有模型": "请选择该令牌支持的模型,留空支持所有模型", + "请选择该套餐支持的模型,留空支持所有模型": "请选择该套餐支持的模型,留空支持所有模型", "请选择该渠道所支持的模型": "请选择该渠道所支持的模型", "请选择该渠道所支持的模型,留空则不更改": "请选择该渠道所支持的模型,留空则不更改", "请选择过期时间": "请选择过期时间", @@ -3375,6 +3377,7 @@ "选择同步来源": "选择同步来源", "选择同步渠道": "选择同步渠道", "选择同步语言": "选择同步语言", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制", "选择容器": "选择容器", "选择您的首选界面语言,设置将自动保存并同步到所有设备": "选择您的首选界面语言,设置将自动保存并同步到所有设备", "选择成功": "选择成功", @@ -3550,6 +3553,7 @@ "降级": "降级", "限制周期": "限制周期", "限制周期统一使用上方配置的“限制周期”值。": "限制周期统一使用上方配置的“限制周期”值。", + "限制该订阅套餐可使用的模型范围": "限制该订阅套餐可使用的模型范围", "限流": "限流", "限购": "限购", "隐私政策": "隐私政策", diff --git a/web/classic/src/i18n/locales/zh-TW.json b/web/classic/src/i18n/locales/zh-TW.json index 98d8e892488..b9971f17169 100644 --- a/web/classic/src/i18n/locales/zh-TW.json +++ b/web/classic/src/i18n/locales/zh-TW.json @@ -2120,6 +2120,7 @@ "模型配置": "模型設定", "模型重定向": "模型重定向", "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "模型重定向裡的下列模型尚未添加到「模型」列表,調用時會因為缺少可用模型而失敗:", + "模型限制": "模型限制", "模型限制列表": "模型限制列表", "模式": "", "模板": "", @@ -3197,6 +3198,7 @@ "请选择至少一个部署位置": "請選擇至少一個部署位置", "请选择订阅套餐": "", "请选择该令牌支持的模型,留空支持所有模型": "請選擇該令牌支援的模型,留空支援所有模型", + "请选择该套餐支持的模型,留空支持所有模型": "請選擇該套餐支援的模型,留空支援所有模型", "请选择该渠道所支持的模型": "請選擇該管道所支援的模型", "请选择该渠道所支持的模型,留空则不更改": "請選擇該管道所支援的模型,留空則不更改", "请选择过期时间": "請選擇過期時間", @@ -3351,6 +3353,7 @@ "选择同步来源": "選擇同步來源", "选择同步渠道": "選擇同步管道", "选择同步语言": "選擇同步語言", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "選擇後,透過該訂閱套餐計費的請求只能使用選中的模型;留空則不限制", "选择容器": "選擇容器", "选择您的首选界面语言,设置将自动保存并同步到所有设备": "選擇您的首選界面語言,設定將自動儲存並同步到所有設備", "选择成功": "選擇成功", @@ -3525,6 +3528,7 @@ "降级": "降級", "限制周期": "限制週期", "限制周期统一使用上方配置的“限制周期”值。": "限制週期統一使用上方設定的「限制週期」值。", + "限制该订阅套餐可使用的模型范围": "限制該訂閱套餐可使用的模型範圍", "限流": "", "限购": "限購", "隐私政策": "隱私政策", diff --git a/web/classic/src/i18n/locales/zh.json b/web/classic/src/i18n/locales/zh.json index e23930f5e1d..4d340ea94f0 100644 --- a/web/classic/src/i18n/locales/zh.json +++ b/web/classic/src/i18n/locales/zh.json @@ -1469,6 +1469,7 @@ "模型配置": "模型配置", "模型重定向": "模型重定向", "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:", + "模型限制": "模型限制", "模型限制列表": "模型限制列表", "模板示例": "模板示例", "模糊搜索模型名称": "模糊搜索模型名称", @@ -1777,6 +1778,7 @@ "站点额度展示类型及汇率": "站点额度展示类型及汇率", "端口号必须在1-65535之间": "端口号必须在1-65535之间", "端口配置详细说明": "限制外部请求只能访问指定端口。支持单个端口(80, 443)或端口范围(8000-8999)。空列表允许所有端口。默认包含常用Web端口。", + "限制该订阅套餐可使用的模型范围": "限制该订阅套餐可使用的模型范围", "端点": "端点", "端点映射": "端点映射", "端点类型": "端点类型", @@ -2237,6 +2239,7 @@ "请选择组类型": "请选择组类型", "请选择至少一个部署位置": "请选择至少一个部署位置", "请选择该令牌支持的模型,留空支持所有模型": "请选择该令牌支持的模型,留空支持所有模型", + "请选择该套餐支持的模型,留空支持所有模型": "请选择该套餐支持的模型,留空支持所有模型", "请选择该渠道所支持的模型": "请选择该渠道所支持的模型", "请选择该渠道所支持的模型,留空则不更改": "请选择该渠道所支持的模型,留空则不更改", "请选择过期时间": "请选择过期时间", @@ -2342,6 +2345,7 @@ "选择同步来源": "选择同步来源", "选择同步渠道": "选择同步渠道", "选择同步语言": "选择同步语言", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制", "选择容器": "选择容器", "选择成功": "选择成功", "选择支付方式": "选择支付方式", From 67dd89be2c2fbbdab3acfb8fae718ab7b5f5b9cd Mon Sep 17 00:00:00 2001 From: wans10 Date: Fri, 1 May 2026 00:29:32 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix(web):=20=E4=BC=98=E5=8C=96=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E5=A5=97=E9=A4=90=E6=A8=A1=E5=9E=8B=E9=99=90=E5=88=B6?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=B9=B6=E6=94=B9=E8=BF=9B=E5=A4=9A=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在编辑订阅套餐初始化时增加 trim() 处理,防止 CSV 中的空格导致多选框匹配失效 - 在套餐列表显示时对模型列表进行去重和格式化,确保模型总数统计准确 - 改进法语 (fr) 和越南语 (vi) 关于模型限制描述的词句,提升语意清晰度 --- .../table/subscriptions/SubscriptionsColumnDefs.jsx | 7 ++++++- .../subscriptions/modals/AddEditSubscriptionModal.jsx | 5 ++++- web/classic/src/i18n/locales/fr.json | 2 +- web/classic/src/i18n/locales/vi.json | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx b/web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx index 705b6919662..c0009593d49 100644 --- a/web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx +++ b/web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx @@ -104,7 +104,12 @@ const renderPlanTitle = (text, record, t) => { {t('模型限制')} {plan?.model_limits_enabled && plan?.model_limits - ? `${plan.model_limits.split(',').filter(Boolean).length} ${t('个模型')}` + ? `${new Set( + plan.model_limits + .split(',') + .map((item) => item.trim()) + .filter(Boolean), + ).size} ${t('个模型')}` : t('不限')}
diff --git a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx index ef994ea5658..b819e162f43 100644 --- a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx +++ b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx @@ -114,7 +114,10 @@ const AddEditSubscriptionModal = ({ const p = editingPlan.plan || {}; let modelLimits = []; if (p.model_limits && p.model_limits !== '') { - modelLimits = p.model_limits.split(',').filter(Boolean); + modelLimits = p.model_limits + .split(',') + .map((m) => m.trim()) + .filter(Boolean); } return { ...base, diff --git a/web/classic/src/i18n/locales/fr.json b/web/classic/src/i18n/locales/fr.json index c4ca4f26ed8..28476966475 100644 --- a/web/classic/src/i18n/locales/fr.json +++ b/web/classic/src/i18n/locales/fr.json @@ -3370,7 +3370,7 @@ "选择同步来源": "Sélectionner la source de synchronisation", "选择同步渠道": "Sélectionner le canal de synchronisation", "选择同步语言": "Sélectionner la langue de synchronisation", - "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "Une fois sélectionné, les requêtes facturées via ce plan d'abonnement ne peuvent utiliser que les modèles sélectionnés ; laissez vide pour aucune restriction", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "Une fois sélectionnés, les modèles facturés via ce forfait d’abonnement ne peuvent être que ceux sélectionnés ; laissez vide pour ne pas imposer de restriction", "选择容器": "Select Container", "选择您的首选界面语言,设置将自动保存并同步到所有设备": "Sélectionnez votre langue d'interface préférée, les paramètres seront automatiquement enregistrés et synchronisés sur tous les appareils", "选择成功": "Sélection réussie", diff --git a/web/classic/src/i18n/locales/vi.json b/web/classic/src/i18n/locales/vi.json index 9c76709b720..5c4d0966040 100644 --- a/web/classic/src/i18n/locales/vi.json +++ b/web/classic/src/i18n/locales/vi.json @@ -3827,7 +3827,7 @@ "选择同步来源": "Chọn nguồn đồng bộ", "选择同步渠道": "Chọn kênh đồng bộ", "选择同步语言": "Chọn ngôn ngữ đồng bộ", - "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "Khi được đặt, các yêu cầu thanh toán qua gói đăng ký này chỉ có thể sử dụng các mô hình đã chọn; để trống để không có giới hạn", + "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制": "Sau khi chọn, các yêu cầu được tính phí qua gói đăng ký này chỉ có thể dùng các mô hình đã chọn; để trống thì không giới hạn", "选择图片": "Chọn hình ảnh", "选择头像": "Chọn ảnh đại diện", "选择容器": "Select Container", From 935f4533278c9c87ab0953809584e00c2e20181b Mon Sep 17 00:00:00 2001 From: wans10 Date: Fri, 1 May 2026 00:51:34 +0800 Subject: [PATCH 3/7] =?UTF-8?q?refactor(web):=20=E4=BC=98=E5=8C=96=20AddEd?= =?UTF-8?q?itSubscriptionModal=20=E7=9A=84=E5=89=AF=E4=BD=9C=E7=94=A8?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 useCallback 缓存 loadModels 函数,确保其在组件渲染间的稳定性 - 补全 useEffect 的依赖项(loadModels, t),修复 exhaustive-deps 警告并确保逻辑正确性 --- .../subscriptions/modals/AddEditSubscriptionModal.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx index b819e162f43..9fb4b359e59 100644 --- a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx +++ b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState, useRef, useCallback } from 'react'; import { Avatar, Button, @@ -143,7 +143,7 @@ const AddEditSubscriptionModal = ({ }; }; - const loadModels = async () => { + const loadModels = useCallback(async () => { try { const res = await API.get('/api/user/models'); const { success, data } = res.data; @@ -175,7 +175,7 @@ const AddEditSubscriptionModal = ({ console.error('Failed to load models:', error); setModels([]); } - }; + }, [t]); useEffect(() => { if (!visible) return; @@ -191,7 +191,7 @@ const AddEditSubscriptionModal = ({ .catch(() => setGroupOptions([])) .finally(() => setGroupLoading(false)); loadModels(); - }, [visible]); + }, [visible, loadModels, t]); const submit = async (values) => { if (!values.title || values.title.trim() === '') { From 837c878bacf150f0987ebcc9aee6df461af8bd6e Mon Sep 17 00:00:00 2001 From: wans10 Date: Fri, 1 May 2026 00:55:26 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix(subscription):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=99=90=E5=88=B6=E9=94=99=E8=AF=AF=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E5=B9=B6=E4=BC=98=E5=8C=96=E5=89=8D=E7=AB=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 model/subscription.go 中模型限制错误分类逻辑:只有当没有任何订阅允许该模型时才返回模型限制错误,防止在多订阅场景下因单个订阅不匹配导致误报。 - 优化 SubscriptionPlansCard.jsx 中的模型列表处理:对 model_limits 进行归一化处理(修剪空格并过滤空项),确保数量统计和悬浮提示信息的一致性。 --- model/subscription.go | 8 +++++--- .../src/components/topup/SubscriptionPlansCard.jsx | 13 +++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/model/subscription.go b/model/subscription.go index 0c27f2b00cf..b6e78f2d88e 100644 --- a/model/subscription.go +++ b/model/subscription.go @@ -1055,7 +1055,7 @@ func PreConsumeUserSubscription(requestId string, userId int, modelName string, if len(subs) == 0 { return errors.New("no active subscription") } - modelNotAllowed := false + anyPlanAllowsModel := false for _, candidate := range subs { sub := candidate plan, err := getSubscriptionPlanByIdTx(tx, sub.PlanId) @@ -1064,9 +1064,11 @@ func PreConsumeUserSubscription(requestId string, userId int, modelName string, } // Check model limits: skip subscriptions whose plan doesn't allow the requested model if modelName != "" && !plan.IsModelAllowed(modelName) { - modelNotAllowed = true continue } + if modelName != "" { + anyPlanAllowsModel = true + } if err := maybeResetUserSubscriptionWithPlanTx(tx, &sub, plan, now); err != nil { return err } @@ -1110,7 +1112,7 @@ func PreConsumeUserSubscription(requestId string, userId int, modelName string, returnValue.AmountUsedAfter = sub.AmountUsed return nil } - if modelNotAllowed { + if modelName != "" && !anyPlanAllowsModel { return fmt.Errorf("no subscription allows model %s", modelName) } return fmt.Errorf("subscription quota insufficient, need=%d", amount) diff --git a/web/classic/src/components/topup/SubscriptionPlansCard.jsx b/web/classic/src/components/topup/SubscriptionPlansCard.jsx index 3319794d312..515130f5c94 100644 --- a/web/classic/src/components/topup/SubscriptionPlansCard.jsx +++ b/web/classic/src/components/topup/SubscriptionPlansCard.jsx @@ -510,9 +510,13 @@ const SubscriptionPlansCard = ({ formatSubscriptionResetPeriod(plan, t) === t('不重置') ? null : `${t('额度重置')}: ${formatSubscriptionResetPeriod(plan, t)}`; + const modelLimitNames = (plan?.model_limits || '') + .split(',') + .map((model) => model.trim()) + .filter(Boolean); const modelLimitsLabel = - plan?.model_limits_enabled && plan?.model_limits - ? `${t('可用模型')}: ${plan.model_limits.split(',').filter(Boolean).length} ${t('个模型')}` + plan?.model_limits_enabled && modelLimitNames.length > 0 + ? `${t('可用模型')}: ${modelLimitNames.length} ${t('个模型')}` : null; const planBenefits = [ { @@ -530,10 +534,7 @@ const SubscriptionPlansCard = ({ modelLimitsLabel ? { label: modelLimitsLabel, - tooltip: plan.model_limits - .split(',') - .filter(Boolean) - .join(', '), + tooltip: modelLimitNames.join(', '), } : null, ].filter(Boolean); From 823492fc074c61f3176917999ef258f9a7855105 Mon Sep 17 00:00:00 2001 From: wans10 Date: Fri, 1 May 2026 01:14:11 +0800 Subject: [PATCH 5/7] =?UTF-8?q?fix(subscription):=20=E6=A0=B9=E6=8D=AE?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=AE=A1=E6=9F=A5=E4=BC=98=E5=8C=96=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E6=A8=A1=E5=9E=8B=E9=99=90=E5=88=B6=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=8F=8A=E5=A4=9A=E8=AF=AD=E8=A8=80=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端逻辑:修复 model/subscription.go 中的模型选择错误分类,确保在多订阅场景下正确区分模型限制与额度不足。 - 数据初始化:在 AddEditSubscriptionModal 中对模型限制初始化增加“启用状态”门控,并进行 trim 处理,防止残留空格干扰匹配。 - 界面展示:归一化 SubscriptionsColumnDefs 和 SubscriptionPlansCard 中的模型解析逻辑,确保模型计数和悬浮提示的一致性。 - React 优化:补全 useEffect 依赖项并使用 useCallback 缓存数据加载函数,消除 Lint 警告并提升组件稳定性。 - 国际化:改进法语 (fr) 和越南语 (vi) 的翻译措辞,使其更加自然且语意明确。 --- .../modals/AddEditSubscriptionModal.jsx | 2 +- .../src/components/topup/SubscriptionPlansCard.jsx | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx index 9fb4b359e59..7b9113c1796 100644 --- a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx +++ b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx @@ -113,7 +113,7 @@ const AddEditSubscriptionModal = ({ if (editingPlan?.plan?.id === undefined) return base; const p = editingPlan.plan || {}; let modelLimits = []; - if (p.model_limits && p.model_limits !== '') { + if (p.model_limits_enabled && p.model_limits && p.model_limits !== '') { modelLimits = p.model_limits .split(',') .map((m) => m.trim()) diff --git a/web/classic/src/components/topup/SubscriptionPlansCard.jsx b/web/classic/src/components/topup/SubscriptionPlansCard.jsx index 515130f5c94..9d1f87aa058 100644 --- a/web/classic/src/components/topup/SubscriptionPlansCard.jsx +++ b/web/classic/src/components/topup/SubscriptionPlansCard.jsx @@ -510,12 +510,15 @@ const SubscriptionPlansCard = ({ formatSubscriptionResetPeriod(plan, t) === t('不重置') ? null : `${t('额度重置')}: ${formatSubscriptionResetPeriod(plan, t)}`; - const modelLimitNames = (plan?.model_limits || '') - .split(',') - .map((model) => model.trim()) - .filter(Boolean); + const modelLimitNames = + plan?.model_limits_enabled && plan?.model_limits + ? plan.model_limits + .split(',') + .map((model) => model.trim()) + .filter(Boolean) + : []; const modelLimitsLabel = - plan?.model_limits_enabled && modelLimitNames.length > 0 + modelLimitNames.length > 0 ? `${t('可用模型')}: ${modelLimitNames.length} ${t('个模型')}` : null; const planBenefits = [ From 79d5e4390b27cc2276f11c9358d6000fc35e55f4 Mon Sep 17 00:00:00 2001 From: wans10 Date: Fri, 1 May 2026 10:25:50 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix(subscription):=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E6=A8=A1=E5=9E=8B=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=B9=B6=E5=A2=9E=E5=BC=BA=E5=BC=B9=E7=AA=97=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将订阅编辑弹窗中的模型加载接口从 /api/user/models 切换为 /api/channel/models_enabled,确保管理员在配置套餐时可见系统中所有已启用的模型。 - 在 loadModels 函数中增加对返回数据的 Array.isArray 校验,并完善 catch 块的兜底处理,提升弹窗在异常情况下的稳定性。 --- .../table/subscriptions/modals/AddEditSubscriptionModal.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx index 7b9113c1796..cb0625a4617 100644 --- a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx +++ b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx @@ -145,9 +145,9 @@ const AddEditSubscriptionModal = ({ const loadModels = useCallback(async () => { try { - const res = await API.get('/api/user/models'); + const res = await API.get('/api/channel/models_enabled'); const { success, data } = res.data; - if (success) { + if (success && Array.isArray(data)) { const categories = getModelCategories(t); const localModelOptions = data.map((model) => { let icon = null; From 9ce85cd269a309139c50702f38a2aeb45d36224b Mon Sep 17 00:00:00 2001 From: wans10 Date: Fri, 1 May 2026 11:21:55 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix(subscription):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=A5=97=E9=A4=90=E7=BC=96=E8=BE=91=E5=BC=B9=E7=AA=97=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=8A=A0=E8=BD=BD=E5=8F=AF=E9=9D=A0=E6=80=A7?= =?UTF-8?q?=E4=B8=8E=E4=BA=A4=E4=BA=92=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入显式的模型加载中 (loading) 与加载失败 (error) 状态管理,防止因接口偶发故障导致模型限制被误清空。 - 增加模型加载失败时的重试机制,并优化错误提示图标为 AlertCircle,提升语意清晰度。 - 当模型加载失败时自动禁用提交按钮与模型选择器,确保套餐配置的数据完整性。 - 清理 AddEditSubscriptionModal.jsx 中的冗余语法及括号,优化代码结构。 --- .../modals/AddEditSubscriptionModal.jsx | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx index cb0625a4617..6070963edb2 100644 --- a/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx +++ b/web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx @@ -39,7 +39,7 @@ import { IconLink, IconSave, } from '@douyinfe/semi-icons'; -import { Clock, RefreshCw } from 'lucide-react'; +import { AlertCircle, Clock, RefreshCw } from 'lucide-react'; import { API, showError, @@ -83,6 +83,8 @@ const AddEditSubscriptionModal = ({ const [groupOptions, setGroupOptions] = useState([]); const [groupLoading, setGroupLoading] = useState(false); const [models, setModels] = useState([]); + const [modelsLoading, setModelsLoading] = useState(false); + const [modelsLoadError, setModelsLoadError] = useState(false); const isMobile = useIsMobile(); const formApiRef = useRef(null); const isEdit = editingPlan?.plan?.id !== undefined; @@ -141,9 +143,11 @@ const AddEditSubscriptionModal = ({ creem_product_id: p.creem_product_id || '', model_limits: modelLimits, }; - }; + }; const loadModels = useCallback(async () => { + setModelsLoading(true); + setModelsLoadError(false); try { const res = await API.get('/api/channel/models_enabled'); const { success, data } = res.data; @@ -169,11 +173,13 @@ const AddEditSubscriptionModal = ({ }); setModels(localModelOptions); } else { - setModels([]); + setModelsLoadError(true); } } catch (error) { console.error('Failed to load models:', error); - setModels([]); + setModelsLoadError(true); + } finally { + setModelsLoading(false); } }, [t]); @@ -194,6 +200,10 @@ const AddEditSubscriptionModal = ({ }, [visible, loadModels, t]); const submit = async (values) => { + if (modelsLoadError) { + showError(t('模型列表加载失败,无法提交,请重试')); + return; + } if (!values.title || values.title.trim() === '') { showError(t('套餐标题不能为空')); return; @@ -281,6 +291,7 @@ const AddEditSubscriptionModal = ({ onClick={() => formApiRef.current?.submitForm()} icon={} loading={loading} + disabled={modelsLoading || modelsLoadError} > {t('提交')} @@ -578,6 +589,25 @@ const AddEditSubscriptionModal = ({ + {modelsLoadError && ( +
+
+ + + {t('模型列表加载失败,请重试')} + +
+ +
+ )}