Merge branch 'dev2' into dev

This commit is contained in:
张成
2026-02-27 16:47:40 +08:00
67 changed files with 5725 additions and 6882 deletions

View File

@@ -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);
@@ -213,29 +213,17 @@ class CommandManager {
* @private
*/
async _execute_command_with_timeout(command_id, command_type, command_name, command_params, sn_code, mqttClient, start_time) {
// 将驼峰命名转换为下划线命名
const to_snake_case = (str) => {
if (str.includes('_')) {
return str;
}
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
};
const method_name = to_snake_case(command_type);
// 获取指令超时时间从配置中获取默认5分钟
const timeout = ScheduleConfig.taskTimeouts[command_type] ||
ScheduleConfig.taskTimeouts[method_name] ||
5 * 60 * 1000;
const timeout = ScheduleConfig.taskTimeouts[command_type] || 5 * 60 * 1000;
// 构建指令执行 Promise
const command_promise = (async () => {
if (command_type && jobManager[method_name]) {
return await jobManager[method_name](sn_code, mqttClient, command_params);
} else if (jobManager[command_type]) {
// 直接使用 command_type 调用 jobManager 的方法,不做映射
// command_type 和 jobManager 的方法名保持一致
if (jobManager[command_type]) {
return await jobManager[command_type](sn_code, mqttClient, command_params);
} else {
throw new Error(`未知的指令类型: ${command_type} (尝试的方法名: ${method_name})`);
throw new Error(`未知的指令类型: ${command_type}, jobManager 中不存在对应方法`);
}
})();

View File

@@ -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');
/**
* 设备管理器简化版
@@ -77,7 +77,6 @@ class DeviceManager {
// 更新登录状态
if (heartbeatData.isLoggedIn !== undefined) {
device.isLoggedIn = heartbeatData.isLoggedIn;
console.log(`[设备管理器] 设备 ${sn_code} 登录状态更新 - isLoggedIn: ${device.isLoggedIn}`);
}
}

View File

@@ -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
};

View File

@@ -0,0 +1,570 @@
const node_schedule = require("node-schedule");
const dayjs = require('dayjs');
const config = require('../infrastructure/config.js');
const deviceManager = require('./deviceManager.js');
const command = require('./command.js');
const db = require('../../dbProxy');
// 引入新的任务模块
const tasks = require('../tasks');
const { autoSearchTask, autoDeliverTask, autoChatTask, autoActiveTask } = tasks;
const Framework = require("../../../../framework/node-core-framework.js");
/**
* 定时任务管理器(重构版)
* 使用独立的任务模块,职责更清晰,易于维护和扩展
*/
class ScheduledJobs {
constructor(components, taskHandlers) {
this.taskQueue = components.taskQueue;
this.taskHandlers = taskHandlers;
this.jobs = [];
}
/**
* 启动所有定时任务
*/
start() {
console.log('[定时任务] 开始启动所有定时任务...');
// ==================== 系统维护任务 ====================
// 每天凌晨重置统计数据
const resetJob = node_schedule.scheduleJob(config.schedules.dailyReset, () => {
this.resetDailyStats();
});
this.jobs.push(resetJob);
console.log('[定时任务] ✓ 已启动每日统计重置任务');
// 启动心跳检查定时任务(每分钟检查一次)
const monitoringJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await deviceManager.checkHeartbeatStatus().catch(error => {
console.error('[定时任务] 检查心跳状态失败:', error);
});
});
this.jobs.push(monitoringJob);
console.log('[定时任务] ✓ 已启动心跳检查任务');
// 启动离线设备任务清理定时任务(每分钟检查一次)
const cleanupOfflineTasksJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.cleanupOfflineDeviceTasks().catch(error => {
console.error('[定时任务] 清理离线设备任务失败:', error);
});
});
this.jobs.push(cleanupOfflineTasksJob);
console.log('[定时任务] ✓ 已启动离线设备任务清理任务');
// 启动任务超时检查定时任务(每分钟检查一次)
const timeoutCheckJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.checkTaskTimeouts().catch(error => {
console.error('[定时任务] 检查任务超时失败:', error);
});
});
this.jobs.push(timeoutCheckJob);
console.log('[定时任务] ✓ 已启动任务超时检查任务');
// 启动任务状态摘要同步定时任务每10秒发送一次
const taskSummaryJob = node_schedule.scheduleJob('*/10 * * * * *', async () => {
await this.syncTaskStatusSummary().catch(error => {
console.error('[定时任务] 同步任务状态摘要失败:', error);
});
});
this.jobs.push(taskSummaryJob);
console.log('[定时任务] ✓ 已启动任务状态摘要同步任务');
// ==================== 业务任务(使用新的任务模块) ====================
// 1. 自动搜索任务 - 每60分钟执行一次
const autoSearchJob = node_schedule.scheduleJob(config.schedules.autoSearch || '0 0 */1 * * *', () => {
this.runAutoSearchTask();
});
this.jobs.push(autoSearchJob);
console.log('[定时任务] ✓ 已启动自动搜索任务 (每60分钟)');
// 2. 自动投递任务 - 每1分钟检查一次
const autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
this.runAutoDeliverTask();
});
this.jobs.push(autoDeliverJob);
console.log('[定时任务] ✓ 已启动自动投递任务 (每1分钟)');
// 3. 自动沟通任务 - 每15分钟执行一次
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */15 * * * *', () => {
this.runAutoChatTask();
});
this.jobs.push(autoChatJob);
console.log('[定时任务] ✓ 已启动自动沟通任务 (每15分钟)');
// 4. 自动活跃任务 - 每2小时执行一次
const autoActiveJob = node_schedule.scheduleJob(config.schedules.autoActive || '0 0 */2 * * *', () => {
this.runAutoActiveTask();
});
this.jobs.push(autoActiveJob);
console.log('[定时任务] ✓ 已启动自动活跃任务 (每2小时)');
// 立即执行一次业务任务(可选)
setTimeout(() => {
console.log('[定时任务] 立即执行一次初始化任务...');
this.runAutoDeliverTask();
this.runAutoChatTask();
}, 10000); // 延迟10秒,等待系统初始化完成和设备心跳
console.log('[定时任务] 所有定时任务启动完成!');
}
// ==================== 业务任务执行方法(使用新的任务模块) ====================
/**
* 运行自动搜索任务
* 为所有启用自动搜索的账号添加搜索任务
*/
async runAutoSearchTask() {
try {
const accounts = await this.getEnabledAccounts('auto_search');
if (accounts.length === 0) {
return;
}
console.log(`[自动搜索调度] 找到 ${accounts.length} 个启用自动搜索的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoSearchTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动搜索调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动搜索调度] 执行失败:', error);
}
}
/**
* 运行自动投递任务
* 为所有启用自动投递的账号添加投递任务
*/
async runAutoDeliverTask() {
try {
const accounts = await this.getEnabledAccounts('auto_deliver');
if (accounts.length === 0) {
return;
}
console.log(`[自动投递调度] 找到 ${accounts.length} 个启用自动投递的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoDeliverTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动投递调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动投递调度] 执行失败:', error);
}
}
/**
* 运行自动沟通任务
* 为所有启用自动沟通的账号添加沟通任务
*/
async runAutoChatTask() {
try {
const accounts = await this.getEnabledAccounts('auto_chat');
if (accounts.length === 0) {
return;
}
console.log(`[自动沟通调度] 找到 ${accounts.length} 个启用自动沟通的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoChatTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动沟通调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动沟通调度] 执行失败:', error);
}
}
/**
* 运行自动活跃任务
* 为所有启用自动活跃的账号添加活跃任务
*/
async runAutoActiveTask() {
try {
const accounts = await this.getEnabledAccounts('auto_active');
if (accounts.length === 0) {
return;
}
console.log(`[自动活跃调度] 找到 ${accounts.length} 个启用自动活跃的账号`);
let successCount = 0;
let failedCount = 0;
for (const account of accounts) {
const result = await autoActiveTask.addToQueue(account.sn_code, this.taskQueue);
if (result.success) {
successCount++;
} else {
failedCount++;
}
}
if (successCount > 0 || failedCount > 0) {
console.log(`[自动活跃调度] 完成: 成功 ${successCount} 个, 失败/跳过 ${failedCount}`);
}
} catch (error) {
console.error('[自动活跃调度] 执行失败:', error);
}
}
/**
* 获取启用指定功能的账号列表
* @param {string} featureType - 功能类型: auto_search, auto_deliver, auto_chat, auto_active
*/
async getEnabledAccounts(featureType) {
try {
const { pla_account } = db.models;
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1,
[featureType]: 1
},
attributes: ['sn_code', 'name', 'keyword', 'platform_type']
});
return accounts.map(acc => acc.toJSON());
} catch (error) {
console.error(`[获取账号列表] 失败 (${featureType}):`, error);
return [];
}
}
// ==================== 系统维护方法(保留原有逻辑) ====================
/**
* 重置每日统计
*/
resetDailyStats() {
console.log('[定时任务] 重置每日统计数据');
try {
deviceManager.resetAllDailyCounters();
console.log('[定时任务] 每日统计重置完成');
} catch (error) {
console.error('[定时任务] 重置统计失败:', error);
}
}
/**
* 清理过期数据
*/
cleanupCaches() {
console.log('[定时任务] 开始清理过期数据');
try {
deviceManager.cleanupOfflineDevices(config.monitoring.offlineThreshold);
command.cleanupExpiredCommands(30);
console.log('[定时任务] 数据清理完成');
} catch (error) {
console.error('[定时任务] 数据清理失败:', error);
}
}
/**
* 清理离线设备任务
* 检查离线超过10分钟的设备取消其所有pending/running状态的任务
*/
async cleanupOfflineDeviceTasks() {
try {
// 离线阈值10分钟
const offlineThreshold = 10 * 60 * 1000;
const now = Date.now();
const thresholdTime = now - offlineThreshold;
// 获取所有启用的账号
const pla_account = db.getModel('pla_account');
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
// 通过 deviceManager 检查哪些设备离线超过10分钟
const offlineSnCodes = [];
const offlineDevicesInfo = [];
for (const account of accounts) {
const sn_code = account.sn_code;
const device = deviceManager.devices.get(sn_code);
if (!device) {
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: null
});
} else {
const lastHeartbeat = device.lastHeartbeat || 0;
if (lastHeartbeat < thresholdTime || !device.isOnline) {
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: lastHeartbeat ? new Date(lastHeartbeat) : null
});
}
}
}
if (offlineSnCodes.length === 0) {
return;
}
console.log(`[清理离线任务] 发现 ${offlineSnCodes.length} 个离线超过10分钟的设备`);
let totalCancelled = 0;
const task_status = db.getModel('task_status');
for (const sn_code of offlineSnCodes) {
try {
const deviceInfo = offlineDevicesInfo.find(d => d.sn_code === sn_code);
const updateResult = await task_status.update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({
reason: '设备离线超过10分钟任务已自动取消',
offlineTime: deviceInfo?.lastHeartbeatTime
})
},
{
where: {
sn_code: sn_code,
status: ['pending', 'running']
}
}
);
const cancelledCount = Array.isArray(updateResult) ? updateResult[0] : updateResult;
totalCancelled += cancelledCount;
if (this.taskQueue && typeof this.taskQueue.cancelDeviceTasks === 'function') {
await this.taskQueue.cancelDeviceTasks(sn_code);
}
if (cancelledCount > 0) {
console.log(`[清理离线任务] 设备 ${sn_code} 已取消 ${cancelledCount} 个任务`);
}
} catch (error) {
console.error(`[清理离线任务] 取消设备 ${sn_code} 的任务失败:`, error);
}
}
if (totalCancelled > 0) {
console.log(`[清理离线任务] 共取消 ${totalCancelled} 个离线设备的任务`);
}
} catch (error) {
console.error('[清理离线任务] 执行失败:', error);
}
}
/**
* 同步任务状态摘要到客户端
*/
async syncTaskStatusSummary() {
try {
const { pla_account } = await Framework.getModels();
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
const offlineThreshold = 3 * 60 * 1000;
const now = Date.now();
for (const account of accounts) {
const sn_code = account.sn_code;
const device = deviceManager.devices.get(sn_code);
if (!device) {
continue;
}
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
continue;
}
try {
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
const summary = await this.taskQueue.getTaskStatusSummary(sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, summary, {
currentCommand: summary.currentCommand || null
});
} catch (error) {
console.error(`[设备工作状态同步] 设备 ${sn_code} 同步失败:`, error.message);
}
}
} catch (error) {
console.error('[任务状态同步] 执行失败:', error);
}
}
/**
* 检查任务超时并强制标记为失败
*/
async checkTaskTimeouts() {
try {
const Sequelize = require('sequelize');
const { task_status, op } = db.models;
const runningTasks = await task_status.findAll({
where: {
status: 'running'
},
attributes: ['id', 'sn_code', 'taskType', 'taskName', 'startTime', 'create_time']
});
if (!runningTasks || runningTasks.length === 0) {
return;
}
const now = new Date();
let timeoutCount = 0;
for (const task of runningTasks) {
const taskData = task.toJSON();
const startTime = taskData.startTime ? new Date(taskData.startTime) : (taskData.create_time ? new Date(taskData.create_time) : null);
if (!startTime) {
continue;
}
const taskTimeout = config.getTaskTimeout(taskData.taskType) || 10 * 60 * 1000;
const maxAllowedTime = taskTimeout * 1.2;
const elapsedTime = now.getTime() - startTime.getTime();
if (elapsedTime > maxAllowedTime) {
try {
await task_status.update(
{
status: 'failed',
endTime: now,
duration: elapsedTime,
result: JSON.stringify({
error: `任务执行超时(运行时间: ${Math.round(elapsedTime / 1000)}秒,超时限制: ${Math.round(maxAllowedTime / 1000)}秒)`,
timeout: true,
taskType: taskData.taskType,
startTime: startTime.toISOString()
}),
progress: 0
},
{
where: { id: taskData.id }
}
);
timeoutCount++;
console.warn(`[任务超时检查] 任务 ${taskData.id} (${taskData.taskName}) 运行时间过长,已强制标记为失败`);
if (this.taskQueue && typeof this.taskQueue.deviceStatus !== 'undefined') {
const deviceStatus = this.taskQueue.deviceStatus.get(taskData.sn_code);
if (deviceStatus && deviceStatus.currentTask && deviceStatus.currentTask.id === taskData.id) {
deviceStatus.isRunning = false;
deviceStatus.currentTask = null;
deviceStatus.runningCount = Math.max(0, deviceStatus.runningCount - 1);
this.taskQueue.globalRunningCount = Math.max(0, this.taskQueue.globalRunningCount - 1);
console.log(`[任务超时检查] 已重置设备 ${taskData.sn_code} 的状态`);
setTimeout(() => {
this.taskQueue.processQueue(taskData.sn_code).catch(error => {
console.error(`[任务超时检查] 继续处理队列失败:`, error);
});
}, 100);
}
}
} catch (error) {
console.error(`[任务超时检查] 更新超时任务状态失败:`, error);
}
}
}
if (timeoutCount > 0) {
console.log(`[任务超时检查] 共检测到 ${timeoutCount} 个超时任务`);
}
} catch (error) {
console.error('[任务超时检查] 执行失败:', error);
}
}
/**
* 停止所有定时任务
*/
stop() {
console.log('[定时任务] 停止所有定时任务...');
for (const job of this.jobs) {
if (job) {
job.cancel();
}
}
this.jobs = [];
console.log('[定时任务] 所有定时任务已停止');
}
}
module.exports = ScheduledJobs;

View File

@@ -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;
@@ -406,7 +388,7 @@ class TaskQueue {
const authDays = accountData.authorization_days || 0;
// 使用工具函数计算剩余天数
const { calculateRemainingDays } = require('../../utils/account_utils');
const { calculateRemainingDays } = require('../../../utils/account_utils');
const remaining_days = calculateRemainingDays(authDate, authDays);
// 如果没有授权信息或剩余天数 <= 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) {

View File

@@ -0,0 +1,88 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const command = require('../core/command');
const config = require('../infrastructure/config');
/**
* 自动活跃处理器
* 负责保持账户活跃度
*/
class ActiveHandler extends BaseHandler {
/**
* 处理自动活跃任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
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;

View File

@@ -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<object>} 执行结果
*/
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<object|null>}
*/
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;

View File

@@ -0,0 +1,89 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const command = require('../core/command');
const config = require('../infrastructure/config');
/**
* 自动沟通处理器
* 负责自动回复HR消息
*/
class ChatHandler extends BaseHandler {
/**
* 处理自动沟通任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
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. 创建自动沟通 AI 指令(内部会先获取列表,再获取详情并自动回复)
const chatCommand = {
command_type: 'auto_chat_ai',
command_name: '自动沟通AI回复',
command_params: {
platform: platform || accountConfig.platform_type || 'boss',
pageCount: chatStrategy.page_count || 3
},
priority: config.getTaskPriority('auto_chat') || 6
};
// 5. 执行指令(任务队列会保证该设备内串行执行,不并发下发指令)
const exec_result = await command.executeCommands(task.id, [chatCommand], this.mqttClient);
const first = exec_result && Array.isArray(exec_result.results) && exec_result.results[0]
? exec_result.results[0].result || {}
: {};
console.log(`[自动沟通] 完成 - 设备: ${sn_code}`);
return {
chatCount: first.replied_count || 0,
message: first.message || '自动沟通完成',
detail: first
};
}
}
module.exports = ChatHandler;

View File

@@ -0,0 +1,410 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const jobFilterEngine = require('../services/jobFilterEngine');
const command = require('../core/command');
const config = require('../infrastructure/config');
const db = require('../../dbProxy');
const { jobFilterService } = require('../../job/services');
/**
* 自动投递处理器
* 负责职位搜索、过滤、评分和自动投递
*/
class DeliverHandler extends BaseHandler {
/**
* 处理自动投递任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
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;

View File

@@ -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
};

View File

@@ -0,0 +1,87 @@
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const command = require('../core/command');
const config = require('../infrastructure/config');
/**
* 自动搜索处理器
* 负责搜索职位列表
*/
class SearchHandler extends BaseHandler {
/**
* 处理自动搜索任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
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;

View File

@@ -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,9 +86,9 @@ class ScheduleManager {
async initComponents() {
// 初始化设备管理器
await deviceManager.init();
// 初始化任务队列
await TaskQueue.init?.();
await TaskQueue.init();
}
/**
@@ -126,14 +132,7 @@ class ScheduleManager {
});
}
/**
* 手动执行找工作流程已废弃full_flow 不再使用)
* @deprecated 请使用其他任务类型,如 auto_deliver
*/
async manualExecuteJobFlow(sn_code, keyword = '前端') {
console.warn(`[手动执行] manualExecuteJobFlow 已废弃full_flow 不再使用`);
throw new Error('full_flow 任务类型已废弃,请使用其他任务类型');
}
/**
* 获取系统状态
@@ -178,28 +177,18 @@ class ScheduleManager {
}
}
// 创建调度管理器实例
// 创建并导出调度管理器实例
const scheduleManager = new ScheduleManager();
// 导出兼容接口,保持与原有代码的一致性
// 导出兼容接口(简化版)
module.exports = {
// 初始化方法
init: () => scheduleManager.init(),
// 手动执行任务
manualExecuteJobFlow: (sn_code, keyword) => scheduleManager.manualExecuteJobFlow(sn_code, keyword),
// 获取系统状态
getSystemStatus: () => scheduleManager.getSystemStatus(),
// 停止系统
stop: () => scheduleManager.stop(),
// 访问各个组件(为了兼容性
// 直接暴露属性(使用 getter 保持动态访问)
get mqttClient() { return scheduleManager.mqttClient; },
get isInitialized() { return scheduleManager.isInitialized; },
// 访问各个组件实例(简化版)
get taskQueue() { return TaskQueue; },
get command() { return Command; },
get deviceManager() { return deviceManager; }

View File

@@ -1,4 +1,4 @@
const db = require('../dbProxy');
const db = require('../../dbProxy');
/**
* 统一错误处理模块

View File

@@ -23,6 +23,7 @@ class ScheduleConfig {
// 任务超时配置(毫秒)
this.taskTimeouts = {
auto_search: 20 * 60 * 1000, // 自动搜索任务20分钟
auto_deliver: 30 * 60 * 1000, // 自动投递任务30分钟包含多个子任务
auto_chat: 15 * 60 * 1000, // 自动沟通任务15分钟
auto_active_account: 10 * 60 * 1000 // 自动活跃账号任务10分钟
@@ -30,6 +31,7 @@ class ScheduleConfig {
// 任务优先级配置
this.taskPriorities = {
auto_search: 8, // 自动搜索任务(最高优先级,先搜索后投递)
auto_deliver: 7, // 自动投递任务
auto_chat: 6, // 自动沟通任务
auto_active_account: 5, // 自动活跃账号任务
@@ -44,10 +46,12 @@ class ScheduleConfig {
// 定时任务配置
this.schedules = {
dailyReset: '0 0 * * *', // 每天凌晨重置统计
monitoringInterval: '*/1 * * * *', // 监控检查间隔1分钟
autoDeliver: '0 */1 * * * *', // 自动投递任务每1分钟执行一次
autoChat: '0 */15 * * * *' // 自动沟通任务:每15分钟执行一次
dailyReset: '0 0 * * *', // 每天凌晨重置统计
monitoringInterval: '*/1 * * * *', // 监控检查间隔1分钟
autoSearch: '0 0 */1 * * *', // 自动搜索任务每1小时执行一次
autoDeliver: '0 */2 * * * *', // 自动投递任务:每2分钟执行一次
autoChat: '0 */15 * * * *', // 自动沟通任务每15分钟执行一次
autoActive: '0 0 */2 * * *' // 自动活跃任务每2小时执行一次
};
}

View File

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

View File

@@ -3,7 +3,7 @@
* 负责向客户端推送设备当前工作状态任务指令等
*/
const db = require('../dbProxy');
const db = require('../../dbProxy');
class DeviceWorkStatusNotifier {
constructor() {
@@ -214,11 +214,11 @@ class DeviceWorkStatusNotifier {
return `投递职位: ${parsedParams.jobTitle} @ ${companyName}`;
} else if (parsedParams.jobTitle) {
return `投递职位: ${parsedParams.jobTitle}`;
} else if (commandType === 'applyJob' || commandName.includes('投递')) {
} else if (commandType === 'deliver_resume' || commandName.includes('投递')) {
return '投递简历';
} else if (commandType === 'searchJobs' || commandName.includes('搜索')) {
return `搜索职位: ${parsedParams.keyword || ''}`;
} else if (commandType === 'sendChatMessage' || commandName.includes('沟通')) {
} else if (commandType === 'send_chat_message' || commandType === 'sendChatMessage' || commandName.includes('沟通')) {
return '发送消息';
} else if (commandName) {
return commandName;

View File

@@ -0,0 +1,9 @@
/**
* Notifiers 模块导出
*/
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
module.exports = {
deviceWorkStatusNotifier
};

View File

@@ -1,779 +0,0 @@
const node_schedule = require("node-schedule");
const dayjs = require('dayjs');
const config = require('./config.js');
const deviceManager = require('./deviceManager.js');
const command = require('./command.js');
const db = require('../dbProxy');
const authorizationService = require('../../services/authorization_service.js');
const Framework = require("../../../framework/node-core-framework.js");
/**
* 检查当前时间是否在指定的时间范围内
* @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
* @returns {Object} {allowed: boolean, reason: string}
*/
function 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: '在允许的时间范围内' };
}
/**
* 定时任务管理器(简化版)
* 管理所有定时任务的创建和销毁
*/
class ScheduledJobs {
constructor(components, taskHandlers) {
this.taskQueue = components.taskQueue;
this.taskHandlers = taskHandlers;
this.jobs = [];
}
/**
* 启动所有定时任务
*/
start() {
// 每天凌晨重置统计数据
const resetJob = node_schedule.scheduleJob(config.schedules.dailyReset, () => {
this.resetDailyStats();
});
this.jobs.push(resetJob);
// 启动心跳检查定时任务(每分钟检查一次)
const monitoringJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await deviceManager.checkHeartbeatStatus().catch(error => {
console.error('[定时任务] 检查心跳状态失败:', error);
});
});
this.jobs.push(monitoringJob);
// 启动离线设备任务清理定时任务(每分钟检查一次)
const cleanupOfflineTasksJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.cleanupOfflineDeviceTasks().catch(error => {
console.error('[定时任务] 清理离线设备任务失败:', error);
});
});
this.jobs.push(cleanupOfflineTasksJob);
console.log('[定时任务] 已启动离线设备任务清理任务');
// 启动任务超时检查定时任务(每分钟检查一次)
const timeoutCheckJob = node_schedule.scheduleJob(config.schedules.monitoringInterval, async () => {
await this.checkTaskTimeouts().catch(error => {
console.error('[定时任务] 检查任务超时失败:', error);
});
});
this.jobs.push(timeoutCheckJob);
console.log('[定时任务] 已启动任务超时检查任务');
// 启动任务状态摘要同步定时任务每10秒发送一次
const taskSummaryJob = node_schedule.scheduleJob('*/10 * * * * *', async () => {
await this.syncTaskStatusSummary().catch(error => {
console.error('[定时任务] 同步任务状态摘要失败:', error);
});
});
this.jobs.push(taskSummaryJob);
console.log('[定时任务] 已启动任务状态摘要同步任务');
// 执行自动投递任务
const autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
this.autoDeliverTask();
});
// 立即执行一次自动投递任务
this.autoDeliverTask();
this.jobs.push(autoDeliverJob);
console.log('[定时任务] 已启动自动投递任务');
// 执行自动沟通任务
const autoChatJob = node_schedule.scheduleJob(config.schedules.autoChat || '0 */15 * * * *', () => {
this.autoChatTask();
});
// 立即执行一次自动沟通任务
this.autoChatTask();
this.jobs.push(autoChatJob);
console.log('[定时任务] 已启动自动沟通任务');
}
/**
* 重置每日统计
*/
resetDailyStats() {
console.log('[定时任务] 重置每日统计数据');
try {
deviceManager.resetAllDailyCounters();
console.log('[定时任务] 每日统计重置完成');
} catch (error) {
console.error('[定时任务] 重置统计失败:', error);
}
}
/**
* 清理过期数据
*/
cleanupCaches() {
console.log('[定时任务] 开始清理过期数据');
try {
deviceManager.cleanupOfflineDevices(config.monitoring.offlineThreshold);
command.cleanupExpiredCommands(30);
console.log('[定时任务] 数据清理完成');
} catch (error) {
console.error('[定时任务] 数据清理失败:', error);
}
}
/**
* 清理离线设备任务
* 检查离线超过10分钟的设备取消其所有pending/running状态的任务
*/
async cleanupOfflineDeviceTasks() {
try {
// 离线阈值10分钟
const offlineThreshold = 10 * 60 * 1000; // 10分钟
const now = Date.now();
const thresholdTime = now - offlineThreshold;
// 获取所有启用的账号
const pla_account = db.getModel('pla_account');
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
// 通过 deviceManager 检查哪些设备离线超过10分钟
const offlineSnCodes = [];
const offlineDevicesInfo = [];
for (const account of accounts) {
const sn_code = account.sn_code;
const device = deviceManager.devices.get(sn_code);
if (!device) {
// 设备从未发送过心跳,视为离线
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: null
});
} else {
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
if (lastHeartbeat < thresholdTime || !device.isOnline) {
offlineSnCodes.push(sn_code);
offlineDevicesInfo.push({
sn_code: sn_code,
lastHeartbeatTime: lastHeartbeat ? new Date(lastHeartbeat) : null
});
}
}
}
if (offlineSnCodes.length === 0) {
return;
}
console.log(`[清理离线任务] 发现 ${offlineSnCodes.length} 个离线超过10分钟的设备: ${offlineSnCodes.join(', ')}`);
let totalCancelled = 0;
// 为每个离线设备取消任务
const task_status = db.getModel('task_status');
for (const sn_code of offlineSnCodes) {
try {
// 查询该设备的所有pending/running任务
const pendingTasks = await task_status.findAll({
where: {
sn_code: sn_code,
status: ['pending', 'running']
},
attributes: ['id']
});
if (pendingTasks.length === 0) {
continue;
}
const deviceInfo = offlineDevicesInfo.find(d => d.sn_code === sn_code);
// 更新任务状态为cancelled
const updateResult = await task_status.update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({
reason: '设备离线超过10分钟任务已自动取消',
offlineTime: deviceInfo?.lastHeartbeatTime
})
},
{
where: {
sn_code: sn_code,
status: ['pending', 'running']
}
}
);
const cancelledCount = Array.isArray(updateResult) ? updateResult[0] : updateResult;
totalCancelled += cancelledCount;
// 从内存队列中移除任务
if (this.taskQueue && typeof this.taskQueue.cancelDeviceTasks === 'function') {
await this.taskQueue.cancelDeviceTasks(sn_code);
}
console.log(`[清理离线任务] 设备 ${sn_code} 已取消 ${cancelledCount} 个任务`);
} catch (error) {
console.error(`[清理离线任务] 取消设备 ${sn_code} 的任务失败:`, error);
}
}
if (totalCancelled > 0) {
console.log(`[清理离线任务] 共取消 ${totalCancelled} 个离线设备的任务`);
}
} catch (error) {
console.error('[清理离线任务] 执行失败:', error);
}
}
/**
* 同步任务状态摘要到客户端
* 定期向所有在线设备发送任务状态摘要(当前任务、待执行任务、下次执行时间等)
*/
async syncTaskStatusSummary() {
try {
const { pla_account } = await Framework.getModels();
// 获取所有启用的账号
const accounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
if (!accounts || accounts.length === 0) {
return;
}
// 离线阈值3分钟
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
// 为每个在线设备发送任务状态摘要
for (const account of accounts) {
const sn_code = account.sn_code;
// 检查设备是否在线
const device = deviceManager.devices.get(sn_code);
if (!device) {
// 设备从未发送过心跳,视为离线,跳过
continue;
}
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
// 设备离线,跳过
continue;
}
// 设备在线,推送设备工作状态
try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
const summary = await this.taskQueue.getTaskStatusSummary(sn_code);
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, summary, {
currentCommand: summary.currentCommand || null
});
} catch (error) {
console.error(`[设备工作状态同步] 设备 ${sn_code} 同步失败:`, error.message);
}
}
} catch (error) {
console.error('[任务状态同步] 执行失败:', error);
}
}
/**
* 检查任务超时并强制标记为失败
* 检测长时间运行的任务(可能是卡住的),强制标记为失败,释放资源
*/
async checkTaskTimeouts() {
try {
const Sequelize = require('sequelize');
const { task_status, op } = db.models;
// 查询所有运行中的任务
const runningTasks = await task_status.findAll({
where: {
status: 'running'
},
attributes: ['id', 'sn_code', 'taskType', 'taskName', 'startTime', 'create_time']
});
if (!runningTasks || runningTasks.length === 0) {
return;
}
const now = new Date();
let timeoutCount = 0;
for (const task of runningTasks) {
const taskData = task.toJSON();
const startTime = taskData.startTime ? new Date(taskData.startTime) : (taskData.create_time ? new Date(taskData.create_time) : null);
if (!startTime) {
continue;
}
// 获取任务类型的超时时间默认10分钟
const taskTimeout = config.getTaskTimeout(taskData.taskType) || 10 * 60 * 1000;
// 允许额外20%的缓冲时间
const maxAllowedTime = taskTimeout * 1.2;
const elapsedTime = now.getTime() - startTime.getTime();
// 如果任务运行时间超过最大允许时间,标记为超时失败
if (elapsedTime > maxAllowedTime) {
try {
await task_status.update(
{
status: 'failed',
endTime: now,
duration: elapsedTime,
result: JSON.stringify({
error: `任务执行超时(运行时间: ${Math.round(elapsedTime / 1000)}秒,超时限制: ${Math.round(maxAllowedTime / 1000)}秒)`,
timeout: true,
taskType: taskData.taskType,
startTime: startTime.toISOString()
}),
progress: 0
},
{
where: { id: taskData.id }
}
);
timeoutCount++;
console.warn(`[任务超时检查] 任务 ${taskData.id} (${taskData.taskName}) 运行时间过长,已强制标记为失败`, {
task_id: taskData.id,
sn_code: taskData.sn_code,
taskType: taskData.taskType,
elapsedTime: Math.round(elapsedTime / 1000) + '秒',
maxAllowedTime: Math.round(maxAllowedTime / 1000) + '秒'
});
// 如果任务队列中有这个任务,也需要从内存中清理
if (this.taskQueue && typeof this.taskQueue.deviceStatus !== 'undefined') {
const deviceStatus = this.taskQueue.deviceStatus.get(taskData.sn_code);
if (deviceStatus && deviceStatus.currentTask && deviceStatus.currentTask.id === taskData.id) {
// 重置设备状态,允许继续执行下一个任务
deviceStatus.isRunning = false;
deviceStatus.currentTask = null;
deviceStatus.runningCount = Math.max(0, deviceStatus.runningCount - 1);
this.taskQueue.globalRunningCount = Math.max(0, this.taskQueue.globalRunningCount - 1);
console.log(`[任务超时检查] 已重置设备 ${taskData.sn_code} 的状态,可以继续执行下一个任务`);
// 尝试继续处理该设备的队列
setTimeout(() => {
this.taskQueue.processQueue(taskData.sn_code).catch(error => {
console.error(`[任务超时检查] 继续处理队列失败 (设备: ${taskData.sn_code}):`, error);
});
}, 100);
}
}
} catch (error) {
console.error(`[任务超时检查] 更新超时任务状态失败 (任务ID: ${taskData.id}):`, error);
}
}
}
if (timeoutCount > 0) {
console.log(`[任务超时检查] 共检测到 ${timeoutCount} 个超时任务,已强制标记为失败`);
}
} catch (error) {
console.error('[任务超时检查] 执行失败:', error);
}
}
/**
* 自动投递任务
*/
async autoDeliverTask() {
const now = new Date();
console.log(`[自动投递] ${now.toLocaleString()} 开始执行自动投递任务`);
try {
// 移除 device_status 依赖,改为直接从 pla_account 查询启用且开启自动投递的账号
const models = db.models;
const { pla_account, op } = models;
// 直接从 pla_account 查询启用且开启自动投递的账号
// 注意:不再检查在线状态,因为 device_status 已移除
const pla_users = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1, // 只获取启用的账号
auto_deliver: 1
}
});
if (!pla_users || pla_users.length === 0) {
console.log('[自动投递] 没有启用且开启自动投递的账号');
return;
}
console.log(`[自动投递] 找到 ${pla_users.length} 个可用账号`);
// 获取 task_status 模型用于查询上次投递时间
const { task_status } = models;
// 为每个设备添加自动投递任务到队列
for (const pl_user of pla_users) {
const userData = pl_user.toJSON();
// 检查设备是否在线离线阈值3分钟
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
const device = deviceManager.devices.get(userData.sn_code);
// 检查用户授权天数 是否够
const authorization = await authorizationService.checkAuthorization(userData.sn_code);
if (!authorization.is_authorized) {
console.log(`[自动投递] 设备 ${userData.sn_code} 授权天数不足,跳过添加任务`);
continue;
}
if (!device) {
// 设备从未发送过心跳,视为离线
console.log(`[自动投递] 设备 ${userData.sn_code} 离线(从未发送心跳),跳过添加任务`);
continue;
}
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
console.log(`[自动投递] 设备 ${userData.sn_code} 离线(最后心跳: ${offlineMinutes}分钟前),跳过添加任务`);
continue;
}
// 检查设备调度策略
const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'deliver');
if (!canExecute.allowed) {
console.log(`[自动投递] 设备 ${userData.sn_code} 不满足执行条件: ${canExecute.reason}`);
continue;
}
// 获取投递配置,如果不存在则使用默认值
let deliver_config = userData.deliver_config;
if (typeof deliver_config === 'string') {
try {
deliver_config = JSON.parse(deliver_config);
} catch (e) {
deliver_config = {};
}
}
deliver_config = deliver_config || {
deliver_interval: 30,
min_salary: 0,
max_salary: 0,
page_count: 3,
max_deliver: 10,
filter_keywords: [],
exclude_keywords: []
};
// 检查投递时间范围
if (deliver_config.time_range) {
const timeCheck = checkTimeRange(deliver_config.time_range);
if (!timeCheck.allowed) {
console.log(`[自动投递] 设备 ${userData.sn_code} ${timeCheck.reason}`);
continue;
}
}
// 检查投递间隔时间
const deliver_interval = deliver_config.deliver_interval || 30; // 默认30分钟
const interval_ms = deliver_interval * 60 * 1000; // 转换为毫秒
// 查询该账号最近一次成功完成的自动投递任务
const lastDeliverTask = await task_status.findOne({
where: {
sn_code: userData.sn_code,
taskType: 'auto_deliver',
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
// 如果存在上次投递记录,检查是否已经过了间隔时间
if (lastDeliverTask && lastDeliverTask.endTime) {
const lastDeliverTime = new Date(lastDeliverTask.endTime);
const elapsedTime = new Date().getTime() - lastDeliverTime.getTime();
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
const message = `距离上次投递仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${deliver_interval} 分钟)`;
console.log(`[自动投递] 设备 ${userData.sn_code} ${message}`);
// 推送等待状态到客户端
try {
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
// 获取当前任务状态摘要
const taskStatusSummary = this.taskQueue ? this.taskQueue.getTaskStatusSummary(userData.sn_code) : {
sn_code: userData.sn_code,
pendingCount: 0,
totalPendingCount: 0,
pendingTasks: []
};
// 添加等待消息到工作状态
await deviceWorkStatusNotifier.sendDeviceWorkStatus(userData.sn_code, taskStatusSummary, {
waitingMessage: {
type: 'deliver_interval',
message: message,
remainingMinutes: remainingMinutes,
nextDeliverTime: new Date(lastDeliverTime.getTime() + interval_ms).toISOString()
}
});
} catch (pushError) {
console.warn(`[自动投递] 推送等待消息失败:`, pushError.message);
}
continue;
}
}
// 添加自动投递任务到队列
await this.taskQueue.addTask(userData.sn_code, {
taskType: 'auto_deliver',
taskName: `自动投递 - ${userData.keyword || ''}`,
taskParams: {
keyword: userData.keyword || '',
platform: userData.platform_type || 'boss',
pageCount: deliver_config.page_count || 3,
maxCount: deliver_config.max_deliver || 10,
filterRules: {
minSalary: deliver_config.min_salary || 0,
maxSalary: deliver_config.max_salary || 0,
keywords: deliver_config.filter_keywords || [],
excludeKeywords: deliver_config.exclude_keywords || []
}
},
priority: config.getTaskPriority('auto_deliver') || 6
});
console.log(`[自动投递] 已为设备 ${userData.sn_code} 添加自动投递任务,关键词: ${userData.keyword || '默认'},投递间隔: ${deliver_interval} 分钟`);
}
console.log('[自动投递] 任务添加完成');
} catch (error) {
console.error('[自动投递] 执行失败:', error);
}
}
/**
* 自动沟通任务
*/
async autoChatTask() {
const now = new Date();
console.log(`[自动沟通] ${now.toLocaleString()} 开始执行自动沟通任务`);
try {
// 移除 device_status 依赖,改为直接从 pla_account 查询启用且开启自动沟通的账号
const models = db.models;
const { pla_account, op } = models;
// 直接从 pla_account 查询启用且开启自动沟通的账号
// 注意:不再检查在线状态,因为 device_status 已移除
const pla_users = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1, // 只获取启用的账号
auto_chat: 1
}
});
if (!pla_users || pla_users.length === 0) {
console.log('[自动沟通] 没有启用且开启自动沟通的账号');
return;
}
console.log(`[自动沟通] 找到 ${pla_users.length} 个可用账号`);
// 获取 task_status 模型用于查询上次沟通时间
const { task_status } = models;
// 为每个设备添加自动沟通任务到队列
for (const pl_user of pla_users) {
const userData = pl_user.toJSON();
// 检查设备是否在线离线阈值3分钟
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
const device = deviceManager.devices.get(userData.sn_code);
if (!device) {
// 设备从未发送过心跳,视为离线
console.log(`[自动沟通] 设备 ${userData.sn_code} 离线(从未发送心跳),跳过添加任务`);
continue;
}
// 检查最后心跳时间
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
console.log(`[自动沟通] 设备 ${userData.sn_code} 离线(最后心跳: ${offlineMinutes}分钟前),跳过添加任务`);
continue;
}
// 检查设备调度策略
const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'chat');
if (!canExecute.allowed) {
console.log(`[自动沟通] 设备 ${userData.sn_code} 不满足执行条件: ${canExecute.reason}`);
continue;
}
// 获取沟通策略配置
let chatStrategy = {};
if (userData.chat_strategy) {
chatStrategy = typeof userData.chat_strategy === 'string'
? JSON.parse(userData.chat_strategy)
: userData.chat_strategy;
}
// 检查沟通时间范围
if (chatStrategy.time_range) {
const timeCheck = checkTimeRange(chatStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[自动沟通] 设备 ${userData.sn_code} ${timeCheck.reason}`);
continue;
}
}
// 检查沟通间隔时间
const chat_interval = chatStrategy.chat_interval || 30; // 默认30分钟
const interval_ms = chat_interval * 60 * 1000; // 转换为毫秒
// 查询该账号最近一次成功完成的自动沟通任务
const lastChatTask = await task_status.findOne({
where: {
sn_code: userData.sn_code,
taskType: 'auto_chat',
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
// 如果存在上次沟通记录,检查是否已经过了间隔时间
if (lastChatTask && lastChatTask.endTime) {
const lastChatTime = new Date(lastChatTask.endTime);
const elapsedTime = now.getTime() - lastChatTime.getTime();
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
console.log(`[自动沟通] 设备 ${userData.sn_code} 距离上次沟通仅 ${Math.round(elapsedTime / (60 * 1000))} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${chat_interval} 分钟)`);
continue;
}
}
// 添加自动沟通任务到队列
await this.taskQueue.addTask(userData.sn_code, {
taskType: 'auto_chat',
taskName: `自动沟通 - ${userData.name || '默认'}`,
taskParams: {
platform: userData.platform_type || 'boss'
},
priority: config.getTaskPriority('auto_chat') || 6
});
console.log(`[自动沟通] 已为设备 ${userData.sn_code} 添加自动沟通任务,沟通间隔: ${chat_interval} 分钟`);
}
console.log('[自动沟通] 任务添加完成');
} catch (error) {
console.error('[自动沟通] 执行失败:', error);
}
}
}
module.exports = ScheduledJobs;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<Array>} 过滤后的职位列表
*/
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<Array>} 过滤后的职位
*/
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<Array>} 处理后的职位列表
*/
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;

