From 61b1a0ec3250d895ad15e9832981c358456b5b82 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 30 Jul 2025 09:18:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin-spa):=20=E4=BF=AE=E5=A4=8D=E6=97=B6?= =?UTF-8?q?=E5=8C=BA=E9=97=AE=E9=A2=98=E5=AF=BC=E8=87=B4=E7=9A=84=E5=9B=BE?= =?UTF-8?q?=E8=A1=A8=E6=97=B6=E9=97=B4=E6=98=BE=E7=A4=BA=E4=B8=8D=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E5=92=8C=E4=BB=8A=E6=97=A5=E7=BB=9F=E8=AE=A1=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/admin.js | 35 ++++++---- web/admin-spa/dist/index.html | 2 +- web/admin-spa/src/stores/dashboard.js | 94 ++++++++++++++++++++++++--- 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index 1273370d..e3d118e8 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -1123,8 +1123,9 @@ router.get('/usage-stats', authenticateAdmin, async (req, res) => { router.get('/model-stats', authenticateAdmin, async (req, res) => { try { const { period = 'daily' } = req.query; // daily, monthly - const today = new Date().toISOString().split('T')[0]; - const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + const today = redis.getDateStringInTimezone(); + const tzDate = redis.getDateInTimezone(); + const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; logger.info(`📊 Getting global model stats, period: ${period}, today: ${today}, currentMonth: ${currentMonth}`); @@ -1265,8 +1266,10 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { currentHour.setMinutes(0, 0, 0); while (currentHour <= endTime) { - const dateStr = currentHour.toISOString().split('T')[0]; - const hour = String(currentHour.getHours()).padStart(2, '0'); + // 使用时区转换后的时间来生成键 + const tzCurrentHour = redis.getDateInTimezone(currentHour); + const dateStr = redis.getDateStringInTimezone(currentHour); + const hour = String(tzCurrentHour.getHours()).padStart(2, '0'); const hourKey = `${dateStr}:${hour}`; // 获取当前小时的模型统计数据 @@ -1338,7 +1341,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { } trendData.push({ - date: hourKey, + date: dateStr, // 保持日期格式一致 hour: currentHour.toISOString(), inputTokens: hourInputTokens, outputTokens: hourOutputTokens, @@ -1362,7 +1365,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { for (let i = 0; i < daysCount; i++) { const date = new Date(today); date.setDate(date.getDate() - i); - const dateStr = date.toISOString().split('T')[0]; + const dateStr = redis.getDateStringInTimezone(date); // 汇总当天所有API Key的使用数据 const pattern = `usage:daily:*:${dateStr}`; @@ -1478,8 +1481,9 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = logger.info(`📊 Getting model stats for API key: ${keyId}, period: ${period}, startDate: ${startDate}, endDate: ${endDate}`); const client = redis.getClientSafe(); - const today = new Date().toISOString().split('T')[0]; - const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + const today = redis.getDateStringInTimezone(); + const tzDate = redis.getDateInTimezone(); + const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; let searchPatterns = []; @@ -1501,7 +1505,7 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = // 生成日期范围内所有日期的搜索模式 for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { - const dateStr = d.toISOString().split('T')[0]; + const dateStr = redis.getDateStringInTimezone(d); searchPatterns.push(`usage:${keyId}:model:daily:*:${dateStr}`); } @@ -1695,7 +1699,11 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => { currentHour.setMinutes(0, 0, 0); while (currentHour <= endTime) { - const hourKey = currentHour.toISOString().split(':')[0].replace('T', ':'); + // 使用时区转换后的时间来生成键 + const tzCurrentHour = redis.getDateInTimezone(currentHour); + const dateStr = redis.getDateStringInTimezone(currentHour); + const hour = String(tzCurrentHour.getHours()).padStart(2, '0'); + const hourKey = `${dateStr}:${hour}`; // 获取这个小时所有API Key的数据 const pattern = `usage:hourly:*:${hourKey}`; @@ -1740,7 +1748,7 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => { for (let i = 0; i < daysCount; i++) { const date = new Date(today); date.setDate(date.getDate() - i); - const dateStr = date.toISOString().split('T')[0]; + const dateStr = redis.getDateStringInTimezone(date); // 获取这一天所有API Key的数据 const pattern = `usage:daily:*:${dateStr}`; @@ -1832,8 +1840,9 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => { // 按模型统计费用 const client = redis.getClientSafe(); - const today = new Date().toISOString().split('T')[0]; - const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`; + const today = redis.getDateStringInTimezone(); + const tzDate = redis.getDateInTimezone(); + const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; let pattern; if (period === 'today') { diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index 1c362b70..4132f9b1 100644 --- a/web/admin-spa/dist/index.html +++ b/web/admin-spa/dist/index.html @@ -18,7 +18,7 @@ - + diff --git a/web/admin-spa/src/stores/dashboard.js b/web/admin-spa/src/stores/dashboard.js index 1165f84e..23df0b0e 100644 --- a/web/admin-spa/src/stores/dashboard.js +++ b/web/admin-spa/src/stores/dashboard.js @@ -144,12 +144,49 @@ export const useDashboardStore = defineStore('dashboard', () => { // 小时粒度,计算时间范围 url += `granularity=hour` - // 根据days参数计算时间范围 - const endTime = new Date() - const startTime = new Date(endTime.getTime() - days * 24 * 60 * 60 * 1000) - - url += `&startDate=${encodeURIComponent(startTime.toISOString())}` - url += `&endDate=${encodeURIComponent(endTime.toISOString())}` + if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { + // 使用自定义时间范围 + url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}` + url += `&endDate=${encodeURIComponent(dateFilter.value.customRange[1])}` + } else { + // 使用预设计算时间范围,与loadApiKeysTrend保持一致 + const now = new Date() + let startTime, endTime + + if (dateFilter.value.type === 'preset') { + switch (dateFilter.value.preset) { + case 'last24h': + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + break + case 'yesterday': + startTime = new Date(now) + startTime.setDate(now.getDate() - 1) + startTime.setHours(0, 0, 0, 0) + endTime = new Date(startTime) + endTime.setHours(23, 59, 59, 999) + break + case 'dayBefore': + startTime = new Date(now) + startTime.setDate(now.getDate() - 2) + startTime.setHours(0, 0, 0, 0) + endTime = new Date(startTime) + endTime.setHours(23, 59, 59, 999) + break + default: + // 默认近24小时 + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + } + } else { + // 默认使用days参数计算 + startTime = new Date(now.getTime() - days * 24 * 60 * 60 * 1000) + endTime = now + } + + url += `&startDate=${encodeURIComponent(startTime.toISOString())}` + url += `&endDate=${encodeURIComponent(endTime.toISOString())}` + } } else { // 天粒度,传递天数 url += `granularity=day&days=${days}` @@ -178,17 +215,58 @@ export const useDashboardStore = defineStore('dashboard', () => { async function loadApiKeysTrend(metric = 'requests') { try { let url = '/admin/api-keys-usage-trend?' + let days = 7 if (trendGranularity.value === 'hour') { - // 小时粒度,传递开始和结束时间 + // 小时粒度,计算时间范围 url += `granularity=hour` + if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) { + // 使用自定义时间范围 url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}` url += `&endDate=${encodeURIComponent(dateFilter.value.customRange[1])}` + } else { + // 使用预设计算时间范围,与setDateFilterPreset保持一致 + const now = new Date() + let startTime, endTime + + if (dateFilter.value.type === 'preset') { + switch (dateFilter.value.preset) { + case 'last24h': + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + break + case 'yesterday': + startTime = new Date(now) + startTime.setDate(now.getDate() - 1) + startTime.setHours(0, 0, 0, 0) + endTime = new Date(startTime) + endTime.setHours(23, 59, 59, 999) + break + case 'dayBefore': + startTime = new Date(now) + startTime.setDate(now.getDate() - 2) + startTime.setHours(0, 0, 0, 0) + endTime = new Date(startTime) + endTime.setHours(23, 59, 59, 999) + break + default: + // 默认近24小时 + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + } + } else { + // 默认近24小时 + startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000) + endTime = now + } + + url += `&startDate=${encodeURIComponent(startTime.toISOString())}` + url += `&endDate=${encodeURIComponent(endTime.toISOString())}` } } else { // 天粒度,传递天数 - const days = dateFilter.value.type === 'preset' + days = dateFilter.value.type === 'preset' ? (dateFilter.value.preset === 'today' ? 1 : dateFilter.value.preset === '7days' ? 7 : 30) : calculateDaysBetween(dateFilter.value.customStart, dateFilter.value.customEnd) url += `granularity=day&days=${days}`