Skip to content

[dev] feat: 用户支持多分组(group0,group1) - #15

Merged
LoSealL merged 2 commits into
developfrom
feat/user-multi-group
Sep 11, 2026
Merged

LoSealL merged 2 commits into
developfrom
feat/user-multi-group

Conversation

@LoSealL

@LoSealL LoSealL commented Sep 11, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

Note

本 PR 代码由 AI(ZCode agent)在本人指导下生成/协助完成,描述已人工整理。

📝 变更描述 / Description

用户分组不再唯一:users.group 存储逗号分隔的分组列表(如 group0,group1),第一个为主分组,存储与解析风格与渠道多分组保持一致。

  • 新增 common.SplitGroupList / PrimaryGroup / NormalizeGroupList 作为统一的逗号分组列表解析入口;GetTopupGroupRatio 同样按列表顺序取第一个命中的配置
  • 计费:GetGroupGroupRatio 对多分组用户按列表顺序取第一个命中的用户-分组专属倍率,无命中回退全局分组倍率(relay 计费、任务计费、定价接口共用该语义)
  • 可用分组:GetUserUsableGroups 对每个所属分组合并特殊可用规则(按列表顺序应用,后分组可移除先分组新增的分组)并保证所属分组本身可选;令牌分组与 Playground 分组的权限校验随之基于该并集
  • 路由:令牌未指定分组时使用主分组作为使用分组;relayInfo.TokenGroup 及任务计费、限流日志等单值消费点均取主分组,保证渠道精确匹配不会收到逗号列表
  • 模型列表:多分组用户返回所有所属分组启用模型的并集
  • 后台:用户搜索/列表按"包含该分组"精确匹配(复用渠道的 ApplyGroupContainsFilter,group1 不会误命中 group10);编辑用户时规范化分组列表(去空格/去重/去空项,空回落 default)并校验长度(单组 ≤64,总长 ≤1024)
  • 列宽:users.group 与 user_subscriptions.prev_user_group 加宽至 varchar(1024)(AutoMigrate 在 MySQL/PG 一次性加宽,SQLite 类型亲和天然兼容;订阅升级/到期仍是"整体替换为套餐分组"语义,升级前将原列表快照进 PrevUserGroup)
  • 前端:用户编辑抽屉分组改为多选(MultiSelect);用户列表分组列渲染多徽标并按分组精确过滤;i18n 补全 7 种语言

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • 无(如需补充 Issue 可后续关联)

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 Issues 与 PRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

本地验证(全部通过):

  • go build ./...、go vet(涉及包)
  • go test ./... 全量通过,新增多分组合约测试:common/group_test.go、service/group_multi_test.go(可用分组并集/特殊规则顺序/倍率首个命中/Auto 分组)、model/user_group_test.go(SearchUsers 包含匹配、EditWithTx 规范化)
  • 前端:tsgo 类型检查、oxlint(无新增告警)、oxfmt 格式检查、生产构建、vitest 208 个测试全部通过
$ go test ./...                # 全部 ok
$ bun run typecheck && bun run test
Test Files  40 passed (40)
     Tests  208 passed (208)

用户分组不再唯一,users.group 存储逗号分隔的分组列表(第一个为主分组):

- common: 新增 SplitGroupList/PrimaryGroup/NormalizeGroupList;GetTopupGroupRatio 按列表顺序取第一个配置项
- 计费: GetGroupGroupRatio 多分组用户按列表顺序取第一个命中的专属倍率,无命中回退全局分组倍率
- 可用分组: GetUserUsableGroups 对每个所属分组合并特殊可用规则并保证所属分组可选(令牌/Playground 分组校验随之生效)
- 路由: 无令牌分组时使用主分组作为使用分组;relayInfo.TokenGroup 保持单值进入渠道精确匹配
- 模型列表: 多分组用户返回所有所属分组启用模型的并集
- 后台: 用户搜索按"包含该分组"过滤;编辑保存时规范化分组列表并校验长度(单组<=64,总长<=1024)
- 列宽: users.group 与 user_subscriptions.prev_user_group 加宽至 varchar(1024)(AutoMigrate 自动迁移)
- 前端: 用户编辑抽屉分组改为多选;用户列表分组列渲染多徽标并按分组精确过滤;i18n 补全 7 种语言
Copilot AI lite review requested due to automatic review settings September 11, 2026 04:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings remain in group authorization, billing, rate limiting, persistence, filtering, and accessibility.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds multi-group user support across billing, authorization, routing, persistence, model visibility, and the admin UI.

