From 4e67e597b0c57d5a4afeec0312d2b496495493c9 Mon Sep 17 00:00:00 2001 From: Edric Li Date: Sat, 6 Sep 2025 22:03:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20API=20Keys=E9=A1=B5=E9=9D=A2=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=85=A8=E9=83=A8=E6=97=B6=E9=97=B4=E9=80=89=E9=A1=B9?= =?UTF-8?q?=E5=92=8CUI=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加"全部时间"选项到时间范围下拉菜单,可查看所有历史使用数据 - 统一费用显示列,根据选择的时间范围动态显示对应标签 - 支持自定义日期范围查询(最多31天) - 优化日期选择器高度与其他控件对齐(38px) - 使用更通用的标签名称(累计费用、总费用等) - 移除调试console.log语句 后端改进: - 添加自定义日期范围查询支持 - 日期范围验证和31天限制 - 支持all时间范围查询 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/routes/admin.js | 63 +++- web/admin-spa/src/views/ApiKeysView.vue | 395 ++++++++++++++++++++---- 2 files changed, 384 insertions(+), 74 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index c26e613c..fe1ffd8b 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -122,7 +122,7 @@ router.get('/api-keys/:keyId/cost-debug', authenticateAdmin, async (req, res) => // 获取所有API Keys router.get('/api-keys', authenticateAdmin, async (req, res) => { try { - const { timeRange = 'all' } = req.query // all, 7days, monthly + const { timeRange = 'all', startDate, endDate } = req.query // all, 7days, monthly, custom const apiKeys = await apiKeyService.getAllApiKeys() // 获取用户服务来补充owner信息 @@ -132,7 +132,32 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { const now = new Date() const searchPatterns = [] - if (timeRange === 'today') { + if (timeRange === 'custom' && startDate && endDate) { + // 自定义日期范围 + const redisClient = require('../models/redis') + const start = new Date(startDate) + const end = new Date(endDate) + + // 确保日期范围有效 + if (start > end) { + return res.status(400).json({ error: 'Start date must be before or equal to end date' }) + } + + // 限制最大范围为31天 + const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1 + if (daysDiff > 31) { + return res.status(400).json({ error: 'Date range cannot exceed 31 days' }) + } + + // 生成日期范围内每天的搜索模式 + const currentDate = new Date(start) + while (currentDate <= end) { + const tzDate = redisClient.getDateInTimezone(currentDate) + const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}` + searchPatterns.push(`usage:daily:*:${dateStr}`) + currentDate.setDate(currentDate.getDate() + 1) + } + } else if (timeRange === 'today') { // 今日 - 使用时区日期 const redisClient = require('../models/redis') const tzDate = redisClient.getDateInTimezone(now) @@ -233,7 +258,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { apiKey.usage.total.formattedCost = CostCalculator.formatCost(totalCost) } } else { - // 7天或本月:重新计算统计数据 + // 7天、本月或自定义日期范围:重新计算统计数据 const tempUsage = { requests: 0, tokens: 0, @@ -274,12 +299,28 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { const tzDate = redisClient.getDateInTimezone(now) const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}` - const modelKeys = - timeRange === 'today' - ? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`) - : timeRange === '7days' - ? await client.keys(`usage:${apiKey.id}:model:daily:*:*`) - : await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`) + let modelKeys = [] + if (timeRange === 'custom' && startDate && endDate) { + // 自定义日期范围:获取范围内所有日期的模型统计 + const start = new Date(startDate) + const end = new Date(endDate) + const currentDate = new Date(start) + + while (currentDate <= end) { + const tzDateForKey = redisClient.getDateInTimezone(currentDate) + const dateStr = `${tzDateForKey.getUTCFullYear()}-${String(tzDateForKey.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDateForKey.getUTCDate()).padStart(2, '0')}` + const dayKeys = await client.keys(`usage:${apiKey.id}:model:daily:*:${dateStr}`) + modelKeys = modelKeys.concat(dayKeys) + currentDate.setDate(currentDate.getDate() + 1) + } + } else { + modelKeys = + timeRange === 'today' + ? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`) + : timeRange === '7days' + ? await client.keys(`usage:${apiKey.id}:model:daily:*:*`) + : await client.keys(`usage:${apiKey.id}:model:monthly:*:${tzMonth}`) + } const modelStatsMap = new Map() @@ -295,8 +336,8 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { continue } } - } else if (timeRange === 'today') { - // today选项已经在查询时过滤了,不需要额外处理 + } else if (timeRange === 'today' || timeRange === 'custom') { + // today和custom选项已经在查询时过滤了,不需要额外处理 } const modelMatch = key.match( diff --git a/web/admin-spa/src/views/ApiKeysView.vue b/web/admin-spa/src/views/ApiKeysView.vue index c623d893..600086e7 100644 --- a/web/admin-spa/src/views/ApiKeysView.vue +++ b/web/admin-spa/src/views/ApiKeysView.vue @@ -64,12 +64,33 @@ class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-blue-500 to-purple-500 opacity-0 blur transition duration-300 group-hover:opacity-20" > + + + +
+
@@ -245,29 +266,13 @@ class="w-[17%] min-w-[140px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300" >
- 使用统计 - 今日费用 + 使用统计(按费用排序) - - - - 总费用 -
- 今日请求 + {{ + getPeriodRequestLabel() + }} {{ formatNumber(key.usage?.daily?.requests || 0) }}次
- 今日费用 - ${{ (key.dailyCost || 0).toFixed(4) }} -
-
- 总费用 + {{ + getPeriodCostLabel() + }} ${{ (key.totalCost || 0).toFixed(4) }}${{ getPeriodCost(key).toFixed(4) }}
@@ -1078,7 +1081,9 @@
- 今日使用 + {{ + globalDateFilter.type === 'custom' ? '累计统计' : '今日使用' + }}