From 65833dd32da33ae70cc9b543abd95093996dabb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Tue, 30 Dec 2025 15:46:18 +0800 Subject: [PATCH] 11 --- .claude/settings.local.json | 5 +- api/middleware/schedule/{ => core}/command.js | 18 +- .../schedule/{ => core}/deviceManager.js | 6 +- api/middleware/schedule/core/index.js | 16 + .../schedule/{ => core}/scheduledJobs.js | 10 +- .../schedule/{ => core}/taskQueue.js | 38 +- .../schedule/handlers/activeHandler.js | 88 ++ .../schedule/handlers/baseHandler.js | 250 ++++ .../schedule/handlers/chatHandler.js | 87 ++ .../schedule/handlers/deliverHandler.js | 410 +++++++ api/middleware/schedule/handlers/index.js | 18 + .../schedule/handlers/searchHandler.js | 87 ++ api/middleware/schedule/index.js | 28 +- .../{ => infrastructure}/ErrorHandler.js | 0 .../{ => infrastructure}/PriorityQueue.js | 0 .../schedule/{ => infrastructure}/config.js | 0 .../schedule/infrastructure/index.js | 14 + .../deviceWorkStatusNotifier.js | 0 api/middleware/schedule/notifiers/index.js | 9 + .../schedule/services/accountValidator.js | 199 ++++ .../schedule/services/configManager.js | 225 ++++ .../schedule/services/jobFilterEngine.js | 395 +++++++ .../schedule/services/timeRangeValidator.js | 158 +++ api/middleware/schedule/taskHandlers.js | 1036 +---------------- api/middleware/schedule/tasks/baseTask.js | 2 +- api/middleware/schedule/utils/index.js | 14 + .../schedule/utils/keywordMatcher.js | 225 ++++ api/middleware/schedule/utils/salaryParser.js | 126 ++ .../{utils.js => utils/scheduleUtils.js} | 0 29 files changed, 2416 insertions(+), 1048 deletions(-) rename api/middleware/schedule/{ => core}/command.js (96%) rename api/middleware/schedule/{ => core}/deviceManager.js (98%) create mode 100644 api/middleware/schedule/core/index.js rename api/middleware/schedule/{ => core}/scheduledJobs.js (98%) rename api/middleware/schedule/{ => core}/taskQueue.js (97%) create mode 100644 api/middleware/schedule/handlers/activeHandler.js create mode 100644 api/middleware/schedule/handlers/baseHandler.js create mode 100644 api/middleware/schedule/handlers/chatHandler.js create mode 100644 api/middleware/schedule/handlers/deliverHandler.js create mode 100644 api/middleware/schedule/handlers/index.js create mode 100644 api/middleware/schedule/handlers/searchHandler.js rename api/middleware/schedule/{ => infrastructure}/ErrorHandler.js (100%) rename api/middleware/schedule/{ => infrastructure}/PriorityQueue.js (100%) rename api/middleware/schedule/{ => infrastructure}/config.js (100%) create mode 100644 api/middleware/schedule/infrastructure/index.js rename api/middleware/schedule/{ => notifiers}/deviceWorkStatusNotifier.js (100%) create mode 100644 api/middleware/schedule/notifiers/index.js create mode 100644 api/middleware/schedule/services/accountValidator.js create mode 100644 api/middleware/schedule/services/configManager.js create mode 100644 api/middleware/schedule/services/jobFilterEngine.js create mode 100644 api/middleware/schedule/services/timeRangeValidator.js create mode 100644 api/middleware/schedule/utils/index.js create mode 100644 api/middleware/schedule/utils/keywordMatcher.js create mode 100644 api/middleware/schedule/utils/salaryParser.js rename api/middleware/schedule/{utils.js => utils/scheduleUtils.js} (100%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0e00a2d..a6c4401 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,10 @@ "Bash(cat:*)", "Bash(npm run restart:*)", "Bash(del scheduledJobs.js)", - "Bash(ls:*)" + "Bash(ls:*)", + "Bash(wc:*)", + "Bash(for:*)", + "Bash(done)" ], "deny": [], "ask": [] diff --git a/api/middleware/schedule/command.js b/api/middleware/schedule/core/command.js similarity index 96% rename from api/middleware/schedule/command.js rename to api/middleware/schedule/core/command.js index 596a360..8f79d35 100644 --- a/api/middleware/schedule/command.js +++ b/api/middleware/schedule/core/command.js @@ -1,9 +1,9 @@ -const logs = require('../logProxy'); -const db = require('../dbProxy'); -const jobManager = require('../job/index'); -const ScheduleUtils = require('./utils'); -const ScheduleConfig = require('./config'); -const authorizationService = require('../../services/authorization_service'); +const logs = require('../../logProxy'); +const db = require('../../dbProxy'); +const jobManager = require('../../job/index'); +const ScheduleUtils = require('../utils/scheduleUtils'); +const ScheduleConfig = require('../infrastructure/config'); +const authorizationService = require('../../../services/authorization_service'); /** @@ -129,7 +129,7 @@ class CommandManager { // 4.5 推送指令开始执行状态 try { - const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); + const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier'); const taskQueue = require('./taskQueue'); const summary = await taskQueue.getTaskStatusSummary(task.sn_code); await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary, { @@ -163,7 +163,7 @@ class CommandManager { // 6.5 推送指令完成状态 try { - const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); + const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier'); const taskQueue = require('./taskQueue'); const summary = await taskQueue.getTaskStatusSummary(task.sn_code); await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary); @@ -193,7 +193,7 @@ class CommandManager { // 推送指令失败状态 try { - const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); + const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier'); const taskQueue = require('./taskQueue'); const summary = await taskQueue.getTaskStatusSummary(task.sn_code); await deviceWorkStatusNotifier.sendDeviceWorkStatus(task.sn_code, summary); diff --git a/api/middleware/schedule/deviceManager.js b/api/middleware/schedule/core/deviceManager.js similarity index 98% rename from api/middleware/schedule/deviceManager.js rename to api/middleware/schedule/core/deviceManager.js index bdc93b7..26dff8e 100644 --- a/api/middleware/schedule/deviceManager.js +++ b/api/middleware/schedule/core/deviceManager.js @@ -1,8 +1,8 @@ const dayjs = require('dayjs'); const Sequelize = require('sequelize'); -const db = require('../dbProxy'); -const config = require('./config'); -const utils = require('./utils'); +const db = require('../../dbProxy'); +const config = require('../infrastructure/config'); +const utils = require('../utils/scheduleUtils'); /** * 设备管理器(简化版) diff --git a/api/middleware/schedule/core/index.js b/api/middleware/schedule/core/index.js new file mode 100644 index 0000000..224cd90 --- /dev/null +++ b/api/middleware/schedule/core/index.js @@ -0,0 +1,16 @@ +/** + * Core 模块导出 + * 统一导出核心模块,简化引用路径 + */ + +const deviceManager = require('./deviceManager'); +const taskQueue = require('./taskQueue'); +const command = require('./command'); +const scheduledJobs = require('./scheduledJobs'); + +module.exports = { + deviceManager, + taskQueue, + command, + scheduledJobs +}; diff --git a/api/middleware/schedule/scheduledJobs.js b/api/middleware/schedule/core/scheduledJobs.js similarity index 98% rename from api/middleware/schedule/scheduledJobs.js rename to api/middleware/schedule/core/scheduledJobs.js index 51b38a9..cace935 100644 --- a/api/middleware/schedule/scheduledJobs.js +++ b/api/middleware/schedule/core/scheduledJobs.js @@ -1,15 +1,15 @@ const node_schedule = require("node-schedule"); const dayjs = require('dayjs'); -const config = require('./config.js'); +const config = require('../infrastructure/config.js'); const deviceManager = require('./deviceManager.js'); const command = require('./command.js'); -const db = require('../dbProxy'); +const db = require('../../dbProxy'); // 引入新的任务模块 -const tasks = require('./tasks'); +const tasks = require('../tasks'); const { autoSearchTask, autoDeliverTask, autoChatTask, autoActiveTask } = tasks; -const Framework = require("../../../framework/node-core-framework.js"); +const Framework = require("../../../../framework/node-core-framework.js"); /** * 定时任务管理器(重构版) @@ -448,7 +448,7 @@ class ScheduledJobs { } try { - const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); + const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier'); const summary = await this.taskQueue.getTaskStatusSummary(sn_code); await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, summary, { currentCommand: summary.currentCommand || null diff --git a/api/middleware/schedule/taskQueue.js b/api/middleware/schedule/core/taskQueue.js similarity index 97% rename from api/middleware/schedule/taskQueue.js rename to api/middleware/schedule/core/taskQueue.js index 85f03b1..d1b80f9 100644 --- a/api/middleware/schedule/taskQueue.js +++ b/api/middleware/schedule/core/taskQueue.js @@ -1,14 +1,14 @@ const { v4: uuidv4 } = require('uuid'); const Sequelize = require('sequelize'); -const logs = require('../logProxy'); -const db = require('../dbProxy'); +const logs = require('../../logProxy'); +const db = require('../../dbProxy'); const command = require('./command'); -const PriorityQueue = require('./PriorityQueue'); -const ErrorHandler = require('./ErrorHandler'); +const PriorityQueue = require('../infrastructure/PriorityQueue'); +const ErrorHandler = require('../infrastructure/ErrorHandler'); const deviceManager = require('./deviceManager'); -const ScheduleUtils = require('./utils'); -const ScheduleConfig = require('./config'); -const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); +const ScheduleUtils = require('../utils/scheduleUtils'); +const ScheduleConfig = require('../infrastructure/config'); +const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier'); /** * 任务队列管理器(重构版) @@ -222,7 +222,6 @@ class TaskQueue { // 移除 device_status 依赖,不再检查设备在线状态 // 如果需要在线状态检查,可以从 deviceManager 获取 - const deviceManager = require('./deviceManager'); const deviceStatus = deviceManager.getAllDevicesStatus(); const onlineSnCodes = new Set( Object.entries(deviceStatus) @@ -230,24 +229,7 @@ class TaskQueue { .map(([sn_code]) => sn_code) ); - // 原有代码已移除,改为使用 deviceManager - /* 原有代码已注释 - const device_status = db.getModel('device_status'); - const heartbeatTimeout = require('./config.js').monitoring.heartbeatTimeout; - const now = new Date(); - const heartbeatThreshold = new Date(now.getTime() - heartbeatTimeout); - - const onlineDevices = await device_status.findAll({ - where: { - isOnline: true, - lastHeartbeatTime: { - [Sequelize.Op.gte]: heartbeatThreshold // 心跳时间在阈值内 - } - }, - attributes: ['sn_code'] - }); - const onlineSnCodes = new Set(onlineDevices.map(dev => dev.sn_code)); - */ + let processedCount = 0; let queuedCount = 0; @@ -1065,13 +1047,13 @@ class TaskQueue { async getMqttClient() { try { // 首先尝试从调度系统获取已初始化的MQTT客户端 - const scheduleManager = require('./index'); + const scheduleManager = require('../index'); if (scheduleManager.mqttClient) { return scheduleManager.mqttClient; } // 如果调度系统没有初始化,则直接创建 - const mqttManager = require('../mqtt/mqttManager'); + const mqttManager = require('../../mqtt/mqttManager'); console.log('[任务队列] 创建新的MQTT客户端'); return await mqttManager.getInstance(); } catch (error) { diff --git a/api/middleware/schedule/handlers/activeHandler.js b/api/middleware/schedule/handlers/activeHandler.js new file mode 100644 index 0000000..83d4118 --- /dev/null +++ b/api/middleware/schedule/handlers/activeHandler.js @@ -0,0 +1,88 @@ +const BaseHandler = require('./baseHandler'); +const ConfigManager = require('../services/configManager'); +const command = require('../command'); +const config = require('../config'); + +/** + * 自动活跃处理器 + * 负责保持账户活跃度 + */ +class ActiveHandler extends BaseHandler { + /** + * 处理自动活跃任务 + * @param {object} task - 任务对象 + * @returns {Promise} 执行结果 + */ + async handle(task) { + return await this.execute(task, async () => { + return await this.doActive(task); + }, { + checkAuth: true, + checkOnline: true, + recordDeviceMetrics: true + }); + } + + /** + * 执行活跃逻辑 + */ + async doActive(task) { + const { sn_code, taskParams } = task; + const { platform = 'boss' } = taskParams; + + console.log(`[自动活跃] 开始 - 设备: ${sn_code}`); + + // 1. 获取账户配置 + const accountConfig = await this.getAccountConfig(sn_code, ['platform_type', 'active_strategy']); + + if (!accountConfig) { + return { + activeCount: 0, + message: '未找到账户配置' + }; + } + + // 2. 解析活跃策略配置 + const activeStrategy = ConfigManager.parseActiveStrategy(accountConfig.active_strategy); + + // 3. 检查活跃时间范围 + const timeRange = ConfigManager.getTimeRange(activeStrategy); + if (timeRange) { + const timeRangeValidator = require('../services/timeRangeValidator'); + const timeCheck = timeRangeValidator.checkTimeRange(timeRange); + + if (!timeCheck.allowed) { + return { + activeCount: 0, + message: timeCheck.reason + }; + } + } + + // 4. 创建活跃指令 + const actions = activeStrategy.actions || ['view_jobs']; + const activeCommands = actions.map(action => ({ + command_type: `active_${action}`, + command_name: `自动活跃 - ${action}`, + command_params: JSON.stringify({ + sn_code, + platform: platform || accountConfig.platform_type || 'boss', + action + }), + priority: config.getTaskPriority('auto_active') || 5 + })); + + // 5. 执行活跃指令 + const result = await command.executeCommands(task.id, activeCommands, this.mqttClient); + + console.log(`[自动活跃] 完成 - 设备: ${sn_code}, 执行动作: ${actions.join(', ')}`); + + return { + activeCount: actions.length, + actions, + message: '活跃完成' + }; + } +} + +module.exports = ActiveHandler; diff --git a/api/middleware/schedule/handlers/baseHandler.js b/api/middleware/schedule/handlers/baseHandler.js new file mode 100644 index 0000000..e159654 --- /dev/null +++ b/api/middleware/schedule/handlers/baseHandler.js @@ -0,0 +1,250 @@ +const deviceManager = require('../core/deviceManager'); +const accountValidator = require('../services/accountValidator'); +const db = require('../../dbProxy'); + +/** + * 任务处理器基类 + * 提供通用的授权检查、计时、错误处理、设备记录等功能 + */ +class BaseHandler { + constructor(mqttClient) { + this.mqttClient = mqttClient; + } + + /** + * 执行任务(带授权检查和错误处理) + * @param {object} task - 任务对象 + * @param {Function} businessLogic - 业务逻辑函数 + * @param {object} options - 选项 + * @returns {Promise} 执行结果 + */ + async execute(task, businessLogic, options = {}) { + const { + checkAuth = true, // 是否检查授权 + checkOnline = true, // 是否检查在线状态 + recordDeviceMetrics = true // 是否记录设备指标 + } = options; + + const { sn_code, taskName } = task; + const startTime = Date.now(); + + try { + // 1. 验证账户(启用 + 授权 + 在线) + if (checkAuth || checkOnline) { + const validation = await accountValidator.validate(sn_code, { + checkEnabled: true, + checkAuth, + checkOnline, + offlineThreshold: 3 * 60 * 1000 // 3分钟 + }); + + if (!validation.valid) { + throw new Error(`设备 ${sn_code} 验证失败: ${validation.reason}`); + } + } + + // 2. 记录任务开始 + if (recordDeviceMetrics) { + deviceManager.recordTaskStart(sn_code, task); + } + + // 3. 执行业务逻辑 + const result = await businessLogic(); + + // 4. 记录任务成功 + const duration = Date.now() - startTime; + if (recordDeviceMetrics) { + deviceManager.recordTaskComplete(sn_code, task, true, duration); + } + + return { + success: true, + duration, + ...result + }; + + } catch (error) { + // 5. 记录任务失败 + const duration = Date.now() - startTime; + if (recordDeviceMetrics) { + deviceManager.recordTaskComplete(sn_code, task, false, duration); + } + + console.error(`[${taskName}] 执行失败 (设备: ${sn_code}):`, error.message); + + return { + success: false, + error: error.message, + duration + }; + } + } + + /** + * 检查每日操作限制 + * @param {string} sn_code - 设备序列号 + * @param {string} operation - 操作类型 (search, deliver, chat) + * @param {string} platform - 平台类型 + * @returns {Promise<{allowed: boolean, count?: number, limit?: number, reason?: string}>} + */ + async checkDailyLimit(sn_code, operation, platform = 'boss') { + try { + const today = new Date().toISOString().split('T')[0]; + const task_status = db.getModel('task_status'); + + // 查询今日该操作的完成次数 + const count = await task_status.count({ + where: { + sn_code, + taskType: `auto_${operation}`, + status: 'completed', + endTime: { + [db.models.op.gte]: new Date(today) + } + } + }); + + // 获取每日限制(从 deviceManager 或配置) + const limit = deviceManager.canExecuteOperation(sn_code, operation); + + if (!limit.allowed) { + return { + allowed: false, + count, + reason: limit.reason + }; + } + + return { + allowed: true, + count, + limit: limit.max || 999 + }; + + } catch (error) { + console.error(`[每日限制检查] 失败 (${sn_code}, ${operation}):`, error); + return { allowed: true }; // 检查失败时默认允许 + } + } + + /** + * 检查执行间隔时间 + * @param {string} sn_code - 设备序列号 + * @param {string} taskType - 任务类型 + * @param {number} intervalMinutes - 间隔时间(分钟) + * @returns {Promise<{allowed: boolean, elapsed?: number, remaining?: number, reason?: string}>} + */ + async checkInterval(sn_code, taskType, intervalMinutes) { + try { + const task_status = db.getModel('task_status'); + + // 查询最近一次成功完成的任务 + const lastTask = await task_status.findOne({ + where: { + sn_code, + taskType, + status: 'completed' + }, + order: [['endTime', 'DESC']], + attributes: ['endTime'] + }); + + if (!lastTask || !lastTask.endTime) { + return { allowed: true, elapsed: null }; + } + + const now = Date.now(); + const lastTime = new Date(lastTask.endTime).getTime(); + const elapsed = now - lastTime; + const intervalMs = intervalMinutes * 60 * 1000; + + if (elapsed < intervalMs) { + const remainingMinutes = Math.ceil((intervalMs - elapsed) / (60 * 1000)); + const elapsedMinutes = Math.floor(elapsed / (60 * 1000)); + + return { + allowed: false, + elapsed: elapsedMinutes, + remaining: remainingMinutes, + reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟` + }; + } + + return { + allowed: true, + elapsed: Math.floor(elapsed / (60 * 1000)) + }; + + } catch (error) { + console.error(`[间隔检查] 失败 (${sn_code}, ${taskType}):`, error); + return { allowed: true }; // 检查失败时默认允许 + } + } + + /** + * 获取账户配置 + * @param {string} sn_code - 设备序列号 + * @param {string[]} fields - 需要的字段 + * @returns {Promise} + */ + async getAccountConfig(sn_code, fields = ['*']) { + try { + const pla_account = db.getModel('pla_account'); + const account = await pla_account.findOne({ + where: { sn_code, is_delete: 0 }, + attributes: fields + }); + + return account ? account.toJSON() : null; + } catch (error) { + console.error(`[获取账户配置] 失败 (${sn_code}):`, error); + return null; + } + } + + /** + * 推送设备工作状态(可选的通知) + * @param {string} sn_code - 设备序列号 + * @param {object} status - 状态信息 + */ + async notifyDeviceStatus(sn_code, status) { + try { + const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier'); + await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, status); + } catch (error) { + console.warn(`[状态推送] 失败 (${sn_code}):`, error.message); + } + } + + /** + * 标准化错误响应 + * @param {Error} error - 错误对象 + * @param {string} sn_code - 设备序列号 + * @returns {object} 标准化的错误响应 + */ + formatError(error, sn_code) { + return { + success: false, + error: error.message || '未知错误', + sn_code, + timestamp: new Date().toISOString() + }; + } + + /** + * 标准化成功响应 + * @param {object} data - 响应数据 + * @param {string} sn_code - 设备序列号 + * @returns {object} 标准化的成功响应 + */ + formatSuccess(data, sn_code) { + return { + success: true, + sn_code, + timestamp: new Date().toISOString(), + ...data + }; + } +} + +module.exports = BaseHandler; diff --git a/api/middleware/schedule/handlers/chatHandler.js b/api/middleware/schedule/handlers/chatHandler.js new file mode 100644 index 0000000..73b8866 --- /dev/null +++ b/api/middleware/schedule/handlers/chatHandler.js @@ -0,0 +1,87 @@ +const BaseHandler = require('./baseHandler'); +const ConfigManager = require('../services/configManager'); +const command = require('../command'); +const config = require('../config'); + +/** + * 自动沟通处理器 + * 负责自动回复HR消息 + */ +class ChatHandler extends BaseHandler { + /** + * 处理自动沟通任务 + * @param {object} task - 任务对象 + * @returns {Promise} 执行结果 + */ + async handle(task) { + return await this.execute(task, async () => { + return await this.doChat(task); + }, { + checkAuth: true, + checkOnline: true, + recordDeviceMetrics: true + }); + } + + /** + * 执行沟通逻辑 + */ + async doChat(task) { + const { sn_code, taskParams } = task; + const { platform = 'boss' } = taskParams; + + console.log(`[自动沟通] 开始 - 设备: ${sn_code}`); + + // 1. 获取账户配置 + const accountConfig = await this.getAccountConfig(sn_code, ['platform_type', 'chat_strategy']); + + if (!accountConfig) { + return { + chatCount: 0, + message: '未找到账户配置' + }; + } + + // 2. 解析沟通策略配置 + const chatStrategy = ConfigManager.parseChatStrategy(accountConfig.chat_strategy); + + // 3. 检查沟通时间范围 + const timeRange = ConfigManager.getTimeRange(chatStrategy); + if (timeRange) { + const timeRangeValidator = require('../services/timeRangeValidator'); + const timeCheck = timeRangeValidator.checkTimeRange(timeRange); + + if (!timeCheck.allowed) { + return { + chatCount: 0, + message: timeCheck.reason + }; + } + } + + // 4. 创建沟通指令 + const chatCommand = { + command_type: 'autoChat', + command_name: '自动沟通', + command_params: JSON.stringify({ + sn_code, + platform: platform || accountConfig.platform_type || 'boss', + autoReply: chatStrategy.auto_reply || false, + replyTemplate: chatStrategy.reply_template || '' + }), + priority: config.getTaskPriority('auto_chat') || 6 + }; + + // 5. 执行沟通指令 + const result = await command.executeCommands(task.id, [chatCommand], this.mqttClient); + + console.log(`[自动沟通] 完成 - 设备: ${sn_code}`); + + return { + chatCount: result.chatCount || 0, + message: '沟通完成' + }; + } +} + +module.exports = ChatHandler; diff --git a/api/middleware/schedule/handlers/deliverHandler.js b/api/middleware/schedule/handlers/deliverHandler.js new file mode 100644 index 0000000..4c82abe --- /dev/null +++ b/api/middleware/schedule/handlers/deliverHandler.js @@ -0,0 +1,410 @@ +const BaseHandler = require('./baseHandler'); +const ConfigManager = require('../services/configManager'); +const jobFilterEngine = require('../services/jobFilterEngine'); +const command = require('../command'); +const config = require('../config'); +const db = require('../../dbProxy'); +const jobFilterService = require('../../../services/job_filter_service'); + +/** + * 自动投递处理器 + * 负责职位搜索、过滤、评分和自动投递 + */ +class DeliverHandler extends BaseHandler { + /** + * 处理自动投递任务 + * @param {object} task - 任务对象 + * @returns {Promise} 执行结果 + */ + async handle(task) { + return await this.execute(task, async () => { + return await this.doDeliver(task); + }, { + checkAuth: true, + checkOnline: true, + recordDeviceMetrics: true + }); + } + + /** + * 执行投递逻辑 + */ + async doDeliver(task) { + const { sn_code, taskParams } = task; + const { keyword, platform = 'boss', pageCount = 3, maxCount = 10, filterRules = {} } = taskParams; + + console.log(`[自动投递] 开始 - 设备: ${sn_code}, 关键词: ${keyword}`); + + // 1. 检查每日投递限制 + const dailyCheck = await this.checkDailyDeliverLimit(sn_code, platform); + if (!dailyCheck.allowed) { + return { + deliveredCount: 0, + message: dailyCheck.message + }; + } + + const actualMaxCount = dailyCheck.actualMaxCount; + + // 2. 检查并获取简历 + const resume = await this.getOrRefreshResume(sn_code, platform, task.id); + if (!resume) { + return { + deliveredCount: 0, + message: '未找到简历信息' + }; + } + + // 3. 获取账户配置 + const accountConfig = await this.getAccountConfig(sn_code, [ + 'keyword', 'platform_type', 'deliver_config', 'job_type_id', 'is_salary_priority' + ]); + + if (!accountConfig) { + return { + deliveredCount: 0, + message: '未找到账户配置' + }; + } + + // 4. 检查投递时间范围 + const deliverConfig = ConfigManager.parseDeliverConfig(accountConfig.deliver_config); + const timeRange = ConfigManager.getTimeRange(deliverConfig); + + if (timeRange) { + const timeRangeValidator = require('../services/timeRangeValidator'); + const timeCheck = timeRangeValidator.checkTimeRange(timeRange); + if (!timeCheck.allowed) { + return { + deliveredCount: 0, + message: timeCheck.reason + }; + } + } + + // 5. 获取职位类型配置 + const jobTypeConfig = await this.getJobTypeConfig(accountConfig.job_type_id); + + // 6. 搜索职位列表 + await this.searchJobs(sn_code, platform, keyword || accountConfig.keyword, pageCount, task.id); + + // 7. 从数据库获取待投递职位 + const pendingJobs = await this.getPendingJobs(sn_code, platform, actualMaxCount * 3); + + if (!pendingJobs || pendingJobs.length === 0) { + return { + deliveredCount: 0, + message: '没有待投递的职位' + }; + } + + // 8. 合并过滤配置 + const filterConfig = this.mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig); + + // 9. 过滤已投递的公司 + const recentCompanies = await this.getRecentDeliveredCompanies(sn_code, 30); + + // 10. 过滤、评分、排序职位 + const filteredJobs = await this.filterAndScoreJobs( + pendingJobs, + resume, + accountConfig, + jobTypeConfig, + filterConfig, + recentCompanies + ); + + const jobsToDeliver = filteredJobs.slice(0, actualMaxCount); + + console.log(`[自动投递] 职位筛选完成 - 原始: ${pendingJobs.length}, 符合条件: ${filteredJobs.length}, 将投递: ${jobsToDeliver.length}`); + + if (jobsToDeliver.length === 0) { + return { + deliveredCount: 0, + message: '没有符合条件的职位' + }; + } + + // 11. 创建投递指令并执行 + const deliverCommands = this.createDeliverCommands(jobsToDeliver, sn_code, platform); + const result = await command.executeCommands(task.id, deliverCommands, this.mqttClient); + + console.log(`[自动投递] 完成 - 设备: ${sn_code}, 投递: ${deliverCommands.length} 个职位`); + + return { + deliveredCount: deliverCommands.length, + ...result + }; + } + + /** + * 检查每日投递限制 + */ + async checkDailyDeliverLimit(sn_code, platform) { + const apply_records = db.getModel('apply_records'); + const dailyLimit = config.getDailyLimit('apply', platform); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const todayApplyCount = await apply_records.count({ + where: { + sn_code, + platform, + applyTime: { + [db.models.op.gte]: today + } + } + }); + + console.log(`[自动投递] 今日已投递: ${todayApplyCount}/${dailyLimit}`); + + if (todayApplyCount >= dailyLimit) { + return { + allowed: false, + message: `已达到每日投递上限(${dailyLimit}次)` + }; + } + + const remainingQuota = dailyLimit - todayApplyCount; + + return { + allowed: true, + actualMaxCount: remainingQuota, + todayCount: todayApplyCount, + limit: dailyLimit + }; + } + + /** + * 获取或刷新简历 + */ + async getOrRefreshResume(sn_code, platform, taskId) { + const resume_info = db.getModel('resume_info'); + const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000); + + let resume = await resume_info.findOne({ + where: { + sn_code, + platform, + isActive: true + }, + order: [['last_modify_time', 'DESC']] + }); + + const needRefresh = !resume || + !resume.last_modify_time || + new Date(resume.last_modify_time) < twoHoursAgo; + + if (needRefresh) { + console.log(`[自动投递] 简历超过2小时未更新,重新获取`); + + try { + await command.executeCommands(taskId, [{ + command_type: 'getOnlineResume', + command_name: '获取在线简历', + command_params: JSON.stringify({ sn_code, platform }), + priority: config.getTaskPriority('get_resume') || 5 + }], this.mqttClient); + + // 重新查询 + resume = await resume_info.findOne({ + where: { sn_code, platform, isActive: true }, + order: [['last_modify_time', 'DESC']] + }); + } catch (error) { + console.warn(`[自动投递] 获取在线简历失败:`, error.message); + } + } + + return resume ? resume.toJSON() : null; + } + + /** + * 获取职位类型配置 + */ + async getJobTypeConfig(jobTypeId) { + if (!jobTypeId) return null; + + try { + const job_types = db.getModel('job_types'); + const jobType = await job_types.findByPk(jobTypeId); + return jobType ? jobType.toJSON() : null; + } catch (error) { + console.error(`[自动投递] 获取职位类型配置失败:`, error); + return null; + } + } + + /** + * 搜索职位列表 + */ + async searchJobs(sn_code, platform, keyword, pageCount, taskId) { + const getJobListCommand = { + command_type: 'getJobList', + command_name: '获取职位列表', + command_params: JSON.stringify({ + sn_code, + keyword, + platform, + pageCount + }), + priority: config.getTaskPriority('search_jobs') || 5 + }; + + await command.executeCommands(taskId, [getJobListCommand], this.mqttClient); + } + + /** + * 获取待投递职位 + */ + async getPendingJobs(sn_code, platform, limit) { + const job_postings = db.getModel('job_postings'); + + const jobs = await job_postings.findAll({ + where: { + sn_code, + platform, + applyStatus: 'pending' + }, + order: [['create_time', 'DESC']], + limit + }); + + return jobs.map(job => job.toJSON ? job.toJSON() : job); + } + + /** + * 合并过滤配置 + */ + mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig) { + // 排除关键词 + const jobTypeExclude = jobTypeConfig?.excludeKeywords + ? ConfigManager.parseConfig(jobTypeConfig.excludeKeywords, []) + : []; + + const deliverExclude = ConfigManager.getExcludeKeywords(deliverConfig); + const filterExclude = filterRules.excludeKeywords || []; + + // 过滤关键词 + const deliverFilter = ConfigManager.getFilterKeywords(deliverConfig); + const filterKeywords = filterRules.keywords || []; + + // 薪资范围 + const salaryRange = filterRules.minSalary || filterRules.maxSalary + ? { min: filterRules.minSalary || 0, max: filterRules.maxSalary || 0 } + : ConfigManager.getSalaryRange(deliverConfig); + + return { + exclude_keywords: [...jobTypeExclude, ...deliverExclude, ...filterExclude], + filter_keywords: filterKeywords.length > 0 ? filterKeywords : deliverFilter, + min_salary: salaryRange.min, + max_salary: salaryRange.max, + priority_weights: ConfigManager.getPriorityWeights(deliverConfig) + }; + } + + /** + * 获取近期已投递的公司 + */ + async getRecentDeliveredCompanies(sn_code, days = 30) { + const apply_records = db.getModel('apply_records'); + const daysAgo = new Date(); + daysAgo.setDate(daysAgo.getDate() - days); + + const recentApplies = await apply_records.findAll({ + where: { + sn_code, + applyTime: { + [db.models.op.gte]: daysAgo + } + }, + attributes: ['companyName'], + group: ['companyName'] + }); + + return new Set(recentApplies.map(apply => apply.companyName).filter(Boolean)); + } + + /** + * 过滤和评分职位 + */ + async filterAndScoreJobs(jobs, resume, accountConfig, jobTypeConfig, filterConfig, recentCompanies) { + const scored = []; + + for (const job of jobs) { + // 1. 过滤近期已投递的公司 + if (job.companyName && recentCompanies.has(job.companyName)) { + console.log(`[自动投递] 跳过已投递公司: ${job.companyName}`); + continue; + } + + // 2. 使用 jobFilterEngine 过滤和评分 + const filtered = await jobFilterEngine.filterJobs([job], filterConfig, resume); + if (filtered.length === 0) { + continue; // 不符合过滤条件 + } + + // 3. 使用原有的评分系统(job_filter_service)计算详细分数 + const scoreResult = jobFilterService.calculateJobScoreWithWeights( + job, + resume, + accountConfig, + jobTypeConfig, + accountConfig.is_salary_priority || [] + ); + + // 4. 计算关键词奖励 + const KeywordMatcher = require('../utils/keywordMatcher'); + const keywordBonus = KeywordMatcher.calculateBonus( + `${job.jobTitle} ${job.companyName} ${job.jobDescription || ''}`, + filterConfig.filter_keywords, + { baseScore: 5, maxBonus: 20 } + ); + + const finalScore = scoreResult.totalScore + keywordBonus.score; + + // 5. 只保留评分 >= 60 的职位 + if (finalScore >= 60) { + scored.push({ + ...job, + matchScore: finalScore, + scoreDetails: { + ...scoreResult.scores, + keywordBonus: keywordBonus.score + } + }); + } + } + + // 按评分降序排序 + scored.sort((a, b) => b.matchScore - a.matchScore); + + return scored; + } + + /** + * 创建投递指令 + */ + createDeliverCommands(jobs, sn_code, platform) { + return jobs.map(job => ({ + command_type: 'deliver_resume', + command_name: `投递简历 - ${job.jobTitle} @ ${job.companyName} (评分:${job.matchScore})`, + command_params: JSON.stringify({ + sn_code, + platform, + jobId: job.jobId, + encryptBossId: job.encryptBossId || '', + securityId: job.securityId || '', + brandName: job.companyName, + jobTitle: job.jobTitle, + companyName: job.companyName, + matchScore: job.matchScore, + scoreDetails: job.scoreDetails + }), + priority: config.getTaskPriority('apply') || 6 + })); + } +} + +module.exports = DeliverHandler; diff --git a/api/middleware/schedule/handlers/index.js b/api/middleware/schedule/handlers/index.js new file mode 100644 index 0000000..6d71570 --- /dev/null +++ b/api/middleware/schedule/handlers/index.js @@ -0,0 +1,18 @@ +/** + * 处理器模块导出 + * 统一导出所有任务处理器 + */ + +const BaseHandler = require('./baseHandler'); +const SearchHandler = require('./searchHandler'); +const DeliverHandler = require('./deliverHandler'); +const ChatHandler = require('./chatHandler'); +const ActiveHandler = require('./activeHandler'); + +module.exports = { + BaseHandler, + SearchHandler, + DeliverHandler, + ChatHandler, + ActiveHandler +}; diff --git a/api/middleware/schedule/handlers/searchHandler.js b/api/middleware/schedule/handlers/searchHandler.js new file mode 100644 index 0000000..6180582 --- /dev/null +++ b/api/middleware/schedule/handlers/searchHandler.js @@ -0,0 +1,87 @@ +const BaseHandler = require('./baseHandler'); +const ConfigManager = require('../services/configManager'); +const command = require('../command'); +const config = require('../config'); + +/** + * 自动搜索处理器 + * 负责搜索职位列表 + */ +class SearchHandler extends BaseHandler { + /** + * 处理自动搜索任务 + * @param {object} task - 任务对象 + * @returns {Promise} 执行结果 + */ + async handle(task) { + return await this.execute(task, async () => { + return await this.doSearch(task); + }, { + checkAuth: true, + checkOnline: true, + recordDeviceMetrics: true + }); + } + + /** + * 执行搜索逻辑 + */ + async doSearch(task) { + const { sn_code, taskParams } = task; + const { keyword, platform = 'boss', pageCount = 3 } = taskParams; + + console.log(`[自动搜索] 开始 - 设备: ${sn_code}, 关键词: ${keyword}`); + + // 1. 获取账户配置 + const accountConfig = await this.getAccountConfig(sn_code, ['keyword', 'platform_type', 'search_config']); + + if (!accountConfig) { + return { + jobsFound: 0, + message: '未找到账户配置' + }; + } + + // 2. 解析搜索配置 + const searchConfig = ConfigManager.parseSearchConfig(accountConfig.search_config); + + // 3. 检查搜索时间范围 + const timeRange = ConfigManager.getTimeRange(searchConfig); + if (timeRange) { + const timeRangeValidator = require('../services/timeRangeValidator'); + const timeCheck = timeRangeValidator.checkTimeRange(timeRange); + + if (!timeCheck.allowed) { + return { + jobsFound: 0, + message: timeCheck.reason + }; + } + } + + // 4. 创建搜索指令 + const searchCommand = { + command_type: 'getJobList', + command_name: `自动搜索职位 - ${keyword || accountConfig.keyword}`, + command_params: JSON.stringify({ + sn_code, + keyword: keyword || accountConfig.keyword || '', + platform: platform || accountConfig.platform_type || 'boss', + pageCount: pageCount || searchConfig.page_count || 3 + }), + priority: config.getTaskPriority('search_jobs') || 8 + }; + + // 5. 执行搜索指令 + const result = await command.executeCommands(task.id, [searchCommand], this.mqttClient); + + console.log(`[自动搜索] 完成 - 设备: ${sn_code}, 结果: ${JSON.stringify(result)}`); + + return { + jobsFound: result.jobCount || 0, + message: '搜索完成' + }; + } +} + +module.exports = SearchHandler; diff --git a/api/middleware/schedule/index.js b/api/middleware/schedule/index.js index 367a00c..4505c5e 100644 --- a/api/middleware/schedule/index.js +++ b/api/middleware/schedule/index.js @@ -1,17 +1,23 @@ const mqttManager = require("../mqtt/mqttManager.js"); -// 导入调度模块(简化版) -const TaskQueue = require('./taskQueue.js'); -const Command = require('./command.js'); -const deviceManager = require('./deviceManager.js'); -const config = require('./config.js'); -const utils = require('./utils.js'); +// 导入核心模块 +const TaskQueue = require('./core/taskQueue.js'); +const Command = require('./core/command.js'); +const deviceManager = require('./core/deviceManager.js'); +const ScheduledJobs = require('./core/scheduledJobs.js'); -// 导入新的模块 +// 导入基础设施模块 +const config = require('./infrastructure/config.js'); +const utils = require('./utils/scheduleUtils.js'); + +// 导入任务处理器 const TaskHandlers = require('./taskHandlers.js'); + +// 导入MQTT模块 const MqttDispatcher = require('../mqtt/mqttDispatcher.js'); -const ScheduledJobs = require('./scheduledJobs.js'); -const DeviceWorkStatusNotifier = require('./deviceWorkStatusNotifier.js'); + +// 导入通知器 +const DeviceWorkStatusNotifier = require('./notifiers/deviceWorkStatusNotifier.js'); /** * 调度系统管理器 @@ -22,7 +28,7 @@ class ScheduleManager { this.mqttClient = null; this.isInitialized = false; this.startTime = new Date(); - + // 子模块 this.taskHandlers = null; this.mqttDispatcher = null; @@ -80,7 +86,7 @@ class ScheduleManager { async initComponents() { // 初始化设备管理器 await deviceManager.init(); - + // 初始化任务队列 await TaskQueue.init(); } diff --git a/api/middleware/schedule/ErrorHandler.js b/api/middleware/schedule/infrastructure/ErrorHandler.js similarity index 100% rename from api/middleware/schedule/ErrorHandler.js rename to api/middleware/schedule/infrastructure/ErrorHandler.js diff --git a/api/middleware/schedule/PriorityQueue.js b/api/middleware/schedule/infrastructure/PriorityQueue.js similarity index 100% rename from api/middleware/schedule/PriorityQueue.js rename to api/middleware/schedule/infrastructure/PriorityQueue.js diff --git a/api/middleware/schedule/config.js b/api/middleware/schedule/infrastructure/config.js similarity index 100% rename from api/middleware/schedule/config.js rename to api/middleware/schedule/infrastructure/config.js diff --git a/api/middleware/schedule/infrastructure/index.js b/api/middleware/schedule/infrastructure/index.js new file mode 100644 index 0000000..f7056f0 --- /dev/null +++ b/api/middleware/schedule/infrastructure/index.js @@ -0,0 +1,14 @@ +/** + * Infrastructure 模块导出 + * 统一导出基础设施模块 + */ + +const PriorityQueue = require('./PriorityQueue'); +const ErrorHandler = require('./ErrorHandler'); +const config = require('./config'); + +module.exports = { + PriorityQueue, + ErrorHandler, + config +}; diff --git a/api/middleware/schedule/deviceWorkStatusNotifier.js b/api/middleware/schedule/notifiers/deviceWorkStatusNotifier.js similarity index 100% rename from api/middleware/schedule/deviceWorkStatusNotifier.js rename to api/middleware/schedule/notifiers/deviceWorkStatusNotifier.js diff --git a/api/middleware/schedule/notifiers/index.js b/api/middleware/schedule/notifiers/index.js new file mode 100644 index 0000000..c557312 --- /dev/null +++ b/api/middleware/schedule/notifiers/index.js @@ -0,0 +1,9 @@ +/** + * Notifiers 模块导出 + */ + +const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier'); + +module.exports = { + deviceWorkStatusNotifier +}; diff --git a/api/middleware/schedule/services/accountValidator.js b/api/middleware/schedule/services/accountValidator.js new file mode 100644 index 0000000..3972972 --- /dev/null +++ b/api/middleware/schedule/services/accountValidator.js @@ -0,0 +1,199 @@ +const db = require('../../dbProxy'); +const authorizationService = require('../../../services/authorization_service'); +const deviceManager = require('../core/deviceManager'); + +/** + * 账户验证服务 + * 统一处理账户启用状态、授权状态、在线状态的检查 + */ +class AccountValidator { + /** + * 检查账户是否启用 + * @param {string} sn_code - 设备序列号 + * @returns {Promise<{enabled: boolean, reason?: string}>} + */ + async checkEnabled(sn_code) { + try { + const pla_account = db.getModel('pla_account'); + const account = await pla_account.findOne({ + where: { sn_code, is_delete: 0 }, + attributes: ['is_enabled', 'name'] + }); + + if (!account) { + return { enabled: false, reason: '账户不存在' }; + } + + if (!account.is_enabled) { + return { enabled: false, reason: '账户未启用' }; + } + + return { enabled: true }; + } catch (error) { + console.error(`[账户验证] 检查启用状态失败 (${sn_code}):`, error); + return { enabled: false, reason: '检查失败' }; + } + } + + /** + * 检查账户授权状态 + * @param {string} sn_code - 设备序列号 + * @returns {Promise<{authorized: boolean, days?: number, reason?: string}>} + */ + async checkAuthorization(sn_code) { + try { + const result = await authorizationService.checkAuthorization(sn_code); + + if (!result.is_authorized) { + return { + authorized: false, + days: result.days_remaining || 0, + reason: result.message || '授权已过期' + }; + } + + return { + authorized: true, + days: result.days_remaining + }; + } catch (error) { + console.error(`[账户验证] 检查授权状态失败 (${sn_code}):`, error); + return { authorized: false, reason: '授权检查失败' }; + } + } + + /** + * 检查设备是否在线 + * @param {string} sn_code - 设备序列号 + * @param {number} offlineThreshold - 离线阈值(毫秒) + * @returns {{online: boolean, lastHeartbeat?: number, reason?: string}} + */ + checkOnline(sn_code, offlineThreshold = 3 * 60 * 1000) { + const device = deviceManager.devices.get(sn_code); + + if (!device) { + return { online: false, reason: '设备从未发送心跳' }; + } + + const now = Date.now(); + const lastHeartbeat = device.lastHeartbeat || 0; + const elapsed = now - lastHeartbeat; + + if (elapsed > offlineThreshold) { + const minutes = Math.round(elapsed / (60 * 1000)); + return { + online: false, + lastHeartbeat, + reason: `设备离线(最后心跳: ${minutes}分钟前)` + }; + } + + if (!device.isOnline) { + return { online: false, lastHeartbeat, reason: '设备标记为离线' }; + } + + return { online: true, lastHeartbeat }; + } + + /** + * 综合验证(启用 + 授权 + 在线) + * @param {string} sn_code - 设备序列号 + * @param {object} options - 验证选项 + * @param {boolean} options.checkEnabled - 是否检查启用状态(默认 true) + * @param {boolean} options.checkAuth - 是否检查授权(默认 true) + * @param {boolean} options.checkOnline - 是否检查在线(默认 true) + * @param {number} options.offlineThreshold - 离线阈值(默认 3分钟) + * @returns {Promise<{valid: boolean, reason?: string, details?: object}>} + */ + async validate(sn_code, options = {}) { + const { + checkEnabled = true, + checkAuth = true, + checkOnline = true, + offlineThreshold = 3 * 60 * 1000 + } = options; + + const details = {}; + + // 检查启用状态 + if (checkEnabled) { + const enabledResult = await this.checkEnabled(sn_code); + details.enabled = enabledResult; + + if (!enabledResult.enabled) { + return { + valid: false, + reason: enabledResult.reason, + details + }; + } + } + + // 检查授权状态 + if (checkAuth) { + const authResult = await this.checkAuthorization(sn_code); + details.authorization = authResult; + + if (!authResult.authorized) { + return { + valid: false, + reason: authResult.reason, + details + }; + } + } + + // 检查在线状态 + if (checkOnline) { + const onlineResult = this.checkOnline(sn_code, offlineThreshold); + details.online = onlineResult; + + if (!onlineResult.online) { + return { + valid: false, + reason: onlineResult.reason, + details + }; + } + } + + return { valid: true, details }; + } + + /** + * 批量验证多个账户 + * @param {string[]} sn_codes - 设备序列号数组 + * @param {object} options - 验证选项 + * @returns {Promise<{valid: string[], invalid: Array<{sn_code: string, reason: string}>}>} + */ + async validateBatch(sn_codes, options = {}) { + const valid = []; + const invalid = []; + + for (const sn_code of sn_codes) { + const result = await this.validate(sn_code, options); + + if (result.valid) { + valid.push(sn_code); + } else { + invalid.push({ sn_code, reason: result.reason }); + } + } + + return { valid, invalid }; + } + + /** + * 检查账户是否已登录(通过心跳数据) + * @param {string} sn_code - 设备序列号 + * @returns {boolean} + */ + checkLoggedIn(sn_code) { + const device = deviceManager.devices.get(sn_code); + return device?.isLoggedIn || false; + } +} + +// 导出单例 +const accountValidator = new AccountValidator(); +module.exports = accountValidator; diff --git a/api/middleware/schedule/services/configManager.js b/api/middleware/schedule/services/configManager.js new file mode 100644 index 0000000..ba5347b --- /dev/null +++ b/api/middleware/schedule/services/configManager.js @@ -0,0 +1,225 @@ +/** + * 配置管理服务 + * 统一处理账户配置的解析和验证 + */ +class ConfigManager { + /** + * 解析 JSON 配置字符串 + * @param {string|object} config - 配置字符串或对象 + * @param {object} defaultValue - 默认值 + * @returns {object} 解析后的配置对象 + */ + static parseConfig(config, defaultValue = {}) { + if (!config) { + return defaultValue; + } + + if (typeof config === 'object') { + return { ...defaultValue, ...config }; + } + + if (typeof config === 'string') { + try { + const parsed = JSON.parse(config); + return { ...defaultValue, ...parsed }; + } catch (error) { + console.warn('[配置管理] JSON 解析失败:', error.message); + return defaultValue; + } + } + + return defaultValue; + } + + /** + * 解析投递配置 + * @param {string|object} deliverConfig - 投递配置 + * @returns {object} 标准化的投递配置 + */ + static parseDeliverConfig(deliverConfig) { + const defaultConfig = { + deliver_interval: 30, // 投递间隔(分钟) + min_salary: 0, // 最低薪资 + max_salary: 0, // 最高薪资 + page_count: 3, // 搜索页数 + max_deliver: 10, // 最大投递数 + filter_keywords: [], // 过滤关键词 + exclude_keywords: [], // 排除关键词 + time_range: null, // 时间范围 + priority_weights: null // 优先级权重 + }; + + return this.parseConfig(deliverConfig, defaultConfig); + } + + /** + * 解析搜索配置 + * @param {string|object} searchConfig - 搜索配置 + * @returns {object} 标准化的搜索配置 + */ + static parseSearchConfig(searchConfig) { + const defaultConfig = { + search_interval: 60, // 搜索间隔(分钟) + page_count: 3, // 搜索页数 + keywords: [], // 搜索关键词 + exclude_keywords: [], // 排除关键词 + time_range: null // 时间范围 + }; + + return this.parseConfig(searchConfig, defaultConfig); + } + + /** + * 解析沟通配置 + * @param {string|object} chatStrategy - 沟通策略 + * @returns {object} 标准化的沟通配置 + */ + static parseChatStrategy(chatStrategy) { + const defaultConfig = { + chat_interval: 30, // 沟通间隔(分钟) + auto_reply: false, // 是否自动回复 + reply_template: '', // 回复模板 + time_range: null // 时间范围 + }; + + return this.parseConfig(chatStrategy, defaultConfig); + } + + /** + * 解析活跃配置 + * @param {string|object} activeStrategy - 活跃策略 + * @returns {object} 标准化的活跃配置 + */ + static parseActiveStrategy(activeStrategy) { + const defaultConfig = { + active_interval: 120, // 活跃间隔(分钟) + actions: ['view_jobs'], // 活跃动作 + time_range: null // 时间范围 + }; + + return this.parseConfig(activeStrategy, defaultConfig); + } + + /** + * 获取优先级权重配置 + * @param {object} config - 投递配置 + * @returns {object} 优先级权重 + */ + static getPriorityWeights(config) { + const defaultWeights = { + salary: 0.4, // 薪资匹配度 + keyword: 0.3, // 关键词匹配度 + company: 0.2, // 公司活跃度 + distance: 0.1 // 距离(未来) + }; + + if (!config.priority_weights) { + return defaultWeights; + } + + return { ...defaultWeights, ...config.priority_weights }; + } + + /** + * 获取排除关键词列表 + * @param {object} config - 配置对象 + * @returns {string[]} 排除关键词数组 + */ + static getExcludeKeywords(config) { + if (!config.exclude_keywords) { + return []; + } + + if (Array.isArray(config.exclude_keywords)) { + return config.exclude_keywords.filter(k => k && k.trim()); + } + + if (typeof config.exclude_keywords === 'string') { + return config.exclude_keywords + .split(/[,,、]/) + .map(k => k.trim()) + .filter(k => k); + } + + return []; + } + + /** + * 获取过滤关键词列表 + * @param {object} config - 配置对象 + * @returns {string[]} 过滤关键词数组 + */ + static getFilterKeywords(config) { + if (!config.filter_keywords) { + return []; + } + + if (Array.isArray(config.filter_keywords)) { + return config.filter_keywords.filter(k => k && k.trim()); + } + + if (typeof config.filter_keywords === 'string') { + return config.filter_keywords + .split(/[,,、]/) + .map(k => k.trim()) + .filter(k => k); + } + + return []; + } + + /** + * 获取薪资范围 + * @param {object} config - 配置对象 + * @returns {{min: number, max: number}} 薪资范围 + */ + static getSalaryRange(config) { + return { + min: parseInt(config.min_salary) || 0, + max: parseInt(config.max_salary) || 0 + }; + } + + /** + * 获取时间范围 + * @param {object} config - 配置对象 + * @returns {object|null} 时间范围配置 + */ + static getTimeRange(config) { + return config.time_range || null; + } + + /** + * 验证配置完整性 + * @param {object} config - 配置对象 + * @param {string[]} requiredFields - 必需字段 + * @returns {{valid: boolean, missing?: string[]}} 验证结果 + */ + static validateConfig(config, requiredFields = []) { + const missing = []; + + for (const field of requiredFields) { + if (config[field] === undefined || config[field] === null) { + missing.push(field); + } + } + + if (missing.length > 0) { + return { valid: false, missing }; + } + + return { valid: true }; + } + + /** + * 合并配置(用于覆盖默认配置) + * @param {object} defaultConfig - 默认配置 + * @param {object} userConfig - 用户配置 + * @returns {object} 合并后的配置 + */ + static mergeConfig(defaultConfig, userConfig) { + return { ...defaultConfig, ...userConfig }; + } +} + +module.exports = ConfigManager; diff --git a/api/middleware/schedule/services/jobFilterEngine.js b/api/middleware/schedule/services/jobFilterEngine.js new file mode 100644 index 0000000..d133022 --- /dev/null +++ b/api/middleware/schedule/services/jobFilterEngine.js @@ -0,0 +1,395 @@ +const SalaryParser = require('../utils/salaryParser'); +const KeywordMatcher = require('../utils/keywordMatcher'); +const db = require('../../dbProxy'); + +/** + * 职位过滤引擎 + * 综合处理职位的过滤、评分和排序 + */ +class JobFilterEngine { + /** + * 过滤职位列表 + * @param {Array} jobs - 职位列表 + * @param {object} config - 过滤配置 + * @param {object} resumeInfo - 简历信息 + * @returns {Promise} 过滤后的职位列表 + */ + async filterJobs(jobs, config, resumeInfo = {}) { + if (!jobs || jobs.length === 0) { + return []; + } + + let filtered = [...jobs]; + + // 1. 薪资过滤 + filtered = this.filterBySalary(filtered, config); + + // 2. 关键词过滤 + filtered = this.filterByKeywords(filtered, config); + + // 3. 公司活跃度过滤 + if (config.filter_inactive_companies) { + filtered = await this.filterByCompanyActivity(filtered, config.company_active_days || 7); + } + + // 4. 去重(同一公司、同一职位名称) + if (config.deduplicate) { + filtered = this.deduplicateJobs(filtered); + } + + return filtered; + } + + /** + * 按薪资过滤 + * @param {Array} jobs - 职位列表 + * @param {object} config - 配置 + * @returns {Array} 过滤后的职位 + */ + filterBySalary(jobs, config) { + const { min_salary = 0, max_salary = 0 } = config; + + if (min_salary === 0 && max_salary === 0) { + return jobs; // 无薪资限制 + } + + return jobs.filter(job => { + const jobSalary = SalaryParser.parse(job.salary || job.salaryDesc || ''); + return SalaryParser.isWithinRange(jobSalary, min_salary, max_salary); + }); + } + + /** + * 按关键词过滤 + * @param {Array} jobs - 职位列表 + * @param {object} config - 配置 + * @returns {Array} 过滤后的职位 + */ + filterByKeywords(jobs, config) { + const { + exclude_keywords = [], + filter_keywords = [] + } = config; + + if (exclude_keywords.length === 0 && filter_keywords.length === 0) { + return jobs; + } + + return KeywordMatcher.filterJobs(jobs, { + excludeKeywords: exclude_keywords, + filterKeywords: filter_keywords + }, (job) => { + // 组合职位名称、描述、技能要求等 + return [ + job.name || job.jobName || '', + job.description || job.jobDescription || '', + job.skills || '', + job.welfare || '' + ].join(' '); + }); + } + + /** + * 按公司活跃度过滤 + * @param {Array} jobs - 职位列表 + * @param {number} activeDays - 活跃天数阈值 + * @returns {Promise} 过滤后的职位 + */ + async filterByCompanyActivity(jobs, activeDays = 7) { + try { + const task_status = db.getModel('task_status'); + const thresholdDate = new Date(Date.now() - activeDays * 24 * 60 * 60 * 1000); + + // 查询近期已投递的公司 + const recentCompanies = await task_status.findAll({ + where: { + taskType: 'auto_deliver', + status: 'completed', + endTime: { + [db.models.op.gte]: thresholdDate + } + }, + attributes: ['result'], + raw: true + }); + + // 提取公司名称 + const deliveredCompanies = new Set(); + for (const task of recentCompanies) { + try { + const result = JSON.parse(task.result || '{}'); + if (result.deliveredJobs) { + result.deliveredJobs.forEach(job => { + if (job.company) { + deliveredCompanies.add(job.company.toLowerCase()); + } + }); + } + } catch (e) { + // 忽略解析错误 + } + } + + // 过滤掉近期已投递的公司 + return jobs.filter(job => { + const company = (job.company || job.companyName || '').toLowerCase().trim(); + return !deliveredCompanies.has(company); + }); + + } catch (error) { + console.error('[职位过滤] 公司活跃度过滤失败:', error); + return jobs; // 失败时返回原列表 + } + } + + /** + * 去重职位 + * @param {Array} jobs - 职位列表 + * @returns {Array} 去重后的职位 + */ + deduplicateJobs(jobs) { + const seen = new Set(); + const unique = []; + + for (const job of jobs) { + const company = (job.company || job.companyName || '').toLowerCase().trim(); + const jobName = (job.name || job.jobName || '').toLowerCase().trim(); + const key = `${company}||${jobName}`; + + if (!seen.has(key)) { + seen.add(key); + unique.push(job); + } + } + + return unique; + } + + /** + * 为职位打分 + * @param {Array} jobs - 职位列表 + * @param {object} resumeInfo - 简历信息 + * @param {object} config - 配置(包含权重) + * @returns {Array} 带分数的职位列表 + */ + scoreJobs(jobs, resumeInfo = {}, config = {}) { + const weights = config.priority_weights || { + salary: 0.4, + keyword: 0.3, + company: 0.2, + freshness: 0.1 + }; + + return jobs.map(job => { + const scores = { + salary: this.scoreSalary(job, resumeInfo), + keyword: this.scoreKeywords(job, config), + company: this.scoreCompany(job), + freshness: this.scoreFreshness(job) + }; + + // 加权总分 + const totalScore = ( + scores.salary * weights.salary + + scores.keyword * weights.keyword + + scores.company * weights.company + + scores.freshness * weights.freshness + ); + + return { + ...job, + _scores: scores, + _totalScore: totalScore + }; + }); + } + + /** + * 薪资匹配度评分 (0-100) + * @param {object} job - 职位信息 + * @param {object} resumeInfo - 简历信息 + * @returns {number} 分数 + */ + scoreSalary(job, resumeInfo) { + const jobSalary = SalaryParser.parse(job.salary || job.salaryDesc || ''); + const expectedSalary = SalaryParser.parse(resumeInfo.expected_salary || ''); + + if (jobSalary.min === 0 || expectedSalary.min === 0) { + return 50; // 无法判断时返回中性分 + } + + const matchScore = SalaryParser.calculateMatch(jobSalary, expectedSalary); + return matchScore * 100; + } + + /** + * 关键词匹配度评分 (0-100) + * @param {object} job - 职位信息 + * @param {object} config - 配置 + * @returns {number} 分数 + */ + scoreKeywords(job, config) { + const bonusKeywords = config.filter_keywords || []; + + if (bonusKeywords.length === 0) { + return 50; // 无关键词时返回中性分 + } + + const jobText = [ + job.name || job.jobName || '', + job.description || job.jobDescription || '', + job.skills || '' + ].join(' '); + + const bonusResult = KeywordMatcher.calculateBonus(jobText, bonusKeywords, { + baseScore: 10, + maxBonus: 100 + }); + + return Math.min(bonusResult.score, 100); + } + + /** + * 公司评分 (0-100) + * @param {object} job - 职位信息 + * @returns {number} 分数 + */ + scoreCompany(job) { + let score = 50; // 基础分 + + // 融资阶段加分 + const fundingStage = (job.financingStage || job.financing || '').toLowerCase(); + const fundingBonus = { + '已上市': 30, + '上市公司': 30, + 'd轮': 25, + 'c轮': 20, + 'b轮': 15, + 'a轮': 10, + '天使轮': 5 + }; + + for (const [stage, bonus] of Object.entries(fundingBonus)) { + if (fundingStage.includes(stage.toLowerCase())) { + score += bonus; + break; + } + } + + // 公司规模加分 + const scale = (job.scale || job.companyScale || '').toLowerCase(); + if (scale.includes('10000') || scale.includes('万人')) { + score += 15; + } else if (scale.includes('1000-9999') || scale.includes('千人')) { + score += 10; + } else if (scale.includes('500-999')) { + score += 5; + } + + return Math.min(score, 100); + } + + /** + * 新鲜度评分 (0-100) + * @param {object} job - 职位信息 + * @returns {number} 分数 + */ + scoreFreshness(job) { + const publishTime = job.publishTime || job.createTime; + + if (!publishTime) { + return 50; // 无时间信息时返回中性分 + } + + try { + const now = Date.now(); + const pubTime = new Date(publishTime).getTime(); + const hoursAgo = (now - pubTime) / (1000 * 60 * 60); + + // 越新鲜分数越高 + if (hoursAgo < 1) return 100; + if (hoursAgo < 24) return 90; + if (hoursAgo < 72) return 70; + if (hoursAgo < 168) return 50; // 一周内 + return 30; + + } catch (error) { + return 50; + } + } + + /** + * 排序职位 + * @param {Array} jobs - 职位列表(带分数) + * @param {string} sortBy - 排序方式: score, salary, freshness + * @returns {Array} 排序后的职位 + */ + sortJobs(jobs, sortBy = 'score') { + const sorted = [...jobs]; + + switch (sortBy) { + case 'score': + sorted.sort((a, b) => (b._totalScore || 0) - (a._totalScore || 0)); + break; + + case 'salary': + sorted.sort((a, b) => { + const salaryA = SalaryParser.parse(a.salary || ''); + const salaryB = SalaryParser.parse(b.salary || ''); + return (salaryB.max || 0) - (salaryA.max || 0); + }); + break; + + case 'freshness': + sorted.sort((a, b) => { + const timeA = new Date(a.publishTime || a.createTime || 0).getTime(); + const timeB = new Date(b.publishTime || b.createTime || 0).getTime(); + return timeB - timeA; + }); + break; + + default: + // 默认按分数排序 + sorted.sort((a, b) => (b._totalScore || 0) - (a._totalScore || 0)); + } + + return sorted; + } + + /** + * 综合处理:过滤 + 评分 + 排序 + * @param {Array} jobs - 职位列表 + * @param {object} config - 过滤配置 + * @param {object} resumeInfo - 简历信息 + * @param {object} options - 选项 + * @returns {Promise} 处理后的职位列表 + */ + async process(jobs, config, resumeInfo = {}, options = {}) { + const { + maxCount = 10, // 最大返回数量 + sortBy = 'score' // 排序方式 + } = options; + + // 1. 过滤 + let filtered = await this.filterJobs(jobs, config, resumeInfo); + + console.log(`[职位过滤] 原始: ${jobs.length} 个,过滤后: ${filtered.length} 个`); + + // 2. 评分 + const scored = this.scoreJobs(filtered, resumeInfo, config); + + // 3. 排序 + const sorted = this.sortJobs(scored, sortBy); + + // 4. 截取 + const result = sorted.slice(0, maxCount); + + console.log(`[职位过滤] 最终返回: ${result.length} 个职位`); + + return result; + } +} + +// 导出单例 +const jobFilterEngine = new JobFilterEngine(); +module.exports = jobFilterEngine; diff --git a/api/middleware/schedule/services/timeRangeValidator.js b/api/middleware/schedule/services/timeRangeValidator.js new file mode 100644 index 0000000..2babe1b --- /dev/null +++ b/api/middleware/schedule/services/timeRangeValidator.js @@ -0,0 +1,158 @@ +/** + * 时间范围验证器 + * 检查当前时间是否在指定的时间范围内(支持工作日限制) + */ +class TimeRangeValidator { + /** + * 检查当前时间是否在指定的时间范围内 + * @param {object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1} + * @returns {{allowed: boolean, reason: string}} 检查结果 + */ + static 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(); // 0=周日, 1=周一, ..., 6=周六 + if (dayOfWeek === 0 || dayOfWeek === 6) { + return { allowed: false, reason: '当前是周末,不在允许的时间范围内' }; + } + } + + // 检查当前时间是否在时间范围内 + if (startTime <= endTime) { + // 正常情况:09:00 - 18:00 + if (currentTime < startTime || currentTime >= endTime) { + const currentTimeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; + return { + allowed: false, + reason: `当前时间 ${currentTimeStr} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` + }; + } + } else { + // 跨天情况:22:00 - 06:00 + if (currentTime < startTime && currentTime >= endTime) { + const currentTimeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; + return { + allowed: false, + reason: `当前时间 ${currentTimeStr} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` + }; + } + } + + return { allowed: true, reason: '在允许的时间范围内' }; + } + + /** + * 检查是否在工作时间内 + * @param {string} startTime - 开始时间 '09:00' + * @param {string} endTime - 结束时间 '18:00' + * @returns {boolean} + */ + static isWithinWorkingHours(startTime = '09:00', endTime = '18:00') { + const result = this.checkTimeRange({ + start_time: startTime, + end_time: endTime, + workdays_only: 0 + }); + return result.allowed; + } + + /** + * 检查是否是工作日 + * @returns {boolean} + */ + static isWorkingDay() { + const dayOfWeek = new Date().getDay(); + return dayOfWeek !== 0 && dayOfWeek !== 6; // 非周六周日 + } + + /** + * 获取下一个可操作时间 + * @param {object} timeRange - 时间范围配置 + * @returns {Date|null} 下一个可操作时间,如果当前可操作则返回 null + */ + static getNextAvailableTime(timeRange) { + const check = this.checkTimeRange(timeRange); + if (check.allowed) { + return null; // 当前可操作 + } + + if (!timeRange || !timeRange.start_time) { + return null; + } + + const now = new Date(); + const [startHour, startMinute] = timeRange.start_time.split(':').map(Number); + + // 如果是工作日限制且当前是周末 + if (timeRange.workdays_only == 1) { + const dayOfWeek = now.getDay(); + if (dayOfWeek === 0) { + // 周日,下一个可操作时间是周一 + const nextTime = new Date(now); + nextTime.setDate(now.getDate() + 1); + nextTime.setHours(startHour, startMinute, 0, 0); + return nextTime; + } else if (dayOfWeek === 6) { + // 周六,下一个可操作时间是下周一 + const nextTime = new Date(now); + nextTime.setDate(now.getDate() + 2); + nextTime.setHours(startHour, startMinute, 0, 0); + return nextTime; + } + } + + // 计算下一个开始时间 + const nextTime = new Date(now); + nextTime.setHours(startHour, startMinute, 0, 0); + + // 如果已经过了今天的开始时间,则设置为明天 + if (nextTime <= now) { + nextTime.setDate(now.getDate() + 1); + } + + return nextTime; + } + + /** + * 格式化剩余时间 + * @param {object} timeRange - 时间范围配置 + * @returns {string} 剩余时间描述 + */ + static formatRemainingTime(timeRange) { + const nextTime = this.getNextAvailableTime(timeRange); + if (!nextTime) { + return '当前可操作'; + } + + const now = Date.now(); + const diff = nextTime.getTime() - now; + const hours = Math.floor(diff / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + + if (hours > 24) { + const days = Math.floor(hours / 24); + return `需要等待 ${days} 天 ${hours % 24} 小时`; + } else if (hours > 0) { + return `需要等待 ${hours} 小时 ${minutes} 分钟`; + } else { + return `需要等待 ${minutes} 分钟`; + } + } +} + +module.exports = TimeRangeValidator; diff --git a/api/middleware/schedule/taskHandlers.js b/api/middleware/schedule/taskHandlers.js index 7ea8d50..56fd40d 100644 --- a/api/middleware/schedule/taskHandlers.js +++ b/api/middleware/schedule/taskHandlers.js @@ -1,1045 +1,101 @@ -const db = require('../dbProxy.js'); -const config = require('./config.js'); -const deviceManager = require('./deviceManager.js'); -const command = require('./command.js'); -const jobFilterService = require('../job/job_filter_service.js'); +const { SearchHandler, DeliverHandler, ChatHandler, ActiveHandler } = require('./handlers'); /** - * 任务处理器(简化版) - * 处理各种类型的任务 + * 任务处理器工厂(重构版) + * 使用独立的处理器类替代原有的内嵌处理方法 + * + * 重构说明: + * - 原 taskHandlers.js: 1045 行,包含所有业务逻辑 + * - 新 taskHandlers.js: 95 行,仅作为处理器工厂 + * - 业务逻辑已分离到 handlers/ 目录下的独立处理器 */ class TaskHandlers { constructor(mqttClient) { this.mqttClient = mqttClient; + + // 初始化各个处理器 + this.searchHandler = new SearchHandler(mqttClient); + this.deliverHandler = new DeliverHandler(mqttClient); + this.chatHandler = new ChatHandler(mqttClient); + this.activeHandler = new ActiveHandler(mqttClient); + + console.log('[任务处理器] 已初始化所有处理器实例'); } - /** - * 注册任务处理器到任务队列 - * @param {object} taskQueue - 任务队列实例 - */ + * 注册任务处理器到任务队列 + * @param {object} taskQueue - 任务队列实例 + */ register(taskQueue) { - // 自动搜索任务 + console.log('[任务处理器] 开始注册处理器...'); + + // 注册自动搜索处理器 taskQueue.registerHandler('auto_search', async (task) => { return await this.handleAutoSearchTask(task); }); - // 自动投递任务 + // 注册自动投递处理器 taskQueue.registerHandler('auto_deliver', async (task) => { return await this.handleAutoDeliverTask(task); }); - // 搜索职位列表任务(新功能) + // 注册搜索职位列表处理器(与 auto_search 相同) taskQueue.registerHandler('search_jobs', async (task) => { - return await this.handleSearchJobListTask(task); + return await this.handleAutoSearchTask(task); }); - // 自动沟通任务 + // 注册自动沟通处理器 taskQueue.registerHandler('auto_chat', async (task) => { return await this.handleAutoChatTask(task); }); - // 自动活跃账号任务 + // 注册自动活跃账户处理器 taskQueue.registerHandler('auto_active_account', async (task) => { return await this.handleAutoActiveAccountTask(task); }); + + console.log('[任务处理器] 所有处理器已注册完成'); } - - - /** * 处理自动搜索任务 + * @param {object} task - 任务对象 + * @returns {Promise} 执行结果 */ 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; - } + console.log(`[任务处理器] 调度自动搜索任务 - 设备: ${task.sn_code}`); + return await this.searchHandler.handle(task); } /** * 处理自动投递任务 - */ - - async handleAutoDeliverTask(task) { - const { sn_code, taskParams } = task; - const { keyword, platform, pageCount, maxCount, filterRules = {} } = 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, - deliveredCount: 0, - message: authCheck.message - }; - } - - deviceManager.recordTaskStart(sn_code, task); - const startTime = Date.now(); - - try { - const job_postings = db.getModel('job_postings'); - const pla_account = db.getModel('pla_account'); - const resume_info = db.getModel('resume_info'); - const job_types = db.getModel('job_types'); - const apply_records = db.getModel('apply_records'); - const Sequelize = require('sequelize'); - const { Op } = Sequelize; - - // 检查今日投递次数限制 - const currentPlatform = platform || 'boss'; - const dailyLimit = config.getDailyLimit('apply', currentPlatform); - - // 获取今日开始时间(00:00:00) - const today = new Date(); - today.setHours(0, 0, 0, 0); - - // 查询今日已投递次数 - const todayApplyCount = await apply_records.count({ - where: { - sn_code: sn_code, - platform: currentPlatform, - applyTime: { - [Op.gte]: today - } - } - }); - - console.log(`[任务处理器] 今日已投递 ${todayApplyCount} 次,限制: ${dailyLimit} 次`); - - // 如果已达到每日投递上限,则跳过 - if (todayApplyCount >= dailyLimit) { - console.log(`[任务处理器] 已达到每日投递上限(${dailyLimit}次),跳过投递`); - return { - success: false, - deliveredCount: 0, - message: `已达到每日投递上限(${dailyLimit}次),今日已投递 ${todayApplyCount} 次` - }; - } - - // 计算本次可投递的数量(不超过剩余限额) - const remainingQuota = dailyLimit - todayApplyCount; - const actualMaxCount = Math.min(maxCount || 10, remainingQuota); - - if (actualMaxCount < (maxCount || 10)) { - console.log(`[任务处理器] 受每日投递上限限制,本次最多投递 ${actualMaxCount} 个职位(剩余限额: ${remainingQuota})`); - } - - // 1. 检查并获取在线简历(如果2小时内没有获取) - const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000); - let resume = await resume_info.findOne({ - where: { - sn_code, - platform: platform || 'boss', - isActive: true - }, - order: [['last_modify_time', 'DESC']] - }); - - const needRefreshResume = !resume || - !resume.last_modify_time || - new Date(resume.last_modify_time) < twoHoursAgo; - - if (needRefreshResume) { - console.log(`[任务处理器] 简历超过2小时未更新,重新获取在线简历`); - try { - // 通过 command 系统获取在线简历,而不是直接调用 jobManager - const getResumeCommand = { - command_type: 'getOnlineResume', - command_name: '获取在线简历', - command_params: JSON.stringify({ sn_code, platform: platform || 'boss' }), - priority: config.getTaskPriority('get_resume') || 5 - }; - await command.executeCommands(task.id, [getResumeCommand], this.mqttClient); - - // 重新查询简历 - resume = await resume_info.findOne({ - where: { - sn_code, - platform: platform || 'boss', - isActive: true - }, - order: [['last_modify_time', 'DESC']] - }); - } catch (error) { - console.warn(`[任务处理器] 获取在线简历失败,使用已有简历:`, error.message); - } - } - - if (!resume) { - console.log(`[任务处理器] 未找到简历信息,无法进行自动投递`); - return { - success: false, - deliveredCount: 0, - message: '未找到简历信息' - }; - } - - // 2. 获取账号配置和职位类型配置 - const account = await pla_account.findOne({ - where: { sn_code, platform_type: platform || 'boss' } - }); - - if (!account) { - console.log(`[任务处理器] 未找到账号配置`); - return { - success: false, - deliveredCount: 0, - message: '未找到账号配置' - }; - } - - const accountConfig = account.toJSON(); - const resumeInfo = resume.toJSON(); - - // 检查投递时间范围 - if (accountConfig.deliver_config) { - const deliverConfig = typeof accountConfig.deliver_config === 'string' - ? JSON.parse(accountConfig.deliver_config) - : accountConfig.deliver_config; - - if (deliverConfig.time_range) { - const timeCheck = this.checkTimeRange(deliverConfig.time_range); - if (!timeCheck.allowed) { - console.log(`[任务处理器] 自动投递任务 - ${timeCheck.reason}`); - return { - success: true, - deliveredCount: 0, - message: timeCheck.reason - }; - } - } - } - - // 获取职位类型配置 - let jobTypeConfig = null; - if (accountConfig.job_type_id) { - const jobType = await job_types.findByPk(accountConfig.job_type_id); - if (jobType) { - jobTypeConfig = jobType.toJSON(); - } - } - - // 获取优先级权重配置 - let priorityWeights = accountConfig.is_salary_priority; - if (!Array.isArray(priorityWeights) || priorityWeights.length === 0) { - priorityWeights = [ - { key: "distance", weight: 50 }, - { key: "salary", weight: 20 }, - { key: "work_years", weight: 10 }, - { key: "education", weight: 20 } - ]; - } - - // 3. 先获取职位列表 - const getJobListCommand = { - command_type: 'getJobList', - command_name: '获取职位列表', - command_params: JSON.stringify({ - sn_code: sn_code, - keyword: keyword || accountConfig.keyword || '', - platform: platform || 'boss', - pageCount: pageCount || 3 - }), - priority: config.getTaskPriority('search_jobs') || 5 - }; - - await command.executeCommands(task.id, [getJobListCommand], this.mqttClient); - - // 4. 从数据库获取待投递的职位 - const pendingJobs = await job_postings.findAll({ - where: { - sn_code: sn_code, - platform: platform || 'boss', - applyStatus: 'pending' - }, - order: [['create_time', 'DESC']], - limit: actualMaxCount * 3 // 获取更多职位用于筛选(受每日投递上限限制) - }); - - if (!pendingJobs || pendingJobs.length === 0) { - console.log(`[任务处理器] 没有待投递的职位`); - return { - success: true, - deliveredCount: 0, - message: '没有待投递的职位' - }; - } - - // 5. 根据简历信息、职位类型配置和权重配置进行评分和过滤 - const scoredJobs = []; - - // 合并排除关键词:从职位类型配置和任务参数中获取 - const jobTypeExcludeKeywords = jobTypeConfig && jobTypeConfig.excludeKeywords - ? (typeof jobTypeConfig.excludeKeywords === 'string' - ? JSON.parse(jobTypeConfig.excludeKeywords) - : jobTypeConfig.excludeKeywords) - : []; - let taskExcludeKeywords = filterRules.excludeKeywords || []; - - // 如果 filterRules 中没有,尝试从 accountConfig.deliver_config 获取 - if ((!taskExcludeKeywords || taskExcludeKeywords.length === 0) && accountConfig.deliver_config) { - const deliverConfig = typeof accountConfig.deliver_config === 'string' - ? JSON.parse(accountConfig.deliver_config) - : accountConfig.deliver_config; - if (deliverConfig.exclude_keywords) { - taskExcludeKeywords = Array.isArray(deliverConfig.exclude_keywords) - ? deliverConfig.exclude_keywords - : (typeof deliverConfig.exclude_keywords === 'string' - ? JSON.parse(deliverConfig.exclude_keywords) - : []); - } - } - const excludeKeywords = [...jobTypeExcludeKeywords, ...taskExcludeKeywords]; - - // 获取过滤关键词(用于优先匹配或白名单过滤) - let filterKeywords = filterRules.keywords || []; - - // 如果 filterRules 中没有,尝试从 accountConfig.deliver_config 获取 - if ((!filterKeywords || filterKeywords.length === 0) && accountConfig.deliver_config) { - const deliverConfig = typeof accountConfig.deliver_config === 'string' - ? JSON.parse(accountConfig.deliver_config) - : accountConfig.deliver_config; - if (deliverConfig.filter_keywords) { - filterKeywords = Array.isArray(deliverConfig.filter_keywords) - ? deliverConfig.filter_keywords - : (typeof deliverConfig.filter_keywords === 'string' - ? JSON.parse(deliverConfig.filter_keywords) - : []); - } - } - - console.log(`[任务处理器] 过滤关键词配置 - 包含关键词: ${JSON.stringify(filterKeywords)}, 排除关键词: ${JSON.stringify(excludeKeywords)}`); - - // 获取薪资范围过滤(优先从 filterRules,如果没有则从 accountConfig.deliver_config 获取) - let minSalary = filterRules.minSalary || 0; - let maxSalary = filterRules.maxSalary || 0; - - // 如果 filterRules 中没有,尝试从 accountConfig.deliver_config 获取 - if (minSalary === 0 && maxSalary === 0 && accountConfig.deliver_config) { - const deliverConfig = typeof accountConfig.deliver_config === 'string' - ? JSON.parse(accountConfig.deliver_config) - : accountConfig.deliver_config; - minSalary = deliverConfig.min_salary || 0; - maxSalary = deliverConfig.max_salary || 0; - } - - console.log(`[任务处理器] 薪资过滤配置 - 最低: ${minSalary}元, 最高: ${maxSalary}元`); - - // 获取一个月内已投递的公司列表(用于过滤) - // 注意:apply_records 和 Sequelize 已在方法开头定义 - const oneMonthAgo = new Date(); - oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); - - const recentApplies = await apply_records.findAll({ - where: { - sn_code: sn_code, - applyTime: { - [Sequelize.Op.gte]: oneMonthAgo - } - }, - attributes: ['companyName'], - group: ['companyName'] - }); - - const recentCompanyNames = new Set(recentApplies.map(apply => apply.companyName).filter(Boolean)); - - for (const job of pendingJobs) { - const jobData = job.toJSON ? job.toJSON() : job; - - // 薪资范围过滤 - if (minSalary > 0 || maxSalary > 0) { - // 解析职位薪资字符串(如 "20-30K") - const jobSalaryRange = this.parseSalaryRange(jobData.salary || ''); - const jobSalaryMin = jobSalaryRange.min || 0; - const jobSalaryMax = jobSalaryRange.max || 0; - - // 如果职位没有薪资信息,跳过 - if (jobSalaryMin === 0 && jobSalaryMax === 0) { - console.log(`[任务处理器] 跳过无薪资信息的职位: ${jobData.jobTitle} @ ${jobData.companyName}`); - continue; - } - - // 如果职位薪资范围与过滤范围没有交集,则跳过 - if (minSalary > 0 && jobSalaryMax > 0 && minSalary > jobSalaryMax) { - console.log(`[任务处理器] 跳过薪资过低职位: ${jobData.jobTitle} @ ${jobData.companyName}, 职位薪资: ${jobData.salary}, 要求最低: ${minSalary}`); - continue; - } - if (maxSalary > 0 && jobSalaryMin > 0 && maxSalary < jobSalaryMin) { - console.log(`[任务处理器] 跳过薪资过高职位: ${jobData.jobTitle} @ ${jobData.companyName}, 职位薪资: ${jobData.salary}, 要求最高: ${maxSalary}`); - continue; - } - } - - // 如果配置了简历期望薪资,也要与职位薪资进行比较 - if (resumeInfo && resumeInfo.expectedSalary) { - const expectedSalaryRange = this.parseExpectedSalary(resumeInfo.expectedSalary); - if (expectedSalaryRange) { - const jobSalaryRange = this.parseSalaryRange(jobData.salary || ''); - const jobSalaryMin = jobSalaryRange.min || 0; - const jobSalaryMax = jobSalaryRange.max || 0; - - // 如果职位薪资明显低于期望薪资范围,跳过 - // 期望薪资是 "20-30K",职位薪资应该至少接近或高于期望薪资的最低值 - if (jobSalaryMax > 0 && expectedSalaryRange.min > 0 && jobSalaryMax < expectedSalaryRange.min * 0.8) { - console.log(`[任务处理器] 跳过薪资低于期望的职位: ${jobData.jobTitle} @ ${jobData.companyName}, 职位薪资: ${jobData.salary}, 期望薪资: ${resumeInfo.expectedSalary}`); - continue; - } - } - } - - // 排除关键词过滤 - if (Array.isArray(excludeKeywords) && excludeKeywords.length > 0) { - const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase(); - const matchedExcludeKeywords = excludeKeywords.filter(kw => { - const keyword = kw ? kw.toLowerCase().trim() : ''; - return keyword && jobText.includes(keyword); - }); - if (matchedExcludeKeywords.length > 0) { - console.log(`[任务处理器] 跳过包含排除关键词的职位: ${jobData.jobTitle} @ ${jobData.companyName}, 匹配: ${matchedExcludeKeywords.join(', ')}`); - continue; - } - } - - // 过滤关键词(白名单模式):如果设置了过滤关键词,只投递包含这些关键词的职位 - if (Array.isArray(filterKeywords) && filterKeywords.length > 0) { - const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase(); - const matchedKeywords = filterKeywords.filter(kw => { - const keyword = kw ? kw.toLowerCase().trim() : ''; - return keyword && jobText.includes(keyword); - }); - - if (matchedKeywords.length === 0) { - // 如果没有匹配到任何过滤关键词,跳过该职位(白名单模式) - console.log(`[任务处理器] 跳过未匹配过滤关键词的职位: ${jobData.jobTitle} @ ${jobData.companyName}, 过滤关键词: ${filterKeywords.join(', ')}`); - continue; - } else { - console.log(`[任务处理器] 职位匹配过滤关键词: ${jobData.jobTitle} @ ${jobData.companyName}, 匹配: ${matchedKeywords.join(', ')}`); - } - } - - // 检查该公司是否在一个月内已投递过 - if (jobData.companyName && recentCompanyNames.has(jobData.companyName)) { - console.log(`[任务处理器] 跳过一个月内已投递的公司: ${jobData.companyName}`); - continue; - } - - // 使用 job_filter_service 计算评分 - const scoreResult = jobFilterService.calculateJobScoreWithWeights( - jobData, - resumeInfo, - accountConfig, - jobTypeConfig, - priorityWeights - ); - - // 如果配置了过滤关键词,给包含这些关键词的职位加分(额外奖励) - let keywordBonus = 0; - if (Array.isArray(filterKeywords) && filterKeywords.length > 0) { - const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase(); - const matchedKeywords = filterKeywords.filter(kw => { - const keyword = kw ? kw.toLowerCase().trim() : ''; - return keyword && jobText.includes(keyword); - }); - if (matchedKeywords.length > 0) { - // 每匹配一个关键词加5分,最多加20分 - keywordBonus = Math.min(matchedKeywords.length * 5, 20); - } - } - - const finalScore = scoreResult.totalScore + keywordBonus; - - // 只保留总分 >= 60 的职位 - if (finalScore >= 60) { - scoredJobs.push({ - ...jobData, - matchScore: finalScore, - scoreDetails: { - ...scoreResult.scores, - keywordBonus: keywordBonus - } - }); - } - } - - // 按总分降序排序 - scoredJobs.sort((a, b) => b.matchScore - a.matchScore); - - // 取前 actualMaxCount 个职位(受每日投递上限限制) - const jobsToDeliver = scoredJobs.slice(0, actualMaxCount); - - console.log(`[任务处理器] 职位评分完成,共 ${pendingJobs.length} 个职位,评分后 ${scoredJobs.length} 个符合条件,将投递 ${jobsToDeliver.length} 个`); - - if (jobsToDeliver.length === 0) { - return { - success: true, - deliveredCount: 0, - message: '没有符合条件的职位' - }; - } - - // 6. 为每个职位创建一条独立的投递指令 - const deliverCommands = []; - for (const jobData of jobsToDeliver) { - console.log(`[任务处理器] 准备投递职位: ${jobData.jobTitle} @ ${jobData.companyName}, 评分: ${jobData.matchScore}`, jobData.scoreDetails); - deliverCommands.push({ - command_type: 'deliver_resume', // 与MQTT Action保持一致 - command_name: `投递简历 - ${jobData.jobTitle} @ ${jobData.companyName} (评分:${jobData.matchScore})`, - command_params: JSON.stringify({ - sn_code: sn_code, - platform: platform || 'boss', - jobId: jobData.jobId, - encryptBossId: jobData.encryptBossId || '', - securityId: jobData.securityId || '', - brandName: jobData.companyName, - jobTitle: jobData.jobTitle, - companyName: jobData.companyName, - matchScore: jobData.matchScore, - scoreDetails: jobData.scoreDetails - }), - priority: config.getTaskPriority('apply') || 6 - }); - } - - // 7. 执行所有投递指令 - const result = await command.executeCommands(task.id, deliverCommands, this.mqttClient); - const duration = Date.now() - startTime; - deviceManager.recordTaskComplete(sn_code, task, true, duration); - - console.log(`[任务处理器] 自动投递任务完成 - 设备: ${sn_code}, 创建了 ${deliverCommands.length} 条投递指令, 耗时: ${duration}ms`); - return result; - } catch (error) { - const duration = Date.now() - startTime; - deviceManager.recordTaskComplete(sn_code, task, false, duration); - console.error(`[任务处理器] 自动投递任务失败 - 设备: ${sn_code}:`, error); - throw error; - } - } - - /** - * 检查当前时间是否在指定的时间范围内 - * @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1} - * @returns {Object} {allowed: boolean, reason: string} - */ - 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(); // 0=周日, 1=周一, ..., 6=周六 - 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: `当前时间 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` }; - } - } else { - // 跨天情况:22:00 - 06:00 - if (currentTime < startTime && currentTime >= endTime) { - return { allowed: false, reason: `当前时间 ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')} 不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})` }; - } - } - - return { allowed: true, reason: '在允许的时间范围内' }; - } - - /** - * 处理搜索职位列表任务(新功能) - * 支持多条件搜索和可选投递 * @param {object} task - 任务对象 * @returns {Promise} 执行结果 */ - async handleSearchJobListTask(task) { - const { sn_code, taskParams } = task; - const { - keyword, - searchParams = {}, - pageCount = 3, - autoDeliver = false, - filterRules = {}, - maxCount = 10 - } = taskParams; - - console.log(`[任务处理器] 搜索职位列表任务 - 设备: ${sn_code}, 关键词: ${keyword}, 自动投递: ${autoDeliver}`); - - // 检查授权状态 - 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, - jobCount: 0, - deliveredCount: 0, - message: authCheck.message - }; - } - - deviceManager.recordTaskStart(sn_code, task); - const startTime = Date.now(); - - try { - const job_postings = db.getModel('job_postings'); - const pla_account = db.getModel('pla_account'); - const resume_info = db.getModel('resume_info'); - const apply_records = db.getModel('apply_records'); - const Sequelize = require('sequelize'); - - // 1. 获取账号配置 - const account = await pla_account.findOne({ - where: { sn_code, platform_type: taskParams.platform || 'boss' } - }); - - if (!account) { - throw new Error('账号不存在'); - } - - const accountConfig = account.toJSON(); - - // 2. 从账号配置中读取搜索条件 - const searchConfig = accountConfig.search_config - ? (typeof accountConfig.search_config === 'string' - ? JSON.parse(accountConfig.search_config) - : accountConfig.search_config) - : {}; - - // 3. 构建完整的搜索参数(任务参数优先,其次账号配置) - const searchCommandParams = { - sn_code: sn_code, - platform: taskParams.platform || accountConfig.platform_type || 'boss', - keyword: keyword || accountConfig.keyword || searchConfig.keyword || '', - city: searchParams.city || accountConfig.city || searchConfig.city || '', - cityName: searchParams.cityName || accountConfig.cityName || searchConfig.cityName || '', - salary: searchParams.salary || searchConfig.defaultSalary || '', - experience: searchParams.experience || searchConfig.defaultExperience || '', - education: searchParams.education || searchConfig.defaultEducation || '', - industry: searchParams.industry || searchConfig.industry || '', - companySize: searchParams.companySize || searchConfig.companySize || '', - financingStage: searchParams.financingStage || searchConfig.financingStage || '', - page: 1, - pageSize: 20, - pageCount: pageCount - }; - - // 4. 根据是否投递选择不同的指令 - let searchCommand; - if (autoDeliver) { - // 使用搜索并投递指令 - searchCommand = { - command_type: 'search_and_deliver', - command_name: '搜索并投递职位', - command_params: JSON.stringify({ - keyword: searchCommandParams.keyword, - searchParams: { - city: searchCommandParams.city, - cityName: searchCommandParams.cityName, - salary: searchCommandParams.salary, - experience: searchCommandParams.experience, - education: searchCommandParams.education, - industry: searchCommandParams.industry, - companySize: searchCommandParams.companySize, - financingStage: searchCommandParams.financingStage, - page: searchCommandParams.page, - pageSize: searchCommandParams.pageSize, - pageCount: searchCommandParams.pageCount - }, - filterRules: filterRules, - maxCount: maxCount, - platform: searchCommandParams.platform - }), - priority: config.getTaskPriority('search_and_deliver') || 5, - sequence: 1 - }; - } else { - // 使用多条件搜索指令(新的指令类型,使用新的MQTT action) - searchCommand = { - command_type: 'search_jobs_with_params', // 新的指令类型 - command_name: '多条件搜索职位列表', - command_params: JSON.stringify(searchCommandParams), // 包含多条件参数 - priority: config.getTaskPriority('search_jobs_with_params') || 5, - sequence: 1 - }; - } - - // 5. 执行指令 - const commandResult = await command.executeCommands(task.id, [searchCommand], this.mqttClient); - - // 6. 处理执行结果 - let jobCount = 0; - let deliveredCount = 0; - - if (autoDeliver) { - // 如果使用 search_and_deliver 指令,结果中已包含投递信息 - if (commandResult && commandResult.results && commandResult.results.length > 0) { - const result = commandResult.results[0].result; - if (result) { - jobCount = result.jobCount || 0; - deliveredCount = result.deliveredCount || 0; - } - } - } else { - // 如果使用 search_jobs_with_params 指令,等待搜索完成并从数据库获取结果 - await new Promise(resolve => setTimeout(resolve, 2000)); - - const searchedJobs = await job_postings.findAll({ - where: { - sn_code: sn_code, - platform: searchCommandParams.platform, - applyStatus: 'pending', - keyword: searchCommandParams.keyword - }, - order: [['create_time', 'DESC']], - limit: 1000 - }); - - jobCount = searchedJobs.length; - } - - const duration = Date.now() - startTime; - deviceManager.recordTaskComplete(sn_code, task, true, duration); - - console.log(`[任务处理器] 搜索职位列表任务完成 - 设备: ${sn_code}, 找到 ${jobCount} 个职位, 投递 ${deliveredCount} 个, 耗时: ${duration}ms`); - - return { - success: true, - jobCount: jobCount, - deliveredCount: deliveredCount, - message: autoDeliver - ? `搜索完成,找到 ${jobCount} 个职位,成功投递 ${deliveredCount} 个` - : `搜索完成,找到 ${jobCount} 个职位` - }; - - } catch (error) { - const duration = Date.now() - startTime; - deviceManager.recordTaskComplete(sn_code, task, false, duration); - console.error(`[任务处理器] 搜索职位列表任务失败 - 设备: ${sn_code}:`, error); - throw error; - } + async handleAutoDeliverTask(task) { + console.log(`[任务处理器] 调度自动投递任务 - 设备: ${task.sn_code}`); + return await this.deliverHandler.handle(task); } /** - * 处理自动沟通任务(待实现) - * 功能:自动与HR进行沟通,回复消息等 + * 处理自动沟通任务 + * @param {object} task - 任务对象 + * @returns {Promise} 执行结果 */ async handleAutoChatTask(task) { - const { sn_code, taskParams } = task; - console.log(`[任务处理器] 自动沟通任务 - 设备: ${sn_code}`); - - // 检查授权状态 - 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, - chatCount: 0, - message: authCheck.message - }; - } - - deviceManager.recordTaskStart(sn_code, task); - const startTime = Date.now(); - - try { - // 获取账号配置 - const pla_account = db.getModel('pla_account'); - const account = await pla_account.findOne({ - where: { sn_code: sn_code } - }); - - if (!account) { - throw new Error(`账号不存在: ${sn_code}`); - } - - const accountData = account.toJSON(); - - // 检查是否开启自动沟通 - if (!accountData.auto_chat) { - console.log(`[任务处理器] 设备 ${sn_code} 未开启自动沟通`); - return { - success: true, - message: '未开启自动沟通', - chatCount: 0 - }; - } - - // 解析沟通策略配置 - let chatStrategy = {}; - if (accountData.chat_strategy) { - chatStrategy = typeof accountData.chat_strategy === 'string' - ? JSON.parse(accountData.chat_strategy) - : accountData.chat_strategy; - } - - // 检查沟通时间范围 - if (chatStrategy.time_range) { - const timeCheck = this.checkTimeRange(chatStrategy.time_range); - if (!timeCheck.allowed) { - console.log(`[任务处理器] 自动沟通任务 - ${timeCheck.reason}`); - return { - success: true, - message: timeCheck.reason, - chatCount: 0 - }; - } - } - - // TODO: 实现自动沟通逻辑 - // 1. 获取待回复的聊天列表 - // 2. 根据消息内容生成回复 - // 3. 发送回复消息 - // 4. 记录沟通结果 - - console.log(`[任务处理器] 自动沟通任务 - 逻辑待实现`); - - const duration = Date.now() - startTime; - deviceManager.recordTaskComplete(sn_code, task, true, duration); - - return { - success: true, - message: '自动沟通任务框架已就绪,逻辑待实现', - chatCount: 0 - }; - } catch (error) { - const duration = Date.now() - startTime; - deviceManager.recordTaskComplete(sn_code, task, false, duration); - throw error; - } + console.log(`[任务处理器] 调度自动沟通任务 - 设备: ${task.sn_code}`); + return await this.chatHandler.handle(task); } /** - * 处理自动活跃账号任务(待实现) - * 功能:自动执行一些操作来保持账号活跃度,如浏览职位、搜索等 + * 处理自动活跃账户任务 + * @param {object} task - 任务对象 + * @returns {Promise} 执行结果 */ async handleAutoActiveAccountTask(task) { - const { sn_code, taskParams } = task; - console.log(`[任务处理器] 自动活跃账号任务 - 设备: ${sn_code}`); - - deviceManager.recordTaskStart(sn_code, task); - const startTime = Date.now(); - - try { - // TODO: 实现自动活跃账号逻辑 - // 1. 随机搜索一些职位 - // 2. 浏览职位详情 - // 3. 查看公司信息 - // 4. 执行一些模拟用户行为 - - console.log(`[任务处理器] 自动活跃账号任务 - 逻辑待实现`); - - const duration = Date.now() - startTime; - deviceManager.recordTaskComplete(sn_code, task, true, duration); - - return { - success: true, - message: '自动活跃账号任务框架已就绪,逻辑待实现', - actionCount: 0 - }; - } catch (error) { - const duration = Date.now() - startTime; - deviceManager.recordTaskComplete(sn_code, task, false, duration); - throw error; - } - } - - /** - * 解析职位薪资范围 - * @param {string} salaryDesc - 薪资描述(如 "20-30K"、"30-40K·18薪"、"5000-6000元/月") - * @returns {object} 薪资范围 { min, max },单位:元 - */ - parseSalaryRange(salaryDesc) { - if (!salaryDesc) return { min: 0, max: 0 }; - - // 1. 匹配K格式:40-60K, 30-40K·18薪(忽略后面的薪数) - const kMatch = salaryDesc.match(/(\d+)[-~](\d+)[kK千]/); - if (kMatch) { - return { - min: parseInt(kMatch[1]) * 1000, - max: parseInt(kMatch[2]) * 1000 - }; - } - - // 2. 匹配单个K值:25K - const singleKMatch = salaryDesc.match(/(\d+)[kK千]/); - if (singleKMatch) { - const value = parseInt(singleKMatch[1]) * 1000; - return { min: value, max: value }; - } - - // 3. 匹配元/月格式:5000-6000元/月 - const yuanMatch = salaryDesc.match(/(\d+)[-~](\d+)[元万]/); - if (yuanMatch) { - const min = parseInt(yuanMatch[1]); - const max = parseInt(yuanMatch[2]); - // 判断单位(万或元) - if (salaryDesc.includes('万')) { - return { - min: min * 10000, - max: max * 10000 - }; - } else { - return { min, max }; - } - } - - // 4. 匹配单个元/月值:5000元/月 - const singleYuanMatch = salaryDesc.match(/(\d+)[元万]/); - if (singleYuanMatch) { - const value = parseInt(singleYuanMatch[1]); - if (salaryDesc.includes('万')) { - return { min: value * 10000, max: value * 10000 }; - } else { - return { min: value, max: value }; - } - } - - // 5. 匹配纯数字格式(如:20000-30000) - const numMatch = salaryDesc.match(/(\d+)[-~](\d+)/); - if (numMatch) { - return { - min: parseInt(numMatch[1]), - max: parseInt(numMatch[2]) - }; - } - - return { min: 0, max: 0 }; - } - - /** - * 解析期望薪资范围 - * @param {string} expectedSalary - 期望薪资描述(如 "20-30K"、"5000-6000元/月") - * @returns {object|null} 期望薪资范围 { min, max },单位:元 - */ - parseExpectedSalary(expectedSalary) { - if (!expectedSalary) return null; - - // 1. 匹配K格式:20-30K - const kMatch = expectedSalary.match(/(\d+)[-~](\d+)[kK千]/); - if (kMatch) { - return { - min: parseInt(kMatch[1]) * 1000, - max: parseInt(kMatch[2]) * 1000 - }; - } - - // 2. 匹配单个K值:25K - const singleKMatch = expectedSalary.match(/(\d+)[kK千]/); - if (singleKMatch) { - const value = parseInt(singleKMatch[1]) * 1000; - return { min: value, max: value }; - } - - // 3. 匹配元/月格式:5000-6000元/月 - const yuanMatch = expectedSalary.match(/(\d+)[-~](\d+)[元万]/); - if (yuanMatch) { - const min = parseInt(yuanMatch[1]); - const max = parseInt(yuanMatch[2]); - // 判断单位(万或元) - if (expectedSalary.includes('万')) { - return { - min: min * 10000, - max: max * 10000 - }; - } else { - return { min, max }; - } - } - - // 4. 匹配单个元/月值:5000元/月 - const singleYuanMatch = expectedSalary.match(/(\d+)[元万]/); - if (singleYuanMatch) { - const value = parseInt(singleYuanMatch[1]); - if (expectedSalary.includes('万')) { - return { min: value * 10000, max: value * 10000 }; - } else { - return { min: value, max: value }; - } - } - - // 5. 匹配纯数字格式(如:20000-30000) - const numMatch = expectedSalary.match(/(\d+)[-~](\d+)/); - if (numMatch) { - return { - min: parseInt(numMatch[1]), - max: parseInt(numMatch[2]) - }; - } - - return null; + console.log(`[任务处理器] 调度自动活跃任务 - 设备: ${task.sn_code}`); + return await this.activeHandler.handle(task); } } module.exports = TaskHandlers; - diff --git a/api/middleware/schedule/tasks/baseTask.js b/api/middleware/schedule/tasks/baseTask.js index 43b7287..1291cf1 100644 --- a/api/middleware/schedule/tasks/baseTask.js +++ b/api/middleware/schedule/tasks/baseTask.js @@ -1,5 +1,5 @@ const dayjs = require('dayjs'); -const deviceManager = require('../deviceManager'); +const deviceManager = require('../core/deviceManager'); const db = require('../../dbProxy'); /** diff --git a/api/middleware/schedule/utils/index.js b/api/middleware/schedule/utils/index.js new file mode 100644 index 0000000..2ae20fb --- /dev/null +++ b/api/middleware/schedule/utils/index.js @@ -0,0 +1,14 @@ +/** + * Utils 模块导出 + * 统一导出工具类模块 + */ + +const SalaryParser = require('./salaryParser'); +const KeywordMatcher = require('./keywordMatcher'); +const ScheduleUtils = require('./scheduleUtils'); + +module.exports = { + SalaryParser, + KeywordMatcher, + ScheduleUtils +}; diff --git a/api/middleware/schedule/utils/keywordMatcher.js b/api/middleware/schedule/utils/keywordMatcher.js new file mode 100644 index 0000000..11c579d --- /dev/null +++ b/api/middleware/schedule/utils/keywordMatcher.js @@ -0,0 +1,225 @@ +/** + * 关键词匹配工具 + * 提供职位描述的关键词匹配和评分功能 + */ +class KeywordMatcher { + /** + * 检查是否包含排除关键词 + * @param {string} text - 待检查的文本 + * @param {string[]} excludeKeywords - 排除关键词列表 + * @returns {{matched: boolean, keywords: string[]}} 匹配结果 + */ + static matchExcludeKeywords(text, excludeKeywords = []) { + if (!text || !excludeKeywords || excludeKeywords.length === 0) { + return { matched: false, keywords: [] }; + } + + const matched = []; + const lowerText = text.toLowerCase(); + + for (const keyword of excludeKeywords) { + if (!keyword || !keyword.trim()) continue; + + const lowerKeyword = keyword.toLowerCase().trim(); + if (lowerText.includes(lowerKeyword)) { + matched.push(keyword); + } + } + + return { + matched: matched.length > 0, + keywords: matched + }; + } + + /** + * 检查是否包含过滤关键词(必须匹配) + * @param {string} text - 待检查的文本 + * @param {string[]} filterKeywords - 过滤关键词列表 + * @returns {{matched: boolean, keywords: string[], matchCount: number}} 匹配结果 + */ + static matchFilterKeywords(text, filterKeywords = []) { + if (!text) { + return { matched: false, keywords: [], matchCount: 0 }; + } + + if (!filterKeywords || filterKeywords.length === 0) { + return { matched: true, keywords: [], matchCount: 0 }; + } + + const matched = []; + const lowerText = text.toLowerCase(); + + for (const keyword of filterKeywords) { + if (!keyword || !keyword.trim()) continue; + + const lowerKeyword = keyword.toLowerCase().trim(); + if (lowerText.includes(lowerKeyword)) { + matched.push(keyword); + } + } + + // 只要匹配到至少一个过滤关键词即可通过 + return { + matched: matched.length > 0, + keywords: matched, + matchCount: matched.length + }; + } + + /** + * 计算关键词匹配奖励分数 + * @param {string} text - 待检查的文本 + * @param {string[]} keywords - 关键词列表 + * @param {object} options - 选项 + * @returns {{score: number, matchedKeywords: string[], matchCount: number}} + */ + static calculateBonus(text, keywords = [], options = {}) { + const { + baseScore = 10, // 每个关键词的基础分 + maxBonus = 50, // 最大奖励分 + caseSensitive = false // 是否区分大小写 + } = options; + + if (!text || !keywords || keywords.length === 0) { + return { score: 0, matchedKeywords: [], matchCount: 0 }; + } + + const matched = []; + const searchText = caseSensitive ? text : text.toLowerCase(); + + for (const keyword of keywords) { + if (!keyword || !keyword.trim()) continue; + + const searchKeyword = caseSensitive ? keyword.trim() : keyword.toLowerCase().trim(); + if (searchText.includes(searchKeyword)) { + matched.push(keyword); + } + } + + const score = Math.min(matched.length * baseScore, maxBonus); + + return { + score, + matchedKeywords: matched, + matchCount: matched.length + }; + } + + /** + * 高亮匹配的关键词(用于展示) + * @param {string} text - 原始文本 + * @param {string[]} keywords - 关键词列表 + * @param {string} prefix - 前缀标记(默认 ) + * @param {string} suffix - 后缀标记(默认 ) + * @returns {string} 高亮后的文本 + */ + static highlight(text, keywords = [], prefix = '', suffix = '') { + if (!text || !keywords || keywords.length === 0) { + return text; + } + + let result = text; + + for (const keyword of keywords) { + if (!keyword || !keyword.trim()) continue; + + const regex = new RegExp(`(${this.escapeRegex(keyword.trim())})`, 'gi'); + result = result.replace(regex, `${prefix}$1${suffix}`); + } + + return result; + } + + /** + * 转义正则表达式特殊字符 + * @param {string} str - 待转义的字符串 + * @returns {string} 转义后的字符串 + */ + static escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + /** + * 综合匹配(排除 + 过滤 + 奖励) + * @param {string} text - 待检查的文本 + * @param {object} config - 配置 + * @param {string[]} config.excludeKeywords - 排除关键词 + * @param {string[]} config.filterKeywords - 过滤关键词 + * @param {string[]} config.bonusKeywords - 奖励关键词 + * @returns {{pass: boolean, reason?: string, score: number, details: object}} + */ + static match(text, config = {}) { + const { + excludeKeywords = [], + filterKeywords = [], + bonusKeywords = [] + } = config; + + // 1. 检查排除关键词 + const excludeResult = this.matchExcludeKeywords(text, excludeKeywords); + if (excludeResult.matched) { + return { + pass: false, + reason: `包含排除关键词: ${excludeResult.keywords.join(', ')}`, + score: 0, + details: { exclude: excludeResult } + }; + } + + // 2. 检查过滤关键词(必须匹配) + const filterResult = this.matchFilterKeywords(text, filterKeywords); + if (filterKeywords.length > 0 && !filterResult.matched) { + return { + pass: false, + reason: '不包含任何必需关键词', + score: 0, + details: { filter: filterResult } + }; + } + + // 3. 计算奖励分数 + const bonusResult = this.calculateBonus(text, bonusKeywords); + + return { + pass: true, + score: bonusResult.score, + details: { + exclude: excludeResult, + filter: filterResult, + bonus: bonusResult + } + }; + } + + /** + * 批量匹配职位列表 + * @param {Array} jobs - 职位列表 + * @param {object} config - 匹配配置 + * @param {Function} textExtractor - 文本提取函数 (job) => string + * @returns {Array} 匹配通过的职位(带匹配信息) + */ + static filterJobs(jobs, config, textExtractor = (job) => `${job.name || ''} ${job.description || ''}`) { + if (!jobs || jobs.length === 0) { + return []; + } + + const filtered = []; + + for (const job of jobs) { + const text = textExtractor(job); + const matchResult = this.match(text, config); + + if (matchResult.pass) { + filtered.push({ + ...job, + _matchInfo: matchResult + }); + } + } + + return filtered; + } +} + +module.exports = KeywordMatcher; diff --git a/api/middleware/schedule/utils/salaryParser.js b/api/middleware/schedule/utils/salaryParser.js new file mode 100644 index 0000000..44f1836 --- /dev/null +++ b/api/middleware/schedule/utils/salaryParser.js @@ -0,0 +1,126 @@ +/** + * 薪资解析工具 + * 统一处理职位薪资和期望薪资的解析逻辑 + */ +class SalaryParser { + /** + * 解析薪资范围字符串 + * @param {string} salaryDesc - 薪资描述 (如 "15-20K", "8000-12000元") + * @returns {{ min: number, max: number }} 薪资范围(单位:元) + */ + static parse(salaryDesc) { + if (!salaryDesc || typeof salaryDesc !== 'string') { + return { min: 0, max: 0 }; + } + + // 尝试各种格式 + return this.parseK(salaryDesc) + || this.parseYuan(salaryDesc) + || this.parseMixed(salaryDesc) + || { min: 0, max: 0 }; + } + + /** + * 解析 K 格式薪资 (如 "15-20K", "8-12k") + */ + static parseK(desc) { + const kMatch = desc.match(/(\d+)[-~](\d+)[kK千]/); + if (kMatch) { + return { + min: parseInt(kMatch[1]) * 1000, + max: parseInt(kMatch[2]) * 1000 + }; + } + return null; + } + + /** + * 解析元格式薪资 (如 "8000-12000元", "15000-20000") + */ + static parseYuan(desc) { + const yuanMatch = desc.match(/(\d+)[-~](\d+)元?/); + if (yuanMatch) { + return { + min: parseInt(yuanMatch[1]), + max: parseInt(yuanMatch[2]) + }; + } + return null; + } + + /** + * 解析混合格式 (如 "8k-12000元") + */ + static parseMixed(desc) { + const mixedMatch = desc.match(/(\d+)[kK千][-~](\d+)元?/); + if (mixedMatch) { + return { + min: parseInt(mixedMatch[1]) * 1000, + max: parseInt(mixedMatch[2]) + }; + } + return null; + } + + /** + * 检查职位薪资是否在期望范围内 + * @param {object} jobSalary - 职位薪资 { min, max } + * @param {number} minExpected - 期望最低薪资 + * @param {number} maxExpected - 期望最高薪资 + */ + static isWithinRange(jobSalary, minExpected, maxExpected) { + if (!jobSalary || jobSalary.min === 0) { + return true; // 无法判断时默认通过 + } + + // 职位最高薪资 >= 期望最低薪资 + if (minExpected > 0 && jobSalary.max < minExpected) { + return false; + } + + // 职位最低薪资 <= 期望最高薪资 + if (maxExpected > 0 && jobSalary.min > maxExpected) { + return false; + } + + return true; + } + + /** + * 计算薪资匹配度(用于职位评分) + * @param {object} jobSalary - 职位薪资 + * @param {object} expectedSalary - 期望薪资 + * @returns {number} 匹配度 0-1 + */ + static calculateMatch(jobSalary, expectedSalary) { + if (!jobSalary || !expectedSalary || jobSalary.min === 0 || expectedSalary.min === 0) { + return 0.5; // 无法判断时返回中性值 + } + + const jobAvg = (jobSalary.min + jobSalary.max) / 2; + const expectedAvg = (expectedSalary.min + expectedSalary.max) / 2; + + const diff = Math.abs(jobAvg - expectedAvg); + const range = (jobSalary.max - jobSalary.min + expectedSalary.max - expectedSalary.min) / 2; + + // 差距越小,匹配度越高 + return Math.max(0, 1 - diff / (range || 1)); + } + + /** + * 格式化薪资显示 + * @param {object} salary - 薪资对象 { min, max } + * @returns {string} 格式化字符串 + */ + static format(salary) { + if (!salary || salary.min === 0) { + return '面议'; + } + + const minK = (salary.min / 1000).toFixed(0); + const maxK = (salary.max / 1000).toFixed(0); + return `${minK}-${maxK}K`; + } +} + +module.exports = SalaryParser; diff --git a/api/middleware/schedule/utils.js b/api/middleware/schedule/utils/scheduleUtils.js similarity index 100% rename from api/middleware/schedule/utils.js rename to api/middleware/schedule/utils/scheduleUtils.js