This commit is contained in:
张成
2025-11-24 13:23:42 +08:00
commit 5d7444cd65
156 changed files with 50653 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
const db = require('../dbProxy');
/**
* 统一错误处理模块
* 负责错误分类、记录、恢复决策
*/
class ErrorHandler {
/**
* 可重试的错误类型
*/
static RETRYABLE_ERRORS = [
'ETIMEDOUT',
'ECONNRESET',
'ENOTFOUND',
'NetworkError',
'MQTT客户端未初始化',
'设备离线',
'超时'
];
/**
* 处理错误
* @param {Error} error - 错误对象
* @param {Object} context - 上下文信息
* @returns {Object} 错误处理结果
*/
static async handleError(error, context = {}) {
const errorInfo = {
message: error.message || '未知错误',
stack: error.stack || '',
code: error.code || '',
context: {
taskId: context.taskId,
sn_code: context.sn_code,
taskType: context.taskType,
...context
},
timestamp: new Date().toISOString(),
isRetryable: this.isRetryableError(error)
};
// 记录到日志
console.error(`[错误处理] ${errorInfo.message}`, {
context: errorInfo.context,
isRetryable: errorInfo.isRetryable,
stack: errorInfo.stack
});
// 错误信息已通过 console.error 记录到控制台日志
return errorInfo;
}
/**
* 判断错误是否可重试
* @param {Error} error - 错误对象
* @returns {boolean}
*/
static isRetryableError(error) {
if (!error) return false;
const errorMessage = (error.message || '').toLowerCase();
const errorCode = error.code || '';
// 检查错误代码
if (this.RETRYABLE_ERRORS.some(code => errorCode === code)) {
return true;
}
// 检查错误消息
return this.RETRYABLE_ERRORS.some(code =>
errorMessage.includes(code.toLowerCase())
);
}
/**
* 计算重试延迟(指数退避)
* @param {number} retryCount - 当前重试次数
* @param {number} baseDelay - 基础延迟(毫秒)
* @param {number} maxDelay - 最大延迟(毫秒)
* @returns {number} 延迟时间(毫秒)
*/
static calculateRetryDelay(retryCount, baseDelay = 1000, maxDelay = 30000) {
const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), maxDelay);
return delay;
}
/**
* 创建可重试错误
* @param {string} message - 错误消息
* @param {Object} context - 上下文
* @returns {Error}
*/
static createRetryableError(message, context = {}) {
const error = new Error(message);
error.code = 'RETRYABLE';
error.context = context;
error.isRetryable = true;
return error;
}
/**
* 创建致命错误
* @param {string} message - 错误消息
* @param {Object} context - 上下文
* @returns {Error}
*/
static createFatalError(message, context = {}) {
const error = new Error(message);
error.code = 'FATAL';
error.context = context;
error.isRetryable = false;
return error;
}
}
module.exports = ErrorHandler;

View File

