perf(后端): 优化删除操作的数据库查询性能
- 新增 ExistsByID 方法用于账号存在性检查,避免加载完整对象 - 新增 GetOwnerID 方法用于 API Key 所有权验证,仅查询 user_id 字段 - 优化 AccountService.Delete 使用轻量级存在性检查 - 优化 ApiKeyService.Delete 使用轻量级权限验证 - 改进前端删除错误提示,显示后端返回的具体错误消息 - 添加详细的中文注释说明优化原因 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,19 @@ func (r *accountRepository) GetByID(ctx context.Context, id int64) (*service.Acc
|
||||
return &accounts[0], nil
|
||||
}
|
||||
|
||||
// ExistsByID 检查指定 ID 的账号是否存在。
|
||||
// 相比 GetByID,此方法性能更优,因为:
|
||||
// - 使用 Exist() 方法生成 SELECT EXISTS 查询,只返回布尔值
|
||||
// - 不加载完整的账号实体及其关联数据(Groups、Proxy 等)
|
||||
// - 适用于删除前的存在性检查等只需判断有无的场景
|
||||
func (r *accountRepository) ExistsByID(ctx context.Context, id int64) (bool, error) {
|
||||
exists, err := r.client.Account.Query().Where(dbaccount.IDEQ(id)).Exist(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (r *accountRepository) GetByCRSAccountID(ctx context.Context, crsAccountID string) (*service.Account, error) {
|
||||
if crsAccountID == "" {
|
||||
return nil, nil
|
||||
|
||||
@@ -49,6 +49,25 @@ func (r *apiKeyRepository) GetByID(ctx context.Context, id int64) (*service.ApiK
|
||||
return apiKeyEntityToService(m), nil
|
||||
}
|
||||
|
||||
// GetOwnerID 根据 API Key ID 获取其所有者(用户)的 ID。
|
||||
// 相比 GetByID,此方法性能更优,因为:
|
||||
// - 使用 Select() 只查询 user_id 字段,减少数据传输量
|
||||
// - 不加载完整的 ApiKey 实体及其关联数据(User、Group 等)
|
||||
// - 适用于权限验证等只需用户 ID 的场景(如删除前的所有权检查)
|
||||
func (r *apiKeyRepository) GetOwnerID(ctx context.Context, id int64) (int64, error) {
|
||||
m, err := r.client.ApiKey.Query().
|
||||
Where(apikey.IDEQ(id)).
|
||||
Select(apikey.FieldUserID).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
if dbent.IsNotFound(err) {
|
||||
return 0, service.ErrApiKeyNotFound
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return m.UserID, nil
|
||||
}
|
||||
|
||||
func (r *apiKeyRepository) GetByKey(ctx context.Context, key string) (*service.ApiKey, error) {
|
||||
m, err := r.client.ApiKey.Query().
|
||||
Where(apikey.KeyEQ(key)).
|
||||
|
||||
@@ -16,6 +16,8 @@ var (
|
||||
type AccountRepository interface {
|
||||
Create(ctx context.Context, account *Account) error
|
||||
GetByID(ctx context.Context, id int64) (*Account, error)
|
||||
// ExistsByID 检查账号是否存在,仅返回布尔值,用于删除前的轻量级存在性检查
|
||||
ExistsByID(ctx context.Context, id int64) (bool, error)
|
||||
// GetByCRSAccountID finds an account previously synced from CRS.
|
||||
// Returns (nil, nil) if not found.
|
||||
GetByCRSAccountID(ctx context.Context, crsAccountID string) (*Account, error)
|
||||
@@ -235,11 +237,17 @@ func (s *AccountService) Update(ctx context.Context, id int64, req UpdateAccount
|
||||
}
|
||||
|
||||
// Delete 删除账号
|
||||
// 优化:使用 ExistsByID 替代 GetByID 进行存在性检查,
|
||||
// 避免加载完整账号对象及其关联数据,提升删除操作的性能
|
||||
func (s *AccountService) Delete(ctx context.Context, id int64) error {
|
||||
// 检查账号是否存在
|
||||
_, err := s.accountRepo.GetByID(ctx, id)
|
||||
// 使用轻量级的存在性检查,而非加载完整账号对象
|
||||
exists, err := s.accountRepo.ExistsByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get account: %w", err)
|
||||
return fmt.Errorf("check account: %w", err)
|
||||
}
|
||||
// 明确返回账号不存在错误,便于调用方区分错误类型
|
||||
if !exists {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
|
||||
if err := s.accountRepo.Delete(ctx, id); err != nil {
|
||||
|
||||
@@ -29,6 +29,8 @@ const (
|
||||
type ApiKeyRepository interface {
|
||||
Create(ctx context.Context, key *ApiKey) error
|
||||
GetByID(ctx context.Context, id int64) (*ApiKey, error)
|
||||
// GetOwnerID 仅获取 API Key 的所有者 ID,用于删除前的轻量级权限验证
|
||||
GetOwnerID(ctx context.Context, id int64) (int64, error)
|
||||
GetByKey(ctx context.Context, key string) (*ApiKey, error)
|
||||
Update(ctx context.Context, key *ApiKey) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
@@ -350,20 +352,23 @@ func (s *ApiKeyService) Update(ctx context.Context, id int64, userID int64, req
|
||||
}
|
||||
|
||||
// Delete 删除API Key
|
||||
// 优化:使用 GetOwnerID 替代 GetByID 进行权限验证,
|
||||
// 避免加载完整 ApiKey 对象及其关联数据(User、Group),提升删除操作的性能
|
||||
func (s *ApiKeyService) Delete(ctx context.Context, id int64, userID int64) error {
|
||||
apiKey, err := s.apiKeyRepo.GetByID(ctx, id)
|
||||
// 仅获取所有者 ID 用于权限验证,而非加载完整对象
|
||||
ownerID, err := s.apiKeyRepo.GetOwnerID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get api key: %w", err)
|
||||
}
|
||||
|
||||
// 验证所有权
|
||||
if apiKey.UserID != userID {
|
||||
// 验证当前用户是否为该 API Key 的所有者
|
||||
if ownerID != userID {
|
||||
return ErrInsufficientPerms
|
||||
}
|
||||
|
||||
// 清除Redis缓存
|
||||
// 清除Redis缓存(使用 ownerID 而非 apiKey.UserID)
|
||||
if s.cache != nil {
|
||||
_ = s.cache.DeleteCreateAttemptCount(ctx, apiKey.UserID)
|
||||
_ = s.cache.DeleteCreateAttemptCount(ctx, ownerID)
|
||||
}
|
||||
|
||||
if err := s.apiKeyRepo.Delete(ctx, id); err != nil {
|
||||
|
||||
@@ -823,6 +823,11 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理删除 API Key 的操作
|
||||
* 优化:错误处理改进,优先显示后端返回的具体错误消息(如权限不足等),
|
||||
* 若后端未返回消息则显示默认的国际化文本
|
||||
*/
|
||||
const handleDelete = async () => {
|
||||
if (!selectedKey.value) return
|
||||
|
||||
@@ -831,8 +836,10 @@ const handleDelete = async () => {
|
||||
appStore.showSuccess(t('keys.keyDeletedSuccess'))
|
||||
showDeleteDialog.value = false
|
||||
loadApiKeys()
|
||||
} catch (error) {
|
||||
appStore.showError(t('keys.failedToDelete'))
|
||||
} catch (error: any) {
|
||||
// 优先使用后端返回的错误消息,提供更具体的错误信息给用户
|
||||
const errorMsg = error?.message || t('keys.failedToDelete')
|
||||
appStore.showError(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user