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

157 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 日报告每日只触发一次
let _deviceLastRun = new Map(); // deviceId → timestamp(用于 per-device interval)
/**
* 启动 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);
// 临时验证:忽略 cfg.enabled
// if (!cfg.enabled) return;
const now = Date.now();
const dueTargets = [];
// null/undefined = 全部;空数组 = 不巡检;非空数组 = 指定设备
let deviceIds = cfg.enabledDeviceIds;
if (deviceIds === null || deviceIds === undefined) {
// 全部设备 —— 通过回调拿(由调用方注入,避免依赖不存在的 deviceStore.listDevices)
if (typeof _getAllDeviceIds === "function") {
deviceIds = _getAllDeviceIds();
console.error("[reportScheduler] tick: enabledDeviceIds=null, _getAllDeviceIds →", deviceIds?.length || 0, "台");
} else {
console.error("[reportScheduler] ❌ 无 getAllDeviceIds 回调,跳过");
return;
}
} else if (Array.isArray(deviceIds) && deviceIds.length === 0) {
console.log("[reportScheduler] tick: enabledDeviceIds=[], 跳过(不巡检任何设备)");
return;
} else {
console.log("[reportScheduler] tick: enabledDeviceIds 非空数组 →", JSON.stringify(deviceIds));
}
for (const deviceId of deviceIds || []) {
if (!reportScheduleStore.isDeviceEnabled(readFn, deviceId)) continue;
// 临时验证:强制 2 分钟间隔 + 首次立即触发
const intervalMs = 2 * 60 * 1000;
const lastRun = _deviceLastRun.get(deviceId) || 0;
if (now - lastRun >= intervalMs) {
dueTargets.push(deviceId);
}
}
console.log("[reportScheduler] tick: 总设备", deviceIds?.length || 0, "台, 到点", dueTargets.length, "台");
if (dueTargets.length === 0) return;
console.log(`[reportScheduler] tick: ${dueTargets.length} 台设备到点,interval=${cfg.defaultIntervalHours}h`);
try {
await runSnapshotFn(dueTargets, "scheduled");
for (const id of dueTargets) _deviceLastRun.set(id, now);
} catch (e) {
console.error("[reportScheduler] runSnapshot 失败:", e?.message || String(e));
}
};
// 临时验证:启动后立刻执行一次 tick(不等间隔)
console.log("[reportScheduler] 启动,立即执行首次 tick");
tick().catch(e => console.error("[reportScheduler] 首次 tick 异常:", e));
// 临时验证:每 5 秒 tick 一次
_tickTimer = setInterval(() => { tick().catch(e => console.error("[reportScheduler] tick 异常:", e)); }, 5 * 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?.();
console.log("[reportScheduler] 已启动");
}
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,
};