feat(admin): 用户管理新增分组列、分组筛选与专属分组一键替换
- 新增分组列:展示用户的专属/公开分组,支持 hover 查看详情 - 新增分组筛选:下拉选择或模糊搜索分组名过滤用户 - 专属分组替换:点击专属分组弹出操作菜单,选择目标分组后 自动授予新分组权限、迁移绑定的 Key、移除旧分组权限 - 后端新增 POST /admin/users/:id/replace-group 端点,事务内 完成分组替换并失效认证缓存
This commit is contained in:
@@ -50,6 +50,9 @@ type AdminService interface {
|
||||
// API Key management (admin)
|
||||
AdminUpdateAPIKeyGroupID(ctx context.Context, keyID int64, groupID *int64) (*AdminUpdateAPIKeyGroupIDResult, error)
|
||||
|
||||
// ReplaceUserGroup 替换用户的专属分组:授予新分组权限、迁移 Key、移除旧分组权限
|
||||
ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*ReplaceUserGroupResult, error)
|
||||
|
||||
// Account management
|
||||
ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64) ([]Account, int64, error)
|
||||
GetAccount(ctx context.Context, id int64) (*Account, error)
|
||||
@@ -270,6 +273,11 @@ type AdminUpdateAPIKeyGroupIDResult struct {
|
||||
GrantedGroupName string // the group name that was auto-granted
|
||||
}
|
||||
|
||||
// ReplaceUserGroupResult 分组替换操作的结果
|
||||
type ReplaceUserGroupResult struct {
|
||||
MigratedKeys int64 // 迁移的 Key 数量
|
||||
}
|
||||
|
||||
// BulkUpdateAccountsResult is the aggregated response for bulk updates.
|
||||
type BulkUpdateAccountsResult struct {
|
||||
Success int `json:"success"`
|
||||
@@ -1377,6 +1385,71 @@ func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID i
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ReplaceUserGroup 替换用户的专属分组
|
||||
func (s *adminServiceImpl) ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*ReplaceUserGroupResult, error) {
|
||||
if oldGroupID == newGroupID {
|
||||
return nil, infraerrors.BadRequest("SAME_GROUP", "old and new group must be different")
|
||||
}
|
||||
|
||||
// 验证新分组存在且为活跃的专属标准分组
|
||||
newGroup, err := s.groupRepo.GetByID(ctx, newGroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if newGroup.Status != StatusActive {
|
||||
return nil, infraerrors.BadRequest("GROUP_NOT_ACTIVE", "target group is not active")
|
||||
}
|
||||
if !newGroup.IsExclusive {
|
||||
return nil, infraerrors.BadRequest("GROUP_NOT_EXCLUSIVE", "target group is not exclusive")
|
||||
}
|
||||
if newGroup.IsSubscriptionType() {
|
||||
return nil, infraerrors.BadRequest("GROUP_IS_SUBSCRIPTION", "subscription groups are not supported for replacement")
|
||||
}
|
||||
|
||||
// 事务保证原子性
|
||||
if s.entClient == nil {
|
||||
return nil, fmt.Errorf("entClient is nil, cannot perform group replacement")
|
||||
}
|
||||
tx, err := s.entClient.Tx(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin transaction: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
opCtx := dbent.NewTxContext(ctx, tx)
|
||||
|
||||
// 1. 授予新分组权限
|
||||
if err := s.userRepo.AddGroupToAllowedGroups(opCtx, userID, newGroupID); err != nil {
|
||||
return nil, fmt.Errorf("add new group to allowed groups: %w", err)
|
||||
}
|
||||
|
||||
// 2. 迁移绑定旧分组的 Key 到新分组
|
||||
migrated, err := s.apiKeyRepo.UpdateGroupIDByUserAndGroup(opCtx, userID, oldGroupID, newGroupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migrate api keys: %w", err)
|
||||
}
|
||||
|
||||
// 3. 移除旧分组权限
|
||||
if err := s.userRepo.RemoveGroupFromUserAllowedGroups(opCtx, userID, oldGroupID); err != nil {
|
||||
return nil, fmt.Errorf("remove old group from allowed groups: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("commit transaction: %w", err)
|
||||
}
|
||||
|
||||
// 失效该用户所有 Key 的认证缓存
|
||||
if s.authCacheInvalidator != nil {
|
||||
keys, keyErr := s.apiKeyRepo.ListKeysByUserID(ctx, userID)
|
||||
if keyErr == nil {
|
||||
for _, k := range keys {
|
||||
s.authCacheInvalidator.InvalidateAuthCacheByKey(ctx, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ReplaceUserGroupResult{MigratedKeys: migrated}, nil
|
||||
}
|
||||
|
||||
// Account management implementations
|
||||
func (s *adminServiceImpl) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64) ([]Account, int64, error) {
|
||||
params := pagination.PaginationParams{Page: page, PageSize: pageSize}
|
||||
|
||||
Reference in New Issue
Block a user