diff --git a/src/routes/apiStats.js b/src/routes/apiStats.js index d144162e..6f24819d 100644 --- a/src/routes/apiStats.js +++ b/src/routes/apiStats.js @@ -49,13 +49,12 @@ router.get('/style.css', (req, res) => { serveStaticFile(req, res, 'style.css', 'text/css; charset=utf-8'); }); -// 📊 用户API Key统计查询接口 - 安全的自查询接口 -router.post('/api/user-stats', async (req, res) => { +// 🔑 获取 API Key 对应的 ID +router.post('/api/get-key-id', async (req, res) => { try { const { apiKey } = req.body; if (!apiKey) { - logger.security(`🔒 Missing API key in user stats query from ${req.ip || 'unknown'}`); return res.status(400).json({ error: 'API Key is required', message: 'Please provide your API Key' @@ -64,19 +63,18 @@ router.post('/api/user-stats', async (req, res) => { // 基本API Key格式验证 if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) { - logger.security(`🔒 Invalid API key format in user stats query from ${req.ip || 'unknown'}`); return res.status(400).json({ error: 'Invalid API key format', message: 'API key format is invalid' }); } - // 验证API Key(重用现有的验证逻辑) + // 验证API Key const validation = await apiKeyService.validateApiKey(apiKey); if (!validation.valid) { const clientIP = req.ip || req.connection?.remoteAddress || 'unknown'; - logger.security(`🔒 Invalid API key in user stats query: ${validation.error} from ${clientIP}`); + logger.security(`🔒 Invalid API key in get-key-id: ${validation.error} from ${clientIP}`); return res.status(401).json({ error: 'Invalid API key', message: validation.error @@ -84,12 +82,147 @@ router.post('/api/user-stats', async (req, res) => { } const keyData = validation.keyData; + + res.json({ + success: true, + data: { + id: keyData.id + } + }); + + } catch (error) { + logger.error('❌ Failed to get API key ID:', error); + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to retrieve API key ID' + }); + } +}); + +// 📊 用户API Key统计查询接口 - 安全的自查询接口 +router.post('/api/user-stats', async (req, res) => { + try { + const { apiKey, apiId } = req.body; + + let keyData; + let keyId; + + if (apiId) { + // 通过 apiId 查询 + if (typeof apiId !== 'string' || !apiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)) { + return res.status(400).json({ + error: 'Invalid API ID format', + message: 'API ID must be a valid UUID' + }); + } + + // 直接通过 ID 获取 API Key 数据 + keyData = await redis.getApiKey(apiId); + + if (!keyData || Object.keys(keyData).length === 0) { + logger.security(`🔒 API key not found for ID: ${apiId} from ${req.ip || 'unknown'}`); + return res.status(404).json({ + error: 'API key not found', + message: 'The specified API key does not exist' + }); + } + + // 检查是否激活 + if (keyData.isActive !== 'true') { + return res.status(403).json({ + error: 'API key is disabled', + message: 'This API key has been disabled' + }); + } + + // 检查是否过期 + if (keyData.expiresAt && new Date() > new Date(keyData.expiresAt)) { + return res.status(403).json({ + error: 'API key has expired', + message: 'This API key has expired' + }); + } + + keyId = apiId; + + // 获取使用统计 + const usage = await redis.getUsageStats(keyId); + + // 获取当日费用统计 + const dailyCost = await redis.getDailyCost(keyId); + + // 处理数据格式,与 validateApiKey 返回的格式保持一致 + // 解析限制模型数据 + let restrictedModels = []; + try { + restrictedModels = keyData.restrictedModels ? JSON.parse(keyData.restrictedModels) : []; + } catch (e) { + restrictedModels = []; + } + + // 解析允许的客户端数据 + let allowedClients = []; + try { + allowedClients = keyData.allowedClients ? JSON.parse(keyData.allowedClients) : []; + } catch (e) { + allowedClients = []; + } + + // 格式化 keyData + keyData = { + ...keyData, + tokenLimit: parseInt(keyData.tokenLimit) || 0, + concurrencyLimit: parseInt(keyData.concurrencyLimit) || 0, + rateLimitWindow: parseInt(keyData.rateLimitWindow) || 0, + rateLimitRequests: parseInt(keyData.rateLimitRequests) || 0, + dailyCostLimit: parseFloat(keyData.dailyCostLimit) || 0, + dailyCost: dailyCost || 0, + enableModelRestriction: keyData.enableModelRestriction === 'true', + restrictedModels: restrictedModels, + enableClientRestriction: keyData.enableClientRestriction === 'true', + allowedClients: allowedClients, + permissions: keyData.permissions || 'all', + usage: usage // 使用完整的 usage 数据,而不是只有 total + }; + + } else if (apiKey) { + // 通过 apiKey 查询(保持向后兼容) + if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) { + logger.security(`🔒 Invalid API key format in user stats query from ${req.ip || 'unknown'}`); + return res.status(400).json({ + error: 'Invalid API key format', + message: 'API key format is invalid' + }); + } + + // 验证API Key(重用现有的验证逻辑) + const validation = await apiKeyService.validateApiKey(apiKey); + + if (!validation.valid) { + const clientIP = req.ip || req.connection?.remoteAddress || 'unknown'; + logger.security(`🔒 Invalid API key in user stats query: ${validation.error} from ${clientIP}`); + return res.status(401).json({ + error: 'Invalid API key', + message: validation.error + }); + } + + keyData = validation.keyData; + keyId = keyData.id; + + } else { + logger.security(`🔒 Missing API key or ID in user stats query from ${req.ip || 'unknown'}`); + return res.status(400).json({ + error: 'API Key or ID is required', + message: 'Please provide your API Key or API ID' + }); + } // 记录合法查询 - logger.api(`📊 User stats query from key: ${keyData.name} (${keyData.id}) from ${req.ip || 'unknown'}`); + logger.api(`📊 User stats query from key: ${keyData.name} (${keyId}) from ${req.ip || 'unknown'}`); // 获取验证结果中的完整keyData(包含isActive状态和cost信息) - const fullKeyData = validation.keyData; + const fullKeyData = keyData; // 计算总费用 - 使用与模型统计相同的逻辑(按模型分别计算) let totalCost = 0; @@ -99,7 +232,7 @@ router.post('/api/user-stats', async (req, res) => { const client = redis.getClientSafe(); // 获取所有月度模型统计(与model-stats接口相同的逻辑) - const allModelKeys = await client.keys(`usage:${fullKeyData.id}:model:monthly:*:*`); + const allModelKeys = await client.keys(`usage:${keyId}:model:monthly:*:*`); const modelUsageMap = new Map(); for (const key of allModelKeys) { @@ -157,7 +290,7 @@ router.post('/api/user-stats', async (req, res) => { formattedCost = CostCalculator.formatCost(totalCost); } catch (error) { - logger.warn(`Failed to calculate detailed cost for key ${fullKeyData.id}:`, error); + logger.warn(`Failed to calculate detailed cost for key ${keyId}:`, error); // 回退到简单计算 if (fullKeyData.usage?.total?.allTokens > 0) { const usage = fullKeyData.usage.total; @@ -176,7 +309,7 @@ router.post('/api/user-stats', async (req, res) => { // 构建响应数据(只返回该API Key自己的信息,确保不泄露其他信息) const responseData = { - id: fullKeyData.id, + id: keyId, name: fullKeyData.name, description: keyData.description || '', isActive: true, // 如果能通过validateApiKey验证,说明一定是激活的 @@ -242,30 +375,71 @@ router.post('/api/user-stats', async (req, res) => { // 📊 用户模型统计查询接口 - 安全的自查询接口 router.post('/api/user-model-stats', async (req, res) => { try { - const { apiKey, period = 'monthly' } = req.body; + const { apiKey, apiId, period = 'monthly' } = req.body; - if (!apiKey) { - logger.security(`🔒 Missing API key in user model stats query from ${req.ip || 'unknown'}`); + let keyData; + let keyId; + + if (apiId) { + // 通过 apiId 查询 + if (typeof apiId !== 'string' || !apiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)) { + return res.status(400).json({ + error: 'Invalid API ID format', + message: 'API ID must be a valid UUID' + }); + } + + // 直接通过 ID 获取 API Key 数据 + keyData = await redis.getApiKey(apiId); + + if (!keyData || Object.keys(keyData).length === 0) { + logger.security(`🔒 API key not found for ID: ${apiId} from ${req.ip || 'unknown'}`); + return res.status(404).json({ + error: 'API key not found', + message: 'The specified API key does not exist' + }); + } + + // 检查是否激活 + if (keyData.isActive !== 'true') { + return res.status(403).json({ + error: 'API key is disabled', + message: 'This API key has been disabled' + }); + } + + keyId = apiId; + + // 获取使用统计 + const usage = await redis.getUsageStats(keyId); + keyData.usage = { total: usage.total }; + + } else if (apiKey) { + // 通过 apiKey 查询(保持向后兼容) + // 验证API Key + const validation = await apiKeyService.validateApiKey(apiKey); + + if (!validation.valid) { + const clientIP = req.ip || req.connection?.remoteAddress || 'unknown'; + logger.security(`🔒 Invalid API key in user model stats query: ${validation.error} from ${clientIP}`); + return res.status(401).json({ + error: 'Invalid API key', + message: validation.error + }); + } + + keyData = validation.keyData; + keyId = keyData.id; + + } else { + logger.security(`🔒 Missing API key or ID in user model stats query from ${req.ip || 'unknown'}`); return res.status(400).json({ - error: 'API Key is required', - message: 'Please provide your API Key' + error: 'API Key or ID is required', + message: 'Please provide your API Key or API ID' }); } - - // 验证API Key - const validation = await apiKeyService.validateApiKey(apiKey); - if (!validation.valid) { - const clientIP = req.ip || req.connection?.remoteAddress || 'unknown'; - logger.security(`🔒 Invalid API key in user model stats query: ${validation.error} from ${clientIP}`); - return res.status(401).json({ - error: 'Invalid API key', - message: validation.error - }); - } - - const keyData = validation.keyData; - logger.api(`📊 User model stats query from key: ${keyData.name} (${keyData.id}) for period: ${period}`); + logger.api(`📊 User model stats query from key: ${keyData.name} (${keyId}) for period: ${period}`); // 重用管理后台的模型统计逻辑,但只返回该API Key的数据 const client = redis.getClientSafe(); @@ -273,8 +447,8 @@ router.post('/api/user-model-stats', async (req, res) => { const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; const pattern = period === 'daily' ? - `usage:${keyData.id}:model:daily:*:${today}` : - `usage:${keyData.id}:model:monthly:*:${currentMonth}`; + `usage:${keyId}:model:daily:*:${today}` : + `usage:${keyId}:model:monthly:*:${currentMonth}`; const keys = await client.keys(pattern); const modelStats = []; diff --git a/web/apiStats/app.js b/web/apiStats/app.js index b61de143..6471aa76 100644 --- a/web/apiStats/app.js +++ b/web/apiStats/app.js @@ -10,11 +10,13 @@ const app = createApp({ return { // 用户输入 apiKey: '', + apiId: null, // 存储 API Key 对应的 ID // 状态控制 loading: false, modelStatsLoading: false, error: '', + showAdminButton: true, // 控制管理后端按钮显示 // 时间范围控制 statsPeriod: 'daily', // 默认今日 @@ -41,9 +43,11 @@ const app = createApp({ this.error = ''; this.statsData = null; this.modelStats = []; + this.apiId = null; try { - const response = await fetch('/apiStats/api/user-stats', { + // 首先获取 API Key 对应的 ID + const idResponse = await fetch('/apiStats/api/get-key-id', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -53,22 +57,48 @@ const app = createApp({ }) }); - const result = await response.json(); + const idResult = await idResponse.json(); - if (!response.ok) { - throw new Error(result.message || '查询失败'); + if (!idResponse.ok) { + throw new Error(idResult.message || '获取 API Key ID 失败'); } - if (result.success) { - this.statsData = result.data; + if (idResult.success) { + this.apiId = idResult.data.id; - // 同时加载今日和本月的统计数据 - await this.loadAllPeriodStats(); + // 使用 apiId 查询统计数据 + const response = await fetch('/apiStats/api/user-stats', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + apiId: this.apiId + }) + }); - // 清除错误信息 - this.error = ''; + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.message || '查询失败'); + } + + if (result.success) { + this.statsData = result.data; + + // 同时加载今日和本月的统计数据 + await this.loadAllPeriodStats(); + + // 清除错误信息 + this.error = ''; + + // 更新 URL + this.updateURL(); + } else { + throw new Error(result.message || '查询失败'); + } } else { - throw new Error(result.message || '查询失败'); + throw new Error(idResult.message || '获取 API Key ID 失败'); } } catch (error) { @@ -76,6 +106,7 @@ const app = createApp({ this.error = error.message || '查询统计数据失败,请检查您的 API Key 是否正确'; this.statsData = null; this.modelStats = []; + this.apiId = null; } finally { this.loading = false; } @@ -83,7 +114,7 @@ const app = createApp({ // 📊 加载所有时间段的统计数据 async loadAllPeriodStats() { - if (!this.apiKey.trim()) { + if (!this.apiId) { return; } @@ -106,7 +137,7 @@ const app = createApp({ 'Content-Type': 'application/json' }, body: JSON.stringify({ - apiKey: this.apiKey, + apiId: this.apiId, period: period }) }); @@ -156,7 +187,7 @@ const app = createApp({ // 📊 加载模型统计数据 async loadModelStats(period = 'daily') { - if (!this.apiKey.trim()) { + if (!this.apiId) { return; } @@ -169,7 +200,7 @@ const app = createApp({ 'Content-Type': 'application/json' }, body: JSON.stringify({ - apiKey: this.apiKey, + apiId: this.apiId, period: period }) }); @@ -373,6 +404,7 @@ const app = createApp({ this.monthlyStats = null; this.error = ''; this.statsPeriod = 'daily'; // 重置为默认值 + this.apiId = null; }, // 🔄 刷新数据 @@ -384,10 +416,70 @@ const app = createApp({ // 📊 刷新当前时间段数据 async refreshCurrentPeriod() { - if (this.apiKey) { + if (this.apiId) { await this.loadPeriodStats(this.statsPeriod); await this.loadModelStats(this.statsPeriod); } + }, + + // 🔄 更新 URL + updateURL() { + if (this.apiId) { + const url = new URL(window.location); + url.searchParams.set('apiId', this.apiId); + window.history.pushState({}, '', url); + } + }, + + // 📊 使用 apiId 直接加载数据 + async loadStatsWithApiId() { + if (!this.apiId) { + return; + } + + this.loading = true; + this.error = ''; + this.statsData = null; + this.modelStats = []; + + try { + // 使用 apiId 查询统计数据 + const response = await fetch('/apiStats/api/user-stats', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + apiId: this.apiId + }) + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.message || '查询失败'); + } + + if (result.success) { + this.statsData = result.data; + + // 同时加载今日和本月的统计数据 + await this.loadAllPeriodStats(); + + // 清除错误信息 + this.error = ''; + } else { + throw new Error(result.message || '查询失败'); + } + + } catch (error) { + console.error('Load stats with apiId error:', error); + this.error = error.message || '查询统计数据失败'; + this.statsData = null; + this.modelStats = []; + } finally { + this.loading = false; + } } }, @@ -475,10 +567,18 @@ const app = createApp({ // 页面加载完成后的初始化 console.log('User Stats Page loaded'); - // 检查 URL 参数是否有预填的 API Key(用于开发测试) + // 检查 URL 参数 const urlParams = new URLSearchParams(window.location.search); + const presetApiId = urlParams.get('apiId'); const presetApiKey = urlParams.get('apiKey'); - if (presetApiKey && presetApiKey.length > 10) { + + if (presetApiId && presetApiId.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i)) { + // 如果 URL 中有 apiId,直接使用 apiId 加载数据 + this.apiId = presetApiId; + this.showAdminButton = false; // 隐藏管理后端按钮 + this.loadStatsWithApiId(); + } else if (presetApiKey && presetApiKey.length > 10) { + // 向后兼容,支持 apiKey 参数 this.apiKey = presetApiKey; } diff --git a/web/apiStats/index.html b/web/apiStats/index.html index a425ae3d..5294f722 100644 --- a/web/apiStats/index.html +++ b/web/apiStats/index.html @@ -39,7 +39,7 @@

使用统计查询

-
+
管理后台