// migrate_timezone_v3.js - 正确的时区迁移(移动hourlyUsage数据) const { mongoose } = require('../index'); const readline = require('readline'); const TIMEZONE_OFFSET = 8; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); function question(query) { return new Promise(resolve => { rl.question(query, resolve); }); } // 将 UTC 时间转换为 +8 时区的零点 function convertUtcToLocalDayStart(utcDate) { const date = new Date(utcDate); const localTime = new Date(date.getTime() + TIMEZONE_OFFSET * 60 * 60 * 1000); 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)); return new Date(localDayStart.getTime() - TIMEZONE_OFFSET * 60 * 60 * 1000); } function formatDate(date) { return new Date(date).toISOString(); } function formatLocalDate(date) { const d = new Date(new Date(date).getTime() + TIMEZONE_OFFSET * 60 * 60 * 1000); return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`; } // 关键函数:将 hourlyUsage 从 UTC 时间转换为本地时间 function convertHourlyUsageToLocal(hourlyUsage, utcDate) { const newHourlyUsage = Array(24).fill(0); // 对于每个小时的数据 for (let utcHour = 0; utcHour < 24; utcHour++) { if (hourlyUsage[utcHour] > 0) { // 计算这个 UTC 小时对应的本地时间 const utcTimestamp = new Date(utcDate); utcTimestamp.setUTCHours(utcHour, 0, 0, 0); // 转换为本地时间 const localTimestamp = new Date(utcTimestamp.getTime() + TIMEZONE_OFFSET * 60 * 60 * 1000); // 获取本地时间的日期和小时 const localYear = localTimestamp.getUTCFullYear(); const localMonth = localTimestamp.getUTCMonth(); const localDay = localTimestamp.getUTCDate(); const localHour = localTimestamp.getUTCHours(); // 计算目标日期(本地零点) const targetLocalDay = new Date(Date.UTC(localYear, localMonth, localDay, 0, 0, 0, 0)); const targetDate = new Date(targetLocalDay.getTime() - TIMEZONE_OFFSET * 60 * 60 * 1000); const minutes = Number(hourlyUsage[utcHour]) || 0; } } } // 生成器函数版本 function* convertHourlyUsageToLocalGen(hourlyUsage, utcDate) { for (let utcHour = 0; utcHour < 24; utcHour++) { if (hourlyUsage[utcHour] > 0) { const utcTimestamp = new Date(utcDate); utcTimestamp.setUTCHours(utcHour, 0, 0, 0); const localTimestamp = new Date(utcTimestamp.getTime() + TIMEZONE_OFFSET * 60 * 60 * 1000); const localYear = localTimestamp.getUTCFullYear(); const localMonth = localTimestamp.getUTCMonth(); const localDay = localTimestamp.getUTCDate(); const localHour = localTimestamp.getUTCHours(); const targetLocalDay = new Date(Date.UTC(localYear, localMonth, localDay, 0, 0, 0, 0)); const targetDate = new Date(targetLocalDay.getTime() - TIMEZONE_OFFSET * 60 * 60 * 1000); const minutes = Number(hourlyUsage[utcHour]) || 0; yield { targetDate, targetHour: localHour, minutes, sourceHour: utcHour }; } } } async function migrateData() { try { console.log('='.repeat(80)); console.log('数据迁移脚本:UTC 转 +8 时区(物理移动 hourlyUsage)'); console.log('='.repeat(80)); console.log(); const DailyStat = mongoose.model('DailyStat'); // 1. 扫描数据 console.log('正在扫描数据库...\n'); const allStats = await DailyStat.find({}).sort({ date: 1 }); if (allStats.length === 0) { console.log('❌ 数据库中没有数据,无需迁移。'); return; } console.log(`📊 找到 ${allStats.length} 条记录\n`); // 2. 分析数据移动 console.log('📈 分析数据移动...\n'); const moveAnalysis = []; for (const stat of allStats.slice(0, Math.min(5, allStats.length))) { const moves = []; for (const move of convertHourlyUsageToLocalGen(stat.hourlyUsage, stat.date)) { moves.push(move); } if (moves.length > 0) { moveAnalysis.push({ deviceId: stat.deviceId, appName: stat.appName, oldDate: formatLocalDate(stat.date), moves }); } } console.log('数据移动示例(前5条有数据的记录):'); moveAnalysis.forEach((analysis, idx) => { console.log(`\n${idx + 1}. ${analysis.deviceId} | ${analysis.appName} | ${analysis.oldDate}`); analysis.moves.forEach(move => { console.log(` UTC ${move.sourceHour}时 (${move.minutes}分) → ` + `本地 ${formatLocalDate(move.targetDate)} ${move.targetHour}时`); }); }); console.log(); // 3. 确认执行 console.log('='.repeat(80)); console.log('⚠️ 重要提示:'); console.log('1. 此操作将物理移动 hourlyUsage 数据到正确的本地时间位置'); console.log('2. 数据会按照 +8 时区重新分配到正确的日期和小时'); console.log('3. 建议先备份数据库'); console.log('4. 如果数据跨天,会自动合并到正确的日期记录'); console.log('='.repeat(80)); console.log(); const answer = await question('确认执行迁移?(yes/no): '); if (answer.toLowerCase() !== 'yes') { console.log('\n❌ 迁移已取消'); return; } // 4. 执行迁移 console.log('\n开始迁移...\n'); // 临时存储:设备_应用_新日期 -> hourlyUsage const newRecords = new Map(); let processedCount = 0; // 第一步:收集所有需要移动的数据 for (const stat of allStats) { for (const move of convertHourlyUsageToLocalGen(stat.hourlyUsage, stat.date)) { const key = `${stat.deviceId}_${stat.appName}_${formatDate(move.targetDate)}`; if (!newRecords.has(key)) { newRecords.set(key, { deviceId: stat.deviceId, appName: stat.appName, date: move.targetDate, hourlyUsage: Array(24).fill(0) }); } const record = newRecords.get(key); record.hourlyUsage[move.targetHour] += move.minutes; } processedCount++; const progress = Math.floor((processedCount / allStats.length) * 100); process.stdout.write(`\r第一阶段 - 收集数据: ${progress}%`); } console.log(`\n\n收集完成,生成 ${newRecords.size} 条新记录\n`); // 第二步:删除所有旧记录 console.log('第二阶段 - 清理旧数据...'); const deleteResult = await DailyStat.deleteMany({}); console.log(`✅ 删除了 ${deleteResult.deletedCount} 条旧记录\n`); // 第三步:插入新记录 console.log('第三阶段 - 插入新数据...'); let insertedCount = 0; for (const [key, record] of newRecords) { // 四舍五入到2位小数 record.hourlyUsage = record.hourlyUsage.map(v => Math.round(v * 100) / 100 ); await DailyStat.create(record); insertedCount++; const progress = Math.floor((insertedCount / newRecords.size) * 100); process.stdout.write(`\r插入新数据: ${progress}%`); } console.log('\n\n='.repeat(80)); console.log('✅ 迁移完成!'); console.log('='.repeat(80)); console.log(`📊 统计:`); console.log(` - 原始记录数:${allStats.length}`); console.log(` - 删除记录数:${deleteResult.deletedCount}`); console.log(` - 新记录数:${insertedCount}`); console.log(` - 差异:${insertedCount - allStats.length} (负数表示合并,正数表示拆分)`); console.log('='.repeat(80)); // 5. 验证结果 console.log('\n验证迁移结果...\n'); const verifyStats = await DailyStat.find({}).sort({ date: 1 }).limit(5); console.log('迁移后的数据示例(前5条):'); verifyStats.forEach(stat => { const total = stat.hourlyUsage.reduce((sum, val) => sum + val, 0); const nonZero = stat.hourlyUsage .map((v, i) => v > 0 ? `${i}时:${v}分` : '') .filter(s => s) .join(', '); console.log(`\n${stat.deviceId} | ${stat.appName}`); console.log(` 日期: ${formatLocalDate(stat.date)} (UTC: ${formatDate(stat.date)})`); console.log(` 总计: ${total.toFixed(2)} 分钟`); console.log(` 分布: ${nonZero || '无数据'}`); }); } catch (error) { console.error('\n❌ 迁移过程中发生错误:', error); throw error; } } async function main() { try { await migrateData(); } catch (error) { console.error('脚本执行失败:', error); process.exit(1); } finally { rl.close(); await mongoose.connection.close(); console.log('\n数据库连接已关闭'); process.exit(0); } } main();