This commit is contained in:
张成
2025-12-30 15:46:18 +08:00
parent d14f89e008
commit 65833dd32d
29 changed files with 2416 additions and 1048 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: {
task_id: context.task_id,
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,102 @@
const dayjs = require('dayjs');
/**
* 调度系统配置中心
* 统一管理所有配置参数
*/
class ScheduleConfig {
constructor() {
// 单日操作限制
this.dailyLimits = {
maxSearch: 20, // 每天最多搜索20次
maxApply: 50, // 每天最多投递50份简历默认值
maxChat: 100, // 每天最多发送100条聊天
};
// 平台特定的每日投递限制
this.platformDailyLimits = {
boss: 150, // Boss直聘每天最多投递150次
liepin: 50 // 猎聘每天最多投递50次默认值
};
// 任务超时配置(毫秒)
this.taskTimeouts = {
auto_search: 20 * 60 * 1000, // 自动搜索任务20分钟
auto_deliver: 30 * 60 * 1000, // 自动投递任务30分钟包含多个子任务
auto_chat: 15 * 60 * 1000, // 自动沟通任务15分钟
auto_active_account: 10 * 60 * 1000 // 自动活跃账号任务10分钟
};
// 任务优先级配置
this.taskPriorities = {
auto_search: 8, // 自动搜索任务(最高优先级,先搜索后投递)
auto_deliver: 7, // 自动投递任务
auto_chat: 6, // 自动沟通任务
auto_active_account: 5, // 自动活跃账号任务
cleanup: 1
};
// 监控配置
this.monitoring = {
heartbeatTimeout: 3 * 60 * 1000, // 心跳超时3分钟
offlineThreshold: 24 * 60 * 60 * 1000 // 离线设备清理24小时
};
// 定时任务配置
this.schedules = {
dailyReset: '0 0 * * *', // 每天凌晨重置统计
monitoringInterval: '*/1 * * * *', // 监控检查间隔1分钟
autoSearch: '0 0 */1 * * *', // 自动搜索任务每1小时执行一次
autoDeliver: '0 */1 * * * *', // 自动投递任务每1分钟执行一次
autoChat: '0 */15 * * * *', // 自动沟通任务每15分钟执行一次
autoActive: '0 0 */2 * * *' // 自动活跃任务每2小时执行一次
};
}
/**
* 获取任务超时时间
* @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 - 操作类型
* @param {string} platform - 平台类型(可选,用于平台特定的限制)
* @returns {number} 日限制次数
*/
getDailyLimit(operation, platform = null) {
// 如果是投递操作且指定了平台,使用平台特定的限制
if (operation === 'apply' && platform && this.platformDailyLimits[platform]) {
return this.platformDailyLimits[platform];
}
// 否则使用通用限制
return this.dailyLimits[`max${operation.charAt(0).toUpperCase() + operation.slice(1)}`] || Infinity;
}
}
// 导出单例
const scheduleConfig = new ScheduleConfig();
module.exports = scheduleConfig;

View File

@@ -0,0 +1,14 @@
/**
* Infrastructure 模块导出
* 统一导出基础设施模块
*/
const PriorityQueue = require('./PriorityQueue');
const ErrorHandler = require('./ErrorHandler');
const config = require('./config');
module.exports = {
PriorityQueue,
ErrorHandler,
config
};