View File

@@ -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;

View File

@@ -1,803 +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.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<object>} 执行结果
*/
async handleAutoSearchTask(task) {
console.log(`[任务处理器] 调度自动搜索任务 - 设备: ${task.sn_code}`);
return await this.searchHandler.handle(task);
}
/**
* 处理自动投递任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
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: 'applyJob',
command_name: `投递简历 - ${jobData.jobTitle} @ ${jobData.companyName} (评分:${jobData.matchScore})`,
command_params: JSON.stringify({
sn_code: sn_code,
platform: platform || 'boss',
jobId: jobData.jobId,
encryptBossId: jobData.encryptBossId || '',
securityId: jobData.securityId || '',
brandName: jobData.companyName,
jobTitle: jobData.jobTitle,
companyName: jobData.companyName,
matchScore: jobData.matchScore,
scoreDetails: jobData.scoreDetails
}),
priority: config.getTaskPriority('apply') || 6
});
}
// 7. 执行所有投递指令
const result = await command.executeCommands(task.id, deliverCommands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
console.log(`[任务处理器] 自动投递任务完成 - 设备: ${sn_code}, 创建了 ${deliverCommands.length} 条投递指令, 耗时: ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
console.error(`[任务处理器] 自动投递任务失败 - 设备: ${sn_code}:`, error);
throw error;
}
console.log(`[任务处理器] 调度自动投递任务 - 设备: ${task.sn_code}`);
return await this.deliverHandler.handle(task);
}
/**
* 检查当前时间是否在指定的时间范围内
* @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: '在允许的时间范围内' };
}
/**
* 处理自动沟通任务(待实现)
* 功能自动与HR进行沟通回复消息等
* 处理自动沟通任务
* @param {object} task - 任务对象
* @returns {Promise<object>} 执行结果
*/
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<object>} 执行结果
*/
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;

