feat(keys): 新增 Provider Keys 批量删除 API

- 后端新增 POST /keys/batch-delete 接口,支持一次删除最多 100 个 Key
- 按 provider_id 聚合执行副作用,避免逐个删除导致的重复 Redis 操作
- 前端批量操作对话框中删除操作改用批量 API,按 100 个一批分批调用
This commit is contained in:
fawney19
2026-03-07 03:49:44 +08:00
parent 7b0908cd87
commit bb268310e2
5 changed files with 136 additions and 5 deletions

View File

@@ -83,6 +83,20 @@ export async function deleteEndpointKey(keyId: string): Promise<{ message: strin
return response.data
}
/**
* 批量删除 Keys
*/
export interface BatchDeleteKeysResult {
success_count: number
failed_count: number
failed: Array<{ id: string; error: string }>
}
export async function batchDeleteEndpointKeys(ids: string[]): Promise<BatchDeleteKeysResult> {
const response = await client.post('/api/admin/endpoints/keys/batch-delete', { ids })
return response.data
}
// ========== Provider 级别的 Keys API ==========

View File

@@ -267,7 +267,7 @@ import { useToast } from '@/composables/useToast'
import { useConfirm } from '@/composables/useConfirm'
import { parseApiError } from '@/utils/errorParser'
import { listPoolKeys, type PoolKeyDetail } from '@/api/endpoints/pool'
import { deleteEndpointKey, refreshProviderQuota, updateProviderKey } from '@/api/endpoints/keys'
import { batchDeleteEndpointKeys, refreshProviderQuota, updateProviderKey } from '@/api/endpoints/keys'
import { refreshProviderOAuth } from '@/api/endpoints/provider_oauth'
import { useProxyNodesStore } from '@/stores/proxy-nodes'
import { hasNoFiveHourLimit as hasNoFiveHourLimitByQuota, hasNoWeeklyLimit as hasNoWeeklyLimitByQuota } from '@/features/pool/utils/quota-selectors'
@@ -617,14 +617,34 @@ async function executeAction(): Promise<void> {
failedCount += batch.length
}
progressDone.value = Math.min(i + BATCH_SIZE, targetIds.length)
}
} else if (selectedAction.value === 'delete') {
// 使用批量删除 API按 100 个一批分批调用(后端限制 max_length=100
const targetIds = selectedKeys.map((key) => key.key_id)
const BATCH_SIZE = 100
const totalBatches = Math.ceil(targetIds.length / BATCH_SIZE)
for (let i = 0; i < targetIds.length; i += BATCH_SIZE) {
const batchIndex = Math.floor(i / BATCH_SIZE) + 1
const batch = targetIds.slice(i, i + BATCH_SIZE)
if (totalBatches > 1) {
progressLabel.value = `正在${actionLabel}...(第 ${batchIndex}/${totalBatches} 批)`
}
try {
const result = await batchDeleteEndpointKeys(batch)
successCount += result.success_count
failedCount += result.failed_count
} catch {
failedCount += batch.length
}
progressDone.value = Math.min(i + BATCH_SIZE, targetIds.length)
}
} else {
const CONCURRENCY = props.batchConcurrency || 8
const taskForKey = (key: PoolKeyDetail): (() => Promise<'success' | 'skip'>) | null => {
if (selectedAction.value === 'delete') {
return () => deleteEndpointKey(key.key_id).then(() => 'success' as const)
}
if (selectedAction.value === 'refresh_oauth') {
if (normalizeText(key.auth_type) !== 'oauth') return null
return () => refreshProviderOAuth(key.key_id).then(() => 'success' as const)