feat: 优化专属账号删除逻辑

This commit is contained in:
shaw
2025-09-23 15:48:38 +08:00
parent bd091ede61
commit 0ba048aced
3 changed files with 200 additions and 31 deletions

View File

@@ -2256,6 +2256,9 @@ router.delete('/claude-accounts/:accountId', authenticateAdmin, async (req, res)
try { try {
const { accountId } = req.params const { accountId } = req.params
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'claude')
// 获取账户信息以检查是否在分组中 // 获取账户信息以检查是否在分组中
const account = await claudeAccountService.getAccount(accountId) const account = await claudeAccountService.getAccount(accountId)
if (account && account.accountType === 'group') { if (account && account.accountType === 'group') {
@@ -2267,8 +2270,17 @@ router.delete('/claude-accounts/:accountId', authenticateAdmin, async (req, res)
await claudeAccountService.deleteAccount(accountId) await claudeAccountService.deleteAccount(accountId)
logger.success(`🗑️ Admin deleted Claude account: ${accountId}`) let message = 'Claude账号已成功删除'
return res.json({ success: true, message: 'Claude account deleted successfully' }) if (unboundCount > 0) {
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(`🗑️ Admin deleted Claude account: ${accountId}, unbound ${unboundCount} keys`)
return res.json({
success: true,
message,
unboundKeys: unboundCount
})
} catch (error) { } catch (error) {
logger.error('❌ Failed to delete Claude account:', error) logger.error('❌ Failed to delete Claude account:', error)
return res return res
@@ -2634,6 +2646,9 @@ router.delete('/claude-console-accounts/:accountId', authenticateAdmin, async (r
try { try {
const { accountId } = req.params const { accountId } = req.params
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'claude-console')
// 获取账户信息以检查是否在分组中 // 获取账户信息以检查是否在分组中
const account = await claudeConsoleAccountService.getAccount(accountId) const account = await claudeConsoleAccountService.getAccount(accountId)
if (account && account.accountType === 'group') { if (account && account.accountType === 'group') {
@@ -2645,8 +2660,19 @@ router.delete('/claude-console-accounts/:accountId', authenticateAdmin, async (r
await claudeConsoleAccountService.deleteAccount(accountId) await claudeConsoleAccountService.deleteAccount(accountId)
logger.success(`🗑️ Admin deleted Claude Console account: ${accountId}`) let message = 'Claude Console账号已成功删除'
return res.json({ success: true, message: 'Claude Console account deleted successfully' }) if (unboundCount > 0) {
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(
`🗑️ Admin deleted Claude Console account: ${accountId}, unbound ${unboundCount} keys`
)
return res.json({
success: true,
message,
unboundKeys: unboundCount
})
} catch (error) { } catch (error) {
logger.error('❌ Failed to delete Claude Console account:', error) logger.error('❌ Failed to delete Claude Console account:', error)
return res return res
@@ -3028,6 +3054,9 @@ router.delete('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) =>
try { try {
const { accountId } = req.params const { accountId } = req.params
// 尝试自动解绑CCR账户实际上不会绑定API Key但保持代码一致性
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'ccr')
// 获取账户信息以检查是否在分组中 // 获取账户信息以检查是否在分组中
const account = await ccrAccountService.getAccount(accountId) const account = await ccrAccountService.getAccount(accountId)
if (account && account.accountType === 'group') { if (account && account.accountType === 'group') {
@@ -3039,8 +3068,18 @@ router.delete('/ccr-accounts/:accountId', authenticateAdmin, async (req, res) =>
await ccrAccountService.deleteAccount(accountId) await ccrAccountService.deleteAccount(accountId)
let message = 'CCR账号已成功删除'
if (unboundCount > 0) {
// 理论上不会发生,但保持消息格式一致
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(`🗑️ Admin deleted CCR account: ${accountId}`) logger.success(`🗑️ Admin deleted CCR account: ${accountId}`)
return res.json({ success: true, message: 'CCR account deleted successfully' }) return res.json({
success: true,
message,
unboundKeys: unboundCount
})
} catch (error) { } catch (error) {
logger.error('❌ Failed to delete CCR account:', error) logger.error('❌ Failed to delete CCR account:', error)
return res.status(500).json({ error: 'Failed to delete CCR account', message: error.message }) return res.status(500).json({ error: 'Failed to delete CCR account', message: error.message })
@@ -3384,6 +3423,9 @@ router.delete('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res
try { try {
const { accountId } = req.params const { accountId } = req.params
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'bedrock')
const result = await bedrockAccountService.deleteAccount(accountId) const result = await bedrockAccountService.deleteAccount(accountId)
if (!result.success) { if (!result.success) {
@@ -3392,8 +3434,17 @@ router.delete('/bedrock-accounts/:accountId', authenticateAdmin, async (req, res
.json({ error: 'Failed to delete Bedrock account', message: result.error }) .json({ error: 'Failed to delete Bedrock account', message: result.error })
} }
logger.success(`🗑️ Admin deleted Bedrock account: ${accountId}`) let message = 'Bedrock账号已成功删除'
return res.json({ success: true, message: 'Bedrock account deleted successfully' }) if (unboundCount > 0) {
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(`🗑️ Admin deleted Bedrock account: ${accountId}, unbound ${unboundCount} keys`)
return res.json({
success: true,
message,
unboundKeys: unboundCount
})
} catch (error) { } catch (error) {
logger.error('❌ Failed to delete Bedrock account:', error) logger.error('❌ Failed to delete Bedrock account:', error)
return res return res
@@ -3830,6 +3881,9 @@ router.delete('/gemini-accounts/:accountId', authenticateAdmin, async (req, res)
try { try {
const { accountId } = req.params const { accountId } = req.params
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(accountId, 'gemini')
// 获取账户信息以检查是否在分组中 // 获取账户信息以检查是否在分组中
const account = await geminiAccountService.getAccount(accountId) const account = await geminiAccountService.getAccount(accountId)
if (account && account.accountType === 'group') { if (account && account.accountType === 'group') {
@@ -3841,8 +3895,17 @@ router.delete('/gemini-accounts/:accountId', authenticateAdmin, async (req, res)
await geminiAccountService.deleteAccount(accountId) await geminiAccountService.deleteAccount(accountId)
logger.success(`🗑️ Admin deleted Gemini account: ${accountId}`) let message = 'Gemini账号已成功删除'
return res.json({ success: true, message: 'Gemini account deleted successfully' }) if (unboundCount > 0) {
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(`🗑️ Admin deleted Gemini account: ${accountId}, unbound ${unboundCount} keys`)
return res.json({
success: true,
message,
unboundKeys: unboundCount
})
} catch (error) { } catch (error) {
logger.error('❌ Failed to delete Gemini account:', error) logger.error('❌ Failed to delete Gemini account:', error)
return res.status(500).json({ error: 'Failed to delete account', message: error.message }) return res.status(500).json({ error: 'Failed to delete account', message: error.message })
@@ -6692,6 +6755,9 @@ router.delete('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
}) })
} }
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(id, 'openai')
// 如果账户在分组中,从分组中移除 // 如果账户在分组中,从分组中移除
if (account.accountType === 'group') { if (account.accountType === 'group') {
const group = await accountGroupService.getAccountGroup(id) const group = await accountGroupService.getAccountGroup(id)
@@ -6702,11 +6768,19 @@ router.delete('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
await openaiAccountService.deleteAccount(id) await openaiAccountService.deleteAccount(id)
logger.success(`✅ 删除 OpenAI 账户成功: ${account.name} (ID: ${id})`) let message = 'OpenAI账号已成功删除'
if (unboundCount > 0) {
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(
`✅ 删除 OpenAI 账户成功: ${account.name} (ID: ${id}), unbound ${unboundCount} keys`
)
return res.json({ return res.json({
success: true, success: true,
message: '账户删除成功' message,
unboundKeys: unboundCount
}) })
} catch (error) { } catch (error) {
logger.error('删除 OpenAI 账户失败:', error) logger.error('删除 OpenAI 账户失败:', error)
@@ -7055,11 +7129,22 @@ router.delete('/azure-openai-accounts/:id', authenticateAdmin, async (req, res)
try { try {
const { id } = req.params const { id } = req.params
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(id, 'azure_openai')
await azureOpenaiAccountService.deleteAccount(id) await azureOpenaiAccountService.deleteAccount(id)
let message = 'Azure OpenAI账号已成功删除'
if (unboundCount > 0) {
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(`🗑️ Admin deleted Azure OpenAI account: ${id}, unbound ${unboundCount} keys`)
res.json({ res.json({
success: true, success: true,
message: 'Azure OpenAI account deleted successfully' message,
unboundKeys: unboundCount
}) })
} catch (error) { } catch (error) {
logger.error('Failed to delete Azure OpenAI account:', error) logger.error('Failed to delete Azure OpenAI account:', error)
@@ -7424,6 +7509,9 @@ router.delete('/openai-responses-accounts/:id', authenticateAdmin, async (req, r
}) })
} }
// 自动解绑所有绑定的 API Keys
const unboundCount = await apiKeyService.unbindAccountFromAllKeys(id, 'openai-responses')
// 检查是否在分组中 // 检查是否在分组中
const groups = await accountGroupService.getAllGroups() const groups = await accountGroupService.getAllGroups()
for (const group of groups) { for (const group of groups) {
@@ -7434,7 +7522,20 @@ router.delete('/openai-responses-accounts/:id', authenticateAdmin, async (req, r
} }
const result = await openaiResponsesAccountService.deleteAccount(id) const result = await openaiResponsesAccountService.deleteAccount(id)
res.json({ success: true, ...result })
let message = 'OpenAI-Responses账号已成功删除'
if (unboundCount > 0) {
message += `${unboundCount} 个 API Key 已切换为共享池模式`
}
logger.success(`🗑️ Admin deleted OpenAI-Responses account: ${id}, unbound ${unboundCount} keys`)
res.json({
success: true,
...result,
message,
unboundKeys: unboundCount
})
} catch (error) { } catch (error) {
logger.error('Failed to delete OpenAI-Responses account:', error) logger.error('Failed to delete OpenAI-Responses account:', error)
res.status(500).json({ res.status(500).json({

View File

@@ -1328,6 +1328,70 @@ class ApiKeyService {
} }
} }
// 🔓 解绑账号从所有API Keys
async unbindAccountFromAllKeys(accountId, accountType) {
try {
// 账号类型与字段的映射关系
const fieldMap = {
claude: 'claudeAccountId',
'claude-console': 'claudeConsoleAccountId',
gemini: 'geminiAccountId',
openai: 'openaiAccountId',
'openai-responses': 'openaiAccountId', // 特殊处理,带 responses: 前缀
azure_openai: 'azureOpenaiAccountId',
bedrock: 'bedrockAccountId',
ccr: null // CCR 账号没有对应的 API Key 字段
}
const field = fieldMap[accountType]
if (!field) {
logger.info(`账号类型 ${accountType} 不需要解绑 API Key`)
return 0
}
// 获取所有API Keys
const allKeys = await this.getAllApiKeys()
// 筛选绑定到此账号的 API Keys
let boundKeys = []
if (accountType === 'openai-responses') {
// OpenAI-Responses 特殊处理:查找 openaiAccountId 字段中带 responses: 前缀的
boundKeys = allKeys.filter((key) => key.openaiAccountId === `responses:${accountId}`)
} else {
// 其他账号类型正常匹配
boundKeys = allKeys.filter((key) => key[field] === accountId)
}
// 批量解绑
for (const key of boundKeys) {
const updates = {}
if (accountType === 'openai-responses') {
updates.openaiAccountId = null
} else if (accountType === 'claude-console') {
updates.claudeConsoleAccountId = null
} else {
updates[field] = null
}
await this.updateApiKey(key.id, updates)
logger.info(
`✅ 自动解绑 API Key ${key.id} (${key.name}) 从 ${accountType} 账号 ${accountId}`
)
}
if (boundKeys.length > 0) {
logger.success(
`🔓 成功解绑 ${boundKeys.length} 个 API Key 从 ${accountType} 账号 ${accountId}`
)
}
return boundKeys.length
} catch (error) {
logger.error(`❌ 解绑 API Keys 失败 (${accountType} 账号 ${accountId}):`, error)
return 0
}
}
// 🧹 清理过期的API Keys // 🧹 清理过期的API Keys
async cleanupExpiredKeys() { async cleanupExpiredKeys() {
try { try {

View File

@@ -1598,7 +1598,7 @@ const editAccount = (account) => {
// 删除账户 // 删除账户
const deleteAccount = async (account) => { const deleteAccount = async (account) => {
// 检查是否有API Key绑定到此账号 // 检查是否有API Key绑定到此账号
const boundKeysCount = apiKeys.value.filter( const boundKeys = apiKeys.value.filter(
(key) => (key) =>
key.claudeAccountId === account.id || key.claudeAccountId === account.id ||
key.claudeConsoleAccountId === account.id || key.claudeConsoleAccountId === account.id ||
@@ -1606,22 +1606,18 @@ const deleteAccount = async (account) => {
key.openaiAccountId === account.id || key.openaiAccountId === account.id ||
key.azureOpenaiAccountId === account.id || key.azureOpenaiAccountId === account.id ||
key.openaiAccountId === `responses:${account.id}` key.openaiAccountId === `responses:${account.id}`
).length
if (boundKeysCount > 0) {
showToast(
`无法删除此账号,有 ${boundKeysCount} 个API Key绑定到此账号请先解绑所有API Key`,
'error'
)
return
}
const confirmed = await showConfirm(
'删除账户',
`确定要删除账户 "${account.name}" \n\n此操作不可恢复`,
'删除',
'取消'
) )
const boundKeysCount = boundKeys.length
// 构建确认消息
let confirmMessage = `确定要删除账户 "${account.name}" `
if (boundKeysCount > 0) {
confirmMessage += `\n\n⚠ 注意:此账号有 ${boundKeysCount} 个 API Key 绑定。`
confirmMessage += `\n删除后这些 API Key 将自动切换为共享池模式。`
}
confirmMessage += '\n\n此操作不可恢复。'
const confirmed = await showConfirm('删除账户', confirmMessage, '删除', '取消')
if (!confirmed) return if (!confirmed) return
@@ -1648,10 +1644,18 @@ const deleteAccount = async (account) => {
const data = await apiClient.delete(endpoint) const data = await apiClient.delete(endpoint)
if (data.success) { if (data.success) {
showToast('账户已删除', 'success') // 根据解绑结果显示不同的消息
// 清空分组成员缓存因为账户可能从分组中移除 let toastMessage = '账户已成功删除'
if (data.unboundKeys > 0) {
toastMessage += `${data.unboundKeys} 个 API Key 已切换为共享池模式`
}
showToast(toastMessage, 'success')
// 清空相关缓存
groupMembersLoaded.value = false groupMembersLoaded.value = false
apiKeysLoaded.value = false // 重新加载API Keys以反映解绑变化
loadAccounts() loadAccounts()
loadApiKeys(true) // 强制重新加载API Keys
} else { } else {
showToast(data.message || '删除失败', 'error') showToast(data.message || '删除失败', 'error')
} }