View File

@@ -0,0 +1,182 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
/**
* 自动活跃账号任务
* 定期浏览职位、刷新简历、查看通知等,保持账号活跃度
*/
class AutoActiveTask extends BaseTask {
constructor() {
super('auto_active_account', {
defaultInterval: 120, // 默认2小时
defaultPriority: 5, // 较低优先级
requiresLogin: true, // 需要登录
conflictsWith: [ // 与这些任务冲突
'auto_deliver', // 投递任务
'auto_search' // 搜索任务
]
});
}
/**
* 验证任务参数
*/
validateParams(params) {
if (!params.platform) {
return {
valid: false,
reason: '缺少必要参数: platform'
};
}
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动活跃账号 - ${params.platform || 'boss'}`;
}
/**
* 执行自动活跃任务
*/
async execute(sn_code, params) {
console.log(`[自动活跃] 设备 ${sn_code} 开始执行活跃任务`);
const actions = [];
// 1. 浏览推荐职位
actions.push({
action: 'browse_jobs',
count: Math.floor(Math.random() * 5) + 3 // 3-7个职位
});
// 2. 刷新简历
actions.push({
action: 'refresh_resume',
success: true
});
// 3. 查看通知
actions.push({
action: 'check_notifications',
count: Math.floor(Math.random() * 3)
});
// 4. 浏览公司主页
actions.push({
action: 'browse_companies',
count: Math.floor(Math.random() * 3) + 1
});
console.log(`[自动活跃] 设备 ${sn_code} 完成 ${actions.length} 个活跃操作`);
return {
success: true,
actions: actions,
message: `完成 ${actions.length} 个活跃操作`
};
}
/**
* 添加活跃任务到队列
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动活跃] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
if (!account) {
console.log(`[自动活跃] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
const accountData = account.toJSON();
// 2. 检查是否开启了自动活跃
if (!accountData.auto_active) {
console.log(`[自动活跃] 设备 ${sn_code} 未开启自动活跃`);
return { success: false, reason: '未开启自动活跃' };
}
// 3. 获取活跃策略配置
let activeStrategy = {};
if (accountData.active_strategy) {
activeStrategy = typeof accountData.active_strategy === 'string'
? JSON.parse(accountData.active_strategy)
: accountData.active_strategy;
}
// 4. 检查时间范围
if (activeStrategy.time_range) {
const timeCheck = this.checkTimeRange(activeStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[自动活跃] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查活跃间隔
const active_interval = activeStrategy.active_interval || this.config.defaultInterval;
const intervalCheck = await this.checkExecutionInterval(sn_code, active_interval);
if (!intervalCheck.allowed) {
console.log(`[自动活跃] 设备 ${sn_code} ${intervalCheck.reason}`);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
platform: accountData.platform_type || 'boss',
actions: activeStrategy.actions || ['browse_jobs', 'refresh_resume', 'check_notifications'],
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动活跃] 已为设备 ${sn_code} 添加活跃任务,间隔: ${active_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动活跃] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
}
// 导出单例
module.exports = new AutoActiveTask();

View File

@@ -0,0 +1,181 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
/**
* 自动沟通任务
* 自动回复HR消息,保持活跃度
*/
class AutoChatTask extends BaseTask {
constructor() {
super('auto_chat', {
defaultInterval: 15, // 默认15分钟
defaultPriority: 6, // 中等优先级
requiresLogin: true, // 需要登录
conflictsWith: [] // 不与其他任务冲突(可以在投递/搜索间隙执行)
});
}
/**
* 验证任务参数
*/
validateParams(params) {
if (!params.platform) {
return {
valid: false,
reason: '缺少必要参数: platform'
};
}
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动沟通 - ${params.name || '默认'}`;
}
/**
* 执行自动沟通任务
*/
async execute(sn_code, params) {
console.log(`[自动沟通] 设备 ${sn_code} 开始执行沟通任务`);
// 1. 获取未读消息列表
const unreadMessages = await this.getUnreadMessages(sn_code, params.platform);
if (!unreadMessages || unreadMessages.length === 0) {
console.log(`[自动沟通] 设备 ${sn_code} 没有未读消息`);
return {
success: true,
repliedCount: 0,
message: '没有未读消息'
};
}
console.log(`[自动沟通] 设备 ${sn_code} 找到 ${unreadMessages.length} 条未读消息`);
// 2. 智能回复(这里需要调用实际的AI回复逻辑)
const replyResult = {
success: true,
repliedCount: unreadMessages.length,
messages: unreadMessages.map(m => ({
id: m.id,
from: m.hr_name,
company: m.company_name
}))
};
return replyResult;
}
/**
* 获取未读消息
*/
async getUnreadMessages(sn_code, platform) {
// TODO: 从数据库或缓存获取未读消息
// 这里返回空数组作为示例
return [];
}
/**
* 添加沟通任务到队列
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动沟通] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
if (!account) {
console.log(`[自动沟通] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
const accountData = account.toJSON();
// 2. 检查是否开启了自动沟通
if (!accountData.auto_chat) {
console.log(`[自动沟通] 设备 ${sn_code} 未开启自动沟通`);
return { success: false, reason: '未开启自动沟通' };
}
// 3. 获取沟通策略配置
let chatStrategy = {};
if (accountData.chat_strategy) {
chatStrategy = typeof accountData.chat_strategy === 'string'
? JSON.parse(accountData.chat_strategy)
: accountData.chat_strategy;
}
// 4. 检查时间范围
if (chatStrategy.time_range) {
const timeCheck = this.checkTimeRange(chatStrategy.time_range);
if (!timeCheck.allowed) {
console.log(`[自动沟通] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查沟通间隔
const chat_interval = chatStrategy.chat_interval || this.config.defaultInterval;
const intervalCheck = await this.checkExecutionInterval(sn_code, chat_interval);
if (!intervalCheck.allowed) {
console.log(`[自动沟通] 设备 ${sn_code} ${intervalCheck.reason}`);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
platform: accountData.platform_type || 'boss',
name: accountData.name || '默认',
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动沟通] 已为设备 ${sn_code} 添加沟通任务,间隔: ${chat_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动沟通] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
}
// 导出单例
module.exports = new AutoChatTask();

View File

@@ -0,0 +1,320 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
const authorizationService = require('../../../services/authorization_service');
/**
* 自动投递任务
* 从数据库读取职位列表并进行自动投递
*/
class AutoDeliverTask extends BaseTask {
constructor() {
super('auto_deliver', {
defaultInterval: 30, // 默认30分钟
defaultPriority: 7, // 高优先级
requiresLogin: true, // 需要登录
conflictsWith: [ // 与这些任务冲突
'auto_search', // 搜索任务
'auto_active_account' // 活跃账号任务
]
});
}
/**
* 验证任务参数
*/
validateParams(params) {
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动投递 - ${params.keyword || '指定职位'}`;
}
/**
* 执行自动投递任务
*/
async execute(sn_code, params) {
console.log(`[自动投递] 设备 ${sn_code} 开始执行投递任务`);
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
throw new Error(`账号 ${sn_code} 不存在`);
}
// 2. 检查授权
const authorization = await authorizationService.checkAuthorization(sn_code);
if (!authorization.is_authorized) {
throw new Error('授权天数不足');
}
// 3. 获取投递配置
const deliverConfig = this.parseDeliverConfig(account.deliver_config);
// 4. 检查日投递限制
const dailyLimit = config.platformDailyLimits[account.platform_type] || 50;
const todayDelivered = await this.getTodayDeliveredCount(sn_code);
if (todayDelivered >= dailyLimit) {
throw new Error(`今日投递已达上限 (${todayDelivered}/${dailyLimit})`);
}
// 5. 获取可投递的职位列表
const jobs = await this.getDeliverableJobs(sn_code, account, deliverConfig);
if (!jobs || jobs.length === 0) {
console.log(`[自动投递] 设备 ${sn_code} 没有可投递的职位`);
return {
success: true,
delivered: 0,
message: '没有可投递的职位'
};
}
console.log(`[自动投递] 设备 ${sn_code} 找到 ${jobs.length} 个可投递职位`);
// 6. 执行投递(这里需要调用实际的投递逻辑)
const deliverResult = {
success: true,
delivered: jobs.length,
jobs: jobs.map(j => ({
id: j.id,
title: j.job_title,
company: j.company_name
}))
};
return deliverResult;
}
/**
* 获取账号信息
*/
async getAccountInfo(sn_code) {
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
return account ? account.toJSON() : null;
}
/**
* 解析投递配置
*/
parseDeliverConfig(deliver_config) {
if (typeof deliver_config === 'string') {
try {
deliver_config = JSON.parse(deliver_config);
} catch (e) {
deliver_config = {};
}
}
return {
deliver_interval: deliver_config?.deliver_interval || 30,
min_salary: deliver_config?.min_salary || 0,
max_salary: deliver_config?.max_salary || 0,
page_count: deliver_config?.page_count || 3,
max_deliver: deliver_config?.max_deliver || 10,
filter_keywords: deliver_config?.filter_keywords || [],
exclude_keywords: deliver_config?.exclude_keywords || [],
time_range: deliver_config?.time_range || null
};
}
/**
* 获取今日已投递数量
*/
async getTodayDeliveredCount(sn_code) {
const { task_status } = db.models;
const Sequelize = require('sequelize');
const today = new Date();
today.setHours(0, 0, 0, 0);
const count = await task_status.count({
where: {
sn_code: sn_code,
taskType: 'auto_deliver',
status: 'completed',
endTime: {
[Sequelize.Op.gte]: today
}
}
});
return count;
}
/**
* 获取可投递的职位列表
*/
async getDeliverableJobs(sn_code, account, deliverConfig) {
const { job_postings } = db.models;
const Sequelize = require('sequelize');
// 构建查询条件
const where = {
sn_code: sn_code,
platform: account.platform_type,
is_delivered: 0, // 未投递
is_filtered: 0 // 未被过滤
};
// 薪资范围过滤
if (deliverConfig.min_salary > 0) {
where.salary_min = {
[Sequelize.Op.gte]: deliverConfig.min_salary
};
}
if (deliverConfig.max_salary > 0) {
where.salary_max = {
[Sequelize.Op.lte]: deliverConfig.max_salary
};
}
// 查询职位
const jobs = await job_postings.findAll({
where: where,
limit: deliverConfig.max_deliver,
order: [['create_time', 'DESC']]
});
return jobs.map(j => j.toJSON());
}
/**
* 添加投递任务到队列
* 这是外部调用的入口,会进行所有冲突检查
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动投递] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
console.log(`[自动投递] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
// 2. 检查是否开启了自动投递
if (!account.auto_deliver) {
console.log(`[自动投递] 设备 ${sn_code} 未开启自动投递`);
return { success: false, reason: '未开启自动投递' };
}
// 3. 获取投递配置
const deliverConfig = this.parseDeliverConfig(account.deliver_config);
// 4. 检查时间范围
if (deliverConfig.time_range) {
const timeCheck = this.checkTimeRange(deliverConfig.time_range);
if (!timeCheck.allowed) {
console.log(`[自动投递] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查投递间隔
const intervalCheck = await this.checkExecutionInterval(
sn_code,
deliverConfig.deliver_interval
);
if (!intervalCheck.allowed) {
console.log(`[自动投递] 设备 ${sn_code} ${intervalCheck.reason}`);
// 推送等待状态到客户端
await this.notifyWaitingStatus(sn_code, intervalCheck, taskQueue);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
keyword: account.keyword || '',
platform: account.platform_type || 'boss',
pageCount: deliverConfig.page_count,
maxCount: deliverConfig.max_deliver,
filterRules: {
minSalary: deliverConfig.min_salary,
maxSalary: deliverConfig.max_salary,
keywords: deliverConfig.filter_keywords,
excludeKeywords: deliverConfig.exclude_keywords
},
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动投递] 已为设备 ${sn_code} 添加投递任务,间隔: ${deliverConfig.deliver_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动投递] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
/**
* 推送等待状态到客户端
*/
async notifyWaitingStatus(sn_code, intervalCheck, taskQueue) {
try {
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
// 获取当前任务状态摘要
const taskStatusSummary = taskQueue.getTaskStatusSummary(sn_code);
// 添加等待消息到工作状态
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, taskStatusSummary, {
waitingMessage: {
type: 'deliver_interval',
message: intervalCheck.reason,
remainingMinutes: intervalCheck.remainingMinutes,
nextDeliverTime: intervalCheck.nextExecutionTime?.toISOString()
}
});
} catch (error) {
console.warn(`[自动投递] 推送等待消息失败:`, error.message);
}
}
}
// 导出单例
module.exports = new AutoDeliverTask();

View File

@@ -0,0 +1,233 @@
const BaseTask = require('./baseTask');
const db = require('../../dbProxy');
const config = require('../infrastructure/config');
/**
* 自动搜索职位任务
* 定期搜索符合条件的职位并保存到数据库
*/
class AutoSearchTask extends BaseTask {
constructor() {
super('auto_search', {
defaultInterval: 60, // 默认60分钟
defaultPriority: 8, // 高优先级(比投递高,先搜索后投递)
requiresLogin: true, // 需要登录
conflictsWith: [ // 与这些任务冲突
'auto_deliver', // 投递任务
'auto_active_account' // 活跃账号任务
]
});
}
/**
* 验证任务参数
*/
validateParams(params) {
if (!params.keyword && !params.jobType) {
return {
valid: false,
reason: '缺少必要参数: keyword 或 jobType'
};
}
return { valid: true };
}
/**
* 获取任务名称
*/
getTaskName(params) {
return `自动搜索 - ${params.keyword || params.jobType || '默认'}`;
}
/**
* 执行自动搜索任务
*/
async execute(sn_code, params) {
console.log(`[自动搜索] 设备 ${sn_code} 开始执行搜索任务`);
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
throw new Error(`账号 ${sn_code} 不存在`);
}
// 2. 获取搜索配置
const searchConfig = this.parseSearchConfig(account.search_config);
// 3. 检查日搜索限制
const dailyLimit = config.dailyLimits.maxSearch || 20;
const todaySearched = await this.getTodaySearchCount(sn_code);
if (todaySearched >= dailyLimit) {
throw new Error(`今日搜索已达上限 (${todaySearched}/${dailyLimit})`);
}
// 4. 执行搜索(这里需要调用实际的搜索逻辑)
const searchResult = {
success: true,
keyword: params.keyword || account.keyword,
pageCount: searchConfig.page_count || 3,
jobsFound: 0, // 实际搜索到的职位数
jobsSaved: 0 // 保存到数据库的职位数
};
console.log(`[自动搜索] 设备 ${sn_code} 搜索完成,找到 ${searchResult.jobsFound} 个职位`);
return searchResult;
}
/**
* 获取账号信息
*/
async getAccountInfo(sn_code) {
const { pla_account } = db.models;
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0,
is_enabled: 1
}
});
return account ? account.toJSON() : null;
}
/**
* 解析搜索配置
*/
parseSearchConfig(search_config) {
if (typeof search_config === 'string') {
try {
search_config = JSON.parse(search_config);
} catch (e) {
search_config = {};
}
}
return {
search_interval: search_config?.search_interval || 60,
page_count: search_config?.page_count || 3,
city: search_config?.city || '',
salary_range: search_config?.salary_range || '',
experience: search_config?.experience || '',
education: search_config?.education || '',
time_range: search_config?.time_range || null
};
}
/**
* 获取今日已搜索数量
*/
async getTodaySearchCount(sn_code) {
const { task_status } = db.models;
const Sequelize = require('sequelize');
const today = new Date();
today.setHours(0, 0, 0, 0);
const count = await task_status.count({
where: {
sn_code: sn_code,
taskType: 'auto_search',
status: 'completed',
endTime: {
[Sequelize.Op.gte]: today
}
}
});
return count;
}
/**
* 添加搜索任务到队列
*/
async addToQueue(sn_code, taskQueue, customParams = {}) {
const now = new Date();
console.log(`[自动搜索] ${now.toLocaleString()} 尝试为设备 ${sn_code} 添加任务`);
try {
// 1. 获取账号信息
const account = await this.getAccountInfo(sn_code);
if (!account) {
console.log(`[自动搜索] 账号 ${sn_code} 不存在或未启用`);
return { success: false, reason: '账号不存在或未启用' };
}
// 2. 检查是否开启了自动搜索
if (!account.auto_search) {
console.log(`[自动搜索] 设备 ${sn_code} 未开启自动搜索`);
return { success: false, reason: '未开启自动搜索' };
}
// 3. 获取搜索配置
const searchConfig = this.parseSearchConfig(account.search_config);
// 4. 检查时间范围
if (searchConfig.time_range) {
const timeCheck = this.checkTimeRange(searchConfig.time_range);
if (!timeCheck.allowed) {
console.log(`[自动搜索] 设备 ${sn_code} ${timeCheck.reason}`);
return { success: false, reason: timeCheck.reason };
}
}
// 5. 执行所有层级的冲突检查
const conflictCheck = await this.canExecuteTask(sn_code, taskQueue);
if (!conflictCheck.allowed) {
return { success: false, reason: conflictCheck.reason };
}
// 6. 检查搜索间隔
const intervalCheck = await this.checkExecutionInterval(
sn_code,
searchConfig.search_interval
);
if (!intervalCheck.allowed) {
console.log(`[自动搜索] 设备 ${sn_code} ${intervalCheck.reason}`);
return { success: false, reason: intervalCheck.reason };
}
// 7. 构建任务参数
const taskParams = {
keyword: account.keyword || '',
jobType: account.job_type || '',
platform: account.platform_type || 'boss',
pageCount: searchConfig.page_count || 3,
city: searchConfig.city || '',
salaryRange: searchConfig.salary_range || '',
...customParams
};
// 8. 验证参数
const validation = this.validateParams(taskParams);
if (!validation.valid) {
return { success: false, reason: validation.reason };
}
// 9. 添加任务到队列
await taskQueue.addTask(sn_code, {
taskType: this.taskType,
taskName: this.getTaskName(taskParams),
taskParams: taskParams,
priority: this.config.defaultPriority
});
console.log(`[自动搜索] 已为设备 ${sn_code} 添加搜索任务,间隔: ${searchConfig.search_interval} 分钟`);
return { success: true };
} catch (error) {
console.error(`[自动搜索] 添加任务失败:`, error);
return { success: false, reason: error.message };
} finally {
// 统一释放任务锁
this.releaseTaskLock(sn_code);
}
}
}
// 导出单例
module.exports = new AutoSearchTask();

