feat(retention): 删除用户/Key 时保留历史记录,外键改 SET NULL 并添加名称快照

- Usage/RequestCandidate/VideoTask/Stats 等表的 user_id/api_key_id 外键从
  CASCADE 改为 SET NULL,删除用户或 Key 后历史记录不再丢失
- 各表添加 username/api_key_name 快照字段,删除后仍可追溯归属
- 新增 bulk_cleanup 模块,分批置空大表外键避免长事务锁
- 删除用户/Key 流程集成预清理步骤,先置空再删除
- 精简 candidate_builder 冗余 debug 日志
- 修复 proxy_nodes 启动日志 format 占位符错误({} -> %s)
- 前端批量操作请求增加 5 分钟超时配置
This commit is contained in:
fawney19
2026-03-08 14:31:15 +08:00
parent 25c33846be
commit bd3f73c2fc
26 changed files with 799 additions and 96 deletions

View File

@@ -23,6 +23,7 @@ from src.database import get_db
from src.models.api import CreateApiKeyRequest
from src.models.database import ApiKey, Wallet
from src.services.user.apikey import ApiKeyService
from src.services.user.bulk_cleanup import pre_clean_api_key
from src.services.wallet import WalletService
# 应用时区配置,默认为 Asia/Shanghai
@@ -349,10 +350,7 @@ class AdminCreateStandaloneKeyAdapter(AdminApiAdapter):
db = context.db
# 独立Key支持无限制额度initial_balance_usd = null
if (
self.key_data.initial_balance_usd is not None
and self.key_data.initial_balance_usd <= 0
):
if self.key_data.initial_balance_usd is not None and self.key_data.initial_balance_usd <= 0:
raise HTTPException(
status_code=400,
detail="创建独立余额Key时初始余额必须大于 0或设置为 null 表示无限制)",
@@ -568,6 +566,7 @@ class AdminDeleteApiKeyAdapter(AdminApiAdapter):
raise InvalidRequestException("仅支持删除独立密钥")
user = api_key.user
pre_clean_api_key(db, api_key.id)
db.delete(api_key)
db.commit()

View File

@@ -21,6 +21,7 @@ from src.models.api import CreateApiKeyRequest, CreateUserRequest
from src.models.database import ApiKey, User, UserRole
from src.services.system.config import SystemConfigService
from src.services.user.apikey import ApiKeyService
from src.services.user.bulk_cleanup import pre_clean_api_key
from src.services.user.service import UserService
from src.services.wallet import WalletService
from src.utils.cache_decorator import cache_result
@@ -584,6 +585,7 @@ class AdminDeleteUserKeyAdapter(AdminApiAdapter):
if not api_key:
raise NotFoundException("API Key不存在或不属于该用户", "api_key")
pre_clean_api_key(db, api_key.id)
db.delete(api_key)
db.commit()

View File

@@ -634,6 +634,8 @@ class GeminiVeoHandler(VideoHandlerBase):
external_task_id=external_task_id,
user_id=self.user.id,
api_key_id=self.api_key.id,
username=self.user.username,
api_key_name=self.api_key.name,
provider_id=candidate.provider.id,
endpoint_id=candidate.endpoint.id,
key_id=candidate.key.id,

View File

@@ -887,6 +887,8 @@ class OpenAIVideoHandler(VideoHandlerBase):
external_task_id=external_task_id,
user_id=self.user.id,
api_key_id=self.api_key.id,
username=self.user.username,
api_key_name=self.api_key.name,
provider_id=candidate.provider.id,
endpoint_id=candidate.endpoint.id,
key_id=candidate.key.id,
@@ -1049,6 +1051,8 @@ class OpenAIVideoHandler(VideoHandlerBase):
external_task_id=None,
user_id=self.user.id,
api_key_id=self.api_key.id,
username=self.user.username,
api_key_name=self.api_key.name,
provider_id=None,
endpoint_id=None,
key_id=None,

View File

@@ -46,6 +46,7 @@ from src.models.database import (
from src.services.system.time_range import TimeRangeParams
from src.services.usage.service import UsageService
from src.services.user.apikey import ApiKeyService
from src.services.user.bulk_cleanup import pre_clean_api_key
from src.services.user.preference import PreferenceService
from src.services.wallet import WalletService
from src.utils.cache_decorator import cache_result
@@ -731,6 +732,7 @@ class DeleteMyApiKeyAdapter(AuthenticatedApiAdapter):
raise NotFoundException("API密钥不存在", "api_key")
if api_key.is_locked:
raise ForbiddenException("该密钥已被管理员锁定,无法删除")
pre_clean_api_key(context.db, api_key.id)
context.db.delete(api_key)
context.db.commit()
return {"message": "API密钥已删除"}