mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
fix(admin-spa): 修复时区问题导致的图表时间显示不一致和今日统计错误
This commit is contained in:
@@ -1123,8 +1123,9 @@ router.get('/usage-stats', authenticateAdmin, async (req, res) => {
|
|||||||
router.get('/model-stats', authenticateAdmin, async (req, res) => {
|
router.get('/model-stats', authenticateAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { period = 'daily' } = req.query; // daily, monthly
|
const { period = 'daily' } = req.query; // daily, monthly
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = redis.getDateStringInTimezone();
|
||||||
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
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}`);
|
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);
|
currentHour.setMinutes(0, 0, 0);
|
||||||
|
|
||||||
while (currentHour <= endTime) {
|
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}`;
|
const hourKey = `${dateStr}:${hour}`;
|
||||||
|
|
||||||
// 获取当前小时的模型统计数据
|
// 获取当前小时的模型统计数据
|
||||||
@@ -1338,7 +1341,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trendData.push({
|
trendData.push({
|
||||||
date: hourKey,
|
date: dateStr, // 保持日期格式一致
|
||||||
hour: currentHour.toISOString(),
|
hour: currentHour.toISOString(),
|
||||||
inputTokens: hourInputTokens,
|
inputTokens: hourInputTokens,
|
||||||
outputTokens: hourOutputTokens,
|
outputTokens: hourOutputTokens,
|
||||||
@@ -1362,7 +1365,7 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
|||||||
for (let i = 0; i < daysCount; i++) {
|
for (let i = 0; i < daysCount; i++) {
|
||||||
const date = new Date(today);
|
const date = new Date(today);
|
||||||
date.setDate(date.getDate() - i);
|
date.setDate(date.getDate() - i);
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
const dateStr = redis.getDateStringInTimezone(date);
|
||||||
|
|
||||||
// 汇总当天所有API Key的使用数据
|
// 汇总当天所有API Key的使用数据
|
||||||
const pattern = `usage:daily:*:${dateStr}`;
|
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}`);
|
logger.info(`📊 Getting model stats for API key: ${keyId}, period: ${period}, startDate: ${startDate}, endDate: ${endDate}`);
|
||||||
|
|
||||||
const client = redis.getClientSafe();
|
const client = redis.getClientSafe();
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = redis.getDateStringInTimezone();
|
||||||
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
const tzDate = redis.getDateInTimezone();
|
||||||
|
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
|
||||||
let searchPatterns = [];
|
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)) {
|
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}`);
|
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);
|
currentHour.setMinutes(0, 0, 0);
|
||||||
|
|
||||||
while (currentHour <= endTime) {
|
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的数据
|
// 获取这个小时所有API Key的数据
|
||||||
const pattern = `usage:hourly:*:${hourKey}`;
|
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++) {
|
for (let i = 0; i < daysCount; i++) {
|
||||||
const date = new Date(today);
|
const date = new Date(today);
|
||||||
date.setDate(date.getDate() - i);
|
date.setDate(date.getDate() - i);
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
const dateStr = redis.getDateStringInTimezone(date);
|
||||||
|
|
||||||
// 获取这一天所有API Key的数据
|
// 获取这一天所有API Key的数据
|
||||||
const pattern = `usage:daily:*:${dateStr}`;
|
const pattern = `usage:daily:*:${dateStr}`;
|
||||||
@@ -1832,8 +1840,9 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
|||||||
|
|
||||||
// 按模型统计费用
|
// 按模型统计费用
|
||||||
const client = redis.getClientSafe();
|
const client = redis.getClientSafe();
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = redis.getDateStringInTimezone();
|
||||||
const currentMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}`;
|
const tzDate = redis.getDateInTimezone();
|
||||||
|
const currentMonth = `${tzDate.getFullYear()}-${String(tzDate.getMonth() + 1).padStart(2, '0')}`;
|
||||||
|
|
||||||
let pattern;
|
let pattern;
|
||||||
if (period === 'today') {
|
if (period === 'today') {
|
||||||
|
|||||||
2
web/admin-spa/dist/index.html
vendored
2
web/admin-spa/dist/index.html
vendored
@@ -18,7 +18,7 @@
|
|||||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
||||||
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
|
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
|
||||||
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
|
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
|
||||||
<script type="module" crossorigin src="/admin-next/assets/index-BboR9Cvm.js"></script>
|
<script type="module" crossorigin src="/admin-next/assets/index-Di89_tIg.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/admin-next/assets/vue-vendor-CKToUHZx.js">
|
<link rel="modulepreload" crossorigin href="/admin-next/assets/vue-vendor-CKToUHZx.js">
|
||||||
<link rel="modulepreload" crossorigin href="/admin-next/assets/vendor-BDiMbLwQ.js">
|
<link rel="modulepreload" crossorigin href="/admin-next/assets/vendor-BDiMbLwQ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/admin-next/assets/element-plus-B8Fs_0jW.js">
|
<link rel="modulepreload" crossorigin href="/admin-next/assets/element-plus-B8Fs_0jW.js">
|
||||||
|
|||||||
@@ -144,12 +144,49 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
// 小时粒度,计算时间范围
|
// 小时粒度,计算时间范围
|
||||||
url += `granularity=hour`
|
url += `granularity=hour`
|
||||||
|
|
||||||
// 根据days参数计算时间范围
|
if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) {
|
||||||
const endTime = new Date()
|
// 使用自定义时间范围
|
||||||
const startTime = new Date(endTime.getTime() - days * 24 * 60 * 60 * 1000)
|
url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}`
|
||||||
|
url += `&endDate=${encodeURIComponent(dateFilter.value.customRange[1])}`
|
||||||
|
} else {
|
||||||
|
// 使用预设计算时间范围,与loadApiKeysTrend保持一致
|
||||||
|
const now = new Date()
|
||||||
|
let startTime, endTime
|
||||||
|
|
||||||
url += `&startDate=${encodeURIComponent(startTime.toISOString())}`
|
if (dateFilter.value.type === 'preset') {
|
||||||
url += `&endDate=${encodeURIComponent(endTime.toISOString())}`
|
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 {
|
} else {
|
||||||
// 天粒度,传递天数
|
// 天粒度,传递天数
|
||||||
url += `granularity=day&days=${days}`
|
url += `granularity=day&days=${days}`
|
||||||
@@ -178,17 +215,58 @@ export const useDashboardStore = defineStore('dashboard', () => {
|
|||||||
async function loadApiKeysTrend(metric = 'requests') {
|
async function loadApiKeysTrend(metric = 'requests') {
|
||||||
try {
|
try {
|
||||||
let url = '/admin/api-keys-usage-trend?'
|
let url = '/admin/api-keys-usage-trend?'
|
||||||
|
let days = 7
|
||||||
|
|
||||||
if (trendGranularity.value === 'hour') {
|
if (trendGranularity.value === 'hour') {
|
||||||
// 小时粒度,传递开始和结束时间
|
// 小时粒度,计算时间范围
|
||||||
url += `granularity=hour`
|
url += `granularity=hour`
|
||||||
|
|
||||||
if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) {
|
if (dateFilter.value.customRange && dateFilter.value.customRange.length === 2) {
|
||||||
|
// 使用自定义时间范围
|
||||||
url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}`
|
url += `&startDate=${encodeURIComponent(dateFilter.value.customRange[0])}`
|
||||||
url += `&endDate=${encodeURIComponent(dateFilter.value.customRange[1])}`
|
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 {
|
} else {
|
||||||
// 天粒度,传递天数
|
// 天粒度,传递天数
|
||||||
const days = dateFilter.value.type === 'preset'
|
days = dateFilter.value.type === 'preset'
|
||||||
? (dateFilter.value.preset === 'today' ? 1 : dateFilter.value.preset === '7days' ? 7 : 30)
|
? (dateFilter.value.preset === 'today' ? 1 : dateFilter.value.preset === '7days' ? 7 : 30)
|
||||||
: calculateDaysBetween(dateFilter.value.customStart, dateFilter.value.customEnd)
|
: calculateDaysBetween(dateFilter.value.customStart, dateFilter.value.customEnd)
|
||||||
url += `granularity=day&days=${days}`
|
url += `granularity=day&days=${days}`
|
||||||
|
|||||||
Reference in New Issue
Block a user