1
This commit is contained in:
118
api/middleware/schedule/ErrorHandler.js
Normal file
118
api/middleware/schedule/ErrorHandler.js
Normal 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;
|
||||
|
||||
215
api/middleware/schedule/PriorityQueue.js
Normal file
215
api/middleware/schedule/PriorityQueue.js
Normal 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;
|
||||
|
||||
308
api/middleware/schedule/command.js
Normal file
308
api/middleware/schedule/command.js
Normal 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();
|
||||
150
api/middleware/schedule/config.js
Normal file
150
api/middleware/schedule/config.js
Normal 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;
|
||||
287
api/middleware/schedule/deviceManager.js
Normal file
287
api/middleware/schedule/deviceManager.js
Normal 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;
|
||||
|
||||
203
api/middleware/schedule/index.js
Normal file
203
api/middleware/schedule/index.js
Normal 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; }
|
||||
};
|
||||
168
api/middleware/schedule/scheduledJobs.js
Normal file
168
api/middleware/schedule/scheduledJobs.js
Normal 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;
|
||||
|
||||
432
api/middleware/schedule/taskHandlers.js
Normal file
432
api/middleware/schedule/taskHandlers.js
Normal 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;
|
||||
|
||||
927
api/middleware/schedule/taskQueue.js
Normal file
927
api/middleware/schedule/taskQueue.js
Normal 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;
|
||||
265
api/middleware/schedule/utils.js
Normal file
265
api/middleware/schedule/utils.js
Normal 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;
|
||||
Reference in New Issue
Block a user