@@ -0,0 +1,215 @@
/**
* 优先级队列实现(使用最小堆)
* 优先级高的任务priority值大会优先出队
*/
class PriorityQueue {
constructor() {
this.heap = [];
}
/**
* 获取父节点索引
*/
parent(index) {
return Math.floor((index - 1) / 2);
}
/**
* 获取左子节点索引
*/
leftChild(index) {
return 2 * index + 1;
}
/**
* 获取右子节点索引
*/
rightChild(index) {
return 2 * index + 2;
}
/**
* 交换两个节点
*/
swap(i, j) {
[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
}
/**
* 上浮操作(插入时使用)
*/
bubbleUp(index) {
if (index === 0) return;
const parentIndex = this.parent(index);
const current = this.heap[index];
const parent = this.heap[parentIndex];
// 优先级高的在前priority值大如果优先级相同创建时间早的在前
if (
current.priority > parent.priority ||
(current.priority === parent.priority && current.createdAt < parent.createdAt)
) {
this.swap(index, parentIndex);
this.bubbleUp(parentIndex);
}
}
/**
* 下沉操作(删除时使用)
*/
bubbleDown(index) {
const leftIndex = this.leftChild(index);
const rightIndex = this.rightChild(index);
let largest = index;
const current = this.heap[index];
// 比较左子节点
if (leftIndex < this.heap.length) {
const left = this.heap[leftIndex];
if (
left.priority > current.priority ||
(left.priority === current.priority && left.createdAt < current.createdAt)
) {
largest = leftIndex;
}
}
// 比较右子节点
if (rightIndex < this.heap.length) {
const right = this.heap[rightIndex];
const largestNode = this.heap[largest];
if (
right.priority > largestNode.priority ||
(right.priority === largestNode.priority && right.createdAt < largestNode.createdAt)
) {
largest = rightIndex;
}
}
if (largest !== index) {
this.swap(index, largest);
this.bubbleDown(largest);
}
}
/**
* 添加任务到队列
* @param {Object} task - 任务对象,必须包含 priority 和 createdAt 属性
*/
push(task) {
if (!task.hasOwnProperty('priority')) {
task.priority = 5; // 默认优先级
}
if (!task.hasOwnProperty('createdAt')) {
task.createdAt = Date.now();
}
this.heap.push(task);
this.bubbleUp(this.heap.length - 1);
}
/**
* 取出优先级最高的任务
* @returns {Object|null} 任务对象或null
*/
pop() {
if (this.heap.length === 0) {
return null;
}
if (this.heap.length === 1) {
return this.heap.pop();
}
const top = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown(0);
return top;
}
/**
* 查看优先级最高的任务(不移除)
* @returns {Object|null} 任务对象或null
*/
peek() {
return this.heap.length > 0 ? this.heap[0] : null;
}
/**
* 获取队列大小
* @returns {number}
*/
size() {
return this.heap.length;
}
/**
* 检查队列是否为空
* @returns {boolean}
*/
isEmpty() {
return this.heap.length === 0;
}
/**
* 清空队列
*/
clear() {
this.heap = [];
}
/**
* 查找任务
* @param {Function} predicate - 查找条件函数
* @returns {Object|null} 任务对象或null
*/
find(predicate) {
return this.heap.find(predicate) || null;
}
/**
* 移除任务
* @param {Function} predicate - 查找条件函数
* @returns {boolean} 是否成功移除
*/
remove(predicate) {
const index = this.heap.findIndex(predicate);
if (index === -1) {
return false;
}
if (index === this.heap.length - 1) {
this.heap.pop();
return true;
}
// 将最后一个元素移到当前位置
this.heap[index] = this.heap.pop();
// 重新调整堆
const parentIndex = this.parent(index);
if (index > 0 && this.heap[parentIndex] &&
(this.heap[index].priority > this.heap[parentIndex].priority ||
(this.heap[index].priority === this.heap[parentIndex].priority &&
this.heap[index].createdAt < this.heap[parentIndex].createdAt))) {
this.bubbleUp(index);
} else {
this.bubbleDown(index);
}
return true;
}
/**
* 转换为数组(用于调试)
* @returns {Array}
*/
toArray() {
return [...this.heap];
}
}
module.exports = PriorityQueue;

View File

@@ -0,0 +1,308 @@
const logs = require('../logProxy');
const db = require('../dbProxy');
const jobManager = require('../job/index');
const ScheduleUtils = require('./utils');
const ScheduleConfig = require('./config');
/**
* 指令管理器
* 负责管理任务下的多个指令简化MQTT通信流程
*/
class CommandManager {
constructor() {
this.pendingCommands = new Map(); // 等待响应的指令 { commandId: { resolve, reject, timeout } }
}
/**
* 执行指令序列
* @param {Array} commands - 指令数组
* @param {object} mqttClient - MQTT客户端
* @param {object} options - 执行选项
* @returns {Promise<object>} 执行结果
*/
async executeCommands(taskId, commands, mqttClient, options = {}) {
// try {
if (!commands || commands.length === 0) {
throw new Error('没有找到要执行的指令');
}
const {
maxRetries = 1, // 最大重试次数
retryDelay = 1000 // 重试延迟(毫秒)
} = options;
console.log(`[指令管理] 开始执行 ${commands.length} 个指令`);
const results = [];
const errors = [];
// 顺序执行指令,失败时停止
for (let i = 0; i < commands.length; i++) {
const command = commands[i];
let retryCount = 0;
let commandResult = null;
// 重试逻辑
while (retryCount <= maxRetries) {
console.log(`[指令管理] 执行指令 ${i + 1}/${commands.length}: ${command.command_name || command.name} (尝试 ${retryCount + 1}/${maxRetries + 1})`);
commandResult = await this.executeCommand(taskId, command, mqttClient);
results. push(commandResult);
break; // 成功执行,跳出重试循环
}
}
const successCount = results.length;
const errorCount = errors.length;
console.log(`[指令管理] 指令执行完成: 成功 ${successCount}/${commands.length}, 失败 ${errorCount}`);
return {
success: errorCount === 0, // 只有全部成功才算成功
results: results,
errors: errors,
totalCommands: commands.length,
successCount: successCount,
errorCount: errorCount
};
// } catch (error) {
// console.error(`[指令管理] 执行指令序列失败:`, error);
// throw error;
// }
}
/**
* 执行单个指令
* @param {object} command - 指令对象
* @param {object} mqttClient - MQTT客户端
* @returns {Promise<object>} 执行结果
*/
async executeCommand(taskId, command, mqttClient) {
const startTime = new Date();
let commandRecord = null;
const task = await db.getModel('task_status').findByPk(taskId);
// 获取指令信息(支持两种格式)
const commandName = command.command_name;
const commandType = command.command_type;
const commandParams = command.command_params ? JSON.parse(command.command_params) : {};
// 创建指令记录
commandRecord = await db.getModel('task_commands').create({
task_id: taskId,
command_type: commandType,
command_name: commandName,
command_params: JSON.stringify(commandParams),
priority: command.priority || 1,
sequence: command.sequence || 1,
max_retries: command.maxRetries || command.max_retries || 3,
status: 'pending'
});
let commandId = commandRecord.id;
console.log(`[指令管理] 创建指令记录: ${commandName} (ID: ${commandId})`);
// 更新指令状态为运行中
await this.updateCommandStatus(commandId, 'running');
console.log(`[指令管理] 执行指令: ${commandName} (ID: ${commandId})`);
const sn_code = task.sn_code;
// 将驼峰命名转换为下划线命名getOnlineResume -> get_online_resume
const toSnakeCase = (str) => {
// 如果已经是下划线格式,直接返回
if (str.includes('_')) {
return str;
}
// 驼峰转下划线
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
};
const methodName = toSnakeCase(commandType);
// 获取指令超时时间从配置中获取默认5分钟
const timeout = ScheduleConfig.taskTimeouts[commandType] || ScheduleConfig.taskTimeouts[methodName] || 5 * 60 * 1000;
let result;
try {
// 使用超时机制包装指令执行
const commandPromise = (async () => {
if (commandType && jobManager[methodName]) {
return await jobManager[methodName](sn_code, mqttClient, commandParams);
} else {
// 如果转换后找不到,尝试直接使用原名称
if (jobManager[commandType]) {
return await jobManager[commandType](sn_code, mqttClient, commandParams);
} else {
throw new Error(`未知的指令类型: ${commandType} (尝试的方法名: ${methodName})`);
}
}
})();
// 使用超时机制
result = await ScheduleUtils.withTimeout(
commandPromise,
timeout,
`指令执行超时: ${commandName} (超时时间: ${timeout / 1000}秒)`
);
} catch (error) {
const endTime = new Date();
const duration = endTime - startTime;
// 如果是超时错误,更新指令状态为失败
const errorMessage = error.message || '指令执行失败';
await this.updateCommandStatus(commandId, 'failed', null, errorMessage);
throw error;
}
const endTime = new Date();
const duration = endTime - startTime;
// 更新指令状态为完成
await this.updateCommandStatus(commandId, 'completed', result);
return {
commandId: commandId,
commandName: commandName,
result: result,
duration: duration,
success: true
};
}
/**
* 更新指令状态
* @param {number} commandId - 指令ID
* @param {string} status - 状态
* @param {object} result - 结果
* @param {string} errorMessage - 错误信息
*/
async updateCommandStatus(commandId, status, result = null, errorMessage = null) {
try {
const updateData = {
status: status,
updated_at: new Date()
};
if (status === 'running') {
updateData.start_time = new Date();
} else if (status === 'completed' || status === 'failed') {
updateData.end_time = new Date();
if (result) {
// 将结果转换为JSON字符串并限制长度TEXT类型最大约65KB
let resultStr = JSON.stringify(result);
const maxLength = 60000; // 限制为60KB留一些余量
if (resultStr.length > maxLength) {
// 如果结果太长,尝试压缩或截断
try {
// 如果是对象,尝试只保存关键信息
if (typeof result === 'object' && result !== null) {
const summary = {
success: result.success !== undefined ? result.success : true,
message: result.message || '执行成功',
dataLength: resultStr.length,
truncated: true,
preview: resultStr.substring(0, 1000) // 保存前1000字符作为预览
};
resultStr = JSON.stringify(summary);
} else {
// 直接截断
resultStr = resultStr.substring(0, maxLength) + '...[数据已截断]';
}
} catch (e) {
// 如果处理失败,直接截断
resultStr = resultStr.substring(0, maxLength) + '...[数据已截断]';
}
}
updateData.result = resultStr;
updateData.progress = 100;
}
if (errorMessage) {
// 错误信息也限制长度
const maxErrorLength = 10000; // 错误信息限制10KB
updateData.error_message = errorMessage.length > maxErrorLength
? errorMessage.substring(0, maxErrorLength) + '...[错误信息已截断]'
: errorMessage;
}
// 计算执行时长
const command = await db.getModel('task_commands').findByPk(commandId);
if (command && command.start_time) {
const duration = new Date() - new Date(command.start_time);
updateData.duration = duration;
}
}
await db.getModel('task_commands').update(updateData, {
where: { id: commandId }
});
} catch (error) {
logs.error(`[指令管理] 更新指令状态失败:`, error, {
commandId: commandId,
status: status
});
// 如果是因为数据太长导致的错误,尝试只保存错误信息
if (error.message && error.message.includes('Data too long')) {
try {
await db.getModel('task_commands').update({
status: status,
error_message: '结果数据过长,无法保存完整结果',
end_time: new Date(),
updated_at: new Date()
}, {
where: { id: commandId }
});
} catch (e) {
console.error(`[指令管理] 保存截断结果也失败:`, e);
}
}
}
}
/**
* 清理过期的指令记录
* @param {number} days - 保留天数
*/
async cleanupExpiredCommands(days = 30) {
try {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const deletedCount = await db.getModel('task_commands').destroy({
where: {
create_time: {
[db.Sequelize.Op.lt]: cutoffDate
}
}
});
console.log(`[指令管理] 清理了 ${deletedCount} 条过期指令记录`);
return deletedCount;
} catch (error) {
logs.error(`[指令管理] 清理过期指令失败:`, error);
return 0;
}
}
}
module.exports = new CommandManager();

View File

@@ -0,0 +1,150 @@
const dayjs = require('dayjs');
const config = require('../../../config/config');
/**
* 调度系统配置中心
* 统一管理所有配置参数
*/
class ScheduleConfig {
constructor() {
// 工作时间配置
this.workHours = {
start: 9,
end: 18
};
// 频率限制配置(毫秒)
this.rateLimits = {
search: 30 * 60 * 1000, // 搜索间隔30分钟
apply: 5 * 60 * 1000, // 投递间隔5分钟
chat: 1 * 60 * 1000, // 聊天间隔1分钟
};
// 单日操作限制
this.dailyLimits = {
maxSearch: 20, // 每天最多搜索20次
maxApply: 50, // 每天最多投递50份简历
maxChat: 100, // 每天最多发送100条聊天
};
// 任务超时配置(毫秒)
this.taskTimeouts = {
get_login_qr_code: 30 * 1000, // 登录检查30秒
get_resume: 60 * 1000, // 获取简历1分钟
search_jobs: 5 * 60 * 1000, // 搜索岗位5分钟
chat: 30 * 1000, // 聊天30秒
apply: 30 * 1000 // 投递30秒
};
// 任务优先级配置
this.taskPriorities = {
get_login_qr_code: 10, // 最高优先级
get_resume: 9,
apply: 8,
auto_deliver: 7, // 自动投递任务
search_jobs: 6,
chat: 5,
cleanup: 1
};
// 监控配置
this.monitoring = {
heartbeatTimeout: 3 * 60 * 1000, // 心跳超时5分钟
taskFailureRate: 0.5, // 任务失败率50%
consecutiveFailures: 3, // 连续失败次数3次
alertCooldown: 5 * 60 * 1000, // 告警冷却5分钟
offlineThreshold: 24 * 60 * 60 * 1000 // 离线设备清理24小时
};
// 定时任务配置
this.schedules = {
dailyReset: '0 0 * * *', // 每天凌晨重置统计
jobFlowInterval: '0 */5 * * * *', // 每10秒执行一次找工作流程
monitoringInterval: '*/1 * * * *', // 监控检查间隔1分钟
autoDeliver: '0 */5 * * * *', // 自动投递任务每5分钟执行一次
// 监控检查间隔1分钟
};
// 测试配置覆盖
if (config.test) {
this.applyTestConfig(config.test);
}
}
/**
* 应用测试配置
* @param {object} testConfig - 测试配置
*/
applyTestConfig(testConfig) {
if (testConfig.skipWorkStartHour) {
this.workHours.start = 0;
this.workHours.end = 24;
}
// 测试模式下缩短所有间隔时间
if (testConfig.fastMode) {
this.rateLimits.search = 10 * 1000; // 10秒
this.rateLimits.apply = 5 * 1000; // 5秒
this.rateLimits.chat = 2 * 1000; // 2秒
}
}
/**
* 检查是否在工作时间
* @returns {boolean}
*/
isWorkingHours() {
const now = dayjs();
const hour = now.hour();
return hour >= this.workHours.start && hour < this.workHours.end;
}
/**
* 获取任务超时时间
* @param {string} taskType - 任务类型
* @returns {number} 超时时间(毫秒)
*/
getTaskTimeout(taskType) {
return this.taskTimeouts[taskType] || 30000; // 默认30秒
}
/**
* 获取任务优先级
* @param {string} taskType - 任务类型
* @param {object} options - 选项
* @returns {number} 优先级1-10
*/
getTaskPriority(taskType, options = {}) {
let priority = this.taskPriorities[taskType] || 5;
if (options.urgent) {
priority = Math.min(10, priority + 2);
}
return priority;
}
/**
* 获取操作频率限制
* @param {string} operation - 操作类型
* @returns {number} 间隔时间(毫秒)
*/
getRateLimit(operation) {
return this.rateLimits[operation] || 0;
}
/**
* 获取日限制
* @param {string} operation - 操作类型
* @returns {number} 日限制次数
*/
getDailyLimit(operation) {
return this.dailyLimits[`max${operation.charAt(0).toUpperCase() + operation.slice(1)}`] || Infinity;
}
}
// 导出单例
const scheduleConfig = new ScheduleConfig();
module.exports = scheduleConfig;

View File

@@ -0,0 +1,287 @@
const dayjs = require('dayjs');
const Sequelize = require('sequelize');
const db = require('../dbProxy');
const config = require('./config');
const utils = require('./utils');
/**
* 设备管理器(简化版)
* 合并了 Monitor 和 Strategy 的核心功能
*/
class DeviceManager {
constructor() {
// 设备状态 { sn_code: { isOnline, lastHeartbeat, lastSearch, lastApply, lastChat, dailyCounts } }
this.devices = new Map();
// 系统统计
this.stats = {
totalDevices: 0,
onlineDevices: 0,
totalTasks: 0,
completedTasks: 0,
failedTasks: 0,
startTime: new Date()
};
}
/**
* 初始化
*/
async init() {
console.log('[设备管理器] 初始化中...');
await this.loadStats();
console.log('[设备管理器] 初始化完成');
}
/**
* 加载统计数据
*/
async loadStats() {
try {
const devices = await db.getModel('pla_account').findAll();
this.stats.totalDevices = devices.length;
const completedCount = await db.getModel('task_status').count({
where: { status: 'completed' }
});
const failedCount = await db.getModel('task_status').count({
where: { status: 'failed' }
});
this.stats.completedTasks = completedCount;
this.stats.failedTasks = failedCount;
this.stats.totalTasks = completedCount + failedCount;
} catch (error) {
console.error('[设备管理器] 加载统计失败:', error);
}
}
/**
* 记录心跳
*/
async recordHeartbeat(sn_code, heartbeatData = {}) {
const now = Date.now();
if (!this.devices.has(sn_code)) {
this.devices.set(sn_code, {
isOnline: true,
lastHeartbeat: now,
dailyCounts: { date: utils.getTodayString(), searchCount: 0, applyCount: 0, chatCount: 0 }
});
}
const device = this.devices.get(sn_code);
device.isOnline = true;
device.lastHeartbeat = now;
}
/**
* 检查设备是否在线
*/
isDeviceOnline(sn_code) {
const device = this.devices.get(sn_code);
if (!device) return false;
const elapsed = Date.now() - device.lastHeartbeat;
if (elapsed > config.monitoring.heartbeatTimeout) {
device.isOnline = false;
return false;
}
return device.isOnline;
}
/**
* 检查是否可以执行操作
*/
canExecuteOperation(sn_code, operationType) {
// 检查工作时间
if (!config.isWorkingHours()) {
return { allowed: false, reason: '不在工作时间内' };
}
// 检查频率限制
const device = this.devices.get(sn_code);
if (device) {
const lastTime = device[`last${operationType.charAt(0).toUpperCase() + operationType.slice(1)}`] || 0;
const interval = config.getRateLimit(operationType);
if (Date.now() - lastTime < interval) {
return { allowed: false, reason: '操作过于频繁' };
}
}
// 检查日限制
if (device && device.dailyCounts) {
const today = utils.getTodayString();
if (device.dailyCounts.date !== today) {
device.dailyCounts = { date: today, searchCount: 0, applyCount: 0, chatCount: 0 };
}
const countKey = `${operationType}Count`;
const current = device.dailyCounts[countKey] || 0;
const max = config.getDailyLimit(operationType);
if (current >= max) {
return { allowed: false, reason: `今日${operationType}操作已达上限` };
}
}
return { allowed: true };
}
/**
* 记录操作
*/
recordOperation(sn_code, operationType) {
const device = this.devices.get(sn_code) || {};
device[`last${operationType.charAt(0).toUpperCase() + operationType.slice(1)}`] = Date.now();
if (device.dailyCounts) {
const countKey = `${operationType}Count`;
device.dailyCounts[countKey] = (device.dailyCounts[countKey] || 0) + 1;
}
this.devices.set(sn_code, device);
}
/**
* 记录任务开始
*/
recordTaskStart(sn_code, task) {
// 简化实现,只记录日志
console.log(`[设备管理器] 设备 ${sn_code} 开始执行任务: ${task.taskName}`);
}
/**
* 记录任务完成
*/
recordTaskComplete(sn_code, task, success, duration) {
if (success) {
this.stats.completedTasks++;
} else {
this.stats.failedTasks++;
}
this.stats.totalTasks++;
console.log(`[设备管理器] 设备 ${sn_code} 任务${success ? '成功' : '失败'}: ${task.taskName} (${duration}ms)`);
}
/**
* 获取系统统计
*/
getSystemStats() {
const onlineCount = Array.from(this.devices.values()).filter(d => d.isOnline).length;
return {
...this.stats,
onlineDevices: onlineCount,
uptime: utils.formatDuration(Date.now() - this.stats.startTime.getTime())
};
}
/**
* 获取所有设备状态
*/
getAllDevicesStatus() {
const result = {};
for (const [sn_code, device] of this.devices.entries()) {
result[sn_code] = {
isOnline: device.isOnline,
lastHeartbeat: device.lastHeartbeat,
dailyCounts: device.dailyCounts || {}
};
}
return result;
}
/**
* 检查心跳状态(异步更新数据库)
*/
async checkHeartbeatStatus() {
try {
const now = Date.now();
const device_status = db.getModel('device_status');
const offlineDevices = [];
for (const [sn_code, device] of this.devices.entries()) {
if (now - device.lastHeartbeat > config.monitoring.heartbeatTimeout) {
// 如果之前是在线状态,现在检测到离线,需要更新数据库
if (device.isOnline) {
device.isOnline = false;
offlineDevices.push(sn_code);
}
}
}
// 批量更新数据库中的离线设备状态
if (offlineDevices.length > 0) {
await device_status.update(
{ isOnline: false },
{
where: {
sn_code: {
[Sequelize.Op.in]: offlineDevices
},
isOnline: true // 只更新当前在线的设备,避免重复更新
}
}
);
console.log(`[设备管理器] 检测到 ${offlineDevices.length} 个设备心跳超时,已同步到数据库: ${offlineDevices.join(', ')}`);
}
// 同时检查数据库中的设备状态(处理内存中没有但数据库中有心跳超时的情况)
const heartbeatTimeout = config.monitoring.heartbeatTimeout;
const heartbeatThreshold = new Date(now - heartbeatTimeout);
const timeoutDevices = await device_status.findAll({
where: {
isOnline: true,
lastHeartbeatTime: {
[Sequelize.Op.lt]: heartbeatThreshold
}
},
attributes: ['sn_code', 'lastHeartbeatTime']
});
if (timeoutDevices.length > 0) {
const timeoutSnCodes = timeoutDevices.map(dev => dev.sn_code);
await device_status.update(
{ isOnline: false },
{
where: {
sn_code: {
[Sequelize.Op.in]: timeoutSnCodes
}
}
}
);
console.log(`[设备管理器] 从数据库检测到 ${timeoutSnCodes.length} 个心跳超时设备,已更新为离线: ${timeoutSnCodes.join(', ')}`);
}
} catch (error) {
console.error('[设备管理器] 检查心跳状态失败:', error);
}
}
/**
* 重置所有日计数器
*/
resetAllDailyCounters() {
const today = utils.getTodayString();
for (const device of this.devices.values()) {
if (device.dailyCounts && device.dailyCounts.date !== today) {
device.dailyCounts = { date: today, searchCount: 0, applyCount: 0, chatCount: 0 };
}
}
}
/**
* 清理离线设备
*/
cleanupOfflineDevices(threshold = 3600000) {
const now = Date.now();
for (const [sn_code, device] of this.devices.entries()) {
if (now - device.lastHeartbeat > threshold) {
this.devices.delete(sn_code);
}
}
}
}
// 导出单例
const deviceManager = new DeviceManager();
module.exports = deviceManager;

View File

@@ -0,0 +1,203 @@
const mqttManager = require("../mqtt/mqttManager.js");
// 导入调度模块(简化版)
const TaskQueue = require('./taskQueue.js');
const Command = require('./command.js');
const deviceManager = require('./deviceManager.js');
const config = require('./config.js');
const utils = require('./utils.js');
// 导入新的模块
const TaskHandlers = require('./taskHandlers.js');
const MqttDispatcher = require('../mqtt/mqttDispatcher.js');
const ScheduledJobs = require('./scheduledJobs.js');
/**
* 调度系统管理器
* 统一管理整个调度系统的生命周期
*/
class ScheduleManager {
constructor() {
this.mqttClient = null;
this.isInitialized = false;
this.startTime = new Date();
// 子模块
this.taskHandlers = null;
this.mqttDispatcher = null;
this.scheduledJobs = null;
}
/**
* 初始化调度系统
*/
async init() {
try {
console.log('[调度管理器] 开始初始化...');
// 1. 初始化MQTT管理器
await this.initMqttClient();
console.log('[调度管理器] MQTT管理器已初始化');
// 2. 初始化各个组件
await this.initComponents();
console.log('[调度管理器] 组件已初始化');
// 3. 初始化子模块
this.initSubModules();
console.log('[调度管理器] 子模块已初始化');
// 4. 启动心跳监听
this.startHeartbeatListener();
console.log('[调度管理器] 心跳监听已启动');
// 5. 启动定时任务
this.scheduledJobs.start();
console.log('[调度管理器] 定时任务已启动');
this.isInitialized = true;
} catch (error) {
console.error('[调度管理器] 初始化失败:', error);
throw error;
}
}
/**
* 初始化MQTT客户端
*/
async initMqttClient() {
this.mqttClient = await mqttManager.getInstance();
}
/**
* 初始化各个组件(简化版)
*/
async initComponents() {
// 初始化设备管理器
await deviceManager.init();
// 初始化任务队列
await TaskQueue.init?.();
}
/**
* 初始化子模块
*/
initSubModules() {
// 初始化任务处理器
this.taskHandlers = new TaskHandlers(this.mqttClient);
this.taskHandlers.register(TaskQueue);
// 初始化 MQTT 分发器(简化:不再需要 components
this.mqttDispatcher = new MqttDispatcher({ deviceManager, taskQueue: TaskQueue }, this.mqttClient);
this.mqttDispatcher.start();
// 初始化定时任务管理器
this.scheduledJobs = new ScheduledJobs({ deviceManager, taskQueue: TaskQueue }, this.taskHandlers);
}
/**
* 启动心跳监听
*/
startHeartbeatListener() {
// 订阅心跳主题,使用 mqttDispatcher 处理
this.mqttClient.subscribe("heartbeat", async (topic, message) => {
try {
await this.mqttDispatcher.handleHeartbeat(message);
} catch (error) {
console.error('[调度管理器] 处理心跳消息失败:', error);
}
});
// 订阅响应主题
this.mqttClient.subscribe("response", async (topic, message) => {
try {
if (this.mqttDispatcher) {
this.mqttDispatcher.handleResponse(message);
}
} catch (error) {
console.error('[调度管理器] 处理响应消息失败:', error);
}
});
}
/**
* 手动执行找工作流程已废弃full_flow 不再使用)
* @deprecated 请使用其他任务类型,如 auto_deliver
*/
async manualExecuteJobFlow(sn_code, keyword = '前端') {
console.warn(`[手动执行] manualExecuteJobFlow 已废弃full_flow 不再使用`);
throw new Error('full_flow 任务类型已废弃,请使用其他任务类型');
}
/**
* 获取系统状态
*/
getSystemStatus() {
const status = this.mqttDispatcher ? this.mqttDispatcher.getSystemStatus() : {};
return {
isInitialized: this.isInitialized,
mqttConnected: this.mqttClient && this.mqttClient.isConnected,
systemStats: deviceManager.getSystemStats(),
allDevices: deviceManager.getAllDevicesStatus(),
taskQueues: TaskQueue.getAllDeviceStatus(),
uptime: utils.formatDuration(Date.now() - this.startTime.getTime()),
startTime: utils.formatTimestamp(this.startTime),
...status
};
}
/**
* 停止调度系统
*/
stop() {
console.log('[调度管理器] 正在停止...');
// 停止所有定时任务
if (this.scheduledJobs) {
this.scheduledJobs.stop();
}
// 停止任务队列扫描器
if (TaskQueue && typeof TaskQueue.stopQueueScanner === 'function') {
TaskQueue.stopQueueScanner();
}
// 关闭MQTT连接
if (this.mqttClient) {
this.mqttClient.end();
}
this.isInitialized = false;
console.log('[调度管理器] 已停止');
}
}
// 创建调度管理器实例
const scheduleManager = new ScheduleManager();
// 导出兼容的接口,保持与原有代码的一致性
module.exports = {
// 初始化方法
init: () => scheduleManager.init(),
// 手动执行任务
manualExecuteJobFlow: (sn_code, keyword) => scheduleManager.manualExecuteJobFlow(sn_code, keyword),
// 获取系统状态
getSystemStatus: () => scheduleManager.getSystemStatus(),
// 停止系统
stop: () => scheduleManager.stop(),
// 访问各个组件(为了兼容性)
get mqttClient() { return scheduleManager.mqttClient; },
get isInitialized() { return scheduleManager.isInitialized; },
// 访问各个组件实例(简化版)
get taskQueue() { return TaskQueue; },
get command() { return Command; },
get deviceManager() { return deviceManager; }
};

View File

@@ -0,0 +1,168 @@
const node_schedule = require("node-schedule");
const config = require('./config.js');
const deviceManager = require('./deviceManager.js');
const command = require('./command.js');
const db = require('../dbProxy');
/**
* 定时任务管理器(简化版)
* 管理所有定时任务的创建和销毁
*/
class ScheduledJobs {
constructor(components, taskHandlers) {
this.taskQueue = components.taskQueue;
this.taskHandlers = taskHandlers;
this.jobs = [];
}
/**
* 启动所有定时任务
*/
start() {
// 每天凌晨重置统计数据
const resetJob = node_schedule.scheduleJob(config.schedules.dailyReset, () => {
this.resetDailyStats();
});
this.jobs.push(resetJob);
// 启动心跳检查定时任务(每分钟检查一次)
const monitoringJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await deviceManager.checkHeartbeatStatus().catch(error => {
console.error('[定时任务] 检查心跳状态失败:', error);
});
});
this.jobs.push(monitoringJob);
// 执行自动投递任务
const autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
this.autoDeliverTask();
});
this.jobs.push(autoDeliverJob);
console.log('[定时任务] 已启动自动投递任务');
}
/**
* 重置每日统计
*/
resetDailyStats() {
console.log('[定时任务] 重置每日统计数据');
try {
deviceManager.resetAllDailyCounters();
console.log('[定时任务] 每日统计重置完成');
} catch (error) {
console.error('[定时任务] 重置统计失败:', error);
}
}
/**
* 清理过期数据
*/
cleanupCaches() {
console.log('[定时任务] 开始清理过期数据');
try {
deviceManager.cleanupOfflineDevices(config.monitoring.offlineThreshold);
command.cleanupExpiredCommands(30);
console.log('[定时任务] 数据清理完成');
} catch (error) {
console.error('[定时任务] 数据清理失败:', error);
}
}
/**
* 自动投递任务
*/
async autoDeliverTask() {
const now = new Date();
console.log(`[自动投递] ${now.toLocaleString()} 开始执行自动投递任务`);
// 检查是否在工作时间
if (!config.isWorkingHours()) {
console.log(`[自动投递] 非工作时间,跳过执行`);
return;
}
try {
// 从 device_status 查询所有在线且已登录的设备
const models = db.models;
const { device_status, pla_account, op } = models;
const onlineDevices = await device_status.findAll({
where: {
isOnline: true,
isLoggedIn: true
},
attributes: ['sn_code', 'accountName', 'platform']
});
if (!onlineDevices || onlineDevices.length === 0) {
console.log('[自动投递] 没有在线且已登录的设备');
return;
}
// 获取这些在线设备对应的账号配置(只获取启用且开启自动投递的账号)
const snCodes = onlineDevices.map(device => device.sn_code);
const pla_users = await pla_account.findAll({
where: {
sn_code: { [op.in]: snCodes },
is_delete: 0,
is_enabled: 1, // 只获取启用的账号
auto_deliver: 1
}
});
if (!pla_users || pla_users.length === 0) {
console.log('[自动投递] 没有启用且开启自动投递的账号');
return;
}
console.log(`[自动投递] 找到 ${pla_users.length} 个可用账号`);
// 为每个设备添加自动投递任务到队列
for (const pl_user of pla_users) {
const userData = pl_user.toJSON();
// 检查设备调度策略
const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'deliver');
if (!canExecute.allowed) {
console.log(`[自动投递] 设备 ${userData.sn_code} 不满足执行条件: ${canExecute.reason}`);
continue;
}
// 添加自动投递任务到队列
await this.taskQueue.addTask(userData.sn_code, {
taskType: 'auto_deliver',
taskName: `自动投递 - ${userData.keyword || '默认关键词'}`,
taskParams: {
keyword: userData.keyword || '',
platform: userData.platform_type || 'boss',
pageCount: 3, // 默认值
maxCount: 10, // 默认值
filterRules: {
minSalary: userData.min_salary || 0,
maxSalary: userData.max_salary || 0,
keywords: [],
excludeKeywords: []
}
},
priority: config.getTaskPriority('auto_deliver') || 6
});
console.log(`[自动投递] 已为设备 ${userData.sn_code} 添加自动投递任务,关键词: ${userData.keyword || '默认'}`);
}
console.log('[自动投递] 任务添加完成');
} catch (error) {
console.error('[自动投递] 执行失败:', error);
}
}
}
module.exports = ScheduledJobs;

