feat(admin): 完整实现管理员修改用户 API Key 分组的功能
## 核心功能 - 添加 AdminUpdateAPIKeyGroupID 服务方法,支持绑定/解绑/保持不变三态语义 - 实现 UserRepository.AddGroupToAllowedGroups 接口,自动同步专属分组权限 - 添加 HTTP PUT /api-keys/:id handler 端点,支持管理员直接修改 API Key 分组 ## 事务一致性 - 使用 ent Tx 保证专属分组绑定时「添加权限」和「更新 Key」的原子性 - Repository 方法支持 clientFromContext,兼容事务内调用 - 事务失败时自动回滚,避免权限孤立 ## 业务逻辑 - 订阅类型分组阻断,需通过订阅管理流程 - 非活跃分组拒绝绑定 - 负 ID 和非法 ID 验证 - 自动授权响应,告知管理员成功授权的分组 ## 代码质量 - 16 个单元测试覆盖所有业务路径和边界用例 - 7 个 handler 集成测试覆盖 HTTP 层 - GroupRepo stub 返回克隆副本,防止测试间数据泄漏 - API 类型安全修复(PaginatedResponse<ApiKey>) - 前端 ref 回调类型对齐 Vue 规范 ## 国际化支持 - 中英文提示信息完整 - 自动授权成功/失败提示
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
@@ -44,7 +45,7 @@ type AdminService interface {
|
||||
UpdateGroupSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error
|
||||
|
||||
// API Key management (admin)
|
||||
AdminUpdateAPIKeyGroupID(ctx context.Context, keyID int64, groupID *int64) (*APIKey, error)
|
||||
AdminUpdateAPIKeyGroupID(ctx context.Context, keyID int64, groupID *int64) (*AdminUpdateAPIKeyGroupIDResult, error)
|
||||
|
||||
// Account management
|
||||
ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64) ([]Account, int64, error)
|
||||
@@ -246,6 +247,14 @@ type BulkUpdateAccountResult struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// AdminUpdateAPIKeyGroupIDResult is the result of AdminUpdateAPIKeyGroupID.
|
||||
type AdminUpdateAPIKeyGroupIDResult struct {
|
||||
APIKey *APIKey
|
||||
AutoGrantedGroupAccess bool // true if a new exclusive group permission was auto-added
|
||||
GrantedGroupID *int64 // the group ID that was auto-granted
|
||||
GrantedGroupName string // the group name that was auto-granted
|
||||
}
|
||||
|
||||
// BulkUpdateAccountsResult is the aggregated response for bulk updates.
|
||||
type BulkUpdateAccountsResult struct {
|
||||
Success int `json:"success"`
|
||||
@@ -410,6 +419,7 @@ type adminServiceImpl struct {
|
||||
proxyProber ProxyExitInfoProber
|
||||
proxyLatencyCache ProxyLatencyCache
|
||||
authCacheInvalidator APIKeyAuthCacheInvalidator
|
||||
entClient *dbent.Client // 用于开启数据库事务
|
||||
}
|
||||
|
||||
type userGroupRateBatchReader interface {
|
||||
@@ -434,6 +444,7 @@ func NewAdminService(
|
||||
proxyProber ProxyExitInfoProber,
|
||||
proxyLatencyCache ProxyLatencyCache,
|
||||
authCacheInvalidator APIKeyAuthCacheInvalidator,
|
||||
entClient *dbent.Client,
|
||||
) AdminService {
|
||||
return &adminServiceImpl{
|
||||
userRepo: userRepo,
|
||||
@@ -448,6 +459,7 @@ func NewAdminService(
|
||||
proxyProber: proxyProber,
|
||||
proxyLatencyCache: proxyLatencyCache,
|
||||
authCacheInvalidator: authCacheInvalidator,
|
||||
entClient: entClient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1191,7 +1203,7 @@ func (s *adminServiceImpl) UpdateGroupSortOrders(ctx context.Context, updates []
|
||||
|
||||
// AdminUpdateAPIKeyGroupID 管理员修改 API Key 分组绑定
|
||||
// groupID: nil=不修改, 指向0=解绑, 指向正整数=绑定到目标分组
|
||||
func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID int64, groupID *int64) (*APIKey, error) {
|
||||
func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID int64, groupID *int64) (*AdminUpdateAPIKeyGroupIDResult, error) {
|
||||
apiKey, err := s.apiKeyRepo.GetByID(ctx, keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1199,15 +1211,17 @@ func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID i
|
||||
|
||||
if groupID == nil {
|
||||
// nil 表示不修改,直接返回
|
||||
return apiKey, nil
|
||||
return &AdminUpdateAPIKeyGroupIDResult{APIKey: apiKey}, nil
|
||||
}
|
||||
|
||||
if *groupID < 0 {
|
||||
return nil, infraerrors.BadRequest("INVALID_GROUP_ID", "group_id must be non-negative")
|
||||
}
|
||||
|
||||
result := &AdminUpdateAPIKeyGroupIDResult{}
|
||||
|
||||
if *groupID == 0 {
|
||||
// 0 表示解绑分组
|
||||
// 0 表示解绑分组(不修改 user_allowed_groups,避免影响用户其他 Key)
|
||||
apiKey.GroupID = nil
|
||||
apiKey.Group = nil
|
||||
} else {
|
||||
@@ -1219,11 +1233,58 @@ 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")
|
||||
}
|
||||
|
||||
gid := *groupID
|
||||
apiKey.GroupID = &gid
|
||||
apiKey.Group = group
|
||||
|
||||
// 专属标准分组:使用事务保证「添加分组权限」与「更新 API Key」的原子性
|
||||
if group.IsExclusive {
|
||||
opCtx := ctx
|
||||
var tx *dbent.Tx
|
||||
if s.entClient == nil {
|
||||
logger.LegacyPrintf("service.admin", "Warning: entClient is nil, skipping transaction protection for exclusive group binding")
|
||||
} else {
|
||||
var txErr error
|
||||
tx, txErr = s.entClient.Tx(ctx)
|
||||
if txErr != nil {
|
||||
return nil, fmt.Errorf("begin transaction: %w", txErr)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
opCtx = dbent.NewTxContext(ctx, tx)
|
||||
}
|
||||
|
||||
if addErr := s.userRepo.AddGroupToAllowedGroups(opCtx, apiKey.UserID, gid); addErr != nil {
|
||||
return nil, fmt.Errorf("add group to user allowed groups: %w", addErr)
|
||||
}
|
||||
if err := s.apiKeyRepo.Update(opCtx, apiKey); err != nil {
|
||||
return nil, fmt.Errorf("update api key: %w", err)
|
||||
}
|
||||
if tx != nil {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("commit transaction: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
result.AutoGrantedGroupAccess = true
|
||||
result.GrantedGroupID = &gid
|
||||
result.GrantedGroupName = group.Name
|
||||
|
||||
// 失效认证缓存(在事务提交后执行)
|
||||
if s.authCacheInvalidator != nil {
|
||||
s.authCacheInvalidator.InvalidateAuthCacheByKey(ctx, apiKey.Key)
|
||||
}
|
||||
|
||||
result.APIKey = apiKey
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 非专属分组 / 解绑:无需事务,单步更新即可
|
||||
if err := s.apiKeyRepo.Update(ctx, apiKey); err != nil {
|
||||
return nil, fmt.Errorf("update api key: %w", err)
|
||||
}
|
||||
@@ -1233,7 +1294,8 @@ func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID i
|
||||
s.authCacheInvalidator.InvalidateAuthCacheByKey(ctx, apiKey.Key)
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
result.APIKey = apiKey
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Account management implementations
|
||||
|
||||
Reference in New Issue
Block a user