diff --git a/src/routes/admin/apiKeys.js b/src/routes/admin/apiKeys.js index 0c763c03..7bbc4f8a 100644 --- a/src/routes/admin/apiKeys.js +++ b/src/routes/admin/apiKeys.js @@ -1031,7 +1031,11 @@ router.post('/api-keys/batch-stats', authenticateAdmin, async (req, res) => { dailyCost: 0, weeklyOpusCost: 0, currentWindowCost: 0, + currentWindowRequests: 0, + currentWindowTokens: 0, windowRemainingSeconds: null, + windowStartTime: null, + windowEndTime: null, allTimeCost: 0, error: error.message } @@ -1110,8 +1114,10 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) { // 获取实时限制数据(窗口数据不受时间范围筛选影响,始终获取当前窗口状态) let dailyCost = 0 - let weeklyOpusCost = 0 // 字段名沿用 weeklyOpusCost*,语义为“Claude 周费用” + let weeklyOpusCost = 0 // 字段名沿用 weeklyOpusCost*,语义为"Claude 周费用" let currentWindowCost = 0 + let currentWindowRequests = 0 // 当前窗口请求次数 + let currentWindowTokens = 0 // 当前窗口 Token 使用量 let windowRemainingSeconds = null let windowStartTime = null let windowEndTime = null @@ -1141,6 +1147,38 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) { weeklyOpusCost = await redis.getWeeklyOpusCost(keyId) } + // 只在启用了窗口限制时查询窗口数据(移到早期返回之前,确保窗口数据始终被获取) + if (rateLimitWindow > 0) { + const requestCountKey = `rate_limit:requests:${keyId}` + const tokenCountKey = `rate_limit:tokens:${keyId}` + const costCountKey = `rate_limit:cost:${keyId}` + const windowStartKey = `rate_limit:window_start:${keyId}` + + currentWindowRequests = parseInt((await client.get(requestCountKey)) || '0') + currentWindowTokens = parseInt((await client.get(tokenCountKey)) || '0') + currentWindowCost = parseFloat((await client.get(costCountKey)) || '0') + + // 获取窗口开始时间和计算剩余时间 + const windowStart = await client.get(windowStartKey) + if (windowStart) { + const now = Date.now() + windowStartTime = parseInt(windowStart) + const windowDuration = rateLimitWindow * 60 * 1000 // 转换为毫秒 + windowEndTime = windowStartTime + windowDuration + + // 如果窗口还有效 + if (now < windowEndTime) { + windowRemainingSeconds = Math.max(0, Math.floor((windowEndTime - now) / 1000)) + } else { + // 窗口已过期 + windowRemainingSeconds = 0 + currentWindowRequests = 0 + currentWindowTokens = 0 + currentWindowCost = 0 + } + } + } + // 🔧 FIX: 对于 "全部时间" 时间范围,直接使用 allTimeCost // 因为 usage:*:model:daily:* 键有 30 天 TTL,旧数据已经过期 if (timeRange === 'all' && allTimeCost > 0) { @@ -1159,38 +1197,14 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) { dailyCost, weeklyOpusCost, currentWindowCost, + currentWindowRequests, + currentWindowTokens, windowRemainingSeconds, windowStartTime, windowEndTime, allTimeCost } } - - // 只在启用了窗口限制时查询窗口数据 - if (rateLimitWindow > 0) { - const costCountKey = `rate_limit:cost:${keyId}` - const windowStartKey = `rate_limit:window_start:${keyId}` - - currentWindowCost = parseFloat((await client.get(costCountKey)) || '0') - - // 获取窗口开始时间和计算剩余时间 - const windowStart = await client.get(windowStartKey) - if (windowStart) { - const now = Date.now() - windowStartTime = parseInt(windowStart) - const windowDuration = rateLimitWindow * 60 * 1000 // 转换为毫秒 - windowEndTime = windowStartTime + windowDuration - - // 如果窗口还有效 - if (now < windowEndTime) { - windowRemainingSeconds = Math.max(0, Math.floor((windowEndTime - now) / 1000)) - } else { - // 窗口已过期 - windowRemainingSeconds = 0 - currentWindowCost = 0 - } - } - } } catch (error) { logger.warn(`⚠️ 获取实时限制数据失败 (key: ${keyId}):`, error.message) } @@ -1210,6 +1224,8 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) { dailyCost, weeklyOpusCost, currentWindowCost, + currentWindowRequests, + currentWindowTokens, windowRemainingSeconds, windowStartTime, windowEndTime, @@ -1329,6 +1345,8 @@ async function calculateKeyStats(keyId, timeRange, startDate, endDate) { dailyCost, weeklyOpusCost, currentWindowCost, + currentWindowRequests, + currentWindowTokens, windowRemainingSeconds, windowStartTime, windowEndTime, diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue index aceeb197..f7c6ae7c 100644 --- a/web/admin-spa/src/views/ApiKeysView.vue +++ b/web/admin-spa/src/views/ApiKeysView.vue @@ -4263,6 +4263,8 @@ const showUsageDetails = (apiKey) => { dailyCost: cachedStats?.dailyCost ?? apiKey.dailyCost ?? 0, weeklyOpusCost: cachedStats?.weeklyOpusCost ?? apiKey.weeklyOpusCost ?? 0, currentWindowCost: cachedStats?.currentWindowCost ?? apiKey.currentWindowCost ?? 0, + currentWindowRequests: cachedStats?.currentWindowRequests ?? apiKey.currentWindowRequests ?? 0, + currentWindowTokens: cachedStats?.currentWindowTokens ?? apiKey.currentWindowTokens ?? 0, windowRemainingSeconds: cachedStats?.windowRemainingSeconds ?? apiKey.windowRemainingSeconds, windowStartTime: cachedStats?.windowStartTime ?? apiKey.windowStartTime ?? null, windowEndTime: cachedStats?.windowEndTime ?? apiKey.windowEndTime ?? null,