mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge branch 'dev' into main
合并dev分支的时区修复和其他改进: - 修复仪表板图表时区处理问题 - 修复自定义时间选择器的UTC转换 - 删除dist构建产物目录
This commit is contained in:
@@ -3,23 +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
|
||||
const utcTime = date.getTime() + (date.getTimezoneOffset() * 60000);
|
||||
const targetTime = new Date(utcTime + (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 {
|
||||
@@ -162,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}`;
|
||||
@@ -287,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')}`;
|
||||
|
||||
// 账户级别统计的键
|
||||
@@ -385,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([
|
||||
@@ -481,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}`;
|
||||
@@ -509,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}`),
|
||||
@@ -533,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([
|
||||
|
||||
@@ -64,7 +64,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天
|
||||
@@ -73,14 +73,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}`);
|
||||
}
|
||||
|
||||
@@ -190,7 +190,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}`)
|
||||
@@ -1321,7 +1321,8 @@ router.get('/dashboard', authenticateAdmin, async (req, res) => {
|
||||
claudeAccountsHealthy: activeClaudeAccounts > 0,
|
||||
geminiAccountsHealthy: activeGeminiAccounts > 0,
|
||||
uptime: process.uptime()
|
||||
}
|
||||
},
|
||||
systemTimezone: config.system.timezoneOffset || 8
|
||||
};
|
||||
|
||||
res.json({ success: true, data: dashboard });
|
||||
@@ -1356,8 +1357,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.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
logger.info(`📊 Getting global model stats, period: ${period}, today: ${today}, currentMonth: ${currentMonth}`);
|
||||
|
||||
@@ -1479,6 +1481,14 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
||||
// 使用自定义时间范围
|
||||
startTime = new Date(startDate);
|
||||
endTime = new Date(endDate);
|
||||
|
||||
// 调试日志
|
||||
logger.info(`📊 Usage trend hour granularity - received times:`);
|
||||
logger.info(` startDate (raw): ${startDate}`);
|
||||
logger.info(` endDate (raw): ${endDate}`);
|
||||
logger.info(` startTime (parsed): ${startTime.toISOString()}`);
|
||||
logger.info(` endTime (parsed): ${endTime.toISOString()}`);
|
||||
logger.info(` System timezone offset: ${config.system.timezoneOffset || 8}`);
|
||||
} else {
|
||||
// 默认最近24小时
|
||||
endTime = new Date();
|
||||
@@ -1498,8 +1508,11 @@ 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');
|
||||
// 注意:前端发送的时间已经是UTC时间,不需要再次转换
|
||||
// 直接从currentHour生成对应系统时区的日期和小时
|
||||
const tzCurrentHour = redis.getDateInTimezone(currentHour);
|
||||
const dateStr = redis.getDateStringInTimezone(currentHour);
|
||||
const hour = String(tzCurrentHour.getUTCHours()).padStart(2, '0');
|
||||
const hourKey = `${dateStr}:${hour}`;
|
||||
|
||||
// 获取当前小时的模型统计数据
|
||||
@@ -1570,9 +1583,16 @@ router.get('/usage-trend', authenticateAdmin, async (req, res) => {
|
||||
hourCost = costResult.costs.total;
|
||||
}
|
||||
|
||||
// 格式化时间标签 - 使用系统时区的显示
|
||||
const tzDateForLabel = redis.getDateInTimezone(currentHour);
|
||||
const month = String(tzDateForLabel.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(tzDateForLabel.getUTCDate()).padStart(2, '0');
|
||||
const hourStr = String(tzDateForLabel.getUTCHours()).padStart(2, '0');
|
||||
|
||||
trendData.push({
|
||||
date: hourKey,
|
||||
hour: currentHour.toISOString(),
|
||||
// 对于小时粒度,只返回hour字段,不返回date字段
|
||||
hour: currentHour.toISOString(), // 保留原始ISO时间用于排序
|
||||
label: `${month}/${day} ${hourStr}:00`, // 添加格式化的标签
|
||||
inputTokens: hourInputTokens,
|
||||
outputTokens: hourOutputTokens,
|
||||
requests: hourRequests,
|
||||
@@ -1595,7 +1615,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}`;
|
||||
@@ -1711,8 +1731,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.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
let searchPatterns = [];
|
||||
|
||||
@@ -1734,7 +1755,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}`);
|
||||
}
|
||||
|
||||
@@ -1928,14 +1949,25 @@ 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.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: currentHour.toISOString(),
|
||||
hour: currentHour.toISOString(), // 使用原始时间,不进行时区转换
|
||||
label: `${monthLabel}/${dayLabel} ${hourLabel}:00`, // 添加格式化的标签
|
||||
apiKeys: {}
|
||||
};
|
||||
|
||||
@@ -1973,7 +2005,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}`;
|
||||
@@ -2065,8 +2097,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.getUTCFullYear()}-${String(tzDate.getUTCMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
let pattern;
|
||||
if (period === 'today') {
|
||||
|
||||
Reference in New Issue
Block a user