From 767a41e263904086ad0bb24fd838d5d4cc16273f Mon Sep 17 00:00:00 2001 From: ischanx Date: Tue, 10 Mar 2026 00:51:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=81=E8=AE=B8=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E4=B8=BA=E6=8C=81=E6=9C=89=E6=9C=89=E6=95=88=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E7=9A=84=E7=94=A8=E6=88=B7=E7=BB=91=E5=AE=9A=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E7=B1=BB=E5=9E=8B=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前管理员无法通过 API 密钥管理将用户绑定到订阅类型分组(直接返回错误)。 现在改为检查用户是否持有该分组的有效订阅,有则允许绑定,无则拒绝。 - admin_service: 新增 userSubRepo 依赖,替换硬拒绝为订阅校验 - admin_service: 区分 ErrSubscriptionNotFound 和内部错误,避免 DB 故障被误报 - wire_gen/api_contract_test: 同步新增参数 - UserApiKeysModal: 管理员分组下拉不再过滤订阅类型分组 Co-Authored-By: Claude Opus 4.6 --- backend/cmd/server/wire_gen.go | 2 +- backend/internal/server/api_contract_test.go | 2 +- backend/internal/service/admin_service.go | 12 ++++++++++-- .../src/components/admin/user/UserApiKeysModal.vue | 3 +-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 7c817e12..8c79d9a4 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -104,7 +104,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { proxyRepository := repository.NewProxyRepository(client, db) proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig) proxyLatencyCache := repository.NewProxyLatencyCache(redisClient) - adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, soraAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService) + adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, soraAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository) concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig) concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig) adminUserHandler := admin.NewUserHandler(adminService, concurrencyService) diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 236bd658..0b36bf66 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -645,7 +645,7 @@ func newContractDeps(t *testing.T) *contractDeps { settingRepo := newStubSettingRepo() settingService := service.NewSettingService(settingRepo, cfg) - adminService := service.NewAdminService(userRepo, groupRepo, &accountRepo, nil, proxyRepo, apiKeyRepo, redeemRepo, nil, nil, nil, nil, nil, nil, nil, nil) + adminService := service.NewAdminService(userRepo, groupRepo, &accountRepo, nil, proxyRepo, apiKeyRepo, redeemRepo, nil, nil, nil, nil, nil, nil, nil, nil, nil) authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService, nil, redeemService, nil) apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService) usageHandler := handler.NewUsageHandler(usageService, apiKeyService) diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 27e2173a..06cde078 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -432,6 +432,7 @@ type adminServiceImpl struct { entClient *dbent.Client // 用于开启数据库事务 settingService *SettingService defaultSubAssigner DefaultSubscriptionAssigner + userSubRepo UserSubscriptionRepository } type userGroupRateBatchReader interface { @@ -459,6 +460,7 @@ func NewAdminService( entClient *dbent.Client, settingService *SettingService, defaultSubAssigner DefaultSubscriptionAssigner, + userSubRepo UserSubscriptionRepository, ) AdminService { return &adminServiceImpl{ userRepo: userRepo, @@ -476,6 +478,7 @@ func NewAdminService( entClient: entClient, settingService: settingService, defaultSubAssigner: defaultSubAssigner, + userSubRepo: userSubRepo, } } @@ -1277,9 +1280,14 @@ func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID i if group.Status != StatusActive { return nil, infraerrors.BadRequest("GROUP_NOT_ACTIVE", "target group is not active") } - // 订阅类型分组:不允许通过此 API 直接绑定,需通过订阅管理流程 + // 订阅类型分组:用户须持有该分组的有效订阅才可绑定 if group.IsSubscriptionType() { - return nil, infraerrors.BadRequest("SUBSCRIPTION_GROUP_NOT_ALLOWED", "subscription groups must be managed through the subscription workflow") + if _, err := s.userSubRepo.GetActiveByUserIDAndGroupID(ctx, apiKey.UserID, *groupID); err != nil { + if errors.Is(err, ErrSubscriptionNotFound) { + return nil, infraerrors.BadRequest("SUBSCRIPTION_REQUIRED", "user does not have an active subscription for this group") + } + return nil, err + } } gid := *groupID diff --git a/frontend/src/components/admin/user/UserApiKeysModal.vue b/frontend/src/components/admin/user/UserApiKeysModal.vue index 7e3c8c25..5e0a0fea 100644 --- a/frontend/src/components/admin/user/UserApiKeysModal.vue +++ b/frontend/src/components/admin/user/UserApiKeysModal.vue @@ -162,8 +162,7 @@ const load = async () => { const loadGroups = async () => { try { const groups = await adminAPI.groups.getAll() - // 过滤掉订阅类型分组(需通过订阅管理流程绑定) - allGroups.value = groups.filter((g) => g.subscription_type !== 'subscription') + allGroups.value = groups } catch (error) { console.error('Failed to load groups:', error) }