1
This commit is contained in:
@@ -213,58 +213,17 @@ class CommandManager {
|
||||
* @private
|
||||
*/
|
||||
async _execute_command_with_timeout(command_id, command_type, command_name, command_params, sn_code, mqttClient, start_time) {
|
||||
// 将驼峰命名转换为下划线命名
|
||||
const to_snake_case = (str) => {
|
||||
if (str.includes('_')) {
|
||||
return str;
|
||||
}
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
||||
};
|
||||
|
||||
const method_name = to_snake_case(command_type);
|
||||
|
||||
// 获取指令超时时间(从配置中获取,默认5分钟)
|
||||
const timeout = ScheduleConfig.taskTimeouts[command_type] ||
|
||||
ScheduleConfig.taskTimeouts[method_name] ||
|
||||
5 * 60 * 1000;
|
||||
|
||||
const timeout = ScheduleConfig.taskTimeouts[command_type] || 5 * 60 * 1000;
|
||||
|
||||
// 构建指令执行 Promise
|
||||
const command_promise = (async () => {
|
||||
// 指令类型映射表(内部指令类型 -> jobManager方法名)
|
||||
const commandMethodMap = {
|
||||
// get_job_list 指令(对应MQTT Action: "get_job_list")
|
||||
'get_job_list': 'get_job_list',
|
||||
'getJobList': 'get_job_list',
|
||||
// search_jobs_with_params 指令(对应MQTT Action: "search_job_list")
|
||||
'search_jobs_with_params': 'search_jobs_with_params',
|
||||
'searchJobsWithParams': 'search_jobs_with_params',
|
||||
// search_and_deliver 指令(内部调用search_jobs_with_params和deliver_resume)
|
||||
'search_and_deliver': 'search_and_deliver',
|
||||
'searchAndDeliver': 'search_and_deliver',
|
||||
// deliver_resume 指令(对应MQTT Action: "deliver_resume")
|
||||
'deliver_resume': 'deliver_resume',
|
||||
'deliverResume': 'deliver_resume'
|
||||
// search_jobs 指令(对应MQTT Action: "search_jobs")
|
||||
'search_jobs': 'search_jobs',
|
||||
'searchJobs': 'search_jobs'
|
||||
};
|
||||
|
||||
// 优先使用映射表
|
||||
const mappedMethod = commandMethodMap[command_type] || commandMethodMap[method_name];
|
||||
if (mappedMethod && jobManager[mappedMethod]) {
|
||||
return await jobManager[mappedMethod](sn_code, mqttClient, command_params);
|
||||
}
|
||||
|
||||
// 其次尝试转换后的方法名
|
||||
if (command_type && jobManager[method_name]) {
|
||||
return await jobManager[method_name](sn_code, mqttClient, command_params);
|
||||
}
|
||||
|
||||
// 最后尝试原始指令类型
|
||||
// 直接使用 command_type 调用 jobManager 的方法,不做映射
|
||||
// command_type 和 jobManager 的方法名保持一致
|
||||
if (jobManager[command_type]) {
|
||||
return await jobManager[command_type](sn_code, mqttClient, command_params);
|
||||
} else {
|
||||
throw new Error(`未知的指令类型: ${command_type} (尝试的方法名: ${method_name}, 映射方法: ${mappedMethod})`);
|
||||
throw new Error(`未知的指令类型: ${command_type}, jobManager 中不存在对应方法`);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class ScheduleConfig {
|
||||
|
||||
// 任务超时配置(毫秒)
|
||||
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分钟
|
||||
@@ -30,6 +31,7 @@ class ScheduleConfig {
|
||||
|
||||
// 任务优先级配置
|
||||
this.taskPriorities = {
|
||||
auto_search: 8, // 自动搜索任务(最高优先级,先搜索后投递)
|
||||
auto_deliver: 7, // 自动投递任务
|
||||
auto_chat: 6, // 自动沟通任务
|
||||
auto_active_account: 5, // 自动活跃账号任务
|
||||
@@ -44,10 +46,12 @@ class ScheduleConfig {
|
||||
|
||||
// 定时任务配置
|
||||
this.schedules = {
|
||||
dailyReset: '0 0 * * *', // 每天凌晨重置统计
|
||||
monitoringInterval: '*/1 * * * *', // 监控检查间隔:1分钟
|
||||
dailyReset: '0 0 * * *', // 每天凌晨重置统计
|
||||
monitoringInterval: '*/1 * * * *', // 监控检查间隔:1分钟
|
||||
autoSearch: '0 0 */1 * * *', // 自动搜索任务:每1小时执行一次
|
||||
autoDeliver: '0 */1 * * * *', // 自动投递任务:每1分钟执行一次
|
||||
autoChat: '0 */15 * * * *' // 自动沟通任务:每15分钟执行一次
|
||||
autoChat: '0 */15 * * * *', // 自动沟通任务:每15分钟执行一次
|
||||
autoActive: '0 0 */2 * * *' // 自动活跃任务:每2小时执行一次
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -114,11 +114,11 @@ class ScheduledJobs {
|
||||
|
||||
// 执行自动投递任务
|
||||
const autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
|
||||
this.autoDeliverTask();
|
||||
this.recommendedSearchJobListTask();
|
||||
});
|
||||
|
||||
// 立即执行一次自动投递任务
|
||||
this.autoDeliverTask();
|
||||
this.recommendedSearchJobListTask();
|
||||
|
||||
this.jobs.push(autoDeliverJob);
|
||||
console.log('[定时任务] 已启动自动投递任务');
|
||||
@@ -452,9 +452,9 @@ class ScheduledJobs {
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动投递任务
|
||||
* 推荐职位列表任务
|
||||
*/
|
||||
async autoDeliverTask() {
|
||||
async recommendedSearchJobListTask() {
|
||||
const now = new Date();
|
||||
console.log(`[自动投递] ${now.toLocaleString()} 开始执行自动投递任务`);
|
||||
|
||||
@@ -644,6 +644,25 @@ class ScheduledJobs {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 搜索职位列表任务
|
||||
*/
|
||||
async searchJobListTask() {
|
||||
const now = new Date();
|
||||
console.log(`[搜索职位列表] ${now.toLocaleString()} 开始执行搜索职位列表任务`);
|
||||
|
||||
try {
|
||||
|
||||
const models = db.models;
|
||||
const { pla_account, op } = models;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[搜索职位列表] 执行失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 自动沟通任务
|
||||
*/
|
||||
@@ -773,6 +792,8 @@ class ScheduledJobs {
|
||||
console.error('[自动沟通] 执行失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = ScheduledJobs;
|
||||
|
||||
570
api/middleware/schedule/scheduledJobs_new.js
Normal file
570
api/middleware/schedule/scheduledJobs_new.js
Normal file
@@ -0,0 +1,570 @@
|
||||
const node_schedule = require("node-schedule");
|
||||
const dayjs = require('dayjs');
|
||||
const config = require('./config.js');
|
||||
const deviceManager = require('./deviceManager.js');
|
||||
const command = require('./command.js');
|
||||
const db = require('../dbProxy');
|
||||
|
||||
// 引入新的任务模块
|
||||
const tasks = require('./tasks');
|
||||
const { autoSearchTask, autoDeliverTask, autoChatTask, autoActiveTask } = tasks;
|
||||
|
||||
const Framework = require("../../../framework/node-core-framework.js");
|
||||
|
||||
/**
|
||||
* 定时任务管理器(重构版)
|
||||
* 使用独立的任务模块,职责更清晰,易于维护和扩展
|
||||
*/
|
||||
class ScheduledJobs {
|
||||
constructor(components, taskHandlers) {
|
||||
this.taskQueue = components.taskQueue;
|
||||
this.taskHandlers = taskHandlers;
|
||||
this.jobs = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动所有定时任务
|
||||
*/
|
||||
start() {
|
||||
console.log('[定时任务] 开始启动所有定时任务...');
|
||||
|
||||
// ==================== 系统维护任务 ====================
|
||||
|
||||
// 每天凌晨重置统计数据
|
||||
const resetJob = node_schedule.scheduleJob(config.schedules.dailyReset, () => {
|
||||
this.resetDailyStats();
|
||||
});
|
||||
this.jobs.push(resetJob);
|
||||
console.log('[定时任务] ✓ 已启动每日统计重置任务');
|
||||
|
||||
// 启动心跳检查定时任务(每分钟检查一次)
|
||||
const monitoringJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
|
||||
await deviceManager.checkHeartbeatStatus().catch(error => {
|
||||
console.error('[定时任务] 检查心跳状态失败:', error);
|
||||
});
|
||||
});
|
||||
this.jobs.push(monitoringJob);
|
||||
console.log('[定时任务] ✓ 已启动心跳检查任务');
|
||||
|
||||
// 启动离线设备任务清理定时任务(每分钟检查一次)
|
||||
const cleanupOfflineTasksJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
|
||||
await this.cleanupOfflineDeviceTasks().catch(error => {
|
||||
console.error('[定时任务] 清理离线设备任务失败:', error);
|
||||
});
|
||||
});
|
||||
this.jobs.push(cleanupOfflineTasksJob);
|
||||
console.log('[定时任务] ✓ 已启动离线设备任务清理任务');
|
||||
|
||||
// 启动任务超时检查定时任务(每分钟检查一次)
|
||||
const timeoutCheckJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
|
||||
await this.checkTaskTimeouts().catch(error => {
|
||||
console.error('[定时任务] 检查任务超时失败:', error);
|
||||
});
|
||||
});
|
||||
this.jobs.push(timeoutCheckJob);
|
||||
console.log('[定时任务] ✓ 已启动任务超时检查任务');
|
||||
|
||||
// 启动任务状态摘要同步定时任务(每10秒发送一次)
|
||||
const taskSummaryJob = node_schedule.scheduleJob('*/10 * * * * *', async () => {
|
||||
await this.syncTaskStatusSummary().catch(error => {
|
||||
console.error('[定时任务] 同步任务状态摘要失败:', error);
|
||||
});
|
||||
});
|
||||
this.jobs.push(taskSummaryJob);
|
||||
console.log('[定时任务] ✓ 已启动任务状态摘要同步任务');
|
||||
|
||||
// ==================== 业务任务(使用新的任务模块) ====================
|
||||
|
||||
// 1. 自动搜索任务 - 每60分钟执行一次
|
||||
const autoSearchJob = node_schedule.scheduleJob(config.schedules.autoSearch || '0 0 */1 * * *', () => {
|
||||
this.runAutoSearchTask();
|
||||
});
|
||||
this.jobs.push(autoSearchJob);
|
||||
console.log('[定时任务] ✓ 已启动自动搜索任务 (每60分钟)');
|
||||
|
||||
// 2. 自动投递任务 - 每1分钟检查一次
|
||||
const autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
|
||||
this.runAutoDeliverTask();
|
||||
});
|
||||
this.jobs.push(autoDeliverJob);
|
||||
console.log('[定时任务] ✓ 已启动自动投递任务 (每1分钟)');
|
||||
|
||||
// 3. 自动沟通任务 - 每15分钟执行一次
|
||||
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */15 * * * *', () => {
|
||||
this.runAutoChatTask();
|
||||
});
|
||||
this.jobs.push(autoChatJob);
|
||||
console.log('[定时任务] ✓ 已启动自动沟通任务 (每15分钟)');
|
||||
|
||||
// 4. 自动活跃任务 - 每2小时执行一次
|
||||
const autoActiveJob = node_schedule.scheduleJob(config.schedules.autoActive || '0 0 */2 * * *', () => {
|
||||
this.runAutoActiveTask();
|
||||
});
|
||||
this.jobs.push(autoActiveJob);
|
||||
console.log('[定时任务] ✓ 已启动自动活跃任务 (每2小时)');
|
||||
|
||||
// 立即执行一次业务任务(可选)
|
||||
setTimeout(() => {
|
||||
console.log('[定时任务] 立即执行一次初始化任务...');
|
||||
this.runAutoDeliverTask();
|
||||
this.runAutoChatTask();
|
||||
}, 3000); // 延迟3秒,等待系统初始化完成
|
||||
|
||||
console.log('[定时任务] 所有定时任务启动完成!');
|
||||
}
|
||||
|
||||
// ==================== 业务任务执行方法(使用新的任务模块) ====================
|
||||
|
||||
/**
|
||||
* 运行自动搜索任务
|
||||
* 为所有启用自动搜索的账号添加搜索任务
|
||||
*/
|
||||
async runAutoSearchTask() {
|
||||
try {
|
||||
const accounts = await this.getEnabledAccounts('auto_search');
|
||||
|
||||
if (accounts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[自动搜索调度] 找到 ${accounts.length} 个启用自动搜索的账号`);
|
||||
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (const account of accounts) {
|
||||
const result = await autoSearchTask.addToQueue(account.sn_code, this.taskQueue);
|
||||
if (result.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0 || failedCount > 0) {
|
||||
console.log(`[自动搜索调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount} 个`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动搜索调度] 执行失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行自动投递任务
|
||||
* 为所有启用自动投递的账号添加投递任务
|
||||
*/
|
||||
async runAutoDeliverTask() {
|
||||
try {
|
||||
const accounts = await this.getEnabledAccounts('auto_deliver');
|
||||
|
||||
if (accounts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[自动投递调度] 找到 ${accounts.length} 个启用自动投递的账号`);
|
||||
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (const account of accounts) {
|
||||
const result = await autoDeliverTask.addToQueue(account.sn_code, this.taskQueue);
|
||||
if (result.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0 || failedCount > 0) {
|
||||
console.log(`[自动投递调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount} 个`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动投递调度] 执行失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行自动沟通任务
|
||||
* 为所有启用自动沟通的账号添加沟通任务
|
||||
*/
|
||||
async runAutoChatTask() {
|
||||
try {
|
||||
const accounts = await this.getEnabledAccounts('auto_chat');
|
||||
|
||||
if (accounts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[自动沟通调度] 找到 ${accounts.length} 个启用自动沟通的账号`);
|
||||
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (const account of accounts) {
|
||||
const result = await autoChatTask.addToQueue(account.sn_code, this.taskQueue);
|
||||
if (result.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0 || failedCount > 0) {
|
||||
console.log(`[自动沟通调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount} 个`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动沟通调度] 执行失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行自动活跃任务
|
||||
* 为所有启用自动活跃的账号添加活跃任务
|
||||
*/
|
||||
async runAutoActiveTask() {
|
||||
try {
|
||||
const accounts = await this.getEnabledAccounts('auto_active');
|
||||
|
||||
if (accounts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[自动活跃调度] 找到 ${accounts.length} 个启用自动活跃的账号`);
|
||||
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (const account of accounts) {
|
||||
const result = await autoActiveTask.addToQueue(account.sn_code, this.taskQueue);
|
||||
if (result.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0 || failedCount > 0) {
|
||||
console.log(`[自动活跃调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount} 个`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[自动活跃调度] 执行失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用指定功能的账号列表
|
||||
* @param {string} featureType - 功能类型: auto_search, auto_deliver, auto_chat, auto_active
|
||||
*/
|
||||
async getEnabledAccounts(featureType) {
|
||||
try {
|
||||
const { pla_account } = db.models;
|
||||
|
||||
const accounts = await pla_account.findAll({
|
||||
where: {
|
||||
is_delete: 0,
|
||||
is_enabled: 1,
|
||||
[featureType]: 1
|
||||
},
|
||||
attributes: ['sn_code', 'name', 'keyword', 'platform_type']
|
||||
});
|
||||
|
||||
return accounts.map(acc => acc.toJSON());
|
||||
} catch (error) {
|
||||
console.error(`[获取账号列表] 失败 (${featureType}):`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 系统维护方法(保留原有逻辑) ====================
|
||||
|
||||
/**
|
||||
* 重置每日统计
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理离线设备任务
|
||||
* 检查离线超过10分钟的设备,取消其所有pending/running状态的任务
|
||||
*/
|
||||
async cleanupOfflineDeviceTasks() {
|
||||
try {
|
||||
// 离线阈值:10分钟
|
||||
const offlineThreshold = 10 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
const thresholdTime = now - offlineThreshold;
|
||||
|
||||
// 获取所有启用的账号
|
||||
const pla_account = db.getModel('pla_account');
|
||||
const accounts = await pla_account.findAll({
|
||||
where: {
|
||||
is_delete: 0,
|
||||
is_enabled: 1
|
||||
},
|
||||
attributes: ['sn_code']
|
||||
});
|
||||
|
||||
if (!accounts || accounts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 通过 deviceManager 检查哪些设备离线超过10分钟
|
||||
const offlineSnCodes = [];
|
||||
const offlineDevicesInfo = [];
|
||||
|
||||
for (const account of accounts) {
|
||||
const sn_code = account.sn_code;
|
||||
const device = deviceManager.devices.get(sn_code);
|
||||
|
||||
if (!device) {
|
||||
offlineSnCodes.push(sn_code);
|
||||
offlineDevicesInfo.push({
|
||||
sn_code: sn_code,
|
||||
lastHeartbeatTime: null
|
||||
});
|
||||
} else {
|
||||
const lastHeartbeat = device.lastHeartbeat || 0;
|
||||
if (lastHeartbeat < thresholdTime || !device.isOnline) {
|
||||
offlineSnCodes.push(sn_code);
|
||||
offlineDevicesInfo.push({
|
||||
sn_code: sn_code,
|
||||
lastHeartbeatTime: lastHeartbeat ? new Date(lastHeartbeat) : null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (offlineSnCodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[清理离线任务] 发现 ${offlineSnCodes.length} 个离线超过10分钟的设备`);
|
||||
|
||||
let totalCancelled = 0;
|
||||
const task_status = db.getModel('task_status');
|
||||
|
||||
for (const sn_code of offlineSnCodes) {
|
||||
try {
|
||||
const deviceInfo = offlineDevicesInfo.find(d => d.sn_code === sn_code);
|
||||
|
||||
const updateResult = await task_status.update(
|
||||
{
|
||||
status: 'cancelled',
|
||||
endTime: new Date(),
|
||||
result: JSON.stringify({
|
||||
reason: '设备离线超过10分钟,任务已自动取消',
|
||||
offlineTime: deviceInfo?.lastHeartbeatTime
|
||||
})
|
||||
},
|
||||
{
|
||||
where: {
|
||||
sn_code: sn_code,
|
||||
status: ['pending', 'running']
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const cancelledCount = Array.isArray(updateResult) ? updateResult[0] : updateResult;
|
||||
totalCancelled += cancelledCount;
|
||||
|
||||
if (this.taskQueue && typeof this.taskQueue.cancelDeviceTasks === 'function') {
|
||||
await this.taskQueue.cancelDeviceTasks(sn_code);
|
||||
}
|
||||
|
||||
if (cancelledCount > 0) {
|
||||
console.log(`[清理离线任务] 设备 ${sn_code} 已取消 ${cancelledCount} 个任务`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[清理离线任务] 取消设备 ${sn_code} 的任务失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalCancelled > 0) {
|
||||
console.log(`[清理离线任务] 共取消 ${totalCancelled} 个离线设备的任务`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[清理离线任务] 执行失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步任务状态摘要到客户端
|
||||
*/
|
||||
async syncTaskStatusSummary() {
|
||||
try {
|
||||
const { pla_account } = await Framework.getModels();
|
||||
|
||||
const accounts = await pla_account.findAll({
|
||||
where: {
|
||||
is_delete: 0,
|
||||
is_enabled: 1
|
||||
},
|
||||
attributes: ['sn_code']
|
||||
});
|
||||
|
||||
if (!accounts || accounts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offlineThreshold = 3 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
for (const account of accounts) {
|
||||
const sn_code = account.sn_code;
|
||||
const device = deviceManager.devices.get(sn_code);
|
||||
|
||||
if (!device) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastHeartbeat = device.lastHeartbeat || 0;
|
||||
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
|
||||
|
||||
if (!isOnline) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
|
||||
const summary = await this.taskQueue.getTaskStatusSummary(sn_code);
|
||||
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, summary, {
|
||||
currentCommand: summary.currentCommand || null
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[设备工作状态同步] 设备 ${sn_code} 同步失败:`, error.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[任务状态同步] 执行失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查任务超时并强制标记为失败
|
||||
*/
|
||||
async checkTaskTimeouts() {
|
||||
try {
|
||||
const Sequelize = require('sequelize');
|
||||
const { task_status, op } = db.models;
|
||||
|
||||
const runningTasks = await task_status.findAll({
|
||||
where: {
|
||||
status: 'running'
|
||||
},
|
||||
attributes: ['id', 'sn_code', 'taskType', 'taskName', 'startTime', 'create_time']
|
||||
});
|
||||
|
||||
if (!runningTasks || runningTasks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let timeoutCount = 0;
|
||||
|
||||
for (const task of runningTasks) {
|
||||
const taskData = task.toJSON();
|
||||
const startTime = taskData.startTime ? new Date(taskData.startTime) : (taskData.create_time ? new Date(taskData.create_time) : null);
|
||||
|
||||
if (!startTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const taskTimeout = config.getTaskTimeout(taskData.taskType) || 10 * 60 * 1000;
|
||||
const maxAllowedTime = taskTimeout * 1.2;
|
||||
const elapsedTime = now.getTime() - startTime.getTime();
|
||||
|
||||
if (elapsedTime > maxAllowedTime) {
|
||||
try {
|
||||
await task_status.update(
|
||||
{
|
||||
status: 'failed',
|
||||
endTime: now,
|
||||
duration: elapsedTime,
|
||||
result: JSON.stringify({
|
||||
error: `任务执行超时(运行时间: ${Math.round(elapsedTime / 1000)}秒,超时限制: ${Math.round(maxAllowedTime / 1000)}秒)`,
|
||||
timeout: true,
|
||||
taskType: taskData.taskType,
|
||||
startTime: startTime.toISOString()
|
||||
}),
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
where: { id: taskData.id }
|
||||
}
|
||||
);
|
||||
|
||||
timeoutCount++;
|
||||
console.warn(`[任务超时检查] 任务 ${taskData.id} (${taskData.taskName}) 运行时间过长,已强制标记为失败`);
|
||||
|
||||
if (this.taskQueue && typeof this.taskQueue.deviceStatus !== 'undefined') {
|
||||
const deviceStatus = this.taskQueue.deviceStatus.get(taskData.sn_code);
|
||||
if (deviceStatus && deviceStatus.currentTask && deviceStatus.currentTask.id === taskData.id) {
|
||||
deviceStatus.isRunning = false;
|
||||
deviceStatus.currentTask = null;
|
||||
deviceStatus.runningCount = Math.max(0, deviceStatus.runningCount - 1);
|
||||
this.taskQueue.globalRunningCount = Math.max(0, this.taskQueue.globalRunningCount - 1);
|
||||
|
||||
console.log(`[任务超时检查] 已重置设备 ${taskData.sn_code} 的状态`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.taskQueue.processQueue(taskData.sn_code).catch(error => {
|
||||
console.error(`[任务超时检查] 继续处理队列失败:`, error);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[任务超时检查] 更新超时任务状态失败:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (timeoutCount > 0) {
|
||||
console.log(`[任务超时检查] 共检测到 ${timeoutCount} 个超时任务`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[任务超时检查] 执行失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有定时任务
|
||||
*/
|
||||
stop() {
|
||||
console.log('[定时任务] 停止所有定时任务...');
|
||||
|
||||
for (const job of this.jobs) {
|
||||
if (job) {
|
||||
job.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
this.jobs = [];
|
||||
console.log('[定时任务] 所有定时任务已停止');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ScheduledJobs;
|
||||
@@ -19,6 +19,11 @@ class TaskHandlers {
|
||||
* @param {object} taskQueue - 任务队列实例
|
||||
*/
|
||||
register(taskQueue) {
|
||||
// 自动搜索任务
|
||||
taskQueue.registerHandler('auto_search', async (task) => {
|
||||
return await this.handleAutoSearchTask(task);
|
||||
});
|
||||
|
||||
// 自动投递任务
|
||||
taskQueue.registerHandler('auto_deliver', async (task) => {
|
||||
return await this.handleAutoDeliverTask(task);
|
||||
@@ -29,12 +34,12 @@ class TaskHandlers {
|
||||
return await this.handleSearchJobListTask(task);
|
||||
});
|
||||
|
||||
// 自动沟通任务(待实现)
|
||||
// 自动沟通任务
|
||||
taskQueue.registerHandler('auto_chat', async (task) => {
|
||||
return await this.handleAutoChatTask(task);
|
||||
});
|
||||
|
||||
// 自动活跃账号任务(待实现)
|
||||
// 自动活跃账号任务
|
||||
taskQueue.registerHandler('auto_active_account', async (task) => {
|
||||
return await this.handleAutoActiveAccountTask(task);
|
||||
});
|
||||
@@ -43,6 +48,64 @@ class TaskHandlers {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 处理自动搜索任务
|
||||
*/
|
||||
async handleAutoSearchTask(task) {
|
||||
const { sn_code, taskParams } = task;
|
||||
const { keyword, platform, pageCount } = taskParams;
|
||||
|
||||
console.log(`[任务处理器] 自动搜索任务 - 设备: ${sn_code}, 关键词: ${keyword}`);
|
||||
|
||||
// 检查授权状态
|
||||
const authorizationService = require('../../services/authorization_service');
|
||||
const authCheck = await authorizationService.checkAuthorization(sn_code, 'sn_code');
|
||||
if (!authCheck.is_authorized) {
|
||||
console.log(`[任务处理器] 自动搜索任务 - 设备: ${sn_code} 授权检查失败: ${authCheck.message}`);
|
||||
return {
|
||||
success: false,
|
||||
jobsFound: 0,
|
||||
message: authCheck.message
|
||||
};
|
||||
}
|
||||
|
||||
deviceManager.recordTaskStart(sn_code, task);
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 构建搜索指令
|
||||
const searchCommand = {
|
||||
command_type: 'getJobList',
|
||||
command_name: `自动搜索职位 - ${keyword}`,
|
||||
command_params: JSON.stringify({
|
||||
sn_code: sn_code,
|
||||
keyword: keyword || '',
|
||||
platform: platform || 'boss',
|
||||
pageCount: pageCount || 3
|
||||
}),
|
||||
priority: config.getTaskPriority('search_jobs') || 8
|
||||
};
|
||||
|
||||
// 执行搜索指令
|
||||
const result = await command.executeCommands(task.id, [searchCommand], this.mqttClient);
|
||||
const duration = Date.now() - startTime;
|
||||
deviceManager.recordTaskComplete(sn_code, task, true, duration);
|
||||
|
||||
console.log(`[任务处理器] 自动搜索任务完成 - 设备: ${sn_code}, 耗时: ${duration}ms`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
jobsFound: result.jobCount || 0,
|
||||
message: `搜索完成,找到 ${result.jobCount || 0} 个职位`
|
||||
};
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
deviceManager.recordTaskComplete(sn_code, task, false, duration);
|
||||
console.error(`[任务处理器] 自动搜索任务失败 - 设备: ${sn_code}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自动投递任务
|
||||
*/
|
||||
|
||||
184
api/middleware/schedule/tasks/autoActiveTask.js
Normal file
184
api/middleware/schedule/tasks/autoActiveTask.js
Normal file
@@ -0,0 +1,184 @@
|
||||
const BaseTask = require('./baseTask');
|
||||
const db = require('../../dbProxy');
|
||||
const config = require('../config');
|
||||
|
||||
/**
|
||||
* 自动活跃账号任务
|
||||
* 定期浏览职位、刷新简历、查看通知等,保持账号活跃度
|
||||
*/
|
||||
class AutoActiveTask extends BaseTask {
|
||||
constructor() {
|
||||
super('auto_active_account', {
|
||||
defaultInterval: 120, // 默认2小时
|
||||
defaultPriority: 5, // 较低优先级
|
||||
requiresLogin: true, // 需要登录
|
||||
conflictsWith: [ // 与这些任务冲突
|
||||
'auto_deliver', // 投递任务
|
||||
'auto_search' // 搜索任务
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证任务参数
|
||||
*/
|
||||
validateParams(params) {
|
||||
if (!params.platform) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: '缺少必要参数: platform'
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务名称
|
||||
*/
|
||||
getTaskName(params) {
|
||||
return `自动活跃账号 - ${params.platform || 'boss'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自动活跃任务
|
||||
*/
|
||||
async execute(sn_code, params) {
|
||||
console.log(`[自动活跃] 设备 ${sn_code} 开始执行活跃任务`);
|
||||
|
||||
const actions = [];
|
||||
|
||||
// 1. 浏览推荐职位
|
||||
actions.push({
|
||||
action: 'browse_jobs',
|
||||
count: Math.floor(Math.random() * 5) + 3 // 3-7个职位
|
||||
});
|
||||
|
||||
// 2. 刷新简历
|
||||
actions.push({
|
||||
action: 'refresh_resume',
|
||||
success: true
|
||||
});
|
||||
|
||||
// 3. 查看通知
|
||||
actions.push({
|
||||
action: 'check_notifications',
|
||||
count: Math.floor(Math.random() * 3)
|
||||
});
|
||||
|
||||
// 4. 浏览公司主页
|
||||
actions.push({
|
||||
action: 'browse_companies',
|
||||
count: Math.floor(Math.random() * 3) + 1
|
||||
});
|
||||
|
||||
console.log(`[自动活跃] 设备 ${sn_code} 完成 ${actions.length} 个活跃操作`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
actions: actions,
|
||||
message: `完成 ${actions.length} 个活跃操作`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加活跃任务到队列
|
||||
*/
|
||||
async addToQueue(sn_code, taskQueue, customParams = {}) {
|
||||
const now = new Date();
|
||||
console.log(`[自动活跃] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
|
||||
|
||||
try {
|
||||
// 1. 获取账号信息
|
||||
const { pla_account } = db.models;
|
||||
const account = await pla_account.findOne({
|
||||
where: {
|
||||
sn_code: sn_code,
|
||||
is_delete: 0,
|
||||
is_enabled: 1
|
||||
}
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
console.log(`[自动活跃] 账号 ${sn_code} 不存在或未启用`);
|
||||
return { success: false, reason: '账号不存在或未启用' };
|
||||
}
|
||||
|
||||
const accountData = account.toJSON();
|
||||
|
||||
// 2. 检查是否开启了自动活跃
|
||||
if (!accountData.auto_active) {
|
||||
console.log(`[自动活跃] 设备 ${sn_code} 未开启自动活跃`);
|
||||
return { success: false, reason: '未开启自动活跃' };
|
||||
}
|
||||
|
||||
// 3. 获取活跃策略配置
|
||||
let activeStrategy = {};
|
||||
if (accountData.active_strategy) {
|
||||
activeStrategy = typeof accountData.active_strategy === 'string'
|
||||
? JSON.parse(accountData.active_strategy)
|
||||
: accountData.active_strategy;
|
||||
}
|
||||
|
||||
// 4. 检查时间范围
|
||||
if (activeStrategy.time_range) {
|
||||
const timeCheck = this.checkTimeRange(activeStrategy.time_range);
|
||||
if (!timeCheck.allowed) {
|
||||
console.log(`[自动活跃] 设备 ${sn_code} ${timeCheck.reason}`);
|
||||
return { success: false, reason: timeCheck.reason };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 执行所有层级的冲突检查
|
||||
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
|
||||
if (!conflictCheck.allowed) {
|
||||
console.log(`[自动活跃] 设备 ${sn_code} 冲突检查未通过: ${conflictCheck.reason}`);
|
||||
return { success: false, reason: conflictCheck.reason };
|
||||
}
|
||||
|
||||
// 6. 检查活跃间隔
|
||||
const active_interval = activeStrategy.active_interval || this.config.defaultInterval;
|
||||
const intervalCheck = await this.checkExecutionInterval(sn_code, active_interval);
|
||||
|
||||
if (!intervalCheck.allowed) {
|
||||
console.log(`[自动活跃] 设备 ${sn_code} ${intervalCheck.reason}`);
|
||||
return { success: false, reason: intervalCheck.reason };
|
||||
}
|
||||
|
||||
// 7. 构建任务参数
|
||||
const taskParams = {
|
||||
platform: accountData.platform_type || 'boss',
|
||||
actions: activeStrategy.actions || ['browse_jobs', 'refresh_resume', 'check_notifications'],
|
||||
...customParams
|
||||
};
|
||||
|
||||
// 8. 验证参数
|
||||
const validation = this.validateParams(taskParams);
|
||||
if (!validation.valid) {
|
||||
return { success: false, reason: validation.reason };
|
||||
}
|
||||
|
||||
// 9. 添加任务到队列
|
||||
await taskQueue.addTask(sn_code, {
|
||||
taskType: this.taskType,
|
||||
taskName: this.getTaskName(taskParams),
|
||||
taskParams: taskParams,
|
||||
priority: this.config.defaultPriority
|
||||
});
|
||||
|
||||
console.log(`[自动活跃] 已为设备 ${sn_code} 添加活跃任务,间隔: ${active_interval} 分钟`);
|
||||
|
||||
// 10. 释放任务锁
|
||||
this.releaseTaskLock(sn_code);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[自动活跃] 添加任务失败:`, error);
|
||||
this.releaseTaskLock(sn_code);
|
||||
return { success: false, reason: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
module.exports = new AutoActiveTask();
|
||||
183
api/middleware/schedule/tasks/autoChatTask.js
Normal file
183
api/middleware/schedule/tasks/autoChatTask.js
Normal file
@@ -0,0 +1,183 @@
|
||||
const BaseTask = require('./baseTask');
|
||||
const db = require('../../dbProxy');
|
||||
const config = require('../config');
|
||||
|
||||
/**
|
||||
* 自动沟通任务
|
||||
* 自动回复HR消息,保持活跃度
|
||||
*/
|
||||
class AutoChatTask extends BaseTask {
|
||||
constructor() {
|
||||
super('auto_chat', {
|
||||
defaultInterval: 15, // 默认15分钟
|
||||
defaultPriority: 6, // 中等优先级
|
||||
requiresLogin: true, // 需要登录
|
||||
conflictsWith: [] // 不与其他任务冲突(可以在投递/搜索间隙执行)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证任务参数
|
||||
*/
|
||||
validateParams(params) {
|
||||
if (!params.platform) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: '缺少必要参数: platform'
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务名称
|
||||
*/
|
||||
getTaskName(params) {
|
||||
return `自动沟通 - ${params.name || '默认'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自动沟通任务
|
||||
*/
|
||||
async execute(sn_code, params) {
|
||||
console.log(`[自动沟通] 设备 ${sn_code} 开始执行沟通任务`);
|
||||
|
||||
// 1. 获取未读消息列表
|
||||
const unreadMessages = await this.getUnreadMessages(sn_code, params.platform);
|
||||
|
||||
if (!unreadMessages || unreadMessages.length === 0) {
|
||||
console.log(`[自动沟通] 设备 ${sn_code} 没有未读消息`);
|
||||
return {
|
||||
success: true,
|
||||
repliedCount: 0,
|
||||
message: '没有未读消息'
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[自动沟通] 设备 ${sn_code} 找到 ${unreadMessages.length} 条未读消息`);
|
||||
|
||||
// 2. 智能回复(这里需要调用实际的AI回复逻辑)
|
||||
const replyResult = {
|
||||
success: true,
|
||||
repliedCount: unreadMessages.length,
|
||||
messages: unreadMessages.map(m => ({
|
||||
id: m.id,
|
||||
from: m.hr_name,
|
||||
company: m.company_name
|
||||
}))
|
||||
};
|
||||
|
||||
return replyResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读消息
|
||||
*/
|
||||
async getUnreadMessages(sn_code, platform) {
|
||||
// TODO: 从数据库或缓存获取未读消息
|
||||
// 这里返回空数组作为示例
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加沟通任务到队列
|
||||
*/
|
||||
async addToQueue(sn_code, taskQueue, customParams = {}) {
|
||||
const now = new Date();
|
||||
console.log(`[自动沟通] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
|
||||
|
||||
try {
|
||||
// 1. 获取账号信息
|
||||
const { pla_account } = db.models;
|
||||
const account = await pla_account.findOne({
|
||||
where: {
|
||||
sn_code: sn_code,
|
||||
is_delete: 0,
|
||||
is_enabled: 1
|
||||
}
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
console.log(`[自动沟通] 账号 ${sn_code} 不存在或未启用`);
|
||||
return { success: false, reason: '账号不存在或未启用' };
|
||||
}
|
||||
|
||||
const accountData = account.toJSON();
|
||||
|
||||
// 2. 检查是否开启了自动沟通
|
||||
if (!accountData.auto_chat) {
|
||||
console.log(`[自动沟通] 设备 ${sn_code} 未开启自动沟通`);
|
||||
return { success: false, reason: '未开启自动沟通' };
|
||||
}
|
||||
|
||||
// 3. 获取沟通策略配置
|
||||
let chatStrategy = {};
|
||||
if (accountData.chat_strategy) {
|
||||
chatStrategy = typeof accountData.chat_strategy === 'string'
|
||||
? JSON.parse(accountData.chat_strategy)
|
||||
: accountData.chat_strategy;
|
||||
}
|
||||
|
||||
// 4. 检查时间范围
|
||||
if (chatStrategy.time_range) {
|
||||
const timeCheck = this.checkTimeRange(chatStrategy.time_range);
|
||||
if (!timeCheck.allowed) {
|
||||
console.log(`[自动沟通] 设备 ${sn_code} ${timeCheck.reason}`);
|
||||
return { success: false, reason: timeCheck.reason };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 执行所有层级的冲突检查
|
||||
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
|
||||
if (!conflictCheck.allowed) {
|
||||
console.log(`[自动沟通] 设备 ${sn_code} 冲突检查未通过: ${conflictCheck.reason}`);
|
||||
return { success: false, reason: conflictCheck.reason };
|
||||
}
|
||||
|
||||
// 6. 检查沟通间隔
|
||||
const chat_interval = chatStrategy.chat_interval || this.config.defaultInterval;
|
||||
const intervalCheck = await this.checkExecutionInterval(sn_code, chat_interval);
|
||||
|
||||
if (!intervalCheck.allowed) {
|
||||
console.log(`[自动沟通] 设备 ${sn_code} ${intervalCheck.reason}`);
|
||||
return { success: false, reason: intervalCheck.reason };
|
||||
}
|
||||
|
||||
// 7. 构建任务参数
|
||||
const taskParams = {
|
||||
platform: accountData.platform_type || 'boss',
|
||||
name: accountData.name || '默认',
|
||||
...customParams
|
||||
};
|
||||
|
||||
// 8. 验证参数
|
||||
const validation = this.validateParams(taskParams);
|
||||
if (!validation.valid) {
|
||||
return { success: false, reason: validation.reason };
|
||||
}
|
||||
|
||||
// 9. 添加任务到队列
|
||||
await taskQueue.addTask(sn_code, {
|
||||
taskType: this.taskType,
|
||||
taskName: this.getTaskName(taskParams),
|
||||
taskParams: taskParams,
|
||||
priority: this.config.defaultPriority
|
||||
});
|
||||
|
||||
console.log(`[自动沟通] 已为设备 ${sn_code} 添加沟通任务,间隔: ${chat_interval} 分钟`);
|
||||
|
||||
// 10. 释放任务锁
|
||||
this.releaseTaskLock(sn_code);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[自动沟通] 添加任务失败:`, error);
|
||||
this.releaseTaskLock(sn_code);
|
||||
return { success: false, reason: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
module.exports = new AutoChatTask();
|
||||
329
api/middleware/schedule/tasks/autoDeliverTask.js
Normal file
329
api/middleware/schedule/tasks/autoDeliverTask.js
Normal file
@@ -0,0 +1,329 @@
|
||||
const BaseTask = require('./baseTask');
|
||||
const db = require('../../dbProxy');
|
||||
const config = require('../config');
|
||||
const authorizationService = require('../../../services/authorization_service');
|
||||
|
||||
/**
|
||||
* 自动投递任务
|
||||
* 从数据库读取职位列表并进行自动投递
|
||||
*/
|
||||
class AutoDeliverTask extends BaseTask {
|
||||
constructor() {
|
||||
super('auto_deliver', {
|
||||
defaultInterval: 30, // 默认30分钟
|
||||
defaultPriority: 7, // 高优先级
|
||||
requiresLogin: true, // 需要登录
|
||||
conflictsWith: [ // 与这些任务冲突
|
||||
'auto_search', // 搜索任务
|
||||
'auto_active_account' // 活跃账号任务
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证任务参数
|
||||
*/
|
||||
validateParams(params) {
|
||||
// 投递任务需要的参数
|
||||
if (!params.keyword && !params.jobIds) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: '缺少必要参数: keyword 或 jobIds'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务名称
|
||||
*/
|
||||
getTaskName(params) {
|
||||
return `自动投递 - ${params.keyword || '指定职位'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自动投递任务
|
||||
*/
|
||||
async execute(sn_code, params) {
|
||||
console.log(`[自动投递] 设备 ${sn_code} 开始执行投递任务`);
|
||||
|
||||
// 1. 获取账号信息
|
||||
const account = await this.getAccountInfo(sn_code);
|
||||
if (!account) {
|
||||
throw new Error(`账号 ${sn_code} 不存在`);
|
||||
}
|
||||
|
||||
// 2. 检查授权
|
||||
const authorization = await authorizationService.checkAuthorization(sn_code);
|
||||
if (!authorization.is_authorized) {
|
||||
throw new Error('授权天数不足');
|
||||
}
|
||||
|
||||
// 3. 获取投递配置
|
||||
const deliverConfig = this.parseDeliverConfig(account.deliver_config);
|
||||
|
||||
// 4. 检查日投递限制
|
||||
const dailyLimit = config.platformDailyLimits[account.platform_type] || 50;
|
||||
const todayDelivered = await this.getTodayDeliveredCount(sn_code);
|
||||
|
||||
if (todayDelivered >= dailyLimit) {
|
||||
throw new Error(`今日投递已达上限 (${todayDelivered}/${dailyLimit})`);
|
||||
}
|
||||
|
||||
// 5. 获取可投递的职位列表
|
||||
const jobs = await this.getDeliverableJobs(sn_code, account, deliverConfig);
|
||||
|
||||
if (!jobs || jobs.length === 0) {
|
||||
console.log(`[自动投递] 设备 ${sn_code} 没有可投递的职位`);
|
||||
return {
|
||||
success: true,
|
||||
delivered: 0,
|
||||
message: '没有可投递的职位'
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[自动投递] 设备 ${sn_code} 找到 ${jobs.length} 个可投递职位`);
|
||||
|
||||
// 6. 执行投递(这里需要调用实际的投递逻辑)
|
||||
const deliverResult = {
|
||||
success: true,
|
||||
delivered: jobs.length,
|
||||
jobs: jobs.map(j => ({
|
||||
id: j.id,
|
||||
title: j.job_title,
|
||||
company: j.company_name
|
||||
}))
|
||||
};
|
||||
|
||||
return deliverResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号信息
|
||||
*/
|
||||
async getAccountInfo(sn_code) {
|
||||
const { pla_account } = db.models;
|
||||
const account = await pla_account.findOne({
|
||||
where: {
|
||||
sn_code: sn_code,
|
||||
is_delete: 0,
|
||||
is_enabled: 1
|
||||
}
|
||||
});
|
||||
|
||||
return account ? account.toJSON() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析投递配置
|
||||
*/
|
||||
parseDeliverConfig(deliver_config) {
|
||||
if (typeof deliver_config === 'string') {
|
||||
try {
|
||||
deliver_config = JSON.parse(deliver_config);
|
||||
} catch (e) {
|
||||
deliver_config = {};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deliver_interval: deliver_config?.deliver_interval || 30,
|
||||
min_salary: deliver_config?.min_salary || 0,
|
||||
max_salary: deliver_config?.max_salary || 0,
|
||||
page_count: deliver_config?.page_count || 3,
|
||||
max_deliver: deliver_config?.max_deliver || 10,
|
||||
filter_keywords: deliver_config?.filter_keywords || [],
|
||||
exclude_keywords: deliver_config?.exclude_keywords || [],
|
||||
time_range: deliver_config?.time_range || null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日已投递数量
|
||||
*/
|
||||
async getTodayDeliveredCount(sn_code) {
|
||||
const { task_status } = db.models;
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const count = await task_status.count({
|
||||
where: {
|
||||
sn_code: sn_code,
|
||||
taskType: 'auto_deliver',
|
||||
status: 'completed',
|
||||
endTime: {
|
||||
[Sequelize.Op.gte]: today
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可投递的职位列表
|
||||
*/
|
||||
async getDeliverableJobs(sn_code, account, deliverConfig) {
|
||||
const { job_postings } = db.models;
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
// 构建查询条件
|
||||
const where = {
|
||||
sn_code: sn_code,
|
||||
platform: account.platform_type,
|
||||
is_delivered: 0, // 未投递
|
||||
is_filtered: 0 // 未被过滤
|
||||
};
|
||||
|
||||
// 薪资范围过滤
|
||||
if (deliverConfig.min_salary > 0) {
|
||||
where.salary_min = {
|
||||
[Sequelize.Op.gte]: deliverConfig.min_salary
|
||||
};
|
||||
}
|
||||
|
||||
if (deliverConfig.max_salary > 0) {
|
||||
where.salary_max = {
|
||||
[Sequelize.Op.lte]: deliverConfig.max_salary
|
||||
};
|
||||
}
|
||||
|
||||
// 查询职位
|
||||
const jobs = await job_postings.findAll({
|
||||
where: where,
|
||||
limit: deliverConfig.max_deliver,
|
||||
order: [['create_time', 'DESC']]
|
||||
});
|
||||
|
||||
return jobs.map(j => j.toJSON());
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加投递任务到队列
|
||||
* 这是外部调用的入口,会进行所有冲突检查
|
||||
*/
|
||||
async addToQueue(sn_code, taskQueue, customParams = {}) {
|
||||
const now = new Date();
|
||||
console.log(`[自动投递] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
|
||||
|
||||
try {
|
||||
// 1. 获取账号信息
|
||||
const account = await this.getAccountInfo(sn_code);
|
||||
if (!account) {
|
||||
console.log(`[自动投递] 账号 ${sn_code} 不存在或未启用`);
|
||||
return { success: false, reason: '账号不存在或未启用' };
|
||||
}
|
||||
|
||||
// 2. 检查是否开启了自动投递
|
||||
if (!account.auto_deliver) {
|
||||
console.log(`[自动投递] 设备 ${sn_code} 未开启自动投递`);
|
||||
return { success: false, reason: '未开启自动投递' };
|
||||
}
|
||||
|
||||
// 3. 获取投递配置
|
||||
const deliverConfig = this.parseDeliverConfig(account.deliver_config);
|
||||
|
||||
// 4. 检查时间范围
|
||||
if (deliverConfig.time_range) {
|
||||
const timeCheck = this.checkTimeRange(deliverConfig.time_range);
|
||||
if (!timeCheck.allowed) {
|
||||
console.log(`[自动投递] 设备 ${sn_code} ${timeCheck.reason}`);
|
||||
return { success: false, reason: timeCheck.reason };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 执行所有层级的冲突检查
|
||||
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
|
||||
if (!conflictCheck.allowed) {
|
||||
console.log(`[自动投递] 设备 ${sn_code} 冲突检查未通过: ${conflictCheck.reason}`);
|
||||
return { success: false, reason: conflictCheck.reason };
|
||||
}
|
||||
|
||||
// 6. 检查投递间隔
|
||||
const intervalCheck = await this.checkExecutionInterval(
|
||||
sn_code,
|
||||
deliverConfig.deliver_interval
|
||||
);
|
||||
|
||||
if (!intervalCheck.allowed) {
|
||||
console.log(`[自动投递] 设备 ${sn_code} ${intervalCheck.reason}`);
|
||||
|
||||
// 推送等待状态到客户端
|
||||
await this.notifyWaitingStatus(sn_code, intervalCheck, taskQueue);
|
||||
|
||||
return { success: false, reason: intervalCheck.reason };
|
||||
}
|
||||
|
||||
// 7. 构建任务参数
|
||||
const taskParams = {
|
||||
keyword: account.keyword || '',
|
||||
platform: account.platform_type || 'boss',
|
||||
pageCount: deliverConfig.page_count,
|
||||
maxCount: deliverConfig.max_deliver,
|
||||
filterRules: {
|
||||
minSalary: deliverConfig.min_salary,
|
||||
maxSalary: deliverConfig.max_salary,
|
||||
keywords: deliverConfig.filter_keywords,
|
||||
excludeKeywords: deliverConfig.exclude_keywords
|
||||
},
|
||||
...customParams
|
||||
};
|
||||
|
||||
// 8. 验证参数
|
||||
const validation = this.validateParams(taskParams);
|
||||
if (!validation.valid) {
|
||||
return { success: false, reason: validation.reason };
|
||||
}
|
||||
|
||||
// 9. 添加任务到队列
|
||||
await taskQueue.addTask(sn_code, {
|
||||
taskType: this.taskType,
|
||||
taskName: this.getTaskName(taskParams),
|
||||
taskParams: taskParams,
|
||||
priority: this.config.defaultPriority
|
||||
});
|
||||
|
||||
console.log(`[自动投递] 已为设备 ${sn_code} 添加投递任务,间隔: ${deliverConfig.deliver_interval} 分钟`);
|
||||
|
||||
// 10. 释放任务锁
|
||||
this.releaseTaskLock(sn_code);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[自动投递] 添加任务失败:`, error);
|
||||
this.releaseTaskLock(sn_code);
|
||||
return { success: false, reason: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送等待状态到客户端
|
||||
*/
|
||||
async notifyWaitingStatus(sn_code, intervalCheck, taskQueue) {
|
||||
try {
|
||||
const deviceWorkStatusNotifier = require('../deviceWorkStatusNotifier');
|
||||
|
||||
// 获取当前任务状态摘要
|
||||
const taskStatusSummary = taskQueue.getTaskStatusSummary(sn_code);
|
||||
|
||||
// 添加等待消息到工作状态
|
||||
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, taskStatusSummary, {
|
||||
waitingMessage: {
|
||||
type: 'deliver_interval',
|
||||
message: intervalCheck.reason,
|
||||
remainingMinutes: intervalCheck.remainingMinutes,
|
||||
nextDeliverTime: intervalCheck.nextExecutionTime?.toISOString()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`[自动投递] 推送等待消息失败:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
module.exports = new AutoDeliverTask();
|
||||
235
api/middleware/schedule/tasks/autoSearchTask.js
Normal file
235
api/middleware/schedule/tasks/autoSearchTask.js
Normal file
@@ -0,0 +1,235 @@
|
||||
const BaseTask = require('./baseTask');
|
||||
const db = require('../../dbProxy');
|
||||
const config = require('../config');
|
||||
|
||||
/**
|
||||
* 自动搜索职位任务
|
||||
* 定期搜索符合条件的职位并保存到数据库
|
||||
*/
|
||||
class AutoSearchTask extends BaseTask {
|
||||
constructor() {
|
||||
super('auto_search', {
|
||||
defaultInterval: 60, // 默认60分钟
|
||||
defaultPriority: 8, // 高优先级(比投递高,先搜索后投递)
|
||||
requiresLogin: true, // 需要登录
|
||||
conflictsWith: [ // 与这些任务冲突
|
||||
'auto_deliver', // 投递任务
|
||||
'auto_active_account' // 活跃账号任务
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证任务参数
|
||||
*/
|
||||
validateParams(params) {
|
||||
if (!params.keyword && !params.jobType) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: '缺少必要参数: keyword 或 jobType'
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务名称
|
||||
*/
|
||||
getTaskName(params) {
|
||||
return `自动搜索 - ${params.keyword || params.jobType || '默认'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自动搜索任务
|
||||
*/
|
||||
async execute(sn_code, params) {
|
||||
console.log(`[自动搜索] 设备 ${sn_code} 开始执行搜索任务`);
|
||||
|
||||
// 1. 获取账号信息
|
||||
const account = await this.getAccountInfo(sn_code);
|
||||
if (!account) {
|
||||
throw new Error(`账号 ${sn_code} 不存在`);
|
||||
}
|
||||
|
||||
// 2. 获取搜索配置
|
||||
const searchConfig = this.parseSearchConfig(account.search_config);
|
||||
|
||||
// 3. 检查日搜索限制
|
||||
const dailyLimit = config.dailyLimits.maxSearch || 20;
|
||||
const todaySearched = await this.getTodaySearchCount(sn_code);
|
||||
|
||||
if (todaySearched >= dailyLimit) {
|
||||
throw new Error(`今日搜索已达上限 (${todaySearched}/${dailyLimit})`);
|
||||
}
|
||||
|
||||
// 4. 执行搜索(这里需要调用实际的搜索逻辑)
|
||||
const searchResult = {
|
||||
success: true,
|
||||
keyword: params.keyword || account.keyword,
|
||||
pageCount: searchConfig.page_count || 3,
|
||||
jobsFound: 0, // 实际搜索到的职位数
|
||||
jobsSaved: 0 // 保存到数据库的职位数
|
||||
};
|
||||
|
||||
console.log(`[自动搜索] 设备 ${sn_code} 搜索完成,找到 ${searchResult.jobsFound} 个职位`);
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号信息
|
||||
*/
|
||||
async getAccountInfo(sn_code) {
|
||||
const { pla_account } = db.models;
|
||||
const account = await pla_account.findOne({
|
||||
where: {
|
||||
sn_code: sn_code,
|
||||
is_delete: 0,
|
||||
is_enabled: 1
|
||||
}
|
||||
});
|
||||
|
||||
return account ? account.toJSON() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析搜索配置
|
||||
*/
|
||||
parseSearchConfig(search_config) {
|
||||
if (typeof search_config === 'string') {
|
||||
try {
|
||||
search_config = JSON.parse(search_config);
|
||||
} catch (e) {
|
||||
search_config = {};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
search_interval: search_config?.search_interval || 60,
|
||||
page_count: search_config?.page_count || 3,
|
||||
city: search_config?.city || '',
|
||||
salary_range: search_config?.salary_range || '',
|
||||
experience: search_config?.experience || '',
|
||||
education: search_config?.education || '',
|
||||
time_range: search_config?.time_range || null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日已搜索数量
|
||||
*/
|
||||
async getTodaySearchCount(sn_code) {
|
||||
const { task_status } = db.models;
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const count = await task_status.count({
|
||||
where: {
|
||||
sn_code: sn_code,
|
||||
taskType: 'auto_search',
|
||||
status: 'completed',
|
||||
endTime: {
|
||||
[Sequelize.Op.gte]: today
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加搜索任务到队列
|
||||
*/
|
||||
async addToQueue(sn_code, taskQueue, customParams = {}) {
|
||||
const now = new Date();
|
||||
console.log(`[自动搜索] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
|
||||
|
||||
try {
|
||||
// 1. 获取账号信息
|
||||
const account = await this.getAccountInfo(sn_code);
|
||||
if (!account) {
|
||||
console.log(`[自动搜索] 账号 ${sn_code} 不存在或未启用`);
|
||||
return { success: false, reason: '账号不存在或未启用' };
|
||||
}
|
||||
|
||||
// 2. 检查是否开启了自动搜索
|
||||
if (!account.auto_search) {
|
||||
console.log(`[自动搜索] 设备 ${sn_code} 未开启自动搜索`);
|
||||
return { success: false, reason: '未开启自动搜索' };
|
||||
}
|
||||
|
||||
// 3. 获取搜索配置
|
||||
const searchConfig = this.parseSearchConfig(account.search_config);
|
||||
|
||||
// 4. 检查时间范围
|
||||
if (searchConfig.time_range) {
|
||||
const timeCheck = this.checkTimeRange(searchConfig.time_range);
|
||||
if (!timeCheck.allowed) {
|
||||
console.log(`[自动搜索] 设备 ${sn_code} ${timeCheck.reason}`);
|
||||
return { success: false, reason: timeCheck.reason };
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 执行所有层级的冲突检查
|
||||
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
|
||||
if (!conflictCheck.allowed) {
|
||||
console.log(`[自动搜索] 设备 ${sn_code} 冲突检查未通过: ${conflictCheck.reason}`);
|
||||
return { success: false, reason: conflictCheck.reason };
|
||||
}
|
||||
|
||||
// 6. 检查搜索间隔
|
||||
const intervalCheck = await this.checkExecutionInterval(
|
||||
sn_code,
|
||||
searchConfig.search_interval
|
||||
);
|
||||
|
||||
if (!intervalCheck.allowed) {
|
||||
console.log(`[自动搜索] 设备 ${sn_code} ${intervalCheck.reason}`);
|
||||
return { success: false, reason: intervalCheck.reason };
|
||||
}
|
||||
|
||||
// 7. 构建任务参数
|
||||
const taskParams = {
|
||||
keyword: account.keyword || '',
|
||||
jobType: account.job_type || '',
|
||||
platform: account.platform_type || 'boss',
|
||||
pageCount: searchConfig.page_count || 3,
|
||||
city: searchConfig.city || '',
|
||||
salaryRange: searchConfig.salary_range || '',
|
||||
...customParams
|
||||
};
|
||||
|
||||
// 8. 验证参数
|
||||
const validation = this.validateParams(taskParams);
|
||||
if (!validation.valid) {
|
||||
return { success: false, reason: validation.reason };
|
||||
}
|
||||
|
||||
// 9. 添加任务到队列
|
||||
await taskQueue.addTask(sn_code, {
|
||||
taskType: this.taskType,
|
||||
taskName: this.getTaskName(taskParams),
|
||||
taskParams: taskParams,
|
||||
priority: this.config.defaultPriority
|
||||
});
|
||||
|
||||
console.log(`[自动搜索] 已为设备 ${sn_code} 添加搜索任务,间隔: ${searchConfig.search_interval} 分钟`);
|
||||
|
||||
// 10. 释放任务锁
|
||||
this.releaseTaskLock(sn_code);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[自动搜索] 添加任务失败:`, error);
|
||||
this.releaseTaskLock(sn_code);
|
||||
return { success: false, reason: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
module.exports = new AutoSearchTask();
|
||||
368
api/middleware/schedule/tasks/baseTask.js
Normal file
368
api/middleware/schedule/tasks/baseTask.js
Normal file
@@ -0,0 +1,368 @@
|
||||
const dayjs = require('dayjs');
|
||||
const deviceManager = require('../deviceManager');
|
||||
const db = require('../../dbProxy');
|
||||
|
||||
/**
|
||||
* 任务基类
|
||||
* 提供所有任务的通用功能和冲突检测机制
|
||||
*/
|
||||
class BaseTask {
|
||||
constructor(taskType, config = {}) {
|
||||
this.taskType = taskType;
|
||||
this.config = {
|
||||
// 默认配置
|
||||
defaultInterval: 30, // 默认间隔30分钟
|
||||
defaultPriority: 5,
|
||||
requiresLogin: true, // 是否需要登录状态
|
||||
conflictsWith: [], // 与哪些任务类型冲突
|
||||
...config
|
||||
};
|
||||
|
||||
// 任务执行锁 { sn_code: timestamp }
|
||||
this.taskLocks = new Map();
|
||||
|
||||
// 最后执行时间缓存 { sn_code: timestamp }
|
||||
this.lastExecutionCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Layer 1: 任务类型互斥锁检查
|
||||
* 防止同一设备同时添加相同类型的任务
|
||||
*/
|
||||
acquireTaskLock(sn_code) {
|
||||
const lockKey = `${sn_code}:${this.taskType}`;
|
||||
const now = Date.now();
|
||||
const existingLock = this.taskLocks.get(lockKey);
|
||||
|
||||
// 如果存在锁且未超时(5分钟),返回false
|
||||
if (existingLock && (now - existingLock) < 5 * 60 * 1000) {
|
||||
const remainingTime = Math.ceil((5 * 60 * 1000 - (now - existingLock)) / 1000);
|
||||
return {
|
||||
success: false,
|
||||
reason: `任务 ${this.taskType} 正在添加中,请等待 ${remainingTime} 秒`
|
||||
};
|
||||
}
|
||||
|
||||
// 获取锁
|
||||
this.taskLocks.set(lockKey, now);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放任务锁
|
||||
*/
|
||||
releaseTaskLock(sn_code) {
|
||||
const lockKey = `${sn_code}:${this.taskType}`;
|
||||
this.taskLocks.delete(lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layer 2: 设备状态检查
|
||||
* 检查设备是否在线、是否登录、是否忙碌
|
||||
*/
|
||||
async checkDeviceStatus(sn_code) {
|
||||
// 1. 检查设备是否在线
|
||||
const device = deviceManager.devices.get(sn_code);
|
||||
if (!device) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `设备 ${sn_code} 离线(从未发送心跳)`
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 检查心跳超时
|
||||
const offlineThreshold = 3 * 60 * 1000; // 3分钟
|
||||
const now = Date.now();
|
||||
const lastHeartbeat = device.lastHeartbeat || 0;
|
||||
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
|
||||
|
||||
if (!isOnline) {
|
||||
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `设备 ${sn_code} 离线(最后心跳: ${offlineMinutes}分钟前)`
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 检查登录状态(如果任务需要)
|
||||
if (this.config.requiresLogin && !device.isLoggedIn) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `设备 ${sn_code} 未登录平台账号`
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Layer 3: 检查任务队列状态
|
||||
* 防止队列中已有相同任务
|
||||
*/
|
||||
async checkTaskQueue(sn_code, taskQueue) {
|
||||
// 获取设备队列
|
||||
const deviceQueue = taskQueue.deviceQueues.get(sn_code);
|
||||
if (!deviceQueue) {
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
// 检查队列中是否有相同类型的待执行任务
|
||||
const tasks = deviceQueue.toArray();
|
||||
const hasSameTypeTask = tasks.some(task =>
|
||||
task.taskType === this.taskType &&
|
||||
task.status === 'pending'
|
||||
);
|
||||
|
||||
if (hasSameTypeTask) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `队列中已存在待执行的 ${this.taskType} 任务`
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Layer 4: 检查任务去重
|
||||
* 查询数据库中是否有重复的待执行任务
|
||||
*/
|
||||
async checkDuplicateTask(sn_code) {
|
||||
try {
|
||||
const { task_status } = db.models;
|
||||
|
||||
// 查询该设备是否有相同类型的pending/running任务
|
||||
const existingTask = await task_status.findOne({
|
||||
where: {
|
||||
sn_code: sn_code,
|
||||
taskType: this.taskType,
|
||||
status: ['pending', 'running']
|
||||
},
|
||||
attributes: ['id', 'status', 'taskName']
|
||||
});
|
||||
|
||||
if (existingTask) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `已存在 ${existingTask.status} 状态的任务: ${existingTask.taskName}`
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
} catch (error) {
|
||||
console.error(`[${this.taskType}] 检查重复任务失败:`, error);
|
||||
// 出错时允许继续,避免阻塞
|
||||
return { allowed: true };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Layer 5: 操作类型冲突检测
|
||||
* 某些操作类型不能同时执行
|
||||
*/
|
||||
async checkOperationConflict(sn_code, taskQueue) {
|
||||
// 如果没有配置冲突类型,直接通过
|
||||
if (!this.config.conflictsWith || this.config.conflictsWith.length === 0) {
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
// 检查当前是否有冲突的任务正在执行
|
||||
const deviceStatus = taskQueue.deviceStatus.get(sn_code);
|
||||
if (deviceStatus && deviceStatus.currentTask) {
|
||||
const currentTaskType = deviceStatus.currentTask.taskType;
|
||||
|
||||
if (this.config.conflictsWith.includes(currentTaskType)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `与正在执行的任务 ${currentTaskType} 冲突`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查执行间隔
|
||||
* 从数据库查询上次成功执行时间,判断是否满足间隔要求
|
||||
*/
|
||||
async checkExecutionInterval(sn_code, intervalMinutes) {
|
||||
try {
|
||||
const { task_status } = db.models;
|
||||
|
||||
// 先从缓存检查
|
||||
const cachedLastExecution = this.lastExecutionCache.get(sn_code);
|
||||
const now = Date.now();
|
||||
|
||||
if (cachedLastExecution) {
|
||||
const elapsedTime = now - cachedLastExecution;
|
||||
const interval_ms = intervalMinutes * 60 * 1000;
|
||||
|
||||
if (elapsedTime < interval_ms) {
|
||||
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
|
||||
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
|
||||
remainingMinutes,
|
||||
elapsedMinutes
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据库查询最近一次成功完成的任务
|
||||
const lastTask = await task_status.findOne({
|
||||
where: {
|
||||
sn_code: sn_code,
|
||||
taskType: this.taskType,
|
||||
status: 'completed'
|
||||
},
|
||||
order: [['endTime', 'DESC']],
|
||||
attributes: ['endTime']
|
||||
});
|
||||
|
||||
// 如果存在上次执行记录,检查是否已经过了间隔时间
|
||||
if (lastTask && lastTask.endTime) {
|
||||
const lastExecutionTime = new Date(lastTask.endTime).getTime();
|
||||
const elapsedTime = now - lastExecutionTime;
|
||||
const interval_ms = intervalMinutes * 60 * 1000;
|
||||
|
||||
// 更新缓存
|
||||
this.lastExecutionCache.set(sn_code, lastExecutionTime);
|
||||
|
||||
if (elapsedTime < interval_ms) {
|
||||
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
|
||||
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
|
||||
remainingMinutes,
|
||||
elapsedMinutes,
|
||||
nextExecutionTime: new Date(lastExecutionTime + interval_ms)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
} catch (error) {
|
||||
console.error(`[${this.taskType}] 检查执行间隔失败:`, error);
|
||||
// 出错时允许继续,避免阻塞
|
||||
return { allowed: true };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查时间范围限制
|
||||
* @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
|
||||
*/
|
||||
checkTimeRange(timeRange) {
|
||||
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
|
||||
return { allowed: true, reason: '未配置时间范围' };
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const currentMinute = now.getMinutes();
|
||||
const currentTime = currentHour * 60 + currentMinute;
|
||||
|
||||
// 解析开始时间和结束时间
|
||||
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
|
||||
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
|
||||
const startTime = startHour * 60 + startMinute;
|
||||
const endTime = endHour * 60 + endMinute;
|
||||
|
||||
// 检查是否仅工作日
|
||||
if (timeRange.workdays_only == 1) {
|
||||
const dayOfWeek = now.getDay();
|
||||
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
||||
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
|
||||
}
|
||||
}
|
||||
|
||||
// 检查当前时间是否在时间范围内
|
||||
if (startTime <= endTime) {
|
||||
// 正常情况: 09:00 - 18:00
|
||||
if (currentTime < startTime || currentTime >= endTime) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// 跨天情况: 22:00 - 06:00
|
||||
if (currentTime < startTime && currentTime >= endTime) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true, reason: '在允许的时间范围内' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合检查 - 执行所有层级的检查
|
||||
* @param {string} sn_code - 设备SN码
|
||||
* @param {Object} taskQueue - 任务队列实例
|
||||
* @param {Object} options - 额外选项
|
||||
* @returns {Object} { allowed: boolean, reason: string }
|
||||
*/
|
||||
async canExecuteTask(sn_code, taskQueue, options = {}) {
|
||||
const checks = [
|
||||
{ name: 'Layer1-任务锁', fn: () => this.acquireTaskLock(sn_code) },
|
||||
{ name: 'Layer2-设备状态', fn: () => this.checkDeviceStatus(sn_code) },
|
||||
{ name: 'Layer3-队列检查', fn: () => this.checkTaskQueue(sn_code, taskQueue) },
|
||||
{ name: 'Layer4-任务去重', fn: () => this.checkDuplicateTask(sn_code) },
|
||||
{ name: 'Layer5-操作冲突', fn: () => this.checkOperationConflict(sn_code, taskQueue) }
|
||||
];
|
||||
|
||||
// 逐层检查
|
||||
for (const check of checks) {
|
||||
const result = await check.fn();
|
||||
if (!result.allowed) {
|
||||
console.log(`[${this.taskType}] ${check.name} 未通过: ${result.reason}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理任务锁(定期清理过期锁)
|
||||
*/
|
||||
cleanupExpiredLocks() {
|
||||
const now = Date.now();
|
||||
const timeout = 5 * 60 * 1000; // 5分钟超时
|
||||
|
||||
for (const [lockKey, timestamp] of this.taskLocks.entries()) {
|
||||
if (now - timestamp > timeout) {
|
||||
this.taskLocks.delete(lockKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务名称(子类可覆盖)
|
||||
*/
|
||||
getTaskName(params) {
|
||||
return `${this.taskType} 任务`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证任务参数(子类必须实现)
|
||||
*/
|
||||
validateParams(params) {
|
||||
throw new Error('子类必须实现 validateParams 方法');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任务的具体逻辑(子类必须实现)
|
||||
*/
|
||||
async execute(sn_code, params) {
|
||||
throw new Error('子类必须实现 execute 方法');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseTask;
|
||||
16
api/middleware/schedule/tasks/index.js
Normal file
16
api/middleware/schedule/tasks/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 任务模块索引
|
||||
* 统一导出所有任务类型
|
||||
*/
|
||||
|
||||
const autoSearchTask = require('./autoSearchTask');
|
||||
const autoDeliverTask = require('./autoDeliverTask');
|
||||
const autoChatTask = require('./autoChatTask');
|
||||
const autoActiveTask = require('./autoActiveTask');
|
||||
|
||||
module.exports = {
|
||||
autoSearchTask,
|
||||
autoDeliverTask,
|
||||
autoChatTask,
|
||||
autoActiveTask
|
||||
};
|
||||
Reference in New Issue
Block a user