View File

@@ -0,0 +1,432 @@
const db = require('../dbProxy.js');
const config = require('./config.js');
const deviceManager = require('./deviceManager.js');
const command = require('./command.js');
const jobFilterService = require('../job/job_filter_service.js');
/**
* 任务处理器(简化版)
* 处理各种类型的任务
*/
class TaskHandlers {
constructor(mqttClient) {
this.mqttClient = mqttClient;
}
/**
* 注册任务处理器到任务队列
* @param {object} taskQueue - 任务队列实例
*/
register(taskQueue) {
taskQueue.registerHandler('get_resume', async (task) => {
return await this.handleGetResumeTask(task);
});
taskQueue.registerHandler('get_job_list', async (task) => {
return await this.handleGetJobListTask(task);
});
taskQueue.registerHandler('send_chat', async (task) => {
return await this.handleSendChatTask(task);
});
taskQueue.registerHandler('apply_job', async (task) => {
return await this.handleApplyJobTask(task);
});
taskQueue.registerHandler('auto_deliver', async (task) => {
return await this.handleAutoDeliverTask(task);
});
}
/**
* 处理获取简历任务
*/
async handleGetResumeTask(task) {
const { sn_code } = task;
console.log(`[任务处理器] 获取简历任务 - 设备: ${sn_code}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
const commands = [{
command_type: 'getOnlineResume',
command_name: '获取在线简历',
command_params: JSON.stringify({ sn_code }),
priority: config.getTaskPriority('get_resume')
}];
const result = await command.executeCommands(task.id, commands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
}
/**
* 处理获取岗位列表任务
*/
async handleGetJobListTask(task) {
const { sn_code, taskParams } = task;
const { keyword, platform } = taskParams;
console.log(`[任务处理器] 获取岗位列表任务 - 设备: ${sn_code}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
const commands = [{
command_type: 'getJobList',
command_name: '获取岗位列表',
command_params: JSON.stringify({ sn_code, keyword, platform }),
priority: config.getTaskPriority('search_jobs')
}];
const result = await command.executeCommands(task.id, commands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
}
/**
* 处理发送聊天任务
*/
async handleSendChatTask(task) {
const { sn_code, taskParams } = task;
console.log(`[任务处理器] 发送聊天任务 - 设备: ${sn_code}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
const commands = [{
command_type: 'sendChatMessage',
command_name: '发送聊天消息',
command_params: JSON.stringify(taskParams),
priority: config.getTaskPriority('chat')
}];
const result = await command.executeCommands(task.id, commands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
}
/**
* 处理投递简历任务
*/
async handleApplyJobTask(task) {
const { sn_code, taskParams } = task;
console.log(`[任务处理器] 投递简历任务 - 设备: ${sn_code}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
const commands = [{
command_type: 'applyJob',
command_name: '投递简历',
command_params: JSON.stringify(taskParams),
priority: config.getTaskPriority('apply')
}];
const result = await command.executeCommands(task.id, commands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
}
/**
* 处理自动投递任务
*/
async handleAutoDeliverTask(task) {
const { sn_code, taskParams } = task;
const { keyword, platform, pageCount, maxCount } = taskParams;
console.log(`[任务处理器] 自动投递任务 - 设备: ${sn_code}, 关键词: ${keyword}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
const job_postings = db.getModel('job_postings');
const pla_account = db.getModel('pla_account');
const resume_info = db.getModel('resume_info');
const job_types = db.getModel('job_types');
// 1. 检查并获取在线简历如果2小时内没有获取
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
let resume = await resume_info.findOne({
where: {
sn_code,
platform: platform || 'boss',
isActive: true
},
order: [['last_modify_time', 'DESC']]
});
const needRefreshResume = !resume ||
!resume.last_modify_time ||
new Date(resume.last_modify_time) < twoHoursAgo;
if (needRefreshResume) {
console.log(`[任务处理器] 简历超过2小时未更新重新获取在线简历`);
try {
// 通过 command 系统获取在线简历,而不是直接调用 jobManager
const getResumeCommand = {
command_type: 'getOnlineResume',
command_name: '获取在线简历',
command_params: JSON.stringify({ sn_code, platform: platform || 'boss' }),
priority: config.getTaskPriority('get_resume') || 5
};
await command.executeCommands(task.id, [getResumeCommand], this.mqttClient);
// 重新查询简历
resume = await resume_info.findOne({
where: {
sn_code,
platform: platform || 'boss',
isActive: true
},
order: [['last_modify_time', 'DESC']]
});
} catch (error) {
console.warn(`[任务处理器] 获取在线简历失败,使用已有简历:`, error.message);
}
}
if (!resume) {
console.log(`[任务处理器] 未找到简历信息,无法进行自动投递`);
return {
success: false,
deliveredCount: 0,
message: '未找到简历信息'
};
}
// 2. 获取账号配置和职位类型配置
const account = await pla_account.findOne({
where: { sn_code, platform_type: platform || 'boss' }
});
if (!account) {
console.log(`[任务处理器] 未找到账号配置`);
return {
success: false,
deliveredCount: 0,
message: '未找到账号配置'
};
}
const accountConfig = account.toJSON();
const resumeInfo = resume.toJSON();
// 获取职位类型配置
let jobTypeConfig = null;
if (accountConfig.job_type_id) {
const jobType = await job_types.findByPk(accountConfig.job_type_id);
if (jobType) {
jobTypeConfig = jobType.toJSON();
}
}
// 获取优先级权重配置
let priorityWeights = accountConfig.is_salary_priority;
if (!Array.isArray(priorityWeights) || priorityWeights.length === 0) {
priorityWeights = [
{ key: "distance", weight: 50 },
{ key: "salary", weight: 20 },
{ key: "work_years", weight: 10 },
{ key: "education", weight: 20 }
];
}
// 3. 先获取职位列表
const getJobListCommand = {
command_type: 'getJobList',
command_name: '获取职位列表',
command_params: JSON.stringify({
sn_code: sn_code,
keyword: keyword || accountConfig.keyword || '',
platform: platform || 'boss',
pageCount: pageCount || 3
}),
priority: config.getTaskPriority('search_jobs') || 5
};
await command.executeCommands(task.id, [getJobListCommand], this.mqttClient);
// 4. 从数据库获取待投递的职位
const pendingJobs = await job_postings.findAll({
where: {
sn_code: sn_code,
platform: platform || 'boss',
applyStatus: 'pending'
},
order: [['create_time', 'DESC']],
limit: (maxCount || 10) * 3 // 获取更多职位用于筛选
});
if (!pendingJobs || pendingJobs.length === 0) {
console.log(`[任务处理器] 没有待投递的职位`);
return {
success: true,
deliveredCount: 0,
message: '没有待投递的职位'
};
}
// 5. 根据简历信息、职位类型配置和权重配置进行评分和过滤
const scoredJobs = [];
const excludeKeywords = jobTypeConfig && jobTypeConfig.excludeKeywords
? (typeof jobTypeConfig.excludeKeywords === 'string'
? JSON.parse(jobTypeConfig.excludeKeywords)
: jobTypeConfig.excludeKeywords)
: [];
// 获取一个月内已投递的公司列表(用于过滤)
const apply_records = db.getModel('apply_records');
const Sequelize = require('sequelize');
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
const recentApplies = await apply_records.findAll({
where: {
sn_code: sn_code,
applyTime: {
[Sequelize.Op.gte]: oneMonthAgo
}
},
attributes: ['companyName'],
group: ['companyName']
});
const recentCompanyNames = new Set(recentApplies.map(apply => apply.companyName).filter(Boolean));
for (const job of pendingJobs) {
const jobData = job.toJSON ? job.toJSON() : job;
// 排除关键词过滤
if (Array.isArray(excludeKeywords) && excludeKeywords.length > 0) {
const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase();
const hasExcluded = excludeKeywords.some(kw => jobText.includes(kw.toLowerCase()));
if (hasExcluded) {
continue;
}
}
// 检查该公司是否在一个月内已投递过
if (jobData.companyName && recentCompanyNames.has(jobData.companyName)) {
console.log(`[任务处理器] 跳过一个月内已投递的公司: ${jobData.companyName}`);
continue;
}
// 使用 job_filter_service 计算评分
const scoreResult = jobFilterService.calculateJobScoreWithWeights(
jobData,
resumeInfo,
accountConfig,
jobTypeConfig,
priorityWeights
);
// 只保留总分 >= 60 的职位
if (scoreResult.totalScore >= 60) {
scoredJobs.push({
...jobData,
matchScore: scoreResult.totalScore,
scoreDetails: scoreResult.scores
});
}
}
// 按总分降序排序
scoredJobs.sort((a, b) => b.matchScore - a.matchScore);
// 取前 maxCount 个职位
const jobsToDeliver = scoredJobs.slice(0, maxCount || 10);
console.log(`[任务处理器] 职位评分完成,共 ${pendingJobs.length} 个职位,评分后 ${scoredJobs.length} 个符合条件,将投递 ${jobsToDeliver.length}`);
if (jobsToDeliver.length === 0) {
return {
success: true,
deliveredCount: 0,
message: '没有符合条件的职位'
};
}
// 6. 为每个职位创建一条独立的投递指令
const deliverCommands = [];
for (const jobData of jobsToDeliver) {
console.log(`[任务处理器] 准备投递职位: ${jobData.jobTitle} @ ${jobData.companyName}, 评分: ${jobData.matchScore}`, jobData.scoreDetails);
deliverCommands.push({
command_type: 'applyJob',
command_name: `投递简历 - ${jobData.jobTitle} @ ${jobData.companyName} (评分:${jobData.matchScore})`,
command_params: JSON.stringify({
sn_code: sn_code,
platform: platform || 'boss',
jobId: jobData.jobId,
encryptBossId: jobData.encryptBossId || '',
securityId: jobData.securityId || '',
brandName: jobData.companyName,
jobTitle: jobData.jobTitle,
companyName: jobData.companyName,
matchScore: jobData.matchScore,
scoreDetails: jobData.scoreDetails
}),
priority: config.getTaskPriority('apply') || 6
});
}
// 7. 执行所有投递指令
const result = await command.executeCommands(task.id, deliverCommands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
console.log(`[任务处理器] 自动投递任务完成 - 设备: ${sn_code}, 创建了 ${deliverCommands.length} 条投递指令, 耗时: ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
console.error(`[任务处理器] 自动投递任务失败 - 设备: ${sn_code}:`, error);
throw error;
}
}
}
module.exports = TaskHandlers;

View File

@@ -0,0 +1,927 @@
const { v4: uuidv4 } = require('uuid');
const Sequelize = require('sequelize');
const logs = require('../logProxy');
const db = require('../dbProxy');
const command = require('./command');
const PriorityQueue = require('./PriorityQueue');
const ErrorHandler = require('./ErrorHandler');
const deviceManager = require('./deviceManager');
/**
* 任务队列管理器(重构版)
* - 使用优先级队列(堆)提升性能
* - 工作池模式:设备内串行执行,设备间并行执行
* - 统一重试机制
* - 统一MQTT管理
*/
class TaskQueue {
constructor(config = {}) {
// 设备任务队列映射 { sn_code: PriorityQueue }
this.deviceQueues = new Map();
// 设备执行状态 { sn_code: { isRunning, currentTask, runningCount } }
this.deviceStatus = new Map();
// 任务处理器映射 { taskType: handler }
this.taskHandlers = new Map();
// 工作池配置
this.config = {
maxConcurrency: config.maxConcurrency || 5, // 全局最大并发数(设备数)
deviceMaxConcurrency: config.deviceMaxConcurrency || 1, // 每个设备最大并发数(保持串行)
...config
};
// 全局运行中的任务数
this.globalRunningCount = 0;
// 全局任务队列(用于跨设备优先级调度,可选)
this.globalQueue = new PriorityQueue();
// 定期扫描定时器
this.scanInterval = null;
}
/**
* 初始化(从数据库恢复未完成的任务)
*/
async init() {
try {
console.log('[任务队列] 初始化中...');
// 从数据库加载pending和running状态的任务
const pendingTasks = await db.getModel('task_status').findAll({
where: {
status: ['pending', 'running']
},
order: [['priority', 'DESC'], ['id', 'ASC']]
});
// 获取所有启用的账号和设备在线状态
const pla_account = db.getModel('pla_account');
const device_status = db.getModel('device_status');
const enabledAccounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
const enabledSnCodes = new Set(enabledAccounts.map(acc => acc.sn_code));
// 检查设备在线状态需要同时满足isOnline = true 且心跳未超时)
const heartbeatTimeout = require('./config.js').monitoring.heartbeatTimeout; // 默认5分钟
const now = new Date();
const heartbeatThreshold = new Date(now.getTime() - heartbeatTimeout);
const onlineDevices = await device_status.findAll({
where: {
isOnline: true,
lastHeartbeatTime: {
[Sequelize.Op.gte]: heartbeatThreshold // 心跳时间在阈值内
}
},
attributes: ['sn_code', 'lastHeartbeatTime']
});
const onlineSnCodes = new Set(onlineDevices.map(dev => dev.sn_code));
let restoredCount = 0;
let skippedCount = 0;
for (const taskRecord of pendingTasks) {
const task = taskRecord.toJSON();
const sn_code = task.sn_code;
// 检查账号是否启用
if (!enabledSnCodes.has(sn_code)) {
console.log(`[任务队列] 初始化时跳过任务 ${task.id}:账号 ${sn_code} 未启用`);
// 标记任务为已取消
await db.getModel('task_status').update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({ error: '账号未启用,任务已取消' })
},
{ where: { id: task.id } }
);
skippedCount++;
continue;
}
// 检查设备是否在线
if (!onlineSnCodes.has(sn_code)) {
console.log(`[任务队列] 初始化时跳过任务 ${task.id}:设备 ${sn_code} 不在线`);
// 不在线的任务仍然恢复,等待设备上线后执行
// 不取消任务,只是不立即执行
}
// 初始化设备队列
if (!this.deviceQueues.has(sn_code)) {
this.deviceQueues.set(sn_code, new PriorityQueue());
}
// 初始化设备状态(重要:确保设备状态存在)
if (!this.deviceStatus.has(sn_code)) {
this.deviceStatus.set(sn_code, {
isRunning: false,
currentTask: null,
runningCount: 0
});
}
// 恢复任务对象
const taskObj = {
id: task.id,
sn_code: task.sn_code,
taskType: task.taskType,
taskName: task.taskName,
taskParams: task.taskParams ? JSON.parse(task.taskParams) : {},
priority: task.priority || 5,
maxRetries: task.maxRetries || 3,
retryCount: task.retryCount || 0,
status: 'pending',
createdAt: task.create_time ? new Date(task.create_time).getTime() : Date.now()
};
// 添加到设备队列
this.deviceQueues.get(sn_code).push(taskObj);
restoredCount++;
// 如果状态是running重置为pending
if (task.status === 'running') {
await db.getModel('task_status').update(
{ status: 'pending' },
{ where: { id: task.id } }
);
}
}
// 恢复任务后,尝试执行所有设备的队列(只执行在线且启用的设备)
for (const sn_code of this.deviceQueues.keys()) {
// 只处理启用且在线的设备
if (enabledSnCodes.has(sn_code) && onlineSnCodes.has(sn_code)) {
this.processQueue(sn_code).catch(error => {
console.error(`[任务队列] 初始化后执行队列失败 (设备: ${sn_code}):`, error);
});
} else {
console.log(`[任务队列] 初始化时跳过设备 ${sn_code} 的队列执行:${!enabledSnCodes.has(sn_code) ? '账号未启用' : '设备不在线'}`);
}
}
console.log(`[任务队列] 初始化完成,恢复 ${restoredCount} 个任务,跳过 ${skippedCount} 个未启用账号的任务`);
// 启动定期扫描机制每10秒扫描一次
this.startQueueScanner();
} catch (error) {
console.error('[任务队列] 初始化失败:', error);
throw error;
}
}
/**
* 启动队列扫描器(定期检查并执行队列中的任务)
*/
startQueueScanner() {
// 如果已经启动,先清除
if (this.scanInterval) {
clearInterval(this.scanInterval);
}
// 每10秒扫描一次所有设备的队列
this.scanInterval = setInterval(() => {
this.scanAndProcessQueues();
}, 10000); // 10秒扫描一次
console.log('[任务队列] 队列扫描器已启动每10秒扫描一次');
}
/**
* 停止队列扫描器
*/
stopQueueScanner() {
if (this.scanInterval) {
clearInterval(this.scanInterval);
this.scanInterval = null;
console.log('[任务队列] 队列扫描器已停止');
}
}
/**
* 扫描所有设备的队列并尝试执行任务(过滤未启用的账号)
*/
async scanAndProcessQueues() {
try {
const deviceCount = this.deviceQueues.size;
if (deviceCount === 0) {
return;
}
// 获取所有启用的账号对应的设备SN码
const pla_account = db.getModel('pla_account');
const enabledAccounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
const enabledSnCodes = new Set(enabledAccounts.map(acc => acc.sn_code));
let processedCount = 0;
let queuedCount = 0;
let skippedCount = 0;
// 遍历所有设备的队列,只处理启用账号的设备
for (const [sn_code, queue] of this.deviceQueues.entries()) {
// 跳过未启用的账号
if (!enabledSnCodes.has(sn_code)) {
skippedCount++;
continue;
}
const queueSize = queue.size();
if (queueSize > 0) {
queuedCount += queueSize;
// 尝试处理该设备的队列
this.processQueue(sn_code).catch(error => {
console.error(`[任务队列] 扫描执行队列失败 (设备: ${sn_code}):`, error);
});
processedCount++;
}
}
if (queuedCount > 0) {
console.log(`[任务队列] 扫描完成: ${processedCount} 个设备有任务,共 ${queuedCount} 个待执行任务`);
}
} catch (error) {
console.error('[任务队列] 扫描队列失败:', error);
}
}
/**
* 注册任务处理器
* @param {string} taskType - 任务类型
* @param {function} handler - 处理函数
*/
registerHandler(taskType, handler) {
this.taskHandlers.set(taskType, handler);
}
/**
* 查找设备是否已有相同类型的任务
* @param {string} sn_code - 设备SN码
* @param {string} taskType - 任务类型
* @returns {object|null} 现有任务或null
*/
findExistingTask(sn_code, taskType) {
// 检查当前正在执行的任务
const deviceStatus = this.deviceStatus.get(sn_code);
if (deviceStatus && deviceStatus.currentTask && deviceStatus.currentTask.taskType === taskType) {
return deviceStatus.currentTask;
}
// 检查队列中等待的任务
const queue = this.deviceQueues.get(sn_code);
if (queue) {
const existingTask = queue.find(task => task.taskType === taskType && task.status === 'pending');
if (existingTask) {
return existingTask;
}
}
return null;
}
/**
* 检查账号是否启用
* @param {string} sn_code - 设备SN码
* @returns {Promise<boolean>} 是否启用
*/
async checkAccountEnabled(sn_code) {
try {
const pla_account = db.getModel('pla_account');
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0
},
attributes: ['is_enabled']
});
if (!account) {
console.warn(`[任务队列] 设备 ${sn_code} 对应的账号不存在`);
return false;
}
const isEnabled = Boolean(account.is_enabled);
if (!isEnabled) {
console.log(`[任务队列] 设备 ${sn_code} 对应的账号未启用,跳过任务`);
}
return isEnabled;
} catch (error) {
console.error(`[任务队列] 检查账号启用状态失败:`, error);
return false;
}
}
/**
* 添加任务到队列
* @param {string} sn_code - 设备SN码
* @param {object} taskConfig - 任务配置
* @returns {Promise<string>} 任务ID
*/
async addTask(sn_code, taskConfig) {
// 检查账号是否启用
const isEnabled = await this.checkAccountEnabled(sn_code);
if (!isEnabled) {
throw new Error(`账号未启用,无法添加任务`);
}
// 检查是否已有相同类型的任务在队列中或正在执行
const existingTask = this.findExistingTask(sn_code, taskConfig.taskType);
if (existingTask) {
console.log(`[任务队列] 设备 ${sn_code} 已有 ${taskConfig.taskType} 任务在执行或等待中,跳过添加`);
return existingTask.id;
}
const task = {
sn_code,
taskType: taskConfig.taskType,
taskName: taskConfig.taskName || taskConfig.taskType,
taskParams: taskConfig.taskParams || {},
priority: taskConfig.priority || 5,
maxRetries: taskConfig.maxRetries || 3,
retryCount: 0,
status: 'pending',
createdAt: Date.now()
};
// 初始化设备队列
if (!this.deviceQueues.has(sn_code)) {
this.deviceQueues.set(sn_code, new PriorityQueue());
}
// 初始化设备状态
if (!this.deviceStatus.has(sn_code)) {
this.deviceStatus.set(sn_code, {
isRunning: false,
currentTask: null,
runningCount: 0
});
}
// 保存到数据库
let res = await db.getModel('task_status').create({
sn_code: task.sn_code,
taskType: task.taskType,
taskName: task.taskName,
taskParams: JSON.stringify(task.taskParams),
status: task.status,
priority: task.priority,
maxRetries: task.maxRetries,
retryCount: task.retryCount,
});
// 使用数据库返回的自增ID
task.id = res.id;
// 添加到优先级队列
const queue = this.deviceQueues.get(sn_code);
queue.push(task);
console.log(`[任务队列] 任务已添加到队列: ${task.taskName} (ID: ${task.id}, 优先级: ${task.priority}),等待扫描机制执行`);
// 不立即执行,等待扫描机制自动执行
// 扫描机制会定期检查队列并执行任务
return res.id;
}
/**
* 处理设备的任务队列(工作池模式)
* 设备内串行执行,设备间并行执行
* @param {string} sn_code - 设备SN码
*/
async processQueue(sn_code) {
try {
// 先检查账号是否启用
const isEnabled = await this.checkAccountEnabled(sn_code);
if (!isEnabled) {
// 如果账号未启用,从队列中移除所有待执行任务
const queue = this.deviceQueues.get(sn_code);
if (queue && queue.size() > 0) {
console.log(`[任务队列] 设备 ${sn_code} 账号未启用,清空队列中的 ${queue.size()} 个待执行任务`);
// 标记所有待执行任务为已取消
const queueArray = queue.toArray();
for (const task of queueArray) {
if (task.status === 'pending') {
try {
await db.getModel('task_status').update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({ error: '账号未启用,任务已取消' })
},
{ where: { id: task.id } }
);
} catch (error) {
console.error(`[任务队列] 更新任务状态失败:`, error);
}
}
}
queue.clear();
}
return;
}
const status = this.deviceStatus.get(sn_code);
if (!status) {
console.warn(`[任务队列] 设备 ${sn_code} 状态不存在,无法执行任务`);
return;
}
// 检查设备是否正在执行任务(设备内串行)
if (status.isRunning || status.runningCount >= this.config.deviceMaxConcurrency) {
console.log(`[任务队列] 设备 ${sn_code} 正在执行任务,等待中... (isRunning: ${status.isRunning}, runningCount: ${status.runningCount})`);
return;
}
// 检查全局并发限制(设备间并行控制)
if (this.globalRunningCount >= this.config.maxConcurrency) {
console.log(`[任务队列] 全局并发数已达上限 (${this.globalRunningCount}/${this.config.maxConcurrency}),等待中...`);
return;
}
const queue = this.deviceQueues.get(sn_code);
if (!queue || queue.isEmpty()) {
console.log(`[任务队列] 设备 ${sn_code} 队列为空,无任务可执行`);
return;
}
// 从优先级队列取出任务
const task = queue.pop();
if (!task) {
console.warn(`[任务队列] 设备 ${sn_code} 队列非空但无法取出任务`);
return;
}
console.log(`[任务队列] 开始执行任务: ${task.taskName} (ID: ${task.id}, 设备: ${sn_code})`);
// 更新状态
status.isRunning = true;
status.currentTask = task;
status.runningCount++;
this.globalRunningCount++;
// 异步执行任务(不阻塞)
this.executeTask(task).finally(() => {
// 任务完成后更新状态
status.isRunning = false;
status.currentTask = null;
status.runningCount--;
this.globalRunningCount--;
console.log(`[任务队列] 任务完成,设备 ${sn_code} 状态已重置,准备处理下一个任务`);
// 继续处理队列中的下一个任务(延迟一小段时间,确保状态已更新)
setTimeout(() => {
this.processQueue(sn_code).catch(error => {
console.error(`[任务队列] processQueue 执行失败 (设备: ${sn_code}):`, error);
});
}, 100); // 延迟100ms确保状态已更新
});
} catch (error) {
console.error(`[任务队列] processQueue 处理失败 (设备: ${sn_code}):`, error);
throw error;
}
}
/**
* 执行任务(统一重试机制)
* @param {object} task - 任务对象
*/
async executeTask(task) {
const startTime = Date.now();
task.status = 'running';
task.startTime = new Date();
try {
// 执行前再次检查账号是否启用(双重保险)
const isEnabled = await this.checkAccountEnabled(task.sn_code);
if (!isEnabled) {
// 更新任务状态为已取消
await db.getModel('task_status').update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({ error: '账号未启用,任务已取消' })
},
{ where: { id: task.id } }
);
throw new Error(`账号未启用,任务已取消`);
}
// 更新数据库状态
await db.getModel('task_status').update(
{
status: 'running',
startTime: task.startTime
},
{ where: { id: task.id } }
);
// 使用注册的任务处理器执行任务
const handler = this.taskHandlers.get(task.taskType);
if (!handler) {
throw new Error(`未找到任务类型 ${task.taskType} 的处理器,请先注册处理器`);
}
// 执行任务处理器
const result = await handler(task);
// 任务成功
task.status = 'completed';
task.endTime = new Date();
task.duration = Date.now() - startTime;
task.result = result;
// 更新数据库
await db.getModel('task_status').update(
{
status: 'completed',
endTime: task.endTime,
duration: task.duration,
result: JSON.stringify(task.result),
progress: 100
},
{ where: { id: task.id } }
);
console.log(`[任务队列] 设备 ${task.sn_code} 任务执行成功: ${task.taskName} (耗时: ${task.duration}ms)`);
} catch (error) {
// 使用统一错误处理
const errorInfo = await ErrorHandler.handleError(error, {
taskId: task.id,
sn_code: task.sn_code,
taskType: task.taskType,
taskName: task.taskName
});
// 直接标记为失败(重试已禁用)
task.status = 'failed';
task.endTime = new Date();
task.duration = Date.now() - startTime;
task.errorMessage = errorInfo.message || error.message || '未知错误';
task.errorStack = errorInfo.stack || error.stack || '';
console.error(`[任务队列] 任务执行失败: ${task.taskName} (ID: ${task.id}), 错误: ${task.errorMessage}`, {
errorStack: task.errorStack,
taskId: task.id,
sn_code: task.sn_code,
taskType: task.taskType
});
}
}
/**
* 取消任务
* @param {string} taskId - 任务ID
* @returns {Promise<boolean>} 是否成功取消
*/
async cancelTask(taskId) {
// 遍历所有设备队列查找任务
for (const [sn_code, queue] of this.deviceQueues.entries()) {
const removed = queue.remove(task => task.id === taskId);
if (removed) {
// 检查是否正在执行
const status = this.deviceStatus.get(sn_code);
if (status && status.currentTask && status.currentTask.id === taskId) {
// 正在执行的任务无法取消,只能标记
console.warn(`[任务队列] 任务 ${taskId} 正在执行,无法取消`);
return false;
}
// 更新数据库
await db.getModel('task_status').update(
{
status: 'cancelled',
endTime: new Date()
},
{ where: { id: taskId } }
);
console.log(`[任务队列] 任务已取消: ${taskId}`);
return true;
}
}
// 未找到可取消的任务
return false;
}
/**
* 取消设备的所有待执行任务
* @param {string} sn_code - 设备SN码
* @returns {Promise<number>} 取消的任务数量
*/
async cancelDeviceTasks(sn_code) {
let cancelledCount = 0;
// 1. 从队列中移除所有待执行任务
const queue = this.deviceQueues.get(sn_code);
if (queue) {
const pendingTasks = [];
// 获取所有待执行任务(不包括正在执行的)
const status = this.deviceStatus.get(sn_code);
const currentTaskId = status && status.currentTask ? status.currentTask.id : null;
// 遍历队列,收集待取消的任务
const queueArray = queue.toArray();
for (const task of queueArray) {
if (task.id !== currentTaskId && (task.status === 'pending' || !task.status)) {
pendingTasks.push(task);
}
}
// 从队列中移除这些任务
for (const task of pendingTasks) {
queue.remove(t => t.id === task.id);
cancelledCount++;
}
}
// 2. 更新数据库中的任务状态
try {
const taskStatusModel = db.getModel('task_status');
const status = this.deviceStatus.get(sn_code);
const currentTaskId = status && status.currentTask ? status.currentTask.id : null;
// 更新所有待执行或运行中的任务(除了当前正在执行的)
const whereCondition = {
sn_code: sn_code,
status: ['pending', 'running']
};
if (currentTaskId) {
whereCondition.id = { [Sequelize.Op.ne]: currentTaskId };
}
const updateResult = await taskStatusModel.update(
{
status: 'cancelled',
endTime: new Date()
},
{ where: whereCondition }
);
const dbCancelledCount = Array.isArray(updateResult) ? updateResult[0] : updateResult;
console.log(`[任务队列] 设备 ${sn_code} 已取消 ${cancelledCount} 个队列任务,${dbCancelledCount} 个数据库任务`);
} catch (error) {
console.error(`[任务队列] 更新数据库任务状态失败:`, error, {
sn_code: sn_code,
cancelledCount: cancelledCount
});
}
return cancelledCount;
}
/**
* 获取设备队列状态
* @param {string} sn_code - 设备SN码
* @returns {object} 队列状态
*/
getDeviceStatus(sn_code) {
const queue = this.deviceQueues.get(sn_code);
const status = this.deviceStatus.get(sn_code) || {
isRunning: false,
currentTask: null,
runningCount: 0
};
return {
sn_code,
isRunning: status.isRunning,
currentTask: status.currentTask,
queueLength: queue ? queue.size() : 0,
pendingTasks: queue ? queue.size() : 0,
runningCount: status.runningCount
};
}
/**
* 获取任务状态
* @param {string} taskId - 任务ID
* @returns {Promise<object|null>} 任务对象
*/
async getTaskStatus(taskId) {
// 先从内存中查找
for (const queue of this.deviceQueues.values()) {
const task = queue.find(t => t.id === taskId);
if (task) {
return task;
}
}
// 从正在执行的任务中查找
for (const status of this.deviceStatus.values()) {
if (status.currentTask && status.currentTask.id === taskId) {
return status.currentTask;
}
}
// 从数据库中查找
try {
const taskRecord = await db.getModel('task_status').findOne({
where: { id: taskId }
});
if (taskRecord) {
return taskRecord.toJSON();
}
} catch (error) {
console.error(`[任务队列] 查询任务状态失败:`, error, {
taskId: taskId
});
}
return null;
}
/**
* 清空设备队列
* @param {string} sn_code - 设备SN码
*/
clearQueue(sn_code) {
if (this.deviceQueues.has(sn_code)) {
const queue = this.deviceQueues.get(sn_code);
const count = queue.size();
queue.clear();
console.log(`[任务队列] 已清空设备 ${sn_code} 的队列,共移除 ${count} 个任务`);
}
}
/**
* 删除所有任务(从内存队列和数据库)
* @returns {Promise<object>} 删除结果
*/
async deleteAllTaskFromDatabase() {
try {
console.log('[任务队列] 开始删除所有任务...');
let totalQueued = 0;
let totalRunning = 0;
// 1. 清空所有设备的内存队列
for (const [sn_code, queue] of this.deviceQueues.entries()) {
const queueSize = queue.size();
totalQueued += queueSize;
queue.clear();
// 重置设备状态(但保留正在执行的任务信息,稍后处理)
const status = this.deviceStatus.get(sn_code);
if (status && status.currentTask) {
totalRunning++;
// 标记正在执行的任务,但不立即取消(让它们自然完成或失败)
console.warn(`[任务队列] 设备 ${sn_code} 有正在执行的任务,将在完成后清理`);
}
}
// 2. 使用 MCP MySQL 删除所有关联数据(先删除关联表,再删除主表)
// 注意MCP MySQL 是只读的,这里使用 Sequelize 执行删除操作
// 但移除数据库层面的外键关联,避免约束问题
const taskCommandsModel = db.getModel('task_commands');
const chatRecordsModel = db.getModel('chat_records');
const applyRecordsModel = db.getModel('apply_records');
const taskStatusModel = db.getModel('task_status');
// 删除任务指令记录(所有记录)
const commandsDeleted = await taskCommandsModel.destroy({
where: {},
truncate: false
});
console.log(`[任务队列] 已删除任务指令记录: ${commandsDeleted}`);
// 删除聊天记录中关联的任务记录(删除所有有 taskId 且不为空的记录)
const chatRecordsDeleted = await chatRecordsModel.destroy({
where: {
[Sequelize.Op.and]: [
{ taskId: { [Sequelize.Op.ne]: null } },
{ taskId: { [Sequelize.Op.ne]: '' } }
]
},
truncate: false
});
console.log(`[任务队列] 已删除聊天记录: ${chatRecordsDeleted}`);
// 删除投递记录中关联的任务记录(删除所有有 taskId 且不为空的记录)
const applyRecordsDeleted = await applyRecordsModel.destroy({
where: {
[Sequelize.Op.and]: [
{ taskId: { [Sequelize.Op.ne]: null } },
{ taskId: { [Sequelize.Op.ne]: '' } }
]
},
truncate: false
});
console.log(`[任务队列] 已删除投递记录: ${applyRecordsDeleted}`);
// 3. 删除数据库中的所有任务记录
const deleteResult = await taskStatusModel.destroy({
where: {},
truncate: false // 使用 DELETE 而不是 TRUNCATE保留表结构
});
console.log(`[任务队列] 已删除所有任务:`);
console.log(` - 内存队列任务: ${totalQueued}`);
console.log(` - 正在执行任务: ${totalRunning} 个(将在完成后清理)`);
console.log(` - 任务指令记录: ${commandsDeleted}`);
console.log(` - 聊天记录: ${chatRecordsDeleted}`);
console.log(` - 投递记录: ${applyRecordsDeleted}`);
console.log(` - 数据库任务记录: ${deleteResult}`);
return {
success: true,
memoryQueued: totalQueued,
memoryRunning: totalRunning,
commandsDeleted: commandsDeleted,
chatRecordsDeleted: chatRecordsDeleted,
applyRecordsDeleted: applyRecordsDeleted,
databaseDeleted: deleteResult,
message: `已删除所有任务及关联数据(任务: ${deleteResult} 条,指令: ${commandsDeleted} 条,聊天: ${chatRecordsDeleted} 条,投递: ${applyRecordsDeleted} 条)`
};
} catch (error) {
console.error('[任务队列] 删除所有任务失败:', error);
throw error;
}
}
/**
* 获取所有设备的队列状态
* @returns {array} 所有设备的队列状态
*/
getAllDeviceStatus() {
const allStatus = [];
for (const sn_code of this.deviceQueues.keys()) {
allStatus.push(this.getDeviceStatus(sn_code));
}
return allStatus;
}
/**
* 获取全局统计信息
* @returns {object} 统计信息
*/
getStatistics() {
let totalQueued = 0;
for (const queue of this.deviceQueues.values()) {
totalQueued += queue.size();
}
return {
globalRunningCount: this.globalRunningCount,
maxConcurrency: this.config.maxConcurrency,
totalDevices: this.deviceQueues.size,
totalQueuedTasks: totalQueued,
deviceStatuses: this.getAllDeviceStatus()
};
}
/**
* 获取MQTT客户端统一管理
* @returns {Promise<object|null>} MQTT客户端实例
*/
async getMqttClient() {
try {
// 首先尝试从调度系统获取已初始化的MQTT客户端
const scheduleManager = require('./index');
if (scheduleManager.mqttClient) {
return scheduleManager.mqttClient;
}
// 如果调度系统没有初始化,则直接创建
const mqttManager = require('../mqtt/mqttManager');
console.log('[任务队列] 创建新的MQTT客户端');
return await mqttManager.getInstance();
} catch (error) {
console.error(`[任务队列] 获取MQTT客户端失败:`, error);
return null;
}
}
}
// 导出单例
const taskQueue = new TaskQueue();
module.exports = taskQueue;

