// StatsRecorder.js - 记录信息模块 const { mongoose } = require('../index'); const EyeTimeRecorder = require('./EyeTimeRecorder'); const TimezoneUtils = require('../utils/timezone'); // 引入时区工具 let eyeTimeRecorder; let timezoneUtils; // 定义新的数据模型 - 按天/小时/应用存储 const DailyStat = mongoose.model('DailyStat', { deviceId: String, date: Date, // 本地时区日期零点 (存储为该时区零点的 UTC 时间戳) appName: String, hourlyUsage: [Number] // 24小时数组,每项代表分钟数 }); class StatsRecorder { constructor(timezoneConfig = 8) { // 初始化时区工具(接收时区偏移或时区名称) timezoneUtils = new TimezoneUtils(timezoneConfig); eyeTimeRecorder = new EyeTimeRecorder({timezoneOffset: timezoneConfig}); // 设备应用切换记录 this.recentAppSwitches = new Map(); // {deviceId: [{appName, timestamp}]} // 电池信息存储 this.batteryInfo = new Map(); // {deviceId: {level, isCharging, timestamp}} } // 记录电池信息和充电状态 recordBattery(deviceId, level, isCharging = false) { const now = new Date(); this.batteryInfo.set(deviceId, { level: level, isCharging: isCharging, timestamp: now }); } // 获取最新电池信息 getLatestBatteryInfo(deviceId) { const info = this.batteryInfo.get(deviceId); if (!info) { return { level: 0, isCharging: false, timestamp: null }; } return info; } // 记录应用使用时间 async recordUsage(deviceId, appName, running) { const now = new Date(); await eyeTimeRecorder.recordActivity(deviceId, running) // 记录公共使用时间(用眼时长) if (!this.recentAppSwitches.has(deviceId)) { this.recentAppSwitches.set(deviceId, []); } const deviceSwitches = this.recentAppSwitches.get(deviceId); // 处理停止运行的情况 if (running === false) { if (deviceSwitches.length > 0) { const lastSwitch = deviceSwitches[0]; if (lastSwitch.running !== false) { const minutesSinceLastSwitch = this.calculatePreciseMinutes(lastSwitch.timestamp, now); // 更新应用分时段时间统计,传递完整的开始时间戳 await this.updateDailyStat(deviceId, lastSwitch.appName, lastSwitch.timestamp, minutesSinceLastSwitch); } deviceSwitches[0].running = false; deviceSwitches.unshift({ appName: "设备待机", timestamp: now, running: false }); } return; } // 使用时间计算 let minutesSinceLastSwitch = 0; if (deviceSwitches.length > 0) { const lastSwitch = deviceSwitches[0]; if (lastSwitch.running !== false) { minutesSinceLastSwitch = this.calculatePreciseMinutes(lastSwitch.timestamp, now); // 关键修改:传递完整的开始时间戳 await this.updateDailyStat(deviceId, lastSwitch.appName, lastSwitch.timestamp, minutesSinceLastSwitch); } } // 添加新记录 deviceSwitches.unshift({ appName: appName, timestamp: now, running: true }); if (deviceSwitches.length > 20) { deviceSwitches.pop(); } } // 精确计算时间差,返回小数分钟(精确到2位) calculatePreciseMinutes(startTime, endTime) { const milliseconds = endTime - startTime; const minutes = milliseconds / (60 * 1000); // 保留2位小数 return Math.round(minutes * 100) / 100; } // 获取本地时区的日期零点(返回该零点对应的 UTC Date 对象) getLocalDayStart(timestamp) { // 将 UTC 时间转换为本地时间 const localTime = timezoneUtils.utcToLocal(new Date(timestamp)); // 获取本地时间的日期部分 const year = localTime.getUTCFullYear(); const month = localTime.getUTCMonth(); const day = localTime.getUTCDate(); // 创建本地零点时间 const localDayStart = new Date(Date.UTC(year, month, day, 0, 0, 0, 0)); // 转换回 UTC(这样存储的就是本地零点对应的 UTC 时间) return timezoneUtils.localToUtc(localDayStart); } // 更新每日统计 async updateDailyStat(deviceId, appName, startTimestamp, durationMinutes) { // 使用本地时区的日期零点 const dayStart = this.getLocalDayStart(startTimestamp); let stat = await DailyStat.findOne({ deviceId, date: dayStart, appName }); if (!stat) { stat = new DailyStat({ deviceId, date: dayStart, appName, hourlyUsage: Array(24).fill(0) }); } await this.distributePreciseMinutes(stat, startTimestamp, durationMinutes); await stat.save(); } async distributePreciseMinutes(stat, startTimestamp, totalMinutes) { let remainingMinutes = totalMinutes; let currentTimestamp = new Date(startTimestamp); while (remainingMinutes > 0) { // 获取当前时间戳对应的本地日期零点 const currentDayStart = this.getLocalDayStart(currentTimestamp); // 获取本地时区的小时、分钟、秒 const localTime = timezoneUtils.utcToLocal(currentTimestamp); const currentHour = localTime.getUTCHours(); const currentMinute = localTime.getUTCMinutes(); const currentSecond = localTime.getUTCSeconds(); // 如果跨日期了,需要获取新的统计记录 let currentStat = stat; if (currentDayStart.getTime() !== stat.date.getTime()) { currentStat = await DailyStat.findOne({ deviceId: stat.deviceId, date: currentDayStart, appName: stat.appName }); if (!currentStat) { currentStat = new DailyStat({ deviceId: stat.deviceId, date: currentDayStart, appName: stat.appName, hourlyUsage: Array(24).fill(0) }); } } // 计算当前小时内已使用的分钟数 const usedInCurrentHour = currentStat.hourlyUsage[currentHour]; // 计算当前时间点到下一个小时开始还有多少分钟 const minutesToNextHour = 60 - currentMinute - (currentSecond > 0 ? (currentSecond / 60) : 0); // 当前小时的剩余容量 const availableSpace = Math.max(0, 60 - usedInCurrentHour); // 实际能在当前小时分配的时间 const minutesToAdd = Math.min(remainingMinutes, minutesToNextHour, availableSpace); if (minutesToAdd > 0) { const preciseMinutesToAdd = Math.round(minutesToAdd * 100) / 100; currentStat.hourlyUsage[currentHour] = Math.round((currentStat.hourlyUsage[currentHour] + preciseMinutesToAdd) * 100) / 100; // 如果是跨日期的新统计记录,需要保存 if (currentStat !== stat) { await currentStat.save(); } remainingMinutes = Math.round((remainingMinutes - preciseMinutesToAdd) * 100) / 100; } // 移动到下一个时间点 if (minutesToAdd >= minutesToNextHour) { // 移动到下一个小时的开始 currentTimestamp = new Date(currentTimestamp.getTime() + minutesToNextHour * 60 * 1000); } else { // 在当前小时内完成了分配 break; } // 避免浮点数精度问题 if (remainingMinutes < 0.01) { remainingMinutes = 0; } // 防止无限循环(最多处理30天) const daysDifference = Math.floor((currentTimestamp - startTimestamp) / (24 * 60 * 60 * 1000)); if (daysDifference > 30) { console.warn(`超过30天限制,剩余 ${remainingMinutes} 分钟无法分配,设备: ${stat.deviceId}, 应用: ${stat.appName}`); break; } } } } module.exports = StatsRecorder;