diff --git a/api/middleware/schedule/command.js b/api/middleware/schedule/command.js index 32c5c6a..596a360 100644 --- a/api/middleware/schedule/command.js +++ b/api/middleware/schedule/command.js @@ -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 中不存在对应方法`); } })(); diff --git a/api/middleware/schedule/config.js b/api/middleware/schedule/config.js index eef5a90..086ad8e 100644 --- a/api/middleware/schedule/config.js +++ b/api/middleware/schedule/config.js @@ -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小时执行一次 }; } diff --git a/api/middleware/schedule/scheduledJobs.js b/api/middleware/schedule/scheduledJobs.js index 08c2a79..de64fc9 100644 --- a/api/middleware/schedule/scheduledJobs.js +++ b/api/middleware/schedule/scheduledJobs.js @@ -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; diff --git a/api/middleware/schedule/scheduledJobs_new.js b/api/middleware/schedule/scheduledJobs_new.js new file mode 100644 index 0000000..51b38a9 --- /dev/null +++ b/api/middleware/schedule/scheduledJobs_new.js @@ -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; diff --git a/api/middleware/schedule/taskHandlers.js b/api/middleware/schedule/taskHandlers.js index 4b589c7..7ea8d50 100644 --- a/api/middleware/schedule/taskHandlers.js +++ b/api/middleware/schedule/taskHandlers.js @@ -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; + } + } + /** * 处理自动投递任务 */ diff --git a/api/middleware/schedule/tasks/autoActiveTask.js b/api/middleware/schedule/tasks/autoActiveTask.js new file mode 100644 index 0000000..3fee774 --- /dev/null +++ b/api/middleware/schedule/tasks/autoActiveTask.js @@ -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(); diff --git a/api/middleware/schedule/tasks/autoChatTask.js b/api/middleware/schedule/tasks/autoChatTask.js new file mode 100644 index 0000000..da15be4 --- /dev/null +++ b/api/middleware/schedule/tasks/autoChatTask.js @@ -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(); diff --git a/api/middleware/schedule/tasks/autoDeliverTask.js b/api/middleware/schedule/tasks/autoDeliverTask.js new file mode 100644 index 0000000..d5e8365 --- /dev/null +++ b/api/middleware/schedule/tasks/autoDeliverTask.js @@ -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(); diff --git a/api/middleware/schedule/tasks/autoSearchTask.js b/api/middleware/schedule/tasks/autoSearchTask.js new file mode 100644 index 0000000..fe878d1 --- /dev/null +++ b/api/middleware/schedule/tasks/autoSearchTask.js @@ -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(); diff --git a/api/middleware/schedule/tasks/baseTask.js b/api/middleware/schedule/tasks/baseTask.js new file mode 100644 index 0000000..43b7287 --- /dev/null +++ b/api/middleware/schedule/tasks/baseTask.js @@ -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; diff --git a/api/middleware/schedule/tasks/index.js b/api/middleware/schedule/tasks/index.js new file mode 100644 index 0000000..a826747 --- /dev/null +++ b/api/middleware/schedule/tasks/index.js @@ -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 +}; diff --git a/scripts/migrate_add_auto_search_active.sql b/scripts/migrate_add_auto_search_active.sql new file mode 100644 index 0000000..500c4f1 --- /dev/null +++ b/scripts/migrate_add_auto_search_active.sql @@ -0,0 +1,59 @@ +-- 任务调度系统重构 - 数据库迁移脚本 +-- 为 pla_account 表添加自动搜索和自动活跃相关字段 + +USE autoAiWorkSys; + +-- 1. 添加自动搜索开关字段 +ALTER TABLE pla_account +ADD COLUMN auto_search TINYINT(1) DEFAULT 0 COMMENT '是否开启自动搜索: 0-关闭, 1-开启'; + +-- 2. 添加搜索配置字段 +ALTER TABLE pla_account +ADD COLUMN search_config TEXT COMMENT '搜索配置(JSON): {search_interval, page_count, city, salary_range, time_range等}'; + +-- 3. 添加自动活跃开关字段 +ALTER TABLE pla_account +ADD COLUMN auto_active TINYINT(1) DEFAULT 0 COMMENT '是否开启自动活跃: 0-关闭, 1-开启'; + +-- 4. 添加活跃策略配置字段 +ALTER TABLE pla_account +ADD COLUMN active_strategy TEXT COMMENT '活跃策略配置(JSON): {active_interval, actions, time_range等}'; + +-- 5. 查看表结构验证 +DESC pla_account; + +-- 示例: 为已有账号设置默认配置 +-- UPDATE pla_account +-- SET +-- auto_search = 0, +-- search_config = JSON_OBJECT( +-- 'search_interval', 60, +-- 'page_count', 3, +-- 'city', '', +-- 'time_range', JSON_OBJECT( +-- 'start_time', '09:00', +-- 'end_time', '18:00', +-- 'workdays_only', 1 +-- ) +-- ), +-- auto_active = 0, +-- active_strategy = JSON_OBJECT( +-- 'active_interval', 120, +-- 'actions', JSON_ARRAY('browse_jobs', 'refresh_resume', 'check_notifications'), +-- 'time_range', JSON_OBJECT( +-- 'start_time', '08:00', +-- 'end_time', '23:00', +-- 'workdays_only', 0 +-- ) +-- ) +-- WHERE is_delete = 0; + +-- 注意: +-- 1. 执行前请先备份数据库 +-- 2. 建议在测试环境先测试 +-- 3. search_config 和 active_strategy 字段存储JSON格式的配置 +-- 4. 如果字段已存在会报错,可以先删除字段后再添加: +-- ALTER TABLE pla_account DROP COLUMN auto_search; +-- ALTER TABLE pla_account DROP COLUMN search_config; +-- ALTER TABLE pla_account DROP COLUMN auto_active; +-- ALTER TABLE pla_account DROP COLUMN active_strategy;