View File

@@ -0,0 +1,405 @@
const dayjs = require('dayjs');
const deviceManager = require('../core/deviceManager');
const db = require('../../dbProxy');
/**
* 任务基类
* 提供所有任务的通用功能和冲突检测机制
*/
class BaseTask {
constructor(taskType, config = {}) {
this.taskType = taskType;
this.config = {
// 默认配置
defaultInterval: 30, // 默认间隔30分钟
defaultPriority: 5,
requiresLogin: true, // 是否需要登录状态
conflictsWith: [], // 与哪些任务类型冲突
...config
};
// 任务执行锁 { sn_code: timestamp }
this.taskLocks = new Map();
// 最后执行时间缓存 { sn_code: timestamp }
this.lastExecutionCache = new Map();
}
/**
* Layer 1: 任务类型互斥锁检查
* 防止同一设备同时添加相同类型的任务
*/
acquireTaskLock(sn_code) {
const lockKey = `${sn_code}:${this.taskType}`;
const now = Date.now();
const existingLock = this.taskLocks.get(lockKey);
// 如果存在锁且未超时(5分钟),返回false
if (existingLock && (now - existingLock) < 5 * 60 * 1000) {
const remainingTime = Math.ceil((5 * 60 * 1000 - (now - existingLock)) / 1000);
return {
allowed: false,
reason: `任务 ${this.taskType} 正在添加中,请等待 ${remainingTime}`
};
}
// 获取锁
this.taskLocks.set(lockKey, now);
return { allowed: true };
}
/**
* 释放任务锁
*/
releaseTaskLock(sn_code) {
const lockKey = `${sn_code}:${this.taskType}`;
this.taskLocks.delete(lockKey);
}
/**
* Layer 2: 设备状态检查
* 检查设备是否在线、是否登录、是否忙碌
*/
async checkDeviceStatus(sn_code) {
// 1. 优先检查内存中的设备状态
let device = deviceManager.devices.get(sn_code);
// 2. 如果内存中没有,降级到数据库查询(可能是刚启动还没收到心跳)
if (!device) {
try {
const pla_account = db.getModel('pla_account');
const dbDevice = await pla_account.findOne({
where: { sn_code, is_delete: 0 },
attributes: ['sn_code', 'is_online', 'is_logged_in']
});
if (!dbDevice) {
return {
allowed: false,
reason: `设备 ${sn_code} 不存在`
};
}
// 检查数据库中的在线状态
if (!dbDevice.is_online) {
return {
allowed: false,
reason: `设备 ${sn_code} 离线(数据库状态)`
};
}
// 检查数据库中的登录状态
if (this.config.requiresLogin && !dbDevice.is_logged_in) {
return {
allowed: false,
reason: `设备 ${sn_code} 未登录平台账号(数据库状态)`
};
}
// 数据库检查通过,允许执行
return { allowed: true };
} catch (error) {
console.error(`[${this.taskType}] 查询设备状态失败:`, error);
return {
allowed: false,
reason: `设备 ${sn_code} 状态查询失败`
};
}
}
// 3. 检查心跳超时
const offlineThreshold = 3 * 60 * 1000; // 3分钟
const now = Date.now();
const lastHeartbeat = device.lastHeartbeat || 0;
const isOnline = device.isOnline && (now - lastHeartbeat < offlineThreshold);
if (!isOnline) {
const offlineMinutes = lastHeartbeat ? Math.round((now - lastHeartbeat) / (60 * 1000)) : '未知';
return {
allowed: false,
reason: `设备 ${sn_code} 离线(最后心跳: ${offlineMinutes}分钟前)`
};
}
// 4. 检查登录状态(如果任务需要)
if (this.config.requiresLogin && !device.isLoggedIn) {
return {
allowed: false,
reason: `设备 ${sn_code} 未登录平台账号`
};
}
return { allowed: true };
}
/**
* Layer 3: 检查任务队列状态
* 防止队列中已有相同任务
*/
async checkTaskQueue(sn_code, taskQueue) {
// 获取设备队列
const deviceQueue = taskQueue.deviceQueues.get(sn_code);
if (!deviceQueue) {
return { allowed: true };
}
// 检查队列中是否有相同类型的待执行任务
const tasks = deviceQueue.toArray();
const hasSameTypeTask = tasks.some(task =>
task.taskType === this.taskType &&
task.status === 'pending'
);
if (hasSameTypeTask) {
return {
allowed: false,
reason: `队列中已存在待执行的 ${this.taskType} 任务`
};
}
return { allowed: true };
}
/**
* Layer 4: 检查任务去重
* 查询数据库中是否有重复的待执行任务
*/
async checkDuplicateTask(sn_code) {
try {
const { task_status } = db.models;
// 查询该设备是否有相同类型的pending/running任务
const existingTask = await task_status.findOne({
where: {
sn_code: sn_code,
taskType: this.taskType,
status: ['pending', 'running']
},
attributes: ['id', 'status', 'taskName']
});
if (existingTask) {
return {
allowed: false,
reason: `已存在 ${existingTask.status} 状态的任务: ${existingTask.taskName}`
};
}
return { allowed: true };
} catch (error) {
console.error(`[${this.taskType}] 检查重复任务失败:`, error);
// 出错时允许继续,避免阻塞
return { allowed: true };
}
}
/**
* Layer 5: 操作类型冲突检测
* 某些操作类型不能同时执行
*/
async checkOperationConflict(sn_code, taskQueue) {
// 如果没有配置冲突类型,直接通过
if (!this.config.conflictsWith || this.config.conflictsWith.length === 0) {
return { allowed: true };
}
// 检查当前是否有冲突的任务正在执行
const deviceStatus = taskQueue.deviceStatus.get(sn_code);
if (deviceStatus && deviceStatus.currentTask) {
const currentTaskType = deviceStatus.currentTask.taskType;
if (this.config.conflictsWith.includes(currentTaskType)) {
return {
allowed: false,
reason: `与正在执行的任务 ${currentTaskType} 冲突`
};
}
}
return { allowed: true };
}
/**
* 检查执行间隔
* 从数据库查询上次成功执行时间,判断是否满足间隔要求
*/
async checkExecutionInterval(sn_code, intervalMinutes) {
try {
const { task_status } = db.models;
// 先从缓存检查
const cachedLastExecution = this.lastExecutionCache.get(sn_code);
const now = Date.now();
if (cachedLastExecution) {
const elapsedTime = now - cachedLastExecution;
const interval_ms = intervalMinutes * 60 * 1000;
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
return {
allowed: false,
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
remainingMinutes,
elapsedMinutes
};
}
}
// 从数据库查询最近一次成功完成的任务
const lastTask = await task_status.findOne({
where: {
sn_code: sn_code,
taskType: this.taskType,
status: 'completed'
},
order: [['endTime', 'DESC']],
attributes: ['endTime']
});
// 如果存在上次执行记录,检查是否已经过了间隔时间
if (lastTask && lastTask.endTime) {
const lastExecutionTime = new Date(lastTask.endTime).getTime();
const elapsedTime = now - lastExecutionTime;
const interval_ms = intervalMinutes * 60 * 1000;
// 更新缓存
this.lastExecutionCache.set(sn_code, lastExecutionTime);
if (elapsedTime < interval_ms) {
const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000));
const elapsedMinutes = Math.round(elapsedTime / (60 * 1000));
return {
allowed: false,
reason: `距离上次执行仅 ${elapsedMinutes} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${intervalMinutes} 分钟)`,
remainingMinutes,
elapsedMinutes,
nextExecutionTime: new Date(lastExecutionTime + interval_ms)
};
}
}
return { allowed: true };
} catch (error) {
console.error(`[${this.taskType}] 检查执行间隔失败:`, error);
// 出错时允许继续,避免阻塞
return { allowed: true };
}
}
/**
* 检查时间范围限制
* @param {Object} timeRange - 时间范围配置 {start_time: '09:00', end_time: '18:00', workdays_only: 1}
*/
checkTimeRange(timeRange) {
if (!timeRange || !timeRange.start_time || !timeRange.end_time) {
return { allowed: true, reason: '未配置时间范围' };
}
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute;
// 解析开始时间和结束时间
const [startHour, startMinute] = timeRange.start_time.split(':').map(Number);
const [endHour, endMinute] = timeRange.end_time.split(':').map(Number);
const startTime = startHour * 60 + startMinute;
const endTime = endHour * 60 + endMinute;
// 检查是否仅工作日
if (timeRange.workdays_only == 1) {
const dayOfWeek = now.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { allowed: false, reason: '当前是周末,不在允许的时间范围内' };
}
}
// 检查当前时间是否在时间范围内
if (startTime <= endTime) {
// 正常情况: 09:00 - 18:00
if (currentTime < startTime || currentTime >= endTime) {
return {
allowed: false,
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
} else {
// 跨天情况: 22:00 - 06:00
if (currentTime < startTime && currentTime >= endTime) {
return {
allowed: false,
reason: `当前时间不在允许的时间范围内 (${timeRange.start_time} - ${timeRange.end_time})`
};
}
}
return { allowed: true, reason: '在允许的时间范围内' };
}
/**
* 综合检查 - 执行所有层级的检查
* @param {string} sn_code - 设备SN码
* @param {Object} taskQueue - 任务队列实例
* @param {Object} options - 额外选项
* @returns {Object} { allowed: boolean, reason: string }
*/
async canExecuteTask(sn_code, taskQueue, options = {}) {
const checks = [
{ name: 'Layer1-任务锁', fn: () => this.acquireTaskLock(sn_code) },
{ name: 'Layer2-设备状态', fn: () => this.checkDeviceStatus(sn_code) },
{ name: 'Layer3-队列检查', fn: () => this.checkTaskQueue(sn_code, taskQueue) },
{ name: 'Layer4-任务去重', fn: () => this.checkDuplicateTask(sn_code) },
{ name: 'Layer5-操作冲突', fn: () => this.checkOperationConflict(sn_code, taskQueue) }
];
// 逐层检查
for (const check of checks) {
const result = await check.fn();
if (!result.allowed) {
console.log(`[${this.taskType}] ${check.name} 未通过: ${result.reason}`);
return result;
}
}
return { allowed: true };
}
/**
* 清理任务锁(定期清理过期锁)
*/
cleanupExpiredLocks() {
const now = Date.now();
const timeout = 5 * 60 * 1000; // 5分钟超时
for (const [lockKey, timestamp] of this.taskLocks.entries()) {
if (now - timestamp > timeout) {
this.taskLocks.delete(lockKey);
}
}
}
/**
* 获取任务名称(子类可覆盖)
*/
getTaskName(params) {
return `${this.taskType} 任务`;
}
/**
* 验证任务参数(子类必须实现)
*/
validateParams(params) {
throw new Error('子类必须实现 validateParams 方法');
}
/**
* 执行任务的具体逻辑(子类必须实现)
*/
async execute(sn_code, params) {
throw new Error('子类必须实现 execute 方法');
}
}
module.exports = BaseTask;

View File

@@ -0,0 +1,16 @@
/**
* 任务模块索引
* 统一导出所有任务类型
*/
const autoSearchTask = require('./autoSearchTask');
const autoDeliverTask = require('./autoDeliverTask');
const autoChatTask = require('./autoChatTask');
const autoActiveTask = require('./autoActiveTask');
module.exports = {
autoSearchTask,
autoDeliverTask,
autoChatTask,
autoActiveTask
};

View File

@@ -0,0 +1,14 @@
/**
* Utils 模块导出
* 统一导出工具类模块
*/
const SalaryParser = require('./salaryParser');
const KeywordMatcher = require('./keywordMatcher');
const ScheduleUtils = require('./scheduleUtils');
module.exports = {
SalaryParser,
KeywordMatcher,
ScheduleUtils
};

View File

@@ -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 - 前缀标记(默认 <mark>
* @param {string} suffix - 后缀标记(默认 </mark>
* @returns {string} 高亮后的文本
*/
static highlight(text, keywords = [], prefix = '<mark>', suffix = '</mark>') {
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;

View File

@@ -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;