From 81ad098678ff2546af977395fe2bdfc4403939a7 Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 2 Sep 2025 21:36:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dapikeys=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E7=9A=84=E4=B8=80=E4=BA=9Bbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin.js | 122 ++++- src/services/apiKeyService.js | 133 ++++++ .../apikeys/BatchEditApiKeyModal.vue | 34 +- web/admin-spa/src/views/ApiKeysView.vue | 444 ++++++++++++------ 4 files changed, 584 insertions(+), 149 deletions(-) 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 @@
费用限制 (美元)
@@ -216,6 +218,24 @@ /> + +
+ + +

+ 设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户 +

+
+
@@ -2309,6 +2355,118 @@ const deleteApiKey = async (keyId) => { } } +// 恢复API Key +const restoreApiKey = async (keyId) => { + let confirmed = false + + if (window.showConfirm) { + confirmed = await window.showConfirm( + '恢复 API Key', + '确定要恢复这个 API Key 吗?恢复后可以重新使用。', + '确定恢复', + '取消' + ) + } else { + // 降级方案 + confirmed = confirm('确定要恢复这个 API Key 吗?恢复后可以重新使用。') + } + + if (!confirmed) return + + try { + const data = await apiClient.post(`/admin/api-keys/${keyId}/restore`) + if (data.success) { + showToast('API Key 已成功恢复', 'success') + // 刷新已删除列表 + await loadDeletedApiKeys() + // 同时刷新活跃列表 + await loadApiKeys() + } else { + showToast(data.error || '恢复失败', 'error') + } + } catch (error) { + showToast(error.response?.data?.error || '恢复失败', 'error') + } +} + +// 彻底删除API Key +const permanentDeleteApiKey = async (keyId) => { + let confirmed = false + + if (window.showConfirm) { + confirmed = await window.showConfirm( + '彻底删除 API Key', + '确定要彻底删除这个 API Key 吗?此操作不可恢复,所有相关数据将被永久删除。', + '确定彻底删除', + '取消' + ) + } else { + // 降级方案 + confirmed = confirm('确定要彻底删除这个 API Key 吗?此操作不可恢复,所有相关数据将被永久删除。') + } + + if (!confirmed) return + + try { + const data = await apiClient.delete(`/admin/api-keys/${keyId}/permanent`) + if (data.success) { + showToast('API Key 已彻底删除', 'success') + // 刷新已删除列表 + loadDeletedApiKeys() + } else { + showToast(data.error || '彻底删除失败', 'error') + } + } catch (error) { + showToast(error.response?.data?.error || '彻底删除失败', 'error') + } +} + +// 清空所有已删除的API Keys +const clearAllDeletedApiKeys = async () => { + const count = deletedApiKeys.value.length + if (count === 0) { + showToast('没有需要清空的 API Keys', 'info') + return + } + + let confirmed = false + + if (window.showConfirm) { + confirmed = await window.showConfirm( + '清空所有已删除的 API Keys', + `确定要彻底删除全部 ${count} 个已删除的 API Keys 吗?此操作不可恢复,所有相关数据将被永久删除。`, + '确定清空全部', + '取消' + ) + } else { + // 降级方案 + confirmed = confirm(`确定要彻底删除全部 ${count} 个已删除的 API Keys 吗?此操作不可恢复。`) + } + + if (!confirmed) return + + try { + const data = await apiClient.delete('/admin/api-keys/deleted/clear-all') + if (data.success) { + showToast(data.message || '已清空所有已删除的 API Keys', 'success') + + // 如果有失败的,显示详细信息 + if (data.details && data.details.failedCount > 0) { + const errors = data.details.errors + console.error('部分API Keys清空失败:', errors) + showToast(`${data.details.failedCount} 个清空失败,请查看控制台`, 'warning') + } + + // 刷新已删除列表 + loadDeletedApiKeys() + } else { + showToast(data.error || '清空失败', 'error') + } + } catch (error) { + showToast(error.response?.data?.error || '清空失败', 'error') + } +} + // 批量删除API Keys const batchDeleteApiKeys = async () => { const selectedCount = selectedApiKeys.value.length