diff --git a/src/statistics/ClusterStats.ts b/src/statistics/ClusterStats.ts index f1698aa..58ddfe4 100644 --- a/src/statistics/ClusterStats.ts +++ b/src/statistics/ClusterStats.ts @@ -2,104 +2,90 @@ import * as fs from 'fs'; import * as path from 'path'; import { Config } from '../Config.js'; -export class StatsStorage { - public readonly id: string; - private data: { date: string, hits: number, bytes: number }[]; - private filePath: string; - private saveInterval: NodeJS.Timeout; - private dataUpdated: boolean; // 标志是否有数据更新 - - constructor(id: string) { - this.id = id; - this.filePath = path.join(Config.getInstance().statsDir, `${this.id}.stats.json`); // 改为 .json 文件 - this.data = []; - this.dataUpdated = false; // 初始时数据未更新 - - // 如果文件存在,加载已有的数据 - if (fs.existsSync(this.filePath)) { - this.loadFromFile(); - } - - // 每10分钟保存一次数据(如果有更新) - this.saveInterval = setInterval(() => { - this.maybeWriteToFile(); - }, 10 * 60 * 1000); // 10分钟 - } - - public addData({ hits, bytes }: { hits: number, bytes: number }): void { - const today = new Date().toISOString().split('T')[0]; - - let todayData = this.data.find(entry => entry.date === today); - if (!todayData) { - todayData = { date: today, hits: 0, bytes: 0 }; - this.data.push(todayData); - if (this.data.length > 30) { - this.data.shift(); // 保持只存30天的数据 - } - } - - todayData.hits += hits; - todayData.bytes += bytes; - this.dataUpdated = true; // 数据已更新 - } +const getLocalDateString = (date = new Date()): string => { + return new Intl.DateTimeFormat('sv-SE', { timeZone: 'system', year: 'numeric', month: '2-digit', day: '2-digit' }).format(date); +}; + +const readFileData = (filePath: string): { date: string, hits: number, bytes: number }[] => { + if (!fs.existsSync(filePath)) return []; + const fileContent = fs.readFileSync(filePath, 'utf8'); + return fileContent.split('\n').filter(line => line.trim() !== '').map(line => { + const [date, hits, bytes] = line.split(' '); + return { date, hits: Number(hits), bytes: Number(bytes) }; + }); +}; + +const writeFileData = (filePath: string, data: { date: string, hits: number, bytes: number }[]) => { + const fileContent = data.map(entry => `${entry.date} ${entry.hits} ${entry.bytes}`).join('\n'); + fs.writeFileSync(filePath, fileContent, 'utf8'); +}; - public getTodayStats(): { hits: number, bytes: number } { - const today = new Date().toISOString().split('T')[0]; - const todayData = this.data.find(entry => entry.date === today); - if (todayData) { - return { hits: todayData.hits, bytes: todayData.bytes }; - } else { - return { hits: 0, bytes: 0 }; - } +export class StatsStorage { + public readonly id: string; + private data: { date: string, hits: number, bytes: number }[]; + private filePath: string; + private saveInterval: NodeJS.Timeout; + private dataUpdated: boolean; + + constructor(id: string) { + this.id = id; + this.filePath = path.join(Config.getInstance().statsDir, `${this.id}.stats`); + this.data = readFileData(this.filePath); + this.dataUpdated = false; + + this.saveInterval = setInterval(() => this.maybeWriteToFile(), 10 * 60 * 1000); + } + + public addData({ hits, bytes }: { hits: number, bytes: number }): void { + const today = getLocalDateString(); + let todayData = this.data.find(entry => entry.date === today); + + if (!todayData) { + todayData = { date: today, hits: 0, bytes: 0 }; + this.data.push(todayData); + if (this.data.length > 30) this.data.shift(); // 保持只存30天的数据 } - public getLast30DaysStats(): { date: string, hits: number, bytes: number }[] { - const now = new Date(); - - const dateMap: { [key: string]: { hits: number, bytes: number } } = {}; - - // 填充最近30天的数据 - for (let i = 0; i < 30; i++) { - const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; - dateMap[date] = { hits: 0, bytes: 0 }; - } - - // 更新映射中的数据 - for (const entry of this.data) { - dateMap[entry.date] = { hits: entry.hits, bytes: entry.bytes }; - } - - // 将映射转换为数组,并按日期排序 - return Object.keys(dateMap) - .sort() - .map(date => ({ - date, - hits: dateMap[date].hits, - bytes: dateMap[date].bytes - })); + todayData.hits += hits; + todayData.bytes += bytes; + this.dataUpdated = true; + } + + public getTodayStats(): { hits: number, bytes: number } { + const today = getLocalDateString(); + const todayData = this.data.find(entry => entry.date === today); + return todayData ? { hits: todayData.hits, bytes: todayData.bytes } : { hits: 0, bytes: 0 }; + } + + public getLast30DaysStats(): { date: string, hits: number, bytes: number }[] { + const now = new Date(); + const dateMap: { [key: string]: { hits: number, bytes: number } } = {}; + + // 填充最近30天的数据 + for (let i = 0; i < 30; i++) { + const date = getLocalDateString(new Date(now.getTime() - i * 24 * 60 * 60 * 1000)); + dateMap[date] = { hits: 0, bytes: 0 }; } - private maybeWriteToFile(): void { - if (this.dataUpdated) { - this.writeToFile(); - this.dataUpdated = false; // 重置更新标志 - } + // 更新映射中的数据 + for (const entry of this.data) { + if (dateMap[entry.date]) { + dateMap[entry.date] = { hits: entry.hits, bytes: entry.bytes }; + } } - private writeToFile(): void { - // 使用 JSON 序列化存储数据 - const fileContent = JSON.stringify(this.data); - fs.writeFileSync(this.filePath, fileContent, 'utf8'); - } + return Object.keys(dateMap).sort().map(date => ({ date, hits: dateMap[date].hits, bytes: dateMap[date].bytes })); + } - private loadFromFile(): void { - // 直接读取 JSON 数据 - const fileContent = fs.readFileSync(this.filePath, 'utf8'); - this.data = JSON.parse(fileContent); + private maybeWriteToFile(): void { + if (this.dataUpdated) { + writeFileData(this.filePath, this.data); + this.dataUpdated = false; } + } - public stopAutoSave(): void { - clearInterval(this.saveInterval); - this.maybeWriteToFile(); // 在停止时立即保存数据(如果有更新) - } + public stopAutoSave(): void { + clearInterval(this.saveInterval); + this.maybeWriteToFile(); + } } diff --git a/src/statistics/HourlyStats.ts b/src/statistics/HourlyStats.ts index 426095e..17b94a7 100644 --- a/src/statistics/HourlyStats.ts +++ b/src/statistics/HourlyStats.ts @@ -2,120 +2,123 @@ import * as fs from 'fs'; import * as path from 'path'; import { Config } from '../Config.js'; -export class HourlyStatsStorage { - private data: { date: string, hourlyStats: { hour: string, hits: number, bytes: number }[] }[]; - private filePath: string; - private saveInterval: NodeJS.Timeout; - private dataUpdated: boolean; - - constructor() { - this.filePath = path.join(Config.getInstance().statsDir, `center.stats.json`); - this.data = []; - this.dataUpdated = false; - - if (fs.existsSync(this.filePath)) { - this.loadFromFile(); - } - - this.saveInterval = setInterval(() => { - this.maybeWriteToFile(); - }, 10 * 60 * 1000); +const getLocalDateString = (date = new Date()): string => { + return new Intl.DateTimeFormat('sv-SE', { timeZone: 'system', year: 'numeric', month: '2-digit', day: '2-digit' }).format(date); +}; + +const getLocalHourString = (date = new Date()): string => { + return date.toLocaleTimeString('en-GB', { timeZone: 'system', hour: '2-digit', hour12: false }); +}; + +const readHourlyData = (filePath: string): { date: string, hourlyStats: { hour: string, hits: number, bytes: number }[] }[] => { + if (!fs.existsSync(filePath)) return []; + const fileContent = fs.readFileSync(filePath, 'utf8'); + const result: { date: string, hourlyStats: { hour: string, hits: number, bytes: number }[] }[] = []; + let currentDay: { date: string, hourlyStats: { hour: string, hits: number, bytes: number }[] } | null = null; + + fileContent.split('\n').filter(line => line.trim() !== '').forEach(line => { + const [date, hour, hits, bytes] = line.split(' '); + if (!currentDay || currentDay.date !== date) { + if (currentDay) result.push(currentDay); + currentDay = { date, hourlyStats: [] }; } + currentDay.hourlyStats.push({ hour, hits: Number(hits), bytes: Number(bytes) }); + }); - public today(): { hits: number, bytes: number } { - const now = new Date(); - const date = now.toISOString().split('T')[0]; - let today = { hits: 0, bytes: 0 }; - this.data.find(entry => entry.date === date)?.hourlyStats.forEach(hourData => { - today.hits += hourData.hits; - today.bytes += hourData.bytes; - }); - return today; - } + if (currentDay) result.push(currentDay); + return result; +}; - public addData({ hits, bytes }: { hits: number, bytes: number }): void { - const now = new Date(); - const date = now.toISOString().split('T')[0]; - const hour = now.getHours().toString().padStart(2, '0'); - - let dayData = this.data.find(entry => entry.date === date); - if (!dayData) { - dayData = { date: date, hourlyStats: [] }; - this.data.push(dayData); - if (this.data.length > 30) { - this.data.shift(); - } - } - - let hourData = dayData.hourlyStats.find(entry => entry.hour === hour); - if (!hourData) { - hourData = { hour: hour, hits: 0, bytes: 0 }; - dayData.hourlyStats.push(hourData); - if (dayData.hourlyStats.length > 24) { - dayData.hourlyStats.shift(); - } - } - - hourData.hits += hits; - hourData.bytes += bytes; - this.dataUpdated = true; - } +const writeHourlyData = (filePath: string, data: { date: string, hourlyStats: { hour: string, hits: number, bytes: number }[] }[]) => { + const fileContent = data.map(day => { + return day.hourlyStats.map(hourData => `${day.date} ${hourData.hour} ${hourData.hits} ${hourData.bytes}`).join('\n'); + }).join('\n'); + fs.writeFileSync(filePath, fileContent, 'utf8'); +}; - public getLast30DaysHourlyStats(): { date: string, hits: number, bytes: number }[][] { - const result: { date: string, hits: number, bytes: number }[][] = []; - const now = new Date(); - - for (let i = 0; i < 30; i++) { - const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000); - const dateString = date.toISOString().split('T')[0]; - - const dayData = this.data.find(entry => entry.date === dateString); - const dayResult: { date: string, hits: number, bytes: number }[] = []; - - for (let hour = 0; hour < 24; hour++) { - const hourString = hour.toString().padStart(2, '0'); - const hourData = dayData?.hourlyStats.find(entry => entry.hour === hourString); - - if (hourData) { - dayResult.push({ - date: `${dateString}T${hourString}:00:00`, - hits: hourData.hits, - bytes: hourData.bytes - }); - } else { - dayResult.push({ - date: `${dateString}T${hourString}:00:00`, - hits: 0, - bytes: 0 - }); - } - } - - result.push(dayResult); - } - - return result; +export class HourlyStatsStorage { + private data: { date: string, hourlyStats: { hour: string, hits: number, bytes: number }[] }[]; + private filePath: string; + private saveInterval: NodeJS.Timeout; + private dataUpdated: boolean; // 标志是否有数据更新 + + constructor() { + this.filePath = path.join(Config.getInstance().statsDir, `center.stats`); + this.data = readHourlyData(this.filePath); + this.dataUpdated = false; + + this.saveInterval = setInterval(() => this.maybeWriteToFile(), 10 * 60 * 1000); + } + + public addData({ hits, bytes }: { hits: number, bytes: number }): void { + const today = getLocalDateString(); + const currentHour = getLocalHourString(); + + let dayData = this.data.find(entry => entry.date === today); + if (!dayData) { + dayData = { date: today, hourlyStats: [] }; + this.data.push(dayData); + if (this.data.length > 30) this.data.shift(); // 保持只存30天的数据 } - private maybeWriteToFile(): void { - if (this.dataUpdated) { - this.writeToFile(); - this.dataUpdated = false; - } + let hourData = dayData.hourlyStats.find(entry => entry.hour === currentHour); + if (!hourData) { + hourData = { hour: currentHour, hits: 0, bytes: 0 }; + dayData.hourlyStats.push(hourData); + if (dayData.hourlyStats.length > 24) { + dayData.hourlyStats.shift(); // 保持只存24小时的数据 + } } - private writeToFile(): void { - const fileContent = JSON.stringify(this.data); - fs.writeFileSync(this.filePath, fileContent, 'utf8'); - } + hourData.hits += hits; + hourData.bytes += bytes; + this.dataUpdated = true; + } + + public today(): { hits: number, bytes: number } { + const today = getLocalDateString(); + let todayStats = { hits: 0, bytes: 0 }; + this.data.find(entry => entry.date === today)?.hourlyStats.forEach(hourData => { + todayStats.hits += hourData.hits; + todayStats.bytes += hourData.bytes; + }); + return todayStats; + } + + public getLast30DaysHourlyStats(): { date: string, hits: number, bytes: number }[][] { + const now = new Date(); + const result: { date: string, hits: number, bytes: number }[][] = []; + + for (let i = 0; i < 30; i++) { + const date = getLocalDateString(new Date(now.getTime() - i * 24 * 60 * 60 * 1000)); + const dayData = this.data.find(entry => entry.date === date); + const dayResult: { date: string, hits: number, bytes: number }[] = []; + + for (let hour = 0; hour < 24; hour++) { + const hourString = hour.toString().padStart(2, '0'); + const hourData = dayData?.hourlyStats.find(entry => entry.hour === hourString); + dayResult.push({ + date: `${date}T${hourString}:00:00`, + hits: hourData ? hourData.hits : 0, + bytes: hourData ? hourData.bytes : 0 + }); + } - private loadFromFile(): void { - const fileContent = fs.readFileSync(this.filePath, 'utf8'); - this.data = JSON.parse(fileContent); + result.push(dayResult); } - public stopAutoSave(): void { - clearInterval(this.saveInterval); - this.maybeWriteToFile(); + return result; + } + + private maybeWriteToFile(): void { + if (this.dataUpdated) { + writeHourlyData(this.filePath, this.data); + this.dataUpdated = false; } + } + + public stopAutoSave(): void { + clearInterval(this.saveInterval); + this.maybeWriteToFile(); // 在停止时立即保存数据(如果有更新) + } }