Files
SenMeshNet/electron/bridges/inspection/reportScheduler.cjs

160 lines
6.1 KiB
JavaScript
Raw Normal View History

"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 日报告每日只触发一次
/**
* 启动 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;
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)} 分钟`);
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));
}
};
// 启动后等 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,
};