From e1a4a7b8c0a28f0c9e5bbfa08f8eb32f14619750 Mon Sep 17 00:00:00 2001 From: liuxiongfeng Date: Mon, 2 Feb 2026 16:46:25 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(groups):=20=E6=B7=BB=E5=8A=A0=E4=BB=8E?= =?UTF-8?q?=E5=85=B6=E4=BB=96=E5=88=86=E7=BB=84=E5=A4=8D=E5=88=B6=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建分组时可选择从已有分组复制账号 - 编辑分组时支持同步账号(全量替换操作) - 仅允许选择相同平台的源分组 - 添加完整的数据校验:去重、自引用检查、平台一致性检查 - 前端支持多选源分组,带提示说明操作行为 --- .../internal/handler/admin/group_handler.go | 72 ++++---- backend/internal/repository/group_repo.go | 58 +++++++ backend/internal/service/admin_service.go | 93 ++++++++++ backend/internal/service/group_service.go | 4 + frontend/src/i18n/locales/en.ts | 8 + frontend/src/i18n/locales/zh.ts | 8 + frontend/src/types/index.ts | 3 + frontend/src/views/admin/GroupsView.vue | 161 +++++++++++++++++- 8 files changed, 372 insertions(+), 35 deletions(-) diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 926624d2..f93edbc8 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -43,6 +43,8 @@ type CreateGroupRequest struct { // 模型路由配置(仅 anthropic 平台使用) ModelRouting map[string][]int64 `json:"model_routing"` ModelRoutingEnabled bool `json:"model_routing_enabled"` + // 从指定分组复制账号(创建后自动绑定) + CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"` } // UpdateGroupRequest represents update group request @@ -66,6 +68,8 @@ type UpdateGroupRequest struct { // 模型路由配置(仅 anthropic 平台使用) ModelRouting map[string][]int64 `json:"model_routing"` ModelRoutingEnabled *bool `json:"model_routing_enabled"` + // 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号) + CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"` } // List handles listing all groups with pagination @@ -155,22 +159,23 @@ func (h *GroupHandler) Create(c *gin.Context) { } group, err := h.adminService.CreateGroup(c.Request.Context(), &service.CreateGroupInput{ - Name: req.Name, - Description: req.Description, - Platform: req.Platform, - RateMultiplier: req.RateMultiplier, - IsExclusive: req.IsExclusive, - SubscriptionType: req.SubscriptionType, - DailyLimitUSD: req.DailyLimitUSD, - WeeklyLimitUSD: req.WeeklyLimitUSD, - MonthlyLimitUSD: req.MonthlyLimitUSD, - ImagePrice1K: req.ImagePrice1K, - ImagePrice2K: req.ImagePrice2K, - ImagePrice4K: req.ImagePrice4K, - ClaudeCodeOnly: req.ClaudeCodeOnly, - FallbackGroupID: req.FallbackGroupID, - ModelRouting: req.ModelRouting, - ModelRoutingEnabled: req.ModelRoutingEnabled, + Name: req.Name, + Description: req.Description, + Platform: req.Platform, + RateMultiplier: req.RateMultiplier, + IsExclusive: req.IsExclusive, + SubscriptionType: req.SubscriptionType, + DailyLimitUSD: req.DailyLimitUSD, + WeeklyLimitUSD: req.WeeklyLimitUSD, + MonthlyLimitUSD: req.MonthlyLimitUSD, + ImagePrice1K: req.ImagePrice1K, + ImagePrice2K: req.ImagePrice2K, + ImagePrice4K: req.ImagePrice4K, + ClaudeCodeOnly: req.ClaudeCodeOnly, + FallbackGroupID: req.FallbackGroupID, + ModelRouting: req.ModelRouting, + ModelRoutingEnabled: req.ModelRoutingEnabled, + CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs, }) if err != nil { response.ErrorFrom(c, err) @@ -196,23 +201,24 @@ func (h *GroupHandler) Update(c *gin.Context) { } group, err := h.adminService.UpdateGroup(c.Request.Context(), groupID, &service.UpdateGroupInput{ - Name: req.Name, - Description: req.Description, - Platform: req.Platform, - RateMultiplier: req.RateMultiplier, - IsExclusive: req.IsExclusive, - Status: req.Status, - SubscriptionType: req.SubscriptionType, - DailyLimitUSD: req.DailyLimitUSD, - WeeklyLimitUSD: req.WeeklyLimitUSD, - MonthlyLimitUSD: req.MonthlyLimitUSD, - ImagePrice1K: req.ImagePrice1K, - ImagePrice2K: req.ImagePrice2K, - ImagePrice4K: req.ImagePrice4K, - ClaudeCodeOnly: req.ClaudeCodeOnly, - FallbackGroupID: req.FallbackGroupID, - ModelRouting: req.ModelRouting, - ModelRoutingEnabled: req.ModelRoutingEnabled, + Name: req.Name, + Description: req.Description, + Platform: req.Platform, + RateMultiplier: req.RateMultiplier, + IsExclusive: req.IsExclusive, + Status: req.Status, + SubscriptionType: req.SubscriptionType, + DailyLimitUSD: req.DailyLimitUSD, + WeeklyLimitUSD: req.WeeklyLimitUSD, + MonthlyLimitUSD: req.MonthlyLimitUSD, + ImagePrice1K: req.ImagePrice1K, + ImagePrice2K: req.ImagePrice2K, + ImagePrice4K: req.ImagePrice4K, + ClaudeCodeOnly: req.ClaudeCodeOnly, + FallbackGroupID: req.FallbackGroupID, + ModelRouting: req.ModelRouting, + ModelRoutingEnabled: req.ModelRoutingEnabled, + CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs, }) if err != nil { response.ErrorFrom(c, err) diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index 5c4d6cf4..002e07da 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -425,3 +425,61 @@ func (r *groupRepository) loadAccountCounts(ctx context.Context, groupIDs []int6 return counts, nil } + +// GetAccountIDsByGroupIDs 获取多个分组的所有账号 ID(去重) +func (r *groupRepository) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) { + if len(groupIDs) == 0 { + return nil, nil + } + + rows, err := r.sql.QueryContext( + ctx, + "SELECT DISTINCT account_id FROM account_groups WHERE group_id = ANY($1) ORDER BY account_id", + pq.Array(groupIDs), + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var accountIDs []int64 + for rows.Next() { + var accountID int64 + if err := rows.Scan(&accountID); err != nil { + return nil, err + } + accountIDs = append(accountIDs, accountID) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return accountIDs, nil +} + +// BindAccountsToGroup 将多个账号绑定到指定分组(批量插入,忽略已存在的绑定) +func (r *groupRepository) BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error { + if len(accountIDs) == 0 { + return nil + } + + // 使用 INSERT ... ON CONFLICT DO NOTHING 忽略已存在的绑定 + _, err := r.sql.ExecContext( + ctx, + `INSERT INTO account_groups (account_id, group_id, priority, created_at) + SELECT unnest($1::bigint[]), $2, 50, NOW() + ON CONFLICT (account_id, group_id) DO NOTHING`, + pq.Array(accountIDs), + groupID, + ) + if err != nil { + return err + } + + // 发送调度器事件 + if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventGroupChanged, nil, &groupID, nil); err != nil { + log.Printf("[SchedulerOutbox] enqueue bind accounts to group failed: group=%d err=%v", groupID, err) + } + + return nil +} diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 0afa0716..ef2d526b 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -110,6 +110,8 @@ type CreateGroupInput struct { // 模型路由配置(仅 anthropic 平台使用) ModelRouting map[string][]int64 ModelRoutingEnabled bool // 是否启用模型路由 + // 从指定分组复制账号(创建分组后在同一事务内绑定) + CopyAccountsFromGroupIDs []int64 } type UpdateGroupInput struct { @@ -132,6 +134,8 @@ type UpdateGroupInput struct { // 模型路由配置(仅 anthropic 平台使用) ModelRouting map[string][]int64 ModelRoutingEnabled *bool // 是否启用模型路由 + // 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号) + CopyAccountsFromGroupIDs []int64 } type CreateAccountInput struct { @@ -572,6 +576,38 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn } } + // 如果指定了复制账号的源分组,先获取账号 ID 列表 + var accountIDsToCopy []int64 + if len(input.CopyAccountsFromGroupIDs) > 0 { + // 去重源分组 IDs + seen := make(map[int64]struct{}) + uniqueSourceGroupIDs := make([]int64, 0, len(input.CopyAccountsFromGroupIDs)) + for _, srcGroupID := range input.CopyAccountsFromGroupIDs { + if _, exists := seen[srcGroupID]; !exists { + seen[srcGroupID] = struct{}{} + uniqueSourceGroupIDs = append(uniqueSourceGroupIDs, srcGroupID) + } + } + + // 校验源分组的平台是否与新分组一致 + for _, srcGroupID := range uniqueSourceGroupIDs { + srcGroup, err := s.groupRepo.GetByIDLite(ctx, srcGroupID) + if err != nil { + return nil, fmt.Errorf("source group %d not found: %w", srcGroupID, err) + } + if srcGroup.Platform != platform { + return nil, fmt.Errorf("source group %d platform mismatch: expected %s, got %s", srcGroupID, platform, srcGroup.Platform) + } + } + + // 获取所有源分组的账号(去重) + var err error + accountIDsToCopy, err = s.groupRepo.GetAccountIDsByGroupIDs(ctx, uniqueSourceGroupIDs) + if err != nil { + return nil, fmt.Errorf("failed to get accounts from source groups: %w", err) + } + } + group := &Group{ Name: input.Name, Description: input.Description, @@ -593,6 +629,15 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn if err := s.groupRepo.Create(ctx, group); err != nil { return nil, err } + + // 如果有需要复制的账号,绑定到新分组 + if len(accountIDsToCopy) > 0 { + if err := s.groupRepo.BindAccountsToGroup(ctx, group.ID, accountIDsToCopy); err != nil { + return nil, fmt.Errorf("failed to bind accounts to new group: %w", err) + } + group.AccountCount = int64(len(accountIDsToCopy)) + } + return group, nil } @@ -728,6 +773,54 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd if err := s.groupRepo.Update(ctx, group); err != nil { return nil, err } + + // 如果指定了复制账号的源分组,同步绑定(替换当前分组的账号) + if len(input.CopyAccountsFromGroupIDs) > 0 { + // 去重源分组 IDs + seen := make(map[int64]struct{}) + uniqueSourceGroupIDs := make([]int64, 0, len(input.CopyAccountsFromGroupIDs)) + for _, srcGroupID := range input.CopyAccountsFromGroupIDs { + // 校验:源分组不能是自身 + if srcGroupID == id { + return nil, fmt.Errorf("cannot copy accounts from self") + } + // 去重 + if _, exists := seen[srcGroupID]; !exists { + seen[srcGroupID] = struct{}{} + uniqueSourceGroupIDs = append(uniqueSourceGroupIDs, srcGroupID) + } + } + + // 校验源分组的平台是否与当前分组一致 + for _, srcGroupID := range uniqueSourceGroupIDs { + srcGroup, err := s.groupRepo.GetByIDLite(ctx, srcGroupID) + if err != nil { + return nil, fmt.Errorf("source group %d not found: %w", srcGroupID, err) + } + if srcGroup.Platform != group.Platform { + return nil, fmt.Errorf("source group %d platform mismatch: expected %s, got %s", srcGroupID, group.Platform, srcGroup.Platform) + } + } + + // 获取所有源分组的账号(去重) + accountIDsToCopy, err := s.groupRepo.GetAccountIDsByGroupIDs(ctx, uniqueSourceGroupIDs) + if err != nil { + return nil, fmt.Errorf("failed to get accounts from source groups: %w", err) + } + + // 先清空当前分组的所有账号绑定 + if _, err := s.groupRepo.DeleteAccountGroupsByGroupID(ctx, id); err != nil { + return nil, fmt.Errorf("failed to clear existing account bindings: %w", err) + } + + // 再绑定源分组的账号 + if len(accountIDsToCopy) > 0 { + if err := s.groupRepo.BindAccountsToGroup(ctx, id, accountIDsToCopy); err != nil { + return nil, fmt.Errorf("failed to bind accounts to group: %w", err) + } + } + } + if s.authCacheInvalidator != nil { s.authCacheInvalidator.InvalidateAuthCacheByGroupID(ctx, id) } diff --git a/backend/internal/service/group_service.go b/backend/internal/service/group_service.go index 324f347b..a2bf2073 100644 --- a/backend/internal/service/group_service.go +++ b/backend/internal/service/group_service.go @@ -29,6 +29,10 @@ type GroupRepository interface { ExistsByName(ctx context.Context, name string) (bool, error) GetAccountCount(ctx context.Context, groupID int64) (int64, error) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) + // GetAccountIDsByGroupIDs 获取多个分组的所有账号 ID(去重) + GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) + // BindAccountsToGroup 将多个账号绑定到指定分组 + BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error } // CreateGroupRequest 创建分组请求 diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 1d53ddb6..7c4df36b 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1004,6 +1004,14 @@ export default { fallbackHint: 'Non-Claude Code requests will use this group. Leave empty to reject directly.', noFallback: 'No Fallback (Reject)' }, + copyAccounts: { + title: 'Copy Accounts from Groups', + tooltip: 'Select one or more groups of the same platform. After creation, all accounts from these groups will be automatically bound to the new group (deduplicated).', + tooltipEdit: 'Select one or more groups of the same platform. After saving, current group accounts will be replaced with accounts from these groups (deduplicated).', + selectPlaceholder: 'Select groups to copy accounts from...', + hint: 'Multiple groups can be selected, accounts will be deduplicated', + hintEdit: '⚠️ Warning: This will replace all existing account bindings' + }, modelRouting: { title: 'Model Routing', tooltip: 'Configure specific model requests to be routed to designated accounts. Supports wildcard matching, e.g., claude-opus-* matches all opus models.', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index a0ed426e..ba1c775f 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -1079,6 +1079,14 @@ export default { fallbackHint: '非 Claude Code 请求将使用此分组,留空则直接拒绝', noFallback: '不降级(直接拒绝)' }, + copyAccounts: { + title: '从分组复制账号', + tooltip: '选择一个或多个相同平台的分组,创建后会自动将这些分组的所有账号绑定到新分组(去重)。', + tooltipEdit: '选择一个或多个相同平台的分组,保存后当前分组的账号会被替换为这些分组的账号(去重)。', + selectPlaceholder: '选择分组以复制其账号...', + hint: '可选多个分组,账号会自动去重', + hintEdit: '⚠️ 注意:这会替换当前分组的所有账号绑定' + }, modelRouting: { title: '模型路由配置', tooltip: '配置特定模型请求优先路由到指定账号。支持通配符匹配,如 claude-opus-* 匹配所有 opus 模型。', diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 9802d5c8..7c6cbf52 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -411,6 +411,8 @@ export interface CreateGroupRequest { image_price_4k?: number | null claude_code_only?: boolean fallback_group_id?: number | null + // 从指定分组复制账号 + copy_accounts_from_group_ids?: number[] } export interface UpdateGroupRequest { @@ -429,6 +431,7 @@ export interface UpdateGroupRequest { image_price_4k?: number | null claude_code_only?: boolean fallback_group_id?: number | null + copy_accounts_from_group_ids?: number[] } // ==================== Account & Proxy Types ==================== diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 78ef2e48..bf924f53 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -240,9 +240,73 @@ v-model="createForm.platform" :options="platformOptions" data-tour="group-form-platform" + @change="createForm.copy_accounts_from_group_ids = []" />

{{ t('admin.groups.platformHint') }}

+ +
+
+ +
+ +
+
+

+ {{ t('admin.groups.copyAccounts.tooltip') }} +

+
+
+
+
+
+ +
+ + {{ copyAccountsGroupOptions.find(o => o.value === groupId)?.label || `#${groupId}` }} + + +
+ + +

{{ t('admin.groups.copyAccounts.hint') }}

+

{{ t('admin.groups.platformNotEditable') }}

+ +
+
+ +
+ +
+
+

+ {{ t('admin.groups.copyAccounts.tooltipEdit') }} +

+
+
+
+
+
+ +
+ + {{ copyAccountsGroupOptionsForEdit.find(o => o.value === groupId)?.label || `#${groupId}` }} + + +
+ + +

{{ t('admin.groups.copyAccounts.hintEdit') }}

+
{ return options }) +// 复制账号的源分组选项(创建时)- 仅包含相同平台且有账号的分组 +const copyAccountsGroupOptions = computed(() => { + const eligibleGroups = groups.value.filter( + (g) => g.platform === createForm.platform && (g.account_count || 0) > 0 + ) + return eligibleGroups.map((g) => ({ + value: g.id, + label: `${g.name} (${g.account_count || 0} 个账号)` + })) +}) + +// 复制账号的源分组选项(编辑时)- 仅包含相同平台且有账号的分组,排除自身 +const copyAccountsGroupOptionsForEdit = computed(() => { + const currentId = editingGroup.value?.id + const eligibleGroups = groups.value.filter( + (g) => g.platform === editForm.platform && (g.account_count || 0) > 0 && g.id !== currentId + ) + return eligibleGroups.map((g) => ({ + value: g.id, + label: `${g.name} (${g.account_count || 0} 个账号)` + })) +}) + const groups = ref([]) const loading = ref(false) const searchQuery = ref('') @@ -1244,7 +1394,9 @@ const createForm = reactive({ claude_code_only: false, fallback_group_id: null as number | null, // 模型路由开关 - model_routing_enabled: false + model_routing_enabled: false, + // 从分组复制账号 + copy_accounts_from_group_ids: [] as number[] }) // 简单账号类型(用于模型路由选择) @@ -1415,7 +1567,9 @@ const editForm = reactive({ claude_code_only: false, fallback_group_id: null as number | null, // 模型路由开关 - model_routing_enabled: false + model_routing_enabled: false, + // 从分组复制账号 + copy_accounts_from_group_ids: [] as number[] }) // 根据分组类型返回不同的删除确认消息 @@ -1497,6 +1651,7 @@ const closeCreateModal = () => { createForm.image_price_4k = null createForm.claude_code_only = false createForm.fallback_group_id = null + createForm.copy_accounts_from_group_ids = [] createModelRoutingRules.value = [] } @@ -1547,6 +1702,7 @@ const handleEdit = async (group: AdminGroup) => { editForm.claude_code_only = group.claude_code_only || false editForm.fallback_group_id = group.fallback_group_id editForm.model_routing_enabled = group.model_routing_enabled || false + editForm.copy_accounts_from_group_ids = [] // 复制账号字段每次编辑时重置为空 // 加载模型路由规则(异步加载账号名称) editModelRoutingRules.value = await convertApiFormatToRoutingRules(group.model_routing) showEditModal.value = true @@ -1556,6 +1712,7 @@ const closeEditModal = () => { showEditModal.value = false editingGroup.value = null editModelRoutingRules.value = [] + editForm.copy_accounts_from_group_ids = [] } const handleUpdateGroup = async () => { From ce1d2904c7a049ceba8717bd43954addc95b90ad Mon Sep 17 00:00:00 2001 From: liuxiongfeng Date: Mon, 2 Feb 2026 22:01:41 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=E4=B8=BA=E6=B5=8B=E8=AF=95=20stub?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E7=BC=BA=E5=A4=B1=E7=9A=84=20GroupReposit?= =?UTF-8?q?ory=20=E6=8E=A5=E5=8F=A3=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 BindAccountsToGroup 和 GetAccountIDsByGroupIDs 方法的 stub 实现, 确保测试文件中的 mock 类型满足 GroupRepository 接口要求。 --- backend/internal/repository/group_repo.go | 2 +- backend/internal/server/api_contract_test.go | 8 ++++++++ .../service/admin_service_delete_test.go | 8 ++++++++ .../internal/service/admin_service_group_test.go | 16 ++++++++++++++++ .../service/gateway_multiplatform_test.go | 8 ++++++++ .../service/gemini_multiplatform_test.go | 8 ++++++++ 6 files changed, 49 insertions(+), 1 deletion(-) diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index 002e07da..a5b0512d 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -440,7 +440,7 @@ func (r *groupRepository) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs if err != nil { return nil, err } - defer rows.Close() + defer func() { _ = rows.Close() }() var accountIDs []int64 for rows.Next() { diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 22e6213e..6adab853 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -880,6 +880,14 @@ func (stubGroupRepo) DeleteAccountGroupsByGroupID(ctx context.Context, groupID i return 0, errors.New("not implemented") } +func (stubGroupRepo) BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error { + return errors.New("not implemented") +} + +func (stubGroupRepo) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) { + return nil, errors.New("not implemented") +} + type stubAccountRepo struct { bulkUpdateIDs []int64 } diff --git a/backend/internal/service/admin_service_delete_test.go b/backend/internal/service/admin_service_delete_test.go index 6472ccbb..923d33ab 100644 --- a/backend/internal/service/admin_service_delete_test.go +++ b/backend/internal/service/admin_service_delete_test.go @@ -164,6 +164,14 @@ func (s *groupRepoStub) DeleteAccountGroupsByGroupID(ctx context.Context, groupI panic("unexpected DeleteAccountGroupsByGroupID call") } +func (s *groupRepoStub) BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error { + panic("unexpected BindAccountsToGroup call") +} + +func (s *groupRepoStub) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) { + panic("unexpected GetAccountIDsByGroupIDs call") +} + type proxyRepoStub struct { deleteErr error countErr error diff --git a/backend/internal/service/admin_service_group_test.go b/backend/internal/service/admin_service_group_test.go index e0574e2e..1daee89f 100644 --- a/backend/internal/service/admin_service_group_test.go +++ b/backend/internal/service/admin_service_group_test.go @@ -108,6 +108,14 @@ func (s *groupRepoStubForAdmin) DeleteAccountGroupsByGroupID(_ context.Context, panic("unexpected DeleteAccountGroupsByGroupID call") } +func (s *groupRepoStubForAdmin) BindAccountsToGroup(_ context.Context, _ int64, _ []int64) error { + panic("unexpected BindAccountsToGroup call") +} + +func (s *groupRepoStubForAdmin) GetAccountIDsByGroupIDs(_ context.Context, _ []int64) ([]int64, error) { + panic("unexpected GetAccountIDsByGroupIDs call") +} + // TestAdminService_CreateGroup_WithImagePricing 测试创建分组时 ImagePrice 字段正确传递 func TestAdminService_CreateGroup_WithImagePricing(t *testing.T) { repo := &groupRepoStubForAdmin{} @@ -378,3 +386,11 @@ func (s *groupRepoStubForFallbackCycle) GetAccountCount(_ context.Context, _ int func (s *groupRepoStubForFallbackCycle) DeleteAccountGroupsByGroupID(_ context.Context, _ int64) (int64, error) { panic("unexpected DeleteAccountGroupsByGroupID call") } + +func (s *groupRepoStubForFallbackCycle) BindAccountsToGroup(_ context.Context, _ int64, _ []int64) error { + panic("unexpected BindAccountsToGroup call") +} + +func (s *groupRepoStubForFallbackCycle) GetAccountIDsByGroupIDs(_ context.Context, _ []int64) ([]int64, error) { + panic("unexpected GetAccountIDsByGroupIDs call") +} diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go index 26eb24e4..4bfa23d1 100644 --- a/backend/internal/service/gateway_multiplatform_test.go +++ b/backend/internal/service/gateway_multiplatform_test.go @@ -266,6 +266,14 @@ func (m *mockGroupRepoForGateway) DeleteAccountGroupsByGroupID(ctx context.Conte return 0, nil } +func (m *mockGroupRepoForGateway) BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error { + return nil +} + +func (m *mockGroupRepoForGateway) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) { + return nil, nil +} + func ptr[T any](v T) *T { return &v } diff --git a/backend/internal/service/gemini_multiplatform_test.go b/backend/internal/service/gemini_multiplatform_test.go index c63a020c..e7ed80fd 100644 --- a/backend/internal/service/gemini_multiplatform_test.go +++ b/backend/internal/service/gemini_multiplatform_test.go @@ -218,6 +218,14 @@ func (m *mockGroupRepoForGemini) DeleteAccountGroupsByGroupID(ctx context.Contex return 0, nil } +func (m *mockGroupRepoForGemini) BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error { + return nil +} + +func (m *mockGroupRepoForGemini) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) { + return nil, nil +} + var _ GroupRepository = (*mockGroupRepoForGemini)(nil) // mockGatewayCacheForGemini Gemini 测试用的 cache mock