"use strict"; /** * 巡检报告定时调度器 * * 和 inspectionScheduler.cjs **物理隔离**: * - 独立 tick 循环(1min) * - 独立配置存储(reportScheduleStore) * - 独立触发路径(调 index.cjs 里 runReportSnapshot 的实现函数) * - 独立日报告聚合(dailyReportStore) * * 防止重蹈「两个调度器竞争同一巡检任务」的覆辙。 */ const reportScheduleStore = require("./reportScheduleStore.cjs"); const reportStore = require("./reportStore.cjs"); const dailyReportStore = require("./dailyReportStore.cjs"); let _tickTimer = null; let _dailyTimer = null; let _lastDailyTriggerKey = null; // 用于 HH:MM 日报告每日只触发一次 let _inProgress = false; // 快照执行中锁 —— 防止执行时间 >60s 时被下一个 tick 重入 /** * 启动 reportScheduler * * @param {object} deps * @param {function} deps.readStorage * @param {function} deps.writeStorage * @param {function} deps.runSnapshotFn - 定时快照执行函数(renderer 侧 Hook 实际负责执行,这里传空函数) * @param {function} [deps.onDailyReportReady] - 日报告聚合完成后回调 async (report) => void */ function start({ readStorage, writeStorage, runSnapshotFn, onDailyReportReady, getAllDeviceIdsFn }) { if (_tickTimer) return; // 已启动 const readFn = (k) => readStorage(k); const writeFn = (k, v) => writeStorage(k, v); const _getAllDeviceIds = getAllDeviceIdsFn; const tick = async () => { const cfg = reportScheduleStore.getSchedule(readFn); if (!cfg.enabled) return; // 防重入:快照正在执行中就跳过(执行时间 >60s 时下一个 tick 会撞到) if (_inProgress) { console.log("[reportScheduler] tick: 上一个快照还在执行,跳过"); return; } const now = Date.now(); const intervalMs = (cfg.defaultIntervalHours || 1) * 3600 * 1000; const lastRun = cfg.lastRunAt || 0; const sinceLast = now - lastRun; // 还没到间隔就跳过 if (lastRun > 0 && sinceLast < intervalMs) { const remainingMin = ((intervalMs - sinceLast) / 60000).toFixed(1); console.log(`[reportScheduler] tick: 未到间隔,还需 ${remainingMin} 分钟`); return; } // null/undefined = 全部;空数组 = 不巡检;非空数组 = 指定设备 let deviceIds = cfg.enabledDeviceIds; if (deviceIds === null || deviceIds === undefined) { // 全部设备 —— 通过回调拿(由调用方注入,避免依赖不存在的 deviceStore.listDevices) if (typeof _getAllDeviceIds === "function") { deviceIds = _getAllDeviceIds(); } else { console.error("[reportScheduler] ❌ 无 getAllDeviceIds 回调,跳过"); return; } } else if (Array.isArray(deviceIds) && deviceIds.length === 0) { console.log("[reportScheduler] tick: enabledDeviceIds=[], 跳过(不巡检任何设备)"); return; } // 过滤掉 isDeviceEnabled=false 的(disabled 状态) const enabledDeviceIds = (deviceIds || []).filter(id => reportScheduleStore.isDeviceEnabled(readFn, id) ); if (enabledDeviceIds.length === 0) { console.log("[reportScheduler] tick: 0 台设备启用,跳过"); return; } console.log(`[reportScheduler] tick: 到点,${enabledDeviceIds.length} 台设备,interval=${cfg.defaultIntervalHours}h,距上次 ${(sinceLast/60000).toFixed(1)} 分钟`); _inProgress = true; try { await runSnapshotFn(enabledDeviceIds, "scheduled"); // 持久化 lastRunAt —— 重启 Electron 也知道上次什么时候跑的 const merged = { ...cfg, lastRunAt: now }; reportScheduleStore.saveSchedule(writeFn, merged); console.log("[reportScheduler] ✅ 快照完成,lastRunAt 已持久化"); } catch (e) { console.error("[reportScheduler] runSnapshot 失败:", e?.message || String(e)); } finally { _inProgress = false; } }; // 启动后等 60s tick 自动处理第一次(不立即触发,避免重启爆发) const curCfg = reportScheduleStore.getSchedule(readFn); console.log("[reportScheduler] 已启动,间隔", (curCfg.defaultIntervalHours || 1), "h,下次 tick 检查在 60s 后"); // 每 60 秒 tick 一次 _tickTimer = setInterval(() => { tick().catch(e => console.error("[reportScheduler] tick 异常:", e)); }, 60 * 1000); _tickTimer.unref?.(); // 每秒检查是否到 dailyReportTime(HH:MM 精确匹配,每日只触发一次) const dailyCheck = () => { const cfg = reportScheduleStore.getSchedule(readFn); if (!cfg.enabled) return; const now = new Date(); const p = (n) => n.toString().padStart(2, "0"); const current = `${p(now.getHours())}:${p(now.getMinutes())}`; const key = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`; if (current === cfg.dailyReportTime && _lastDailyTriggerKey !== key) { _lastDailyTriggerKey = key; console.log("[reportScheduler] 触发日运行报告:", key); triggerDailyReport(readFn, writeFn, { mode: "finalized", trigger: "scheduled" }) .then(report => { if (onDailyReportReady) { onDailyReportReady(report).catch(e => console.error("[reportScheduler] onDailyReportReady 异常:", e?.message || String(e)) ); } }) .catch(e => console.error("[reportScheduler] 日报告生成异常:", e)); } }; _dailyTimer = setInterval(dailyCheck, 30 * 1000); _dailyTimer.unref?.(); } function stop() { if (_tickTimer) { clearInterval(_tickTimer); _tickTimer = null; } if (_dailyTimer) { clearInterval(_dailyTimer); _dailyTimer = null; } console.log("[reportScheduler] 已停止"); } function isRunning() { return !!_tickTimer; } /** * 手动触发日运行报告(立即执行,不等到 HH:MM) * mode: "provisional" — 取当天 0:00 ~ 此刻 * "finalized" — 取当天 0:00 ~ 23:59 */ async function triggerDailyReport(readFn, writeFn, { mode = "provisional", trigger = "manual" } = {}) { const now = Date.now(); const dayStart = dailyReportStore._dayStart(now); const dayEnd = mode === "finalized" ? dailyReportStore._dayEnd(now) : now; const allSnapshots = reportStore.listSnapshots(readFn, 1000); const report = dailyReportStore.buildDailyReport(allSnapshots, dayStart, dayEnd, { mode, trigger }); dailyReportStore.addDailyReport(readFn, writeFn, report); console.log(`[reportScheduler] 日报告 ${report.id} 已生成 (mode=${mode})`); return report; } module.exports = { start, stop, isRunning, triggerDailyReport, };