406 lines
14 KiB
JavaScript
406 lines
14 KiB
JavaScript
const dayjs = require('dayjs');
|
|
const deviceManager = require('../core/deviceManager');
|
|
const db = require('../../dbProxy');
|
|
|
|
/**
|
|
* 任务基类
|
|
* 提供所有任务的通用功能和冲突检测机制
|
|
*/
|
|
class BaseTask {
|
|
constructor(taskType, config = {}) {
|
|
this.taskType = taskType;
|
|
this.config = {
|
|
// 默认配置
|
|
defaultInterval: 30, // 默认间隔30分钟
|
|
defaultPriority: 5,
|
|
requiresLogin: true, // 是否需要登录状态
|
|
conflictsWith: [], // 与哪些任务类型冲突
|
|
...config
|
|
};
|
|
|
|
// 任务执行锁 { sn_code: timestamp }
|
|
this.taskLocks = new Map();
|
|
|
|
// 最后执行时间缓存 { sn_code: timestamp }
|
|
this.lastExecutionCache = new Map();
|
|
}
|
|
|
|
/**
|
|
* Layer 1: 任务类型互斥锁检查
|
|
* 防止同一设备同时添加相同类型的任务
|
|
*/
|
|
acquireTaskLock(sn_code) {
|
|
const lockKey = `${sn_code}:${this.taskType}`;
|
|
const now = Date.now();
|
|
const existingLock = this.taskLocks.get(lockKey);
|
|
|
|
// 如果存在锁且未超时(5分钟),返回false
|
|
if (existingLock && (now - existingLock) < 5 * 60 * 1000) {
|
|
const remainingTime = Math.ceil((5 * 60 * 1000 - (now - existingLock)) / 1000);
|
|
return {
|
|
allowed: false,
|
|
reason: `任务 ${this.taskType} 正在添加中,请等待 ${remainingTime} 秒`
|
|
};
|
|
}
|
|
|
|
// 获取锁
|
|
this.taskLocks.set(lockKey, now);
|
|
return { allowed: true };
|
|
}
|
|
|
|
/**
|
|
* 释放任务锁
|
|
*/
|
|
releaseTaskLock(sn_code) {
|
|
const lockKey = `${sn_code}:${this.taskType}`;
|
|
this.taskLocks.delete(lockKey);
|
|
}
|
|
|
|
/**
|
|
* Layer 2: 设备状态检查
|
|
* 检查设备是否在线、是否登录、是否忙碌
|
|
*/
|
|
async checkDeviceStatus(sn_code) {
|
|
// 1. 优先检查内存中的设备状态
|
|
let device = deviceManager.devices.get(sn_code);
|
|
|
|
// 2. 如果内存中没有,降级到数据库查询(可能是刚启动还没收到心跳)
|
|
if (!device) {
|
|
try {
|
|
const pla_account = db.getModel('pla_account');
|
|
const dbDevice = await pla_account.findOne({
|
|
where: { sn_code, is_delete: 0 },
|
|
attributes: ['sn_code', 'is_online', 'is_logged_in']
|
|
});
|
|
|
|
if (!dbDevice) {
|
|
return {
|
|
allowed: false,
|
|
reason: `设备 ${sn_code} 不存在`
|
|
};
|
|
}
|
|
|
|
// 检查数据库中的在线状态
|
|
if (!dbDevice.is_online) {
|
|
return {
|
|
allowed: false,
|
|
reason: `设备 ${sn_code} 离线(数据库状态)`
|
|
};
|
|
}
|
|
|
|
// 检查数据库中的登录状态
|
|
if (this.config.requiresLogin && !dbDevice.is_logged_in) {
|
|
return {
|
|
allowed: false,
|
|
reason: `设备 ${sn_code} 未登录平台账号(数据库状态)`
|
|
};
|
|
}
|
|
|
|
// 数据库检查通过,允许执行
|
|
return { allowed: true };
|
|
} catch (error) {
|
|
console.error(`[${this.taskType}] 查询设备状态失败:`, error);
|
|
return {
|
|
allowed: false,
|
|
reason: `设备 ${sn_code} 状态查询失败`
|
|
};
|
|
}
|
|
}
|
|
|
|
// 3. 检查心跳超时
|
|
const offlineThreshold = 3 * 60 * 1000; // 3分钟
|
|
const now = Date.now();
|
|
const lastHeartbeat = device.lastHeartbeat || 0;
|
|
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
|
|
|
|
if (!isOnline) {
|
|
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
|
|
return {
|
|
allowed: false,
|
|
reason: `设备 ${sn_code} 离线(最后心跳: ${offlineMinutes}分钟前)`
|
|
};
|
|
}
|
|
|
|
// 4. 检查登录状态(如果任务需要)
|
|
if (this.config.requiresLogin && !device.isLoggedIn) {
|
|
return {
|
|
allowed: false,
|
|
reason: `设备 ${sn_code} 未登录平台账号`
|
|
};
|
|
}
|
|
|
|
return { allowed: true };
|
|
}
|
|
|
|
/**
|
|
* Layer 3: 检查任务队列状态
|
|
* 防止队列中已有相同任务
|
|
*/
|
|
async checkTaskQueue(sn_code, taskQueue) {
|
|
// 获取设备队列
|
|
const deviceQueue = taskQueue.deviceQueues.get(sn_code);
|
|
if (!deviceQueue) {
|
|
return { allowed: true };
|
|
}
|
|
|
|
// 检查队列中是否有相同类型的待执行任务
|
|
const tasks = deviceQueue.toArray();
|
|
const hasSameTypeTask = tasks.some(task =>
|
|
task.taskType === this.taskType &&
|
|
task.status === 'pending'
|
|
);
|
|
|
|
if (hasSameTypeTask) {
|
|
return {
|
|
allowed: false,
|
|
reason: `队列中已存在待执行的 ${this.taskType} 任务`
|
|
};
|
|
}
|
|
|
|
return { allowed: true };
|
|
}
|
|
|
|
/**
|
|
* Layer 4: 检查任务去重
|
|
* 查询数据库中是否有重复的待执行任务
|
|
*/
|
|
async checkDuplicateTask(sn_code) {
|
|
try {
|
|
const { task_status } = db.models;
|
|
|
|
// 查询该设备是否有相同类型的pending/running任务
|
|
const existingTask = await task_status.findOne({
|
|
where: {
|
|
sn_code: sn_code,
|
|
taskType: this.taskType,
|
|
status: ['pending', 'running']
|
|
},
|
|
attributes: ['id', 'status', 'taskName']
|
|
});
|
|
|
|
if (existingTask) {
|
|
return {
|
|
allowed: false,
|
|
reason: `已存在 ${existingTask.status} 状态的任务: ${existingTask.taskName}`
|
|
};
|
|
}
|
|
|
|
return { allowed: true };
|
|
} catch (error) {
|
|
console.error(`[${this.taskType}] 检查重复任务失败:`, error);
|
|
// 出错时允许继续,避免阻塞
|
|
return { allowed: true };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Layer 5: 操作类型冲突检测
|
|
* 某些操作类型不能同时执行
|
|
*/
|
|
async checkOperationConflict(sn_code, taskQueue) {
|
|
// 如果没有配置冲突类型,直接通过
|
|
if (!this.config.conflictsWith || this.config.conflictsWith.length === 0) {
|
|
return { allowed: true };
|
|
}
|
|
|
|
// 检查当前是否有冲突的任务正在执行
|
|
const deviceStatus = taskQueue.deviceStatus.get(sn_code);
|
|
if (deviceStatus && deviceStatus.currentTask) {
|
|
const currentTaskType = deviceStatus.currentTask.taskType;
|
|
|
|
if (this.config.conflictsWith.includes(currentTaskType)) {
|
|
return {
|
|
allowed: false,
|
|
reason: `与正在执行的任务 ${currentTaskType} 冲突`
|
|
};
|
|
}
|
|
}
|
|
|
|
return { allowed: true };
|
|
}
|
|
|
|
/**
|
|
* 检查执行间隔
|
|
* 从数据库查询上次成功执行时间,判断是否满足间隔要求
|
|
*/
|
|
async checkExecutionInterval(sn_code, intervalMinutes) {
|
|
try {
|
|
const { task_status } = db.models;
|
|
|
|
// 先从缓存检查
|
|
const cachedLastExecution = this.lastExecutionCache.get(sn_code);
|
|
const now = Date.now();
|
|
|
|
if (cachedLastExecution) {
|
|
const elapsedTime = now - cachedLastExecution;
|
|
const interval_ms = intervalMinutes * 60 * 1000;
|
|
|
|
if (elapsedTime < interval_ms) {
|
|
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
|
|
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
|
|
return {
|
|
allowed: false,
|
|
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
|
|
remainingMinutes,
|
|
elapsedMinutes
|
|
};
|
|
}
|
|
}
|
|
|
|
// 从数据库查询最近一次成功完成的任务
|
|
const lastTask = await task_status.findOne({
|
|
where: {
|
|
sn_code: sn_code,
|
|
taskType: this.taskType,
|
|
status: 'completed'
|
|
},
|
|
order: [['endTime', 'DESC']],
|
|
attributes: ['endTime']
|
|
});
|
|
|
|
// 如果存在上次执行记录,检查是否已经过了间隔时间
|
|
if (lastTask && lastTask.endTime) {
|
|
const lastExecutionTime = new Date(lastTask.endTime).getTime();
|
|
const elapsedTime = now - lastExecutionTime;
|
|
const interval_ms = intervalMinutes * 60 * 1000;
|
|
|
|
// 更新缓存
|
|
this.lastExecutionCache.set(sn_code, lastExecutionTime);
|
|
|
|
if (elapsedTime < interval_ms) {
|
|
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
|
|
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
|
|
return {
|
|
allowed: false,
|
|
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
|
|
remainingMinutes,
|
|
elapsedMinutes,
|
|
nextExecutionTime: new Date(lastExecutionTime + interval_ms)
|
|
};
|
|
}
|
|
}
|
|
|
|
return { allowed: true };
|
|
} catch (error) {
|
|
console.error(`[${this.taskType}] 检查执行间隔失败:`, error);
|
|
// 出错时允许继续,避免阻塞
|
|
return { allowed: true };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查时间范围限制
|
|
* @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
|
|
*/
|
|
checkTimeRange(timeRange) {
|
|
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
|
|
return { allowed: true, reason: '未配置时间范围' };
|
|
}
|
|
|
|
const now = new Date();
|
|
const currentHour = now.getHours();
|
|
const currentMinute = now.getMinutes();
|
|
const currentTime = currentHour * 60 + currentMinute;
|
|
|
|
// 解析开始时间和结束时间
|
|
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
|
|
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
|
|
const startTime = startHour * 60 + startMinute;
|
|
const endTime = endHour * 60 + endMinute;
|
|
|
|
// 检查是否仅工作日
|
|
if (timeRange.workdays_only == 1) {
|
|
const dayOfWeek = now.getDay();
|
|
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
|
|
}
|
|
}
|
|
|
|
// 检查当前时间是否在时间范围内
|
|
if (startTime <= endTime) {
|
|
// 正常情况: 09:00 - 18:00
|
|
if (currentTime < startTime || currentTime >= endTime) {
|
|
return {
|
|
allowed: false,
|
|
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
|
|
};
|
|
}
|
|
} else {
|
|
// 跨天情况: 22:00 - 06:00
|
|
if (currentTime < startTime && currentTime >= endTime) {
|
|
return {
|
|
allowed: false,
|
|
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
|
|
};
|
|
}
|
|
}
|
|
|
|
return { allowed: true, reason: '在允许的时间范围内' };
|
|
}
|
|
|
|
/**
|
|
* 综合检查 - 执行所有层级的检查
|
|
* @param {string} sn_code - 设备SN码
|
|
* @param {Object} taskQueue - 任务队列实例
|
|
* @param {Object} options - 额外选项
|
|
* @returns {Object} { allowed: boolean, reason: string }
|
|
*/
|
|
async canExecuteTask(sn_code, taskQueue, options = {}) {
|
|
const checks = [
|
|
{ name: 'Layer1-任务锁', fn: () => this.acquireTaskLock(sn_code) },
|
|
{ name: 'Layer2-设备状态', fn: () => this.checkDeviceStatus(sn_code) },
|
|
{ name: 'Layer3-队列检查', fn: () => this.checkTaskQueue(sn_code, taskQueue) },
|
|
{ name: 'Layer4-任务去重', fn: () => this.checkDuplicateTask(sn_code) },
|
|
{ name: 'Layer5-操作冲突', fn: () => this.checkOperationConflict(sn_code, taskQueue) }
|
|
];
|
|
|
|
// 逐层检查
|
|
for (const check of checks) {
|
|
const result = await check.fn();
|
|
if (!result.allowed) {
|
|
console.log(`[${this.taskType}] ${check.name} 未通过: ${result.reason}`);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return { allowed: true };
|
|
}
|
|
|
|
/**
|
|
* 清理任务锁(定期清理过期锁)
|
|
*/
|
|
cleanupExpiredLocks() {
|
|
const now = Date.now();
|
|
const timeout = 5 * 60 * 1000; // 5分钟超时
|
|
|
|
for (const [lockKey, timestamp] of this.taskLocks.entries()) {
|
|
if (now - timestamp > timeout) {
|
|
this.taskLocks.delete(lockKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取任务名称(子类可覆盖)
|
|
*/
|
|
getTaskName(params) {
|
|
return `${this.taskType} 任务`;
|
|
}
|
|
|
|
/**
|
|
* 验证任务参数(子类必须实现)
|
|
*/
|
|
validateParams(params) {
|
|
throw new Error('子类必须实现 validateParams 方法');
|
|
}
|
|
|
|
/**
|
|
* 执行任务的具体逻辑(子类必须实现)
|
|
*/
|
|
async execute(sn_code, params) {
|
|
throw new Error('子类必须实现 execute 方法');
|
|
}
|
|
}
|
|
|
|
module.exports = BaseTask;
|