Changes:

  • Adds shared group parsing, normalization, and ordered group resolution.
  • Updates billing, permissions, routing, rate limits, storage, and filtering.
  • Adds multi-select editing, badges, translations, and regression tests.
File summaries
File Change Final review note
web/src/lib/group-list.ts Frontend group-list parsing Nit (3 votes): Add tests for whitespace, empty entries, and primary-order behavior.
web/src/i18n/locales/zh.json Multi-group translations —
web/src/i18n/locales/zh-TW.json Multi-group translations —
web/src/i18n/locales/vi.json Multi-group translations —
web/src/i18n/locales/ru.json Multi-group translations —
web/src/i18n/locales/ja.json Multi-group translations —
web/src/i18n/locales/fr.json Multi-group translations —
web/src/i18n/locales/en.json Multi-group translations —
web/src/features/users/components/users-mutate-drawer.tsx Multi-group user editor Moderate (2 votes): Forward aria-describedby and aria-invalid through MultiSelect.
web/src/features/users/components/users-columns.tsx Multi-badge group display and filtering —
setting/ratio_setting/group_ratio.go Ordered group billing ratios —
service/task_billing.go Asynchronous task billing Critical (2 votes): Use the full user group list for user-specific ratio lookup.
service/group.go Usable-group union logic Critical (2 votes): Preserve owned groups from removal by later special rules.
service/group_multi_test.go Multi-group service tests Nit (1 vote): Restore the original AutoGroups value in cleanup.
relay/common/relay_info.go Effective relay group selection —
model/user.go Group storage, normalization, and search Moderate (1 vote): Keep all/null channel sentinels separate from user-group filtering.
model/user_group_test.go User group persistence and filter tests —
model/subscription.go Wider subscription group snapshot —
model/channel.go Shared group containment filtering —
middleware/model-rate-limit.go Primary-group rate-limit handling Moderate (3 votes): Resolve the primary/using group for the actual rate-limit lookup.
middleware/distributor.go Playground group authorization —
middleware/auth.go Primary and token-group handling —
controller/user.go Group validation and normalization Moderate (1 vote): Align the 64-character group limit with Task.Group’s varchar(50) storage.
controller/model.go Multi-group model union —
controller/channel.go Group filter usage —
common/topup-ratio.go Ordered top-up ratio lookup —
common/group.go Shared group utilities —
common/group_test.go Group utility tests —
Review details

Suppressed comments (3)

