diff --git a/src/models/redis.js b/src/models/redis.js index 29efdca1..f8f10723 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -3,24 +3,30 @@ const config = require('../../config/config'); const logger = require('../utils/logger'); // 时区辅助函数 +// 注意:这个函数的目的是获取某个时间点在目标时区的"本地"表示 +// 例如:UTC时间 2025-07-30 01:00:00 在 UTC+8 时区表示为 2025-07-30 09:00:00 function getDateInTimezone(date = new Date()) { const offset = config.system.timezoneOffset || 8; // 默认UTC+8 - // 直接基于UTC时间计算目标时区时间 - // 不需要考虑本地时区,因为我们总是基于UTC - const targetTime = new Date(date.getTime() + (offset * 3600000)); - return targetTime; + + // 方法:创建一个偏移后的Date对象,使其getUTCXXX方法返回目标时区的值 + // 这样我们可以用getUTCFullYear()等方法获取目标时区的年月日时分秒 + const offsetMs = offset * 3600000; // 时区偏移的毫秒数 + const adjustedTime = new Date(date.getTime() + offsetMs); + + return adjustedTime; } // 获取配置时区的日期字符串 (YYYY-MM-DD) function getDateStringInTimezone(date = new Date()) { const tzDate = getDateInTimezone(date); - return `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}-${String(tzDate.getDate()).padStart(2, '0')}`; + // 使用UTC方法获取偏移后的日期部分 + return `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`; } // 获取配置时区的小时 (0-23) function getHourInTimezone(date = new Date()) { const tzDate = getDateInTimezone(date); - return tzDate.getHours(); + return tzDate.getUTCHours(); } class RedisClient { @@ -163,7 +169,7 @@ class RedisClient { const now = new Date(); const today = getDateStringInTimezone(now); const tzDate = getDateInTimezone(now); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}`; // 新增小时级别 const daily = `usage:daily:${keyId}:${today}`; @@ -288,7 +294,7 @@ class RedisClient { const now = new Date(); const today = getDateStringInTimezone(now); const tzDate = getDateInTimezone(now); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const currentHour = `${today}:${String(getHourInTimezone(now)).padStart(2, '0')}`; // 账户级别统计的键 @@ -386,7 +392,7 @@ class RedisClient { const today = getDateStringInTimezone(); const dailyKey = `usage:daily:${keyId}:${today}`; const tzDate = getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const monthlyKey = `usage:monthly:${keyId}:${currentMonth}`; const [total, daily, monthly] = await Promise.all([ @@ -482,8 +488,8 @@ class RedisClient { async incrementDailyCost(keyId, amount) { const today = getDateStringInTimezone(); const tzDate = getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; - const currentHour = `${today}:${String(getHourInTimezone()).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; + const currentHour = `${today}:${String(getHourInTimezone(new Date())).padStart(2, '0')}`; const dailyKey = `usage:cost:daily:${keyId}:${today}`; const monthlyKey = `usage:cost:monthly:${keyId}:${currentMonth}`; @@ -510,8 +516,8 @@ class RedisClient { async getCostStats(keyId) { const today = getDateStringInTimezone(); const tzDate = getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; - const currentHour = `${today}:${String(getHourInTimezone()).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; + const currentHour = `${today}:${String(getHourInTimezone(new Date())).padStart(2, '0')}`; const [daily, monthly, hourly, total] = await Promise.all([ this.client.get(`usage:cost:daily:${keyId}:${today}`), @@ -534,7 +540,7 @@ class RedisClient { const today = getDateStringInTimezone(); const accountDailyKey = `account_usage:daily:${accountId}:${today}`; const tzDate = getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const accountMonthlyKey = `account_usage:monthly:${accountId}:${currentMonth}`; const [total, daily, monthly] = await Promise.all([ diff --git a/src/routes/admin.js b/src/routes/admin.js index b4a4a088..0166f759 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -63,7 +63,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { // 今日 - 使用时区日期 const redis = require('../models/redis'); const tzDate = redis.getDateInTimezone(now); - const dateStr = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}-${String(tzDate.getDate()).padStart(2, '0')}`; + const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`; searchPatterns.push(`usage:daily:*:${dateStr}`); } else if (timeRange === '7days') { // 最近7天 @@ -72,14 +72,14 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { const date = new Date(now); date.setDate(date.getDate() - i); const tzDate = redis.getDateInTimezone(date); - const dateStr = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}-${String(tzDate.getDate()).padStart(2, '0')}`; + const dateStr = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}-${String(tzDate.getUTCDate()).padStart(2, '0')}`; searchPatterns.push(`usage:daily:*:${dateStr}`); } } else if (timeRange === 'monthly') { // 本月 const redis = require('../models/redis'); const tzDate = redis.getDateInTimezone(now); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; searchPatterns.push(`usage:monthly:*:${currentMonth}`); } @@ -189,7 +189,7 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { const redis = require('../models/redis'); const tzToday = redis.getDateStringInTimezone(now); const tzDate = redis.getDateInTimezone(now); - const tzMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const tzMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; const modelKeys = timeRange === 'today' ? await client.keys(`usage:${apiKey.id}:model:daily:*:${tzToday}`) @@ -1125,7 +1125,7 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { const { period = 'daily' } = req.query; // daily, monthly const today = redis.getDateStringInTimezone(); const tzDate = redis.getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; logger.info(`📊 Getting global model stats, period: ${period}, today: ${today}, currentMonth: ${currentMonth}`); @@ -1269,7 +1269,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { // 使用时区转换后的时间来生成键 const tzCurrentHour = redis.getDateInTimezone(currentHour); const dateStr = redis.getDateStringInTimezone(currentHour); - const hour = String(tzCurrentHour.getHours()).padStart(2, '0'); + const hour = String(tzCurrentHour.getUTCHours()).padStart(2, '0'); const hourKey = `${dateStr}:${hour}`; // 获取当前小时的模型统计数据 @@ -1340,9 +1340,16 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => { hourCost = costResult.costs.total; } + // 格式化时间标签 + const tzDate = redis.getDateInTimezone(currentHour); + const month = String(tzDate.getUTCMonth() + 1).padStart(2, '0'); + const day = String(tzDate.getUTCDate()).padStart(2, '0'); + const hourStr = String(tzDate.getUTCHours()).padStart(2, '0'); + trendData.push({ // 对于小时粒度,只返回hour字段,不返回date字段 - hour: tzCurrentHour.toISOString(), // 使用转换后的时区时间 + hour: currentHour.toISOString(), // 保留原始ISO时间用于排序 + label: `${month}/${day} ${hourStr}:00`, // 添加格式化的标签 inputTokens: hourInputTokens, outputTokens: hourOutputTokens, requests: hourRequests, @@ -1483,7 +1490,7 @@ router.get('/api-keys/:keyId/model-stats', authenticateAdmin, async (req, res) = const client = redis.getClientSafe(); const today = redis.getDateStringInTimezone(); const tzDate = redis.getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; let searchPatterns = []; @@ -1702,15 +1709,22 @@ router.get('/api-keys-usage-trend', authenticateAdmin, async (req, res) => { // 使用时区转换后的时间来生成键 const tzCurrentHour = redis.getDateInTimezone(currentHour); const dateStr = redis.getDateStringInTimezone(currentHour); - const hour = String(tzCurrentHour.getHours()).padStart(2, '0'); + const hour = String(tzCurrentHour.getUTCHours()).padStart(2, '0'); const hourKey = `${dateStr}:${hour}`; // 获取这个小时所有API Key的数据 const pattern = `usage:hourly:*:${hourKey}`; const keys = await client.keys(pattern); + // 格式化时间标签 + const tzDateForLabel = redis.getDateInTimezone(currentHour); + const monthLabel = String(tzDateForLabel.getUTCMonth() + 1).padStart(2, '0'); + const dayLabel = String(tzDateForLabel.getUTCDate()).padStart(2, '0'); + const hourLabel = String(tzDateForLabel.getUTCHours()).padStart(2, '0'); + const hourData = { - hour: tzCurrentHour.toISOString(), // 使用转换后的时区时间 + hour: currentHour.toISOString(), // 使用原始时间,不进行时区转换 + label: `${monthLabel}/${dayLabel} ${hourLabel}:00`, // 添加格式化的标签 apiKeys: {} }; @@ -1842,7 +1856,7 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => { const client = redis.getClientSafe(); const today = redis.getDateStringInTimezone(); const tzDate = redis.getDateInTimezone(); - const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`; + const currentMonth = `${tzDate.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`; let pattern; if (period === 'today') { diff --git a/web/admin-spa/dist/assets/DashboardView-BNhlTn49.css b/web/admin-spa/dist/assets/DashboardView-BNhlTn49.css deleted file mode 100644 index 8b643d67..00000000 --- a/web/admin-spa/dist/assets/DashboardView-BNhlTn49.css +++ /dev/null @@ -1 +0,0 @@ -.custom-date-picker[data-v-5170898f] .el-input__inner{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.custom-date-picker[data-v-5170898f] .el-input__inner:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1));--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.custom-date-picker[data-v-5170898f] .el-input__inner{font-size:13px;padding:0 10px}.custom-date-picker[data-v-5170898f] .el-range-separator{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1));padding:0 2px}.custom-date-picker[data-v-5170898f] .el-range-input{font-size:13px} diff --git a/web/admin-spa/dist/index.html b/web/admin-spa/dist/index.html index 4132f9b1..b9de86c8 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/views/DashboardView.vue b/web/admin-spa/src/views/DashboardView.vue index 014d976c..68b4e2a3 100644 --- a/web/admin-spa/src/views/DashboardView.vue +++ b/web/admin-spa/src/views/DashboardView.vue @@ -444,6 +444,11 @@ function createUsageTrendChart() { // 根据数据类型确定标签字段和格式 const labelField = data[0]?.date ? 'date' : 'hour' const labels = data.map(d => { + // 优先使用后端提供的label字段 + if (d.label) { + return d.label + } + if (labelField === 'hour') { // 格式化小时显示 const date = new Date(d.hour) @@ -655,6 +660,11 @@ function createApiKeysUsageTrendChart() { const chartData = { labels: data.map(d => { + // 优先使用后端提供的label字段 + if (d.label) { + return d.label + } + if (labelField === 'hour') { // 格式化小时显示 const date = new Date(d.hour)