diff --git a/src/routes/admin.js b/src/routes/admin.js index 49d43fb2..60381f2d 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -679,6 +679,9 @@ router.put('/api-keys/batch', authenticateAdmin, async (req, res) => { if (updates.tokenLimit !== undefined) { finalUpdates.tokenLimit = updates.tokenLimit } + if (updates.rateLimitCost !== undefined) { + finalUpdates.rateLimitCost = updates.rateLimitCost + } if (updates.concurrencyLimit !== undefined) { finalUpdates.concurrencyLimit = updates.concurrencyLimit } @@ -1125,7 +1128,7 @@ router.get('/api-keys/deleted', authenticateAdmin, async (req, res) => { deletedAt: key.deletedAt, deletedBy: key.deletedBy, deletedByType: key.deletedByType, - canRestore: false // Deleted keys cannot be restored per requirement + canRestore: true // 已删除的API Key可以恢复 })) logger.success(`📋 Admin retrieved ${enrichedKeys.length} deleted API keys`) @@ -1138,6 +1141,123 @@ router.get('/api-keys/deleted', authenticateAdmin, async (req, res) => { } }) +// 🔄 恢复已删除的API Key +router.post('/api-keys/:keyId/restore', authenticateAdmin, async (req, res) => { + try { + const { keyId } = req.params + const adminUsername = req.session?.admin?.username || 'unknown' + + // 调用服务层的恢复方法 + const result = await apiKeyService.restoreApiKey(keyId, adminUsername, 'admin') + + if (result.success) { + logger.success(`✅ Admin ${adminUsername} restored API key: ${keyId}`) + return res.json({ + success: true, + message: 'API Key 已成功恢复', + apiKey: result.apiKey + }) + } else { + return res.status(400).json({ + success: false, + error: 'Failed to restore API key' + }) + } + } catch (error) { + logger.error('❌ Failed to restore API key:', error) + + // 根据错误类型返回适当的响应 + if (error.message === 'API key not found') { + return res.status(404).json({ + success: false, + error: 'API Key 不存在' + }) + } else if (error.message === 'API key is not deleted') { + return res.status(400).json({ + success: false, + error: '该 API Key 未被删除,无需恢复' + }) + } + + return res.status(500).json({ + success: false, + error: '恢复 API Key 失败', + message: error.message + }) + } +}) + +// 🗑️ 彻底删除API Key(物理删除) +router.delete('/api-keys/:keyId/permanent', authenticateAdmin, async (req, res) => { + try { + const { keyId } = req.params + const adminUsername = req.session?.admin?.username || 'unknown' + + // 调用服务层的彻底删除方法 + const result = await apiKeyService.permanentDeleteApiKey(keyId) + + if (result.success) { + logger.success(`🗑️ Admin ${adminUsername} permanently deleted API key: ${keyId}`) + return res.json({ + success: true, + message: 'API Key 已彻底删除' + }) + } + } catch (error) { + logger.error('❌ Failed to permanently delete API key:', error) + + if (error.message === 'API key not found') { + return res.status(404).json({ + success: false, + error: 'API Key 不存在' + }) + } else if (error.message === '只能彻底删除已经删除的API Key') { + return res.status(400).json({ + success: false, + error: '只能彻底删除已经删除的API Key' + }) + } + + return res.status(500).json({ + success: false, + error: '彻底删除 API Key 失败', + message: error.message + }) + } +}) + +// 🧹 清空所有已删除的API Keys +router.delete('/api-keys/deleted/clear-all', authenticateAdmin, async (req, res) => { + try { + const adminUsername = req.session?.admin?.username || 'unknown' + + // 调用服务层的清空方法 + const result = await apiKeyService.clearAllDeletedApiKeys() + + logger.success( + `🧹 Admin ${adminUsername} cleared deleted API keys: ${result.successCount}/${result.total}` + ) + + return res.json({ + success: true, + message: `成功清空 ${result.successCount} 个已删除的 API Keys`, + details: { + total: result.total, + successCount: result.successCount, + failedCount: result.failedCount, + errors: result.errors + } + }) + } catch (error) { + logger.error('❌ Failed to clear all deleted API keys:', error) + return res.status(500).json({ + success: false, + error: '清空已删除的 API Keys 失败', + message: error.message + }) + } +}) + // 👥 账户分组管理 // 创建账户分组 diff --git a/src/services/apiKeyService.js b/src/services/apiKeyService.js index 197f8e78..e2546656 100644 --- a/src/services/apiKeyService.js +++ b/src/services/apiKeyService.js @@ -434,6 +434,139 @@ class ApiKeyService { } } + // 🔄 恢复已删除的API Key + async restoreApiKey(keyId, restoredBy = 'system', restoredByType = 'system') { + try { + const keyData = await redis.getApiKey(keyId) + if (!keyData || Object.keys(keyData).length === 0) { + throw new Error('API key not found') + } + + // 检查是否确实是已删除的key + if (keyData.isDeleted !== 'true') { + throw new Error('API key is not deleted') + } + + // 准备更新的数据 + const updatedData = { ...keyData } + updatedData.isActive = 'true' + updatedData.restoredAt = new Date().toISOString() + updatedData.restoredBy = restoredBy + updatedData.restoredByType = restoredByType + + // 从更新的数据中移除删除相关的字段 + delete updatedData.isDeleted + delete updatedData.deletedAt + delete updatedData.deletedBy + delete updatedData.deletedByType + + // 保存更新后的数据 + await redis.setApiKey(keyId, updatedData) + + // 使用Redis的hdel命令删除不需要的字段 + const keyName = `apikey:${keyId}` + await redis.client.hdel(keyName, 'isDeleted', 'deletedAt', 'deletedBy', 'deletedByType') + + // 重新建立哈希映射(恢复API Key的使用能力) + if (keyData.apiKey) { + await redis.setApiKeyHash(keyData.apiKey, { + id: keyId, + name: keyData.name, + isActive: 'true' + }) + } + + logger.success(`✅ Restored API key: ${keyId} by ${restoredBy} (${restoredByType})`) + + return { success: true, apiKey: updatedData } + } catch (error) { + logger.error('❌ Failed to restore API key:', error) + throw error + } + } + + // 🗑️ 彻底删除API Key(物理删除) + async permanentDeleteApiKey(keyId) { + try { + const keyData = await redis.getApiKey(keyId) + if (!keyData || Object.keys(keyData).length === 0) { + throw new Error('API key not found') + } + + // 确保只能彻底删除已经软删除的key + if (keyData.isDeleted !== 'true') { + throw new Error('只能彻底删除已经删除的API Key') + } + + // 删除所有相关的使用统计数据 + const today = new Date().toISOString().split('T')[0] + const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0] + + // 删除每日统计 + await redis.client.del(`usage:daily:${today}:${keyId}`) + await redis.client.del(`usage:daily:${yesterday}:${keyId}`) + + // 删除月度统计 + const currentMonth = today.substring(0, 7) + await redis.client.del(`usage:monthly:${currentMonth}:${keyId}`) + + // 删除所有相关的统计键(通过模式匹配) + const usageKeys = await redis.client.keys(`usage:*:${keyId}*`) + if (usageKeys.length > 0) { + await redis.client.del(...usageKeys) + } + + // 删除API Key本身 + await redis.deleteApiKey(keyId) + + logger.success(`🗑️ Permanently deleted API key: ${keyId}`) + + return { success: true } + } catch (error) { + logger.error('❌ Failed to permanently delete API key:', error) + throw error + } + } + + // 🧹 清空所有已删除的API Keys + async clearAllDeletedApiKeys() { + try { + const allKeys = await this.getAllApiKeys(true) + const deletedKeys = allKeys.filter((key) => key.isDeleted === 'true') + + let successCount = 0 + let failedCount = 0 + const errors = [] + + for (const key of deletedKeys) { + try { + await this.permanentDeleteApiKey(key.id) + successCount++ + } catch (error) { + failedCount++ + errors.push({ + keyId: key.id, + keyName: key.name, + error: error.message + }) + } + } + + logger.success(`🧹 Cleared deleted API keys: ${successCount} success, ${failedCount} failed`) + + return { + success: true, + total: deletedKeys.length, + successCount, + failedCount, + errors + } + } catch (error) { + logger.error('❌ Failed to clear all deleted API keys:', error) + throw error + } + } + // 📊 记录使用情况(支持缓存token和账户级别统计) async recordUsage( keyId, diff --git a/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue index 0fcf517a..2af16a73 100644 --- a/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/BatchEditApiKeyModal.vue @@ -188,12 +188,14 @@
+ 设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户 +
+