controller/user.go:680

  • This accepts group names up to 64 characters, but model.Task.Group is still varchar(50) and NewTask persists relayInfo.UsingGroup into that column. A valid 51–64-character group selected for an asynchronous task can therefore be truncated or rejected by MySQL/PostgreSQL. Widen the task column and migrate it, or align this bound with the narrowest consumer.
		if len(group) > common.MaxGroupNameLength {
			common.ApiErrorI18n(c, i18n.MsgInvalidParams)

model/user.go:458

  • ApplyGroupContainsFilter reuses NormalizeChannelGroupFilter, which treats the literal values all and null as “no filter” for channel query parameters. User group names are sourced from the group-ratio map and are not restricted from using either value, so searching users for one of those groups now returns the unfiltered user list instead of matching the exact group token. Keep the channel sentinel handling separate from the user-group predicate.
		query = ApplyGroupContainsFilter(query, group)

service/group_multi_test.go:91

  • This test changes the process-wide AutoGroups setting but always restores it to [], so it can erase a pre-existing configuration and leak state into later tests when the suite is run with non-empty defaults. Capture the original JSON before updating it and restore that exact value in cleanup, as the neighboring auto-group tests do.
	require.NoError(t, setting.UpdateAutoGroupsByJsonString(`["vip","group1"]`))
	t.Cleanup(func() {
		require.NoError(t, setting.UpdateAutoGroupsByJsonString("[]"))
	})
  • Files reviewed: 28/28 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread service/group.go Outdated
Comment on lines 39 to 44
// 如果分组不在UserUsableGroups中,补充该分组本身
if _, ok := groupsCopy[group]; !ok {
groupsCopy[group] = "用户分组"
}
}
return groupsCopy

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed in d52111b.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d52111b: GetUserUsableGroups now runs in two passes — special rules are applied per group in list order first, then all owned groups are re-added afterwards, so an owned group can never be removed by another member's -: rule (matching the single-group semantics). A test assertion covers the -:group0 case.

Comment thread service/task_billing.go Outdated
Comment on lines +305 to +306
// 多分组用户按主分组(第一个分组)计费
group = common.PrimaryGroup(user.Group)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed in d52111b.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d52111b: RecalculateTaskQuotaByTokens now loads the user's full group list via GetUserById and passes it as the first argument to GetGroupGroupRatio, while task.Group stays as the using group. If the user lookup fails it falls back to the previous exact-match behavior.

group := common.GetContextKeyString(c, constant.ContextKeyTokenGroup)
if group == "" {
group = common.GetContextKeyString(c, constant.ContextKeyUserGroup)
group = common.PrimaryGroup(common.GetContextKeyString(c, constant.ContextKeyUserGroup))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed in d52111b.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d52111b: the ModelRequestRateLimit lookup now applies the same resolution — token group first, then common.PrimaryGroup(ContextKeyUserGroup) — so a multi-group user matches the configured per-group rate limit instead of silently falling back to global limits.

Comment on lines +370 to +377
<MultiSelect
options={groupOptions}
selected={selectedGroups}
onChange={(values) =>
field.onChange(values.join(','))
}
placeholder={t('Select groups')}
/>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed in d52111b.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d52111b: MultiSelect now accepts and forwards aria-describedby / aria-invalid to the underlying ComboboxChipsInput, so the FormControl-injected ids reach the real input element for screen readers.

Comment thread web/src/lib/group-list.ts
Comment on lines +24 to +31
export function parseGroupList(value?: string | null): string[] {
if (!value) {
return []
}
return value
.split(',')
.map((group) => group.trim())
.filter((group) => group.length > 0)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed in d52111b.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added web/src/lib/group-list.test.ts in d52111b covering whitespace trimming, empty-entry handling, primary-order (first entry is the primary group), and no-substring-splitting.

- GetUserUsableGroups 改为两段式:先按列表顺序应用特殊可用规则,最后统一补回所属分组,保证所属分组不可被其他分组的 '-:' 规则移除(与单分组语义一致),并补充测试断言该不变量
- 任务结算 RecalculateTaskQuotaByTokens 将用户完整分组列表作为用户-分组专属倍率的首参,task.Group 仅作为使用分组,避免多分组用户丢失专属倍率;用户查询失败时回退旧的精确匹配行为
- ModelRequestRateLimit 限流查找同样取主分组,避免逗号列表无法命中分组限流配置
- MultiSelect 转发 aria-describedby/aria-invalid 到内部输入框,FormControl 的无障碍接线可达读屏
- 新增 parseGroupList 前端单测(空白/空项/顺序/子串)
@LoSealL
LoSealL merged commit 01a7a2a into develop Sep 11, 2026
2 checks passed
@LoSealL
LoSealL deleted the feat/user-multi-group branch September 11, 2026 05:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants