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:
yangjianbo
2025-12-29 14:06:38 +08:00
parent 89b1b744f2
commit e9c755f428
5 changed files with 62 additions and 10 deletions

View File

@@ -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

View File

@@ -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)).

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)
}
}