11
This commit is contained in:
@@ -12,7 +12,10 @@
|
||||
"Bash(cat:*)",
|
||||
"Bash(npm run restart:*)",
|
||||
"Bash(del scheduledJobs.js)",
|
||||
"Bash(ls:*)"
|
||||
"Bash(ls:*)",
|
||||
"Bash(wc:*)",
|
||||
"Bash(for:*)",
|
||||
"Bash(done)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
|
||||
/**
|
||||
* 设备管理器(简化版)
|
||||
16
api/middleware/schedule/core/index.js
Normal file
16
api/middleware/schedule/core/index.js
Normal 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
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
const node_schedule = require("node-schedule");
|
||||
const dayjs = require('dayjs');
|
||||
const config = require('./config.js');
|
||||
const config = require('../infrastructure/config.js');
|
||||
const deviceManager = require('./deviceManager.js');
|
||||
const command = require('./command.js');
|
||||
const db = require('../dbProxy');
|
||||
const db = require('../../dbProxy');
|
||||
|
||||
// 引入新的任务模块
|
||||
const tasks = require('./tasks');
|
||||
const tasks = require('../tasks');
|
||||
const { autoSearchTask, autoDeliverTask, autoChatTask, autoActiveTask } = tasks;
|
||||
|
||||
const Framework = require("../../../framework/node-core-framework.js");
|
||||
const Framework = require("../../../../framework/node-core-framework.js");
|
||||
|
||||
/**
|
||||
* 定时任务管理器(重构版)
|
||||
@@ -448,7 +448,7 @@ class ScheduledJobs {
|
||||
}
|
||||
|
||||
try {
|
||||
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
|
||||
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
|
||||
const summary = await this.taskQueue.getTaskStatusSummary(sn_code);
|
||||
await deviceWorkStatusNotifier.sendDeviceWorkStatus(sn_code, summary, {
|
||||
currentCommand: summary.currentCommand || null
|
||||
@@ -1,14 +1,14 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const Sequelize = require('sequelize');
|
||||
const logs = require('../logProxy');
|
||||
const db = require('../dbProxy');
|
||||
const logs = require('../../logProxy');
|
||||
const db = require('../../dbProxy');
|
||||
const command = require('./command');
|
||||
const PriorityQueue = require('./PriorityQueue');
|
||||
const ErrorHandler = require('./ErrorHandler');
|
||||
const PriorityQueue = require('../infrastructure/PriorityQueue');
|
||||
const ErrorHandler = require('../infrastructure/ErrorHandler');
|
||||
const deviceManager = require('./deviceManager');
|
||||
const ScheduleUtils = require('./utils');
|
||||
const ScheduleConfig = require('./config');
|
||||
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
|
||||
const ScheduleUtils = require('../utils/scheduleUtils');
|
||||
const ScheduleConfig = require('../infrastructure/config');
|
||||
const deviceWorkStatusNotifier = require('../notifiers/deviceWorkStatusNotifier');
|
||||
|
||||
/**
|
||||
* 任务队列管理器(重构版)
|
||||
@@ -222,7 +222,6 @@ class TaskQueue {
|
||||
|
||||
// 移除 device_status 依赖,不再检查设备在线状态
|
||||
// 如果需要在线状态检查,可以从 deviceManager 获取
|
||||
const deviceManager = require('./deviceManager');
|
||||
const deviceStatus = deviceManager.getAllDevicesStatus();
|
||||
const onlineSnCodes = new Set(
|
||||
Object.entries(deviceStatus)
|
||||
@@ -230,24 +229,7 @@ class TaskQueue {
|
||||
.map(([sn_code]) => sn_code)
|
||||
);
|
||||
|
||||
// 原有代码已移除,改为使用 deviceManager
|
||||
/* 原有代码已注释
|
||||
const device_status = db.getModel('device_status');
|
||||
const heartbeatTimeout = require('./config.js').monitoring.heartbeatTimeout;
|
||||
const now = new Date();
|
||||
const heartbeatThreshold = new Date(now.getTime() - heartbeatTimeout);
|
||||
|
||||
const onlineDevices = await device_status.findAll({
|
||||
where: {
|
||||
isOnline: true,
|
||||
lastHeartbeatTime: {
|
||||
[Sequelize.Op.gte]: heartbeatThreshold // 心跳时间在阈值内
|
||||
}
|
||||
},
|
||||
attributes: ['sn_code']
|
||||
});
|
||||
const onlineSnCodes = new Set(onlineDevices.map(dev => dev.sn_code));
|
||||
*/
|
||||
|
||||
let processedCount = 0;
|
||||
let queuedCount = 0;
|
||||
@@ -1065,13 +1047,13 @@ class TaskQueue {
|
||||
async getMqttClient() {
|
||||
try {
|
||||
// 首先尝试从调度系统获取已初始化的MQTT客户端
|
||||
const scheduleManager = require('./index');
|
||||
const scheduleManager = require('../index');
|
||||
if (scheduleManager.mqttClient) {
|
||||
return scheduleManager.mqttClient;
|
||||
}
|
||||
|
||||
// 如果调度系统没有初始化,则直接创建
|
||||
const mqttManager = require('../mqtt/mqttManager');
|
||||
const mqttManager = require('../../mqtt/mqttManager');
|
||||
console.log('[任务队列] 创建新的MQTT客户端');
|
||||
return await mqttManager.getInstance();
|
||||
} catch (error) {
|
||||
88
api/middleware/schedule/handlers/activeHandler.js
Normal file
88
api/middleware/schedule/handlers/activeHandler.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const BaseHandler = require('./baseHandler');
|
||||
const ConfigManager = require('../services/configManager');
|
||||
const command = require('../command');
|
||||
const config = require('../config');
|
||||
|
||||
/**
|
||||
* 自动活跃处理器
|
||||
* 负责保持账户活跃度
|
||||
*/
|
||||
class ActiveHandler extends BaseHandler {
|
||||
/**
|
||||
* 处理自动活跃任务
|
||||
* @param {object} task - 任务对象
|
||||
* @returns {Promise<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;
|
||||
250
api/middleware/schedule/handlers/baseHandler.js
Normal file
250
api/middleware/schedule/handlers/baseHandler.js
Normal 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;
|
||||
87
api/middleware/schedule/handlers/chatHandler.js
Normal file
87
api/middleware/schedule/handlers/chatHandler.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const BaseHandler = require('./baseHandler');
|
||||
const ConfigManager = require('../services/configManager');
|
||||
const command = require('../command');
|
||||
const config = require('../config');
|
||||
|
||||
/**
|
||||
* 自动沟通处理器
|
||||
* 负责自动回复HR消息
|
||||
*/
|
||||
class ChatHandler extends BaseHandler {
|
||||
/**
|
||||
* 处理自动沟通任务
|
||||
* @param {object} task - 任务对象
|
||||
* @returns {Promise<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. 创建沟通指令
|
||||
const chatCommand = {
|
||||
command_type: 'autoChat',
|
||||
command_name: '自动沟通',
|
||||
command_params: JSON.stringify({
|
||||
sn_code,
|
||||
platform: platform || accountConfig.platform_type || 'boss',
|
||||
autoReply: chatStrategy.auto_reply || false,
|
||||
replyTemplate: chatStrategy.reply_template || ''
|
||||
}),
|
||||
priority: config.getTaskPriority('auto_chat') || 6
|
||||
};
|
||||
|
||||
// 5. 执行沟通指令
|
||||
const result = await command.executeCommands(task.id, [chatCommand], this.mqttClient);
|
||||
|
||||
console.log(`[自动沟通] 完成 - 设备: ${sn_code}`);
|
||||
|
||||
return {
|
||||
chatCount: result.chatCount || 0,
|
||||
message: '沟通完成'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChatHandler;
|
||||
410
api/middleware/schedule/handlers/deliverHandler.js
Normal file
410
api/middleware/schedule/handlers/deliverHandler.js
Normal file
@@ -0,0 +1,410 @@
|
||||
const BaseHandler = require('./baseHandler');
|
||||
const ConfigManager = require('../services/configManager');
|
||||
const jobFilterEngine = require('../services/jobFilterEngine');
|
||||
const command = require('../command');
|
||||
const config = require('../config');
|
||||
const db = require('../../dbProxy');
|
||||
const jobFilterService = require('../../../services/job_filter_service');
|
||||
|
||||
/**
|
||||
* 自动投递处理器
|
||||
* 负责职位搜索、过滤、评分和自动投递
|
||||
*/
|
||||
class DeliverHandler extends BaseHandler {
|
||||
/**
|
||||
* 处理自动投递任务
|
||||
* @param {object} task - 任务对象
|
||||
* @returns {Promise<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;
|
||||
18
api/middleware/schedule/handlers/index.js
Normal file
18
api/middleware/schedule/handlers/index.js
Normal 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
|
||||
};
|
||||
87
api/middleware/schedule/handlers/searchHandler.js
Normal file
87
api/middleware/schedule/handlers/searchHandler.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const BaseHandler = require('./baseHandler');
|
||||
const ConfigManager = require('../services/configManager');
|
||||
const command = require('../command');
|
||||
const config = require('../config');
|
||||
|
||||
/**
|
||||
* 自动搜索处理器
|
||||
* 负责搜索职位列表
|
||||
*/
|
||||
class SearchHandler extends BaseHandler {
|
||||
/**
|
||||
* 处理自动搜索任务
|
||||
* @param {object} task - 任务对象
|
||||
* @returns {Promise<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;
|
||||
@@ -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');
|
||||
|
||||
/**
|
||||
* 调度系统管理器
|
||||
|
||||
14
api/middleware/schedule/infrastructure/index.js
Normal file
14
api/middleware/schedule/infrastructure/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Infrastructure 模块导出
|
||||
* 统一导出基础设施模块
|
||||
*/
|
||||
|
||||
const PriorityQueue = require('./PriorityQueue');
|
||||
const ErrorHandler = require('./ErrorHandler');
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = {
|
||||
PriorityQueue,
|
||||
ErrorHandler,
|
||||
config
|
||||
};
|
||||
9
api/middleware/schedule/notifiers/index.js
Normal file
9
api/middleware/schedule/notifiers/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Notifiers 模块导出
|
||||
*/
|
||||
|
||||
const deviceWorkStatusNotifier = require('./deviceWorkStatusNotifier');
|
||||
|
||||
module.exports = {
|
||||
deviceWorkStatusNotifier
|
||||
};
|
||||
199
api/middleware/schedule/services/accountValidator.js
Normal file
199
api/middleware/schedule/services/accountValidator.js
Normal 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;
|
||||
225
api/middleware/schedule/services/configManager.js
Normal file
225
api/middleware/schedule/services/configManager.js
Normal 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;
|
||||
395
api/middleware/schedule/services/jobFilterEngine.js
Normal file
395
api/middleware/schedule/services/jobFilterEngine.js
Normal 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;
|
||||
158
api/middleware/schedule/services/timeRangeValidator.js
Normal file
158
api/middleware/schedule/services/timeRangeValidator.js
Normal 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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
const dayjs = require('dayjs');
|
||||
const deviceManager = require('../deviceManager');
|
||||
const deviceManager = require('../core/deviceManager');
|
||||
const db = require('../../dbProxy');
|
||||
|
||||
/**
|
||||
|
||||
14
api/middleware/schedule/utils/index.js
Normal file
14
api/middleware/schedule/utils/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Utils 模块导出
|
||||
* 统一导出工具类模块
|
||||
*/
|
||||
|
||||
const SalaryParser = require('./salaryParser');
|
||||
const KeywordMatcher = require('./keywordMatcher');
|
||||
const ScheduleUtils = require('./scheduleUtils');
|
||||
|
||||
module.exports = {
|
||||
SalaryParser,
|
||||
KeywordMatcher,
|
||||
ScheduleUtils
|
||||
};
|
||||
225
api/middleware/schedule/utils/keywordMatcher.js
Normal file
225
api/middleware/schedule/utils/keywordMatcher.js
Normal 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;
|
||||
126
api/middleware/schedule/utils/salaryParser.js
Normal file
126
api/middleware/schedule/utils/salaryParser.js
Normal 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;
|
||||
Reference in New Issue
Block a user