feat(subscription): 支持订阅套餐模型限制功能#4564
Conversation
- 在 SubscriptionPlan 模型中增加 model_limits 字段及相关校验逻辑 - 实现订阅预扣费时的模型权限检查,确保请求模型在套餐允许范围内 - 优化错误提示:区分“模型不受支持”与“订阅额度不足”两种异常情况 - 在管理后台(Classic UI)增加模型限制配置界面,支持带图标的模型选择 - 在订阅列表和选购卡片中展示可用模型限制信息 - 同步更新数据库迁移 DDL 及 8 种语言的 i18n 翻译
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds per-plan model restrictions: DB schema adds two columns, backend persists them and enforces allowed models during pre-consume checks, and frontend + i18n provide configuration UI and display of plan model limits. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as Controller
participant Model as SubscriptionLogic
participant DB as Database
Client->>Controller: PreConsumeUserSubscription(modelName, user)
Controller->>Model: Find active subscriptions for user
Model->>DB: SELECT subscriptions/plans for user
DB-->>Model: subscriptions list
rect rgba(100, 150, 200, 0.5)
Model->>Model: For each plan:
alt ModelLimitsEnabled
Model->>Model: Parse ModelLimits -> set
Model->>Model: IsModelAllowed(modelName)?
alt allowed
Model->>Model: Check quota & other constraints
else not allowed
Model-->>Model: skip plan
end
else NotEnabled
Model->>Model: Check quota & other constraints
end
end
alt a plan matched and quota ok
Model-->>Controller: selected plan
Controller-->>Client: 200 OK (plan)
else no plan allows model
Model-->>Controller: Error: no subscription allows model
Controller-->>Client: 4xx Error (model not allowed)
else matched but quota insufficient
Model-->>Controller: Error: quota insufficient
Controller-->>Client: 402 / appropriate error
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
model/subscription.go (1)
1058-1115:⚠️ Potential issue | 🟠 Major | ⚡ Quick winModel-limit error is misclassified when allowed plans exist but quota is insufficient
Current
modelNotAllowedflag is set if any plan disallows the model. That makes the function returnno subscription allows model ...even when another plan does allow the model but has insufficient remaining quota.Patch
- modelNotAllowed := false + anyPlanAllowsModel := 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 modelName != "" { + anyPlanAllowsModel = true + } if err := maybeResetUserSubscriptionWithPlanTx(tx, &sub, plan, now); err != nil { return err } usedBefore := sub.AmountUsed if sub.AmountTotal > 0 { remain := sub.AmountTotal - usedBefore if remain < amount { continue } } ... } - if modelNotAllowed { + if modelName != "" && !anyPlanAllowsModel { return fmt.Errorf("no subscription allows model %s", modelName) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@model/subscription.go` around lines 1058 - 1115, The code currently sets modelNotAllowed true if any candidate plan disallows the model, causing a "no subscription allows model" error even when another plan does allow it but lacks quota; change this to track whether any plan actually allows the model (e.g., introduce foundAllowed bool) and only return fmt.Errorf("no subscription allows model %s", modelName) when no plans in subs allow the model. Specifically, update the loop around subs and the check that uses plan.IsModelAllowed to set foundAllowed = true when a plan permits the model (or treat empty modelName as allowed), continue to skip disallowed plans for quota checks, and replace the final if modelNotAllowed check with if !foundAllowed to decide the error.web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx (1)
200-219:⚠️ Potential issue | 🟠 Major | ⚡ Quick winModel limits sent by UI won’t persist on plan update unless backend map includes these fields
This payload is correct, but the admin update path currently omits
model_limits_enabledandmodel_limitsincontroller/subscription.go(Line 223-248), so edits can silently fail to persist these values.Suggested backend fix (controller/subscription.go)
updateMap := map[string]interface{}{ "title": req.Plan.Title, "subtitle": req.Plan.Subtitle, "price_amount": req.Plan.PriceAmount, "currency": req.Plan.Currency, "duration_unit": req.Plan.DurationUnit, "duration_value": req.Plan.DurationValue, "custom_seconds": req.Plan.CustomSeconds, "enabled": req.Plan.Enabled, "sort_order": req.Plan.SortOrder, "stripe_price_id": req.Plan.StripePriceId, "creem_product_id": req.Plan.CreemProductId, "max_purchase_per_user": req.Plan.MaxPurchasePerUser, "total_amount": req.Plan.TotalAmount, "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(), }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx` around lines 200 - 219, In the subscription update handler in the controller (the admin subscription update path), read and apply the incoming payload fields model_limits_enabled and model_limits to the plan object before saving so UI edits persist: parse model_limits (string/CSV) and model_limits_enabled from the request body, assign them to the plan's corresponding fields (e.g., plan.ModelLimitsEnabled and plan.ModelLimits or whatever struct fields you use), and include those fields in the update/save operation so they are written to the DB; ensure types match the model (convert CSV to the stored format or vice‑versa) and that the save/update method persists these new fields.
🧹 Nitpick comments (1)
web/classic/src/components/topup/SubscriptionPlansCard.jsx (1)
513-538: ⚡ Quick winNormalize the model-limit list before counting and rendering.
split(',')is repeated here, and the entries are not trimmed. A single parsed list would keep the label and tooltip consistent if the stored string contains spaces or stray separators.♻️ Proposed fix
- const modelLimitsLabel = - plan?.model_limits_enabled && plan?.model_limits - ? `${t('可用模型')}: ${plan.model_limits.split(',').filter(Boolean).length} ${t('个模型')}` - : null; + const modelLimitNames = (plan?.model_limits || '') + .split(',') + .map((model) => model.trim()) + .filter(Boolean); + const modelLimitsLabel = + plan?.model_limits_enabled && modelLimitNames.length > 0 + ? `${t('可用模型')}: ${modelLimitNames.length} ${t('个模型')}` + : null; @@ - tooltip: plan.model_limits - .split(',') - .filter(Boolean) - .join(', '), + tooltip: modelLimitNames.join(', '),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/classic/src/components/topup/SubscriptionPlansCard.jsx` around lines 513 - 538, In SubscriptionPlansCard.jsx, normalize plan.model_limits into a single parsed array (trim each entry and filter out empty strings) once (e.g., const parsedModelLimits = plan?.model_limits?.split(',').map(s => s.trim()).filter(Boolean) || []) and then use parsedModelLimits for computing modelLimitsLabel (parsedModelLimits.length) and for the tooltip (parsedModelLimits.join(', ')) instead of calling plan.model_limits.split(',') in multiple places; update references to modelLimitsLabel and the tooltip construction to use this parsedModelLimits to keep counting and rendering consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx`:
- Around line 116-118: During the edit initialization in
AddEditSubscriptionModal (where p.model_limits is parsed into modelLimits), the
CSV split keeps whitespace and breaks multi-select matching; after splitting
p.model_limits with p.model_limits.split(',') you should map over the results
and trim each entry and filter out empty strings (e.g., .split(',').map(s =>
s.trim()).filter(Boolean)) so modelLimits contains clean tokens for the
multi-select.
In `@web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx`:
- Around line 104-108: The model count calculation using
plan.model_limits.split(',').filter(Boolean) can over-count malformed entries;
in the component (SubscriptionsColumnDefs.jsx) normalize plan.model_limits by
splitting on ',', mapping each item to item.trim(), filtering out empty strings,
and deduplicating (e.g., with a Set) before taking .length; replace the existing
expression that computes `${plan.model_limits.split(',').filter(Boolean).length}
${t('个模型')}` with this normalized-and-unique count so whitespace-padded or
duplicate models are not double-counted.
In `@web/classic/src/i18n/locales/fr.json`:
- Line 3373: The French translation value for the key
"选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制" is awkward; update the string value to a
clearer phrasing such as "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" by replacing the current value in fr.json for
that exact key so the UI reads naturally.
In `@web/classic/src/i18n/locales/vi.json`:
- Line 3830: Replace the ambiguous leading phrase "Khi được đặt" with the
clearer temporal phrase "Sau khi chọn" in the Vietnamese translation for the
JSON key "选择后,通过该订阅套餐计费的请求只能使用选中的模型;留空则不限制" so the value begins "Sau khi chọn,
các yêu cầu..." ensuring the rest of the translated sentence remains unchanged.
---
Outside diff comments:
In `@model/subscription.go`:
- Around line 1058-1115: The code currently sets modelNotAllowed true if any
candidate plan disallows the model, causing a "no subscription allows model"
error even when another plan does allow it but lacks quota; change this to track
whether any plan actually allows the model (e.g., introduce foundAllowed bool)
and only return fmt.Errorf("no subscription allows model %s", modelName) when no
plans in subs allow the model. Specifically, update the loop around subs and the
check that uses plan.IsModelAllowed to set foundAllowed = true when a plan
permits the model (or treat empty modelName as allowed), continue to skip
disallowed plans for quota checks, and replace the final if modelNotAllowed
check with if !foundAllowed to decide the error.
In
`@web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx`:
- Around line 200-219: In the subscription update handler in the controller (the
admin subscription update path), read and apply the incoming payload fields
model_limits_enabled and model_limits to the plan object before saving so UI
edits persist: parse model_limits (string/CSV) and model_limits_enabled from the
request body, assign them to the plan's corresponding fields (e.g.,
plan.ModelLimitsEnabled and plan.ModelLimits or whatever struct fields you use),
and include those fields in the update/save operation so they are written to the
DB; ensure types match the model (convert CSV to the stored format or
vice‑versa) and that the save/update method persists these new fields.
---
Nitpick comments:
In `@web/classic/src/components/topup/SubscriptionPlansCard.jsx`:
- Around line 513-538: In SubscriptionPlansCard.jsx, normalize plan.model_limits
into a single parsed array (trim each entry and filter out empty strings) once
(e.g., const parsedModelLimits = plan?.model_limits?.split(',').map(s =>
s.trim()).filter(Boolean) || []) and then use parsedModelLimits for computing
modelLimitsLabel (parsedModelLimits.length) and for the tooltip
(parsedModelLimits.join(', ')) instead of calling plan.model_limits.split(',')
in multiple places; update references to modelLimitsLabel and the tooltip
construction to use this parsedModelLimits to keep counting and rendering
consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 46f8d73d-9fd2-4e70-bea4-d410e13e72f8
📒 Files selected for processing (14)
controller/subscription.gomodel/main.gomodel/subscription.goweb/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsxweb/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsxweb/classic/src/components/topup/SubscriptionPlansCard.jsxweb/classic/src/i18n/locales/en.jsonweb/classic/src/i18n/locales/fr.jsonweb/classic/src/i18n/locales/ja.jsonweb/classic/src/i18n/locales/ru.jsonweb/classic/src/i18n/locales/vi.jsonweb/classic/src/i18n/locales/zh-CN.jsonweb/classic/src/i18n/locales/zh-TW.jsonweb/classic/src/i18n/locales/zh.json
- 在编辑订阅套餐初始化时增加 trim() 处理,防止 CSV 中的空格导致多选框匹配失效 - 在套餐列表显示时对模型列表进行去重和格式化,确保模型总数统计准确 - 改进法语 (fr) 和越南语 (vi) 关于模型限制描述的词句,提升语意清晰度
There was a problem hiding this comment.
🧹 Nitpick comments (1)
web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx (1)
180-194: 💤 Low valueConsider adding
loadModelsortto the dependency array.The
loadModelsfunction usestfrom props, but neither is listed in the useEffect dependencies. This could trigger ESLint exhaustive-deps warnings. Sincetis typically stable from the i18n context, the functional impact is minimal, but adding it would ensure correctness if the language changes while the modal is open.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx` around lines 180 - 194, The useEffect that runs when the modal becomes visible references loadModels and the translation function t but only lists visible in its dependency array; to satisfy exhaustive-deps and ensure correctness, include loadModels and t in the dependency array of the useEffect where visible is used (i.e., the effect that calls API.get and loadModels), or if loadModels is stable via useCallback/memoization ensure it is declared as such before useEffect; update the dependency list to [visible, loadModels, t] (or memoize loadModels) so ESLint warnings are resolved and the effect responds to changes in those values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx`:
- Around line 180-194: The useEffect that runs when the modal becomes visible
references loadModels and the translation function t but only lists visible in
its dependency array; to satisfy exhaustive-deps and ensure correctness, include
loadModels and t in the dependency array of the useEffect where visible is used
(i.e., the effect that calls API.get and loadModels), or if loadModels is stable
via useCallback/memoization ensure it is declared as such before useEffect;
update the dependency list to [visible, loadModels, t] (or memoize loadModels)
so ESLint warnings are resolved and the effect responds to changes in those
values.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1a55af13-72bf-4f40-88d0-665328e99c1d
📒 Files selected for processing (4)
web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsxweb/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsxweb/classic/src/i18n/locales/fr.jsonweb/classic/src/i18n/locales/vi.json
✅ Files skipped from review due to trivial changes (2)
- web/classic/src/i18n/locales/vi.json
- web/classic/src/i18n/locales/fr.json
🚧 Files skipped from review as they are similar to previous changes (1)
- web/classic/src/components/table/subscriptions/SubscriptionsColumnDefs.jsx
- 使用 useCallback 缓存 loadModels 函数,确保其在组件渲染间的稳定性 - 补全 useEffect 的依赖项(loadModels, t),修复 exhaustive-deps 警告并确保逻辑正确性
- 修复 model/subscription.go 中模型限制错误分类逻辑:只有当没有任何订阅允许该模型时才返回模型限制错误,防止在多订阅场景下因单个订阅不匹配导致误报。 - 优化 SubscriptionPlansCard.jsx 中的模型列表处理:对 model_limits 进行归一化处理(修剪空格并过滤空项),确保数量统计和悬浮提示信息的一致性。
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx`:
- Around line 116-121: 当前初始化会在存在残留字符串时无条件解析 p.model_limits 导致在
model_limits_enabled 为 false 时编辑保存会误启用限制;在 AddEditSubscriptionModal.jsx 中将对
modelLimits 的赋值改为在 p.model_limits_enabled 为真时才解析并赋值(即依赖 p.model_limits_enabled 来
gate 初始化 modelLimits),并在文件中另一个相同处理位置(先前提到的第 142 行处)做相同修改以保持一致性;请查找使用
p.model_limits 和 p.model_limits_enabled 的初始化逻辑并仅在 p.model_limits_enabled ===
true 时执行 .split().map(...).filter(Boolean) 赋值。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9d0b725e-2955-4b56-b8b8-a385a704f85e
📒 Files selected for processing (3)
model/subscription.goweb/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsxweb/classic/src/components/topup/SubscriptionPlansCard.jsx
✅ Files skipped from review due to trivial changes (1)
- model/subscription.go
- 后端逻辑:修复 model/subscription.go 中的模型选择错误分类,确保在多订阅场景下正确区分模型限制与额度不足。 - 数据初始化:在 AddEditSubscriptionModal 中对模型限制初始化增加“启用状态”门控,并进行 trim 处理,防止残留空格干扰匹配。 - 界面展示:归一化 SubscriptionsColumnDefs 和 SubscriptionPlansCard 中的模型解析逻辑,确保模型计数和悬浮提示的一致性。 - React 优化:补全 useEffect 依赖项并使用 useCallback 缓存数据加载函数,消除 Lint 警告并提升组件稳定性。 - 国际化:改进法语 (fr) 和越南语 (vi) 的翻译措辞,使其更加自然且语意明确。
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx`:
- Around line 146-153: loadModels 中调用的 API.get('/api/user/models')
仅按当前登录用户可见模型返回,会在多分组/管理员场景导致漏模型并意外收窄套餐;请将该调用改为使用后台的全量/管理员模型接口(例如改为
API.get('/api/admin/models') 或其他后端提供的全量模型端点),或者在请求中支持传入目标分组(如
groupId)以按目标分组拉取模型;同时在 loadModels 中保留错误和空结果的兜底处理以避免 UI 崩溃。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 91467559-262a-4ff9-b043-5ffb0a9541bd
📒 Files selected for processing (2)
web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsxweb/classic/src/components/topup/SubscriptionPlansCard.jsx
- 将订阅编辑弹窗中的模型加载接口从 /api/user/models 切换为 /api/channel/models_enabled,确保管理员在配置套餐时可见系统中所有已启用的模型。 - 在 loadModels 函数中增加对返回数据的 Array.isArray 校验,并完善 catch 块的兜底处理,提升弹窗在异常情况下的稳定性。
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx`:
- Around line 171-176: The code currently swallows model-load errors by calling
setModels([]), which makes an accidental API failure serialize as
model_limits_enabled: false; instead, add explicit load-state and error-state
(e.g., modelsLoading and modelsLoadFailed) in the AddEditSubscriptionModal logic
and update the load routine (the block using setModels in the try/catch) to call
setModelsLoadFailed(true) on catch rather than setModels([]). Update the UI to
show an error message and disable the model selector and submission path (the
submit handler that serializes model_limits_enabled) when modelsLoadFailed is
true so a transient fetch failure cannot be saved as “no model limits.” Ensure
existing code paths that read models differentiate between an actual empty
models array and a load failure by checking modelsLoadFailed before treating
models as empty.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4814d546-ecc3-4bf7-9ebd-f19d516a6ed8
📒 Files selected for processing (1)
web/classic/src/components/table/subscriptions/modals/AddEditSubscriptionModal.jsx
- 引入显式的模型加载中 (loading) 与加载失败 (error) 状态管理,防止因接口偶发故障导致模型限制被误清空。 - 增加模型加载失败时的重试机制,并优化错误提示图标为 AlertCircle,提升语意清晰度。 - 当模型加载失败时自动禁用提交按钮与模型选择器,确保套餐配置的数据完整性。 - 清理 AddEditSubscriptionModal.jsx 中的冗余语法及括号,优化代码结构。
|
这么多提交这个的,没有一个合并呀 |
📝 变更描述 / Description
本次变更实现了订阅套餐的“模型限制”功能。通过在
SubscriptionPlan模型中引入model_limits字段,管理员可以精确控制每个订阅套餐可调用的模型范围。核心逻辑说明:
PreConsumeUserSubscription的事务流中新增了模型权限前置检查。系统会遍历用户的所有活跃订阅,只有同时满足“模型授权”和“余额充足”的订阅才会被选中进行扣费。no subscription allows model X错误,而非含糊的额度不足提示。🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
Summary by CodeRabbit
New Features
Bug Fixes