View File

@@ -0,0 +1,265 @@
const dayjs = require('dayjs');
/**
* 调度系统工具函数
* 提供通用的辅助功能
*/
class ScheduleUtils {
/**
* 生成唯一任务ID
* @returns {string} 任务ID
*/
static generateTaskId() {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 生成唯一指令ID
* @returns {string} 指令ID
*/
static generateCommandId() {
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 格式化时间戳
* @param {number} timestamp - 时间戳
* @returns {string} 格式化的时间
*/
static formatTimestamp(timestamp) {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss');
}
/**
* 格式化持续时间
* @param {number} ms - 毫秒数
* @returns {string} 格式化的持续时间
*/
static formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60 * 1000) {
return `${(ms / 1000).toFixed(1)}s`;
} else if (ms < 60 * 60 * 1000) {
return `${(ms / (60 * 1000)).toFixed(1)}min`;
} else {
return `${(ms / (60 * 60 * 1000)).toFixed(1)}h`;
}
}
/**
* 深度克隆对象
* @param {object} obj - 要克隆的对象
* @returns {object} 克隆后的对象
*/
static deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map(item => this.deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = this.deepClone(obj[key]);
}
}
return cloned;
}
/**
* 安全解析JSON
* @param {string} jsonString - JSON字符串
* @param {any} defaultValue - 默认值
* @returns {any} 解析结果或默认值
*/
static safeJsonParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
return defaultValue;
}
}
/**
* 安全序列化JSON
* @param {any} obj - 要序列化的对象
* @param {string} defaultValue - 默认值
* @returns {string} JSON字符串或默认值
*/
static safeJsonStringify(obj, defaultValue = '{}') {
try {
return JSON.stringify(obj);
} catch (error) {
return defaultValue;
}
}
/**
* 延迟执行
* @param {number} ms - 延迟毫秒数
* @returns {Promise} Promise对象
*/
static delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 重试执行函数
* @param {function} fn - 要执行的函数
* @param {number} maxRetries - 最大重试次数
* @param {number} delay - 重试延迟(毫秒)
* @returns {Promise} 执行结果
*/
static async retry(fn, maxRetries = 3, delay = 1000) {
let lastError;
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i < maxRetries) {
console.log(`[工具函数] 执行失败,${delay}ms后重试 (${i + 1}/${maxRetries + 1}):`, error.message);
await this.delay(delay);
}
}
}
throw lastError;
}
/**
* 限制并发执行数量
* @param {Array} tasks - 任务数组
* @param {number} concurrency - 并发数量
* @returns {Promise<Array>} 执行结果数组
*/
static async limitConcurrency(tasks, concurrency = 5) {
const results = [];
const executing = [];
for (const task of tasks) {
const promise = Promise.resolve(task()).then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});
results.push(promise);
if (tasks.length >= concurrency) {
executing.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
/**
* 创建带超时的Promise
* @param {Promise} promise - 原始Promise
* @param {number} timeout - 超时时间(毫秒)
* @param {string} timeoutMessage - 超时错误消息
* @returns {Promise} 带超时的Promise
*/
static withTimeout(promise, timeout, timeoutMessage = 'Operation timed out') {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => reject(new Error(timeoutMessage)), timeout);
})
]);
}
/**
* 获取今天的日期字符串
* @returns {string} 日期字符串 YYYY-MM-DD
*/
static getTodayString() {
return dayjs().format('YYYY-MM-DD');
}
/**
* 检查日期是否为今天
* @param {string|Date} date - 日期
* @returns {boolean} 是否为今天
*/
static isToday(date) {
return dayjs(date).format('YYYY-MM-DD') === this.getTodayString();
}
/**
* 获取随机延迟时间
* @param {number} min - 最小延迟(毫秒)
* @param {number} max - 最大延迟(毫秒)
* @returns {number} 随机延迟时间
*/
static getRandomDelay(min = 1000, max = 5000) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* 格式化文件大小
* @param {number} bytes - 字节数
* @returns {string} 格式化的文件大小
*/
static formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* 计算成功率
* @param {number} success - 成功次数
* @param {number} total - 总次数
* @returns {string} 百分比字符串
*/
static calculateSuccessRate(success, total) {
if (total === 0) return '0%';
return ((success / total) * 100).toFixed(2) + '%';
}
/**
* 验证设备SN码格式
* @param {string} sn_code - 设备SN码
* @returns {boolean} 是否有效
*/
static isValidSnCode(sn_code) {
return typeof sn_code === 'string' && sn_code.length > 0 && sn_code.length <= 50;
}
/**
* 清理对象中的空值
* @param {object} obj - 要清理的对象
* @returns {object} 清理后的对象
*/
static cleanObject(obj) {
const cleaned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] !== null && obj[key] !== undefined && obj[key] !== '') {
cleaned[key] = obj[key];
}
}
return cleaned;
}
}
module.exports = ScheduleUtils;