fix(pool): 优化批量删除性能,使用 SQL 批量删除替代逐条 ORM 删除

- 后端批量删除改用 sa_delete 直接执行 SQL,避免逐条加载和删除
- 前端删除操作批次大小从 2000 减小到 50,防止大批量删除超时
- 增加前端批量操作失败时的错误日志输出
This commit is contained in:
fawney19
2026-03-08 03:08:27 +08:00
parent ef0f8dd4d0
commit 124c4ca403
2 changed files with 90 additions and 70 deletions

View File

@@ -620,9 +620,8 @@ async function executeAction(): Promise<void> {
progressDone.value = Math.min(i + BATCH_SIZE, targetIds.length) progressDone.value = Math.min(i + BATCH_SIZE, targetIds.length)
} }
} else if (['delete', 'enable', 'disable', 'clear_proxy', 'set_proxy'].includes(selectedAction.value)) { } else if (['delete', 'enable', 'disable', 'clear_proxy', 'set_proxy'].includes(selectedAction.value)) {
// 使用 batch-action API每批最多 2000 个
const targetIds = selectedKeys.map((key) => key.key_id) const targetIds = selectedKeys.map((key) => key.key_id)
const BATCH_SIZE = 2000 const BATCH_SIZE = selectedAction.value === 'delete' ? 50 : 2000
const totalBatches = Math.ceil(targetIds.length / BATCH_SIZE) const totalBatches = Math.ceil(targetIds.length / BATCH_SIZE)
for (let i = 0; i < targetIds.length; i += BATCH_SIZE) { for (let i = 0; i < targetIds.length; i += BATCH_SIZE) {
@@ -643,7 +642,9 @@ async function executeAction(): Promise<void> {
...(payload ? { payload } : {}), ...(payload ? { payload } : {}),
}) })
successCount += result.affected successCount += result.affected
} catch { } catch (err) {
// eslint-disable-next-line no-console
console.error(`batch ${selectedAction.value} failed (batch ${batchIndex}/${totalBatches}):`, err)
failedCount += batch.length failedCount += batch.length
} }

View File

@@ -16,7 +16,9 @@ from datetime import datetime, timezone
from typing import Any from typing import Any
from fastapi import APIRouter, Depends, Query, Request from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import case, func from sqlalchemy import case
from sqlalchemy import delete as sa_delete
from sqlalchemy import func
from sqlalchemy.orm import Session, load_only from sqlalchemy.orm import Session, load_only
from src.api.base.admin_adapter import AdminApiAdapter from src.api.base.admin_adapter import AdminApiAdapter
@@ -1005,6 +1007,40 @@ class AdminBatchActionKeysAdapter(AdminApiAdapter):
pid = str(provider.id) pid = str(provider.id)
affected = 0 affected = 0
if self.body.action == "delete":
# 批量 SQL 删除,避免逐条 ORM delete
key_ids = self.body.key_ids
try:
result = db.execute(
sa_delete(ProviderAPIKey).where(
ProviderAPIKey.provider_id == pid,
ProviderAPIKey.id.in_(key_ids),
)
)
affected = result.rowcount # type: ignore[assignment]
db.commit()
except Exception as exc:
db.rollback()
logger.error("batch delete commit failed: {}", exc)
return BatchActionResponse(affected=0, message=f"commit failed: {exc}")
if affected > 0:
from src.services.provider_keys.key_side_effects import (
run_delete_key_side_effects,
)
try:
await run_delete_key_side_effects(
db=db,
provider_id=pid,
deleted_key_allowed_models=None,
)
except Exception as exc:
logger.error("batch delete side effects failed: {}", exc)
admin_name = context.user.username if context.user else "admin"
affected_ids = [kid[:8] for kid in key_ids[:20]]
else:
keys = ( keys = (
db.query(ProviderAPIKey) db.query(ProviderAPIKey)
.filter( .filter(
@@ -1025,10 +1061,6 @@ class AdminBatchActionKeysAdapter(AdminApiAdapter):
key.is_active = False key.is_active = False
affected += 1 affected += 1
elif self.body.action == "delete":
db.delete(key)
affected += 1
elif self.body.action == "clear_cooldown": elif self.body.action == "clear_cooldown":
await pool_redis.clear_cooldown(pid, kid) await pool_redis.clear_cooldown(pid, kid)
affected += 1 affected += 1
@@ -1052,7 +1084,6 @@ class AdminBatchActionKeysAdapter(AdminApiAdapter):
if self.body.action in { if self.body.action in {
"enable", "enable",
"disable", "disable",
"delete",
"regenerate_fingerprint", "regenerate_fingerprint",
"clear_proxy", "clear_proxy",
"set_proxy", "set_proxy",
@@ -1064,17 +1095,8 @@ class AdminBatchActionKeysAdapter(AdminApiAdapter):
logger.error("batch action commit failed: {}", exc) logger.error("batch action commit failed: {}", exc)
return BatchActionResponse(affected=0, message=f"commit failed: {exc}") return BatchActionResponse(affected=0, message=f"commit failed: {exc}")
if self.body.action == "delete" and affected > 0: admin_name = context.user.username if context.user else "admin"
from src.services.provider_keys.key_side_effects import run_delete_key_side_effects affected_ids = [str(k.id)[:8] for k in keys]
try:
await run_delete_key_side_effects(
db=db,
provider_id=pid,
deleted_key_allowed_models=None,
)
except Exception as exc:
logger.error("batch delete side effects failed: {}", exc)
action_labels = { action_labels = {
"enable": "enabled", "enable": "enabled",
@@ -1086,9 +1108,6 @@ class AdminBatchActionKeysAdapter(AdminApiAdapter):
"clear_proxy": "proxy cleared", "clear_proxy": "proxy cleared",
"set_proxy": "proxy set", "set_proxy": "proxy set",
} }
admin_name = context.user.username if context.user else "admin"
affected_ids = [str(k.id)[:8] for k in keys]
logger.info( logger.info(
"Pool batch action by {}: provider={}, action={}, affected={}, key_ids={}", "Pool batch action by {}: provider={}, action={}, affected={}, key_ids={}",
admin_name, admin_name,