This commit is contained in:
张成
2025-11-24 13:23:42 +08:00
commit 5d7444cd65
156 changed files with 50653 additions and 0 deletions

75
api/middleware/dbProxy.js Normal file
View File

@@ -0,0 +1,75 @@
const Framework = require('../../framework/node-core-framework');
/**
* 数据库代理模块
* 提供统一的数据库模型访问接口延迟获取models
*/
class DbProxy {
constructor() {
this._models = null;
}
/**
* 获取models实例
* @returns {object} models对象
*/
get models() {
if (!this._models) {
try {
this._models = Framework.getModels();
} catch (error) {
console.warn('无法获取models请确保框架已正确初始化:', error.message);
this._models = {};
}
}
return this._models;
}
/**
* 获取指定的模型
* @param {string} modelName 模型名称
* @returns {object} 模型实例
*/
getModel(modelName) {
const models = this.models;
if (!models[modelName]) {
throw new Error(`模型 '${modelName}' 不存在`);
}
return models[modelName];
}
/**
* 获取所有模型
* @returns {object} 所有模型对象
*/
getAllModels() {
return this.models;
}
/**
* 检查模型是否存在
* @param {string} modelName 模型名称
* @returns {boolean} 是否存在
*/
hasModel(modelName) {
return !!this.models[modelName];
}
/**
* 获取模型列表
* @returns {array} 模型名称列表
*/
getModelNames() {
return Object.keys(this.models);
}
/**
* 重新加载模型(在框架重新初始化后调用)
*/
reload() {
this._models = null;
}
}
// 导出单例实例
module.exports = new DbProxy();

View File

@@ -0,0 +1,289 @@
const axios = require('axios');
const config = require('../../../config/config');
const logs = require('../logProxy');
/**
* DeepSeek大模型服务
* 集成DeepSeek API提供智能化的岗位筛选、聊天生成、简历分析等功能
*/
class aiService {
constructor() {
this.apiKey = config.deepseekApiKey || process.env.DEEPSEEK_API_KEY;
this.apiUrl = config.deepseekApiUrl || 'https://api.deepseek.com/v1/chat/completions';
this.model = config.deepseekModel || 'deepseek-chat';
this.maxRetries = 3;
}
/**
* 调用DeepSeek API
* @param {string} prompt - 提示词
* @param {object} options - 配置选项
* @returns {Promise<object>} API响应结果
*/
async callAPI(prompt, options = {}) {
const requestData = {
model: this.model,
messages: [
{
role: 'system',
content: options.systemPrompt || '你是一个专业的招聘顾问,擅长分析岗位信息、生成聊天内容和分析简历匹配度。'
},
{
role: 'user',
content: prompt
}
],
temperature: options.temperature || 0.7,
max_tokens: options.maxTokens || 2000,
top_p: options.topP || 0.9
};
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const response = await axios.post(this.apiUrl, requestData, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
timeout: 30000
});
return {
data: response.data,
content: response.data.choices?.[0]?.message?.content || ''
};
} catch (error) {
console.log(`DeepSeek API调用失败 (尝试 ${attempt}/${this.maxRetries}): ${error.message}`);
if (attempt === this.maxRetries) {
throw new Error(error.message);
}
// 等待后重试
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
/**
* 岗位智能筛选
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @returns {Promise<object>} 筛选结果
*/
async analyzeJob(jobInfo, resumeInfo) {
const prompt = `
请分析以下岗位信息,并给出详细的评估结果:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 薪资范围:${jobInfo.salary || '未知'}
- 工作地点:${jobInfo.location || '未知'}
- 岗位描述:${jobInfo.description || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
简历信息:
- 技能标签:${resumeInfo.skills || '未知'}
- 工作经验:${resumeInfo.experience || '未知'}
- 教育背景:${resumeInfo.education || '未知'}
- 期望薪资:${resumeInfo.expectedSalary || '未知'}
请从以下维度进行分析:
1. 技能匹配度0-100分
2. 经验匹配度0-100分
3. 薪资合理性0-100分
4. 公司质量评估0-100分
5. 是否为外包岗位(是/否)
6. 综合推荐指数0-100分
7. 详细分析说明
8. 投递建议
请以JSON格式返回结果。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的招聘分析师,擅长评估岗位与简历的匹配度。请提供客观、专业的分析结果。',
temperature: 0.3
});
try {
// 尝试解析JSON响应
const analysis = JSON.parse(result.content);
return {
analysis: analysis
};
} catch (parseError) {
// 如果解析失败,返回原始内容
return {
analysis: {
content: result.content,
parseError: true
}
};
}
}
/**
* 生成个性化聊天内容
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @param {string} chatType - 聊天类型 (greeting/interview/followup)
* @returns {Promise<object>} 聊天内容
*/
async generateChatContent(jobInfo, resumeInfo, chatType = 'greeting') {
const chatTypeMap = {
'greeting': '初次打招呼',
'interview': '面试邀约',
'followup': '跟进沟通'
};
const prompt = `
请为以下场景生成个性化的聊天内容:
聊天类型:${chatTypeMap[chatType] || chatType}
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
简历信息:
- 技能标签:${resumeInfo.skills || '未知'}
- 工作经验:${resumeInfo.experience || '未知'}
- 项目经验:${resumeInfo.projects || '未知'}
要求:
1. 内容要自然、专业、个性化
2. 突出简历与岗位的匹配点
3. 避免过于机械化的表达
4. 长度控制在100-200字
5. 体现求职者的诚意和热情
请直接返回聊天内容,不需要其他格式。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的招聘沟通专家,擅长生成自然、专业的求职聊天内容。',
temperature: 0.8
});
return result;
}
/**
* 分析简历要素
* @param {string} resumeText - 简历文本内容
* @returns {Promise<object>} 简历分析结果
*/
async analyzeResume(resumeText) {
const prompt = `
请分析以下简历内容,提取核心要素:
简历内容:
${resumeText}
请提取以下信息:
1. 技能标签(编程语言、框架、工具等)
2. 工作经验(年限、行业、项目等)
3. 教育背景(学历、专业、证书等)
4. 期望薪资范围
5. 期望工作地点
6. 核心优势
7. 职业发展方向
请以JSON格式返回结果。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的简历分析师,擅长提取简历的核心要素和关键信息。',
temperature: 0.2
});
try {
const analysis = JSON.parse(result.content);
return {
analysis: analysis
};
} catch (parseError) {
return {
analysis: {
content: result.content,
parseError: true
}
};
}
}
/**
* 生成面试邀约内容
* @param {object} jobInfo - 岗位信息
* @param {object} chatHistory - 聊天历史
* @returns {Promise<object>} 面试邀约内容
*/
async generateInterviewInvitation(jobInfo, chatHistory) {
const prompt = `
请基于以下信息生成面试邀约内容:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 工作地点:${jobInfo.location || '未知'}
聊天历史:
${chatHistory || '无'}
要求:
1. 表达面试邀约的诚意
2. 提供灵活的时间选择
3. 说明面试形式和地点
4. 体现对候选人的重视
5. 语言自然、专业
请直接返回面试邀约内容。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的HR擅长生成面试邀约内容。',
temperature: 0.6
});
return result;
}
/**
* 识别外包岗位
* @param {object} jobInfo - 岗位信息
* @returns {Promise<object>} 外包识别结果
*/
async identifyOutsourcingJob(jobInfo) {
const prompt = `
请分析以下岗位信息,判断是否为外包岗位:
岗位信息:
- 公司名称:${jobInfo.companyName || '未知'}
- 职位名称:${jobInfo.jobTitle || '未知'}
- 岗位描述:${jobInfo.description || '未知'}
- 技能要求:${jobInfo.skills || '未知'}
- 工作地点:${jobInfo.location || '未知'}
外包岗位特征:
1. 公司名称包含"外包"、"派遣"、"人力"等关键词
2. 岗位描述提到"项目外包"、"驻场开发"等
3. 技能要求过于宽泛或具体
4. 工作地点频繁变动
5. 薪资结构不明确
请判断是否为外包岗位,并给出详细分析。
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的岗位分析师,擅长识别外包岗位的特征。',
temperature: 0.3
});
return result;
}
}
module.exports = new aiService();

View File

@@ -0,0 +1,421 @@
// const aiService = require('./aiService'); // 二期规划AI 服务暂时禁用
const logs = require('../logProxy');
/**
* 智能聊天管理模块
* 负责聊天内容生成、发送策略和效果监控
*/
class ChatManager {
constructor() {
this.chatHistory = new Map(); // 聊天历史记录
this.chatStrategies = new Map(); // 聊天策略配置
this.effectStats = new Map(); // 聊天效果统计
this.initDefaultStrategies();
}
/**
* 初始化默认聊天策略
*/
initDefaultStrategies() {
// 初次打招呼策略
this.chatStrategies.set('greeting', {
name: '初次打招呼',
description: '向HR发送初次打招呼消息',
template: 'greeting',
timing: 'immediate',
retryCount: 1,
retryInterval: 300000 // 5分钟
});
// 面试邀约策略
this.chatStrategies.set('interview', {
name: '面试邀约',
description: '发送面试邀约消息',
template: 'interview',
timing: 'after_greeting',
retryCount: 2,
retryInterval: 600000 // 10分钟
});
// 跟进沟通策略
this.chatStrategies.set('followup', {
name: '跟进沟通',
description: '跟进之前的沟通',
template: 'followup',
timing: 'after_interview',
retryCount: 1,
retryInterval: 86400000 // 24小时
});
}
/**
* 生成聊天内容
* @param {string} sn_code - 设备SN码
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @param {string} chatType - 聊天类型
* @param {object} context - 聊天上下文
* @returns {Promise<object>} 聊天内容
*/
async generateChatContent(sn_code, jobInfo, resumeInfo, chatType = 'greeting', context = {}) {
console.log(`[聊天管理] 开始生成设备 ${sn_code} 的聊天内容,类型: ${chatType}`);
// 获取聊天策略
const strategy = this.chatStrategies.get(chatType);
if (!strategy) {
throw new Error(`未找到聊天类型 ${chatType} 的策略配置`);
}
// 二期规划AI 生成聊天内容暂时禁用,使用默认模板
// const chatContent = await aiService.generateChatContent(jobInfo, resumeInfo, chatType);
// if (!chatContent.success) {
// console.error(`[聊天管理] AI生成聊天内容失败:`, chatContent.error);
// throw new Error(chatContent.error);
// }
console.log(`[聊天管理] AI生成已禁用二期规划使用默认聊天模板`);
const chatContent = this.generateDefaultChatContent(jobInfo, resumeInfo, chatType);
const result = {
sn_code: sn_code,
jobInfo: jobInfo,
chatType: chatType,
strategy: strategy,
content: chatContent.content,
context: context,
timestamp: Date.now()
};
// 记录聊天历史
this.recordChatHistory(sn_code, result);
console.log(`[聊天管理] 聊天内容生成成功:`, result);
return result;
}
/**
* 发送聊天消息
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} chatData - 聊天数据
* @returns {Promise<object>} 发送结果
*/
async sendChatMessage(sn_code, mqttClient, chatData) {
console.log(`[聊天管理] 开始发送聊天消息到设备 ${sn_code}`);
// 构建发送指令
const sendData = {
platform: 'boss',
action: 'send_chat_message',
data: {
jobId: chatData.jobInfo.jobId,
companyId: chatData.jobInfo.companyId,
message: chatData.content,
chatType: chatData.chatType
}
};
// 发送MQTT指令
const response = await mqttClient.publishAndWait(sn_code, sendData);
if (!response || response.code !== 200) {
// 更新聊天状态
this.updateChatStatus(sn_code, chatData, 'failed', response);
console.error(`[聊天管理] 聊天消息发送失败:`, response);
throw new Error(response?.message || '聊天消息发送失败');
}
// 更新聊天状态
this.updateChatStatus(sn_code, chatData, 'sent', response);
// 记录效果统计
this.recordChatEffect(sn_code, chatData, 'sent');
console.log(`[聊天管理] 聊天消息发送成功:`, response);
return response;
}
/**
* 生成面试邀约
* @param {string} sn_code - 设备SN码
* @param {object} jobInfo - 岗位信息
* @param {object} chatHistory - 聊天历史
* @returns {Promise<object>} 面试邀约内容
*/
async generateInterviewInvitation(sn_code, jobInfo, chatHistory) {
console.log(`[聊天管理] 开始生成设备 ${sn_code} 的面试邀约`);
console.log(`[聊天管理] AI生成已禁用二期规划使用默认模板`);
// 二期规划AI 生成面试邀约暂时禁用,使用默认模板
// const invitation = await aiService.generateInterviewInvitation(jobInfo, chatHistory);
// if (!invitation.success) {
// console.error(`[聊天管理] AI生成面试邀约失败:`, invitation.error);
// throw new Error(invitation.error);
// }
const invitation = this.generateDefaultInterviewInvitation(jobInfo);
const result = {
sn_code: sn_code,
jobInfo: jobInfo,
chatType: 'interview',
content: invitation.content,
timestamp: Date.now()
};
// 记录聊天历史
this.recordChatHistory(sn_code, result);
console.log(`[聊天管理] 面试邀约生成成功:`, result);
return result;
}
/**
* 记录聊天历史
* @param {string} sn_code - 设备SN码
* @param {object} chatData - 聊天数据
*/
recordChatHistory(sn_code, chatData) {
if (!this.chatHistory.has(sn_code)) {
this.chatHistory.set(sn_code, []);
}
const history = this.chatHistory.get(sn_code);
history.push({
...chatData,
id: Date.now() + Math.random(),
status: 'generated'
});
// 限制历史记录数量
if (history.length > 100) {
history.splice(0, history.length - 100);
}
}
/**
* 更新聊天状态
* @param {string} sn_code - 设备SN码
* @param {object} chatData - 聊天数据
* @param {string} status - 新状态
* @param {object} response - 响应数据
*/
updateChatStatus(sn_code, chatData, status, response = {}) {
if (!this.chatHistory.has(sn_code)) {
return;
}
const history = this.chatHistory.get(sn_code);
const chatRecord = history.find(record =>
record.timestamp === chatData.timestamp &&
record.chatType === chatData.chatType
);
if (chatRecord) {
chatRecord.status = status;
chatRecord.response = response;
}
}
/**
* 记录聊天效果
* @param {string} sn_code - 设备SN码
* @param {object} chatData - 聊天数据
* @param {string} action - 动作类型
*/
recordChatEffect(sn_code, chatData, action) {
if (!this.effectStats.has(sn_code)) {
this.effectStats.set(sn_code, {
totalSent: 0,
totalReplied: 0,
totalInterview: 0,
replyRate: 0,
interviewRate: 0,
lastUpdate: Date.now()
});
}
const stats = this.effectStats.get(sn_code);
if (action === 'sent') {
stats.totalSent++;
} else if (action === 'replied') {
stats.totalReplied++;
} else if (action === 'interview') {
stats.totalInterview++;
}
// 计算比率
if (stats.totalSent > 0) {
stats.replyRate = (stats.totalReplied / stats.totalSent * 100).toFixed(2);
stats.interviewRate = (stats.totalInterview / stats.totalSent * 100).toFixed(2);
}
stats.lastUpdate = Date.now();
}
/**
* 获取聊天历史
* @param {string} sn_code - 设备SN码
* @param {object} filters - 过滤条件
* @returns {Array} 聊天历史
*/
getChatHistory(sn_code, filters = {}) {
if (!this.chatHistory.has(sn_code)) {
return [];
}
let history = this.chatHistory.get(sn_code);
// 应用过滤条件
if (filters.chatType) {
history = history.filter(record => record.chatType === filters.chatType);
}
if (filters.status) {
history = history.filter(record => record.status === filters.status);
}
if (filters.startTime) {
history = history.filter(record => record.timestamp >= filters.startTime);
}
if (filters.endTime) {
history = history.filter(record => record.timestamp <= filters.endTime);
}
// 按时间倒序排列
return history.sort((a, b) => b.timestamp - a.timestamp);
}
/**
* 获取聊天效果统计
* @param {string} sn_code - 设备SN码
* @returns {object} 效果统计
*/
getChatEffectStats(sn_code) {
if (!this.effectStats.has(sn_code)) {
return {
totalSent: 0,
totalReplied: 0,
totalInterview: 0,
replyRate: 0,
interviewRate: 0,
lastUpdate: Date.now()
};
}
return this.effectStats.get(sn_code);
}
/**
* 设置聊天策略
* @param {string} chatType - 聊天类型
* @param {object} strategy - 策略配置
*/
setChatStrategy(chatType, strategy) {
this.chatStrategies.set(chatType, {
...this.chatStrategies.get(chatType),
...strategy
});
console.log(`[聊天管理] 更新聊天策略 ${chatType}:`, strategy);
}
/**
* 清理过期数据
*/
cleanup() {
const now = Date.now();
const expireTime = 30 * 24 * 3600000; // 30天
// 清理过期的聊天历史
for (const [sn_code, history] of this.chatHistory.entries()) {
const filteredHistory = history.filter(record =>
now - record.timestamp < expireTime
);
if (filteredHistory.length === 0) {
this.chatHistory.delete(sn_code);
} else {
this.chatHistory.set(sn_code, filteredHistory);
}
}
// 清理过期的效果统计
for (const [sn_code, stats] of this.effectStats.entries()) {
if (now - stats.lastUpdate > expireTime) {
this.effectStats.delete(sn_code);
}
}
console.log(`[聊天管理] 数据清理完成`);
}
/**
* 生成默认聊天内容(替代 AI 生成)
* @param {object} jobInfo - 岗位信息
* @param {object} resumeInfo - 简历信息
* @param {string} chatType - 聊天类型
* @returns {object} 聊天内容
*/
generateDefaultChatContent(jobInfo, resumeInfo, chatType) {
const templates = {
greeting: '您好,我对这个岗位很感兴趣,希望能进一步了解。',
interview: '感谢您的回复,我很期待与您进一步沟通。',
followup: '您好,想了解一下这个岗位的最新进展。'
};
const content = templates[chatType] || templates.greeting;
return {
success: true,
content: content,
chatType: chatType
};
}
/**
* 生成默认面试邀约(替代 AI 生成)
* @param {object} jobInfo - 岗位信息
* @returns {object} 面试邀约内容
*/
generateDefaultInterviewInvitation(jobInfo) {
return {
success: true,
content: '感谢您的邀请,我很期待与您面谈。请问方便的时间是什么时候?',
jobTitle: jobInfo.jobTitle || '该岗位',
companyName: jobInfo.companyName || '贵公司'
};
}
/**
* 获取聊天列表
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数
* @returns {Promise<object>} 聊天列表
*/
async get_chat_list(sn_code, mqttClient, params = {}) {
const { platform = 'boss', pageCount = 3 } = params;
console.log(`[聊天管理] 开始获取设备 ${sn_code} 的聊天列表`);
// 通过MQTT指令获取聊天列表
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: "get_chat_list",
data: { pageCount }
});
if (!response || response.code !== 200) {
console.error(`[聊天管理] 获取聊天列表失败:`, response);
throw new Error('获取聊天列表失败');
}
console.log(`[聊天管理] 成功获取聊天列表`);
return response.data;
}
}
module.exports = new ChatManager();

View File

@@ -0,0 +1,33 @@
/**
* Job 模块统一导出
* 聚合所有 job 相关模块的方法,提供统一的对外接口
*/
const jobManager = require('./jobManager');
const resumeManager = require('./resumeManager');
const chatManager = require('./chatManager');
const pack = (instance) => {
const proto = Object.getPrototypeOf(instance);
const methods = Object.getOwnPropertyNames(proto)
.filter(k => k !== 'constructor')
.reduce((acc, key) => {
acc[key] = proto[key].bind(instance);
return acc;
}, {});
return { ...instance, ...methods };
}
/**
* 便捷方法:直接导出常用方法
* 使用下划线命名规范
*/
module.exports = {
...pack(jobManager),
...pack(resumeManager),
...pack(chatManager),
};

View File

@@ -0,0 +1,876 @@
// const aiService = require('./aiService'); // 二期规划AI 服务暂时禁用
const jobFilterService = require('./job_filter_service'); // 使用文本匹配过滤服务
const locationService = require('../../services/locationService'); // 位置服务
const logs = require('../logProxy');
const db = require('../dbProxy');
const { v4: uuidv4 } = require('uuid');
/**
* 工作管理模块
* 负责简历获取、分析、存储和匹配度计算
*/
class JobManager {
constructor() {
}
// 启动客户端那个平台 用户信息,心跳机制
async set_user_info(sn_code, mqttClient, user_info) {
const response = await mqttClient.publishAndWait(sn_code, {
platform: platform,
action: 'set_user_info',
data: {
user_info: user_info
}
});
}
/**
* 获取登录二维码
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数对象
* @returns {Promise<object>} 二维码信息
*/
async get_login_qr_code(sn_code, mqttClient, params = {}) {
console.log(`[工作管理] 开始获取设备 ${sn_code} 的登录二维码`);
const { platform = 'boss' } = params;
// 通过MQTT指令获取登录二维码
const response = await mqttClient.publishAndWait(sn_code, {
platform: platform,
action: 'get_login_qr_code',
data: {}
});
if (!response || response.code !== 200) {
console.error(`[工作管理] 获取登录二维码失败:`, response);
throw new Error(response?.message || '获取登录二维码失败');
}
const qrCodeData = response.data;
console.log(`[工作管理] 成功获取登录二维码数据:`, qrCodeData);
return qrCodeData;
}
/**
* 打开机器人检测测试页
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数对象
* @returns {Promise<object>} 执行结果
*/
async open_bot_detection(sn_code, mqttClient, params = {}) {
console.log(`[工作管理] 开始打开设备 ${sn_code} 的机器人检测测试页`);
const { platform = 'boss' } = params;
// 通过MQTT指令打开机器人检测测试页
const response = await mqttClient.publishAndWait(sn_code, {
platform: platform,
action: 'open_bot_detection',
data: {}
});
if (!response || response.code !== 200) {
console.error(`[工作管理] 打开机器人检测测试页失败:`, response);
throw new Error(response?.message || '打开机器人检测测试页失败');
}
const result = response.data;
console.log(`[工作管理] 成功打开机器人检测测试页:`, result);
return result;
}
/**
* 获取用户信息
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数对象
* @returns {Promise<object>} 用户信息
*/
async get_user_info(sn_code, mqttClient, params = {}) {
console.log(`[工作管理] 开始获取设备 ${sn_code} 的用户信息`);
const { platform = 'boss' } = params;
// 通过MQTT指令获取用户信息
const response = await mqttClient.publishAndWait(sn_code, {
platform: platform,
action: 'get_user_info',
data: {}
});
if (!response || response.code !== 200) {
console.error(`[工作管理] 获取用户信息失败:`, response);
throw new Error(response?.message || '获取用户信息失败');
}
const userInfo = response.data;
console.log(`[工作管理] 成功获取用户信息:`, userInfo);
return userInfo;
}
/**
* 搜索岗位
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数
* @returns {Promise<object>} 搜索结果
*/
async search_jobs(sn_code, mqttClient, params = {}) {
const { keyword = '前端', platform = 'boss' } = params;
console.log(`[工作管理] 开始搜索设备 ${sn_code} 的岗位,关键词: ${keyword}`);
// 通过MQTT指令搜索岗位
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: "search_jobs",
data: { keyword }
});
if (!response || response.code !== 200) {
console.error(`[工作管理] 搜索岗位失败:`, response);
throw new Error('搜索岗位失败');
}
console.log(`[工作管理] 成功搜索岗位`);
return response.data;
}
/**
* 获取岗位列表
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数
* @returns {Promise<object>} 岗位列表
*/
async get_job_list(sn_code, mqttClient, params = {}) {
const { keyword = '前端', platform = 'boss', pageCount = 3 } = params;
console.log(`[工作管理] 开始获取设备 ${sn_code} 的岗位列表,关键词: ${keyword}`);
// 通过MQTT指令获取岗位列表
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: "get_job_list",
data: { keyword, pageCount }
});
if (!response || response.code !== 200) {
console.error(`[工作管理] 获取岗位列表失败:`, response);
throw new Error('获取岗位列表失败');
}
// 处理职位列表数据response.data 可能是数组(职位列表.json 格式)或单个对象
let jobs = [];
if (Array.isArray(response.data)) {
// 如果是数组格式(职位列表.json遍历每个元素提取岗位数据
for (const item of response.data) {
if (item.data?.zpData?.jobList && Array.isArray(item.data.zpData.jobList)) {
jobs = jobs.concat(item.data.zpData.jobList);
}
}
console.log(`[工作管理] 从 ${response.data.length} 个响应中提取岗位数据`);
} else if (response.data?.data?.zpData?.jobList) {
// 如果是单个对象格式,从 data.zpData.jobList 获取
jobs = response.data.data.zpData.jobList || [];
} else if (response.data?.zpData?.jobList) {
// 兼容旧格式:直接从 zpData.jobList 获取
jobs = response.data.zpData.jobList || [];
}
console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`);
// 保存职位到数据库
try {
await this.saveJobsToDatabase(sn_code, platform, keyword, jobs);
} catch (error) {
console.error(`[工作管理] 保存职位到数据库失败:`, error);
// 不影响主流程,继续返回数据
}
const result = {
jobs: jobs,
keyword: keyword,
platform: platform,
count: jobs.length
};
return result;
}
/**
* 保存职位列表到数据库
* @param {string} sn_code - 设备SN码
* @param {string} platform - 平台
* @param {string} keyword - 搜索关键词
* @param {Array} jobs - 职位列表
*/
async saveJobsToDatabase(sn_code, platform, keyword, jobs) {
const job_postings = db.getModel('job_postings');
console.log(`[工作管理] 开始保存 ${jobs.length} 个职位到数据库`);
for (const job of jobs) {
try {
// 构建职位信息对象
const jobInfo = {
sn_code,
platform,
keyword,
// Boss直聘字段映射
encryptBossId: job.encryptBossId || '',
jobId: job.encryptJobId || '',
jobTitle: job.jobName || '',
companyId: job.encryptBrandId || '',
companyName: job.brandName || '',
companySize: job.brandScaleName || '',
companyIndustry: job.brandIndustry || '',
salary: job.salaryDesc || '',
// 岗位要求(从 jobLabels 和 skills 提取)
jobRequirements: JSON.stringify({
experience: job.jobExperience || '',
education: job.jobDegree || '',
labels: job.jobLabels || [],
skills: job.skills || []
}),
// 工作地点
location: [job.cityName, job.areaDistrict, job.businessDistrict]
.filter(Boolean).join(' '),
experience: job.jobExperience || '',
education: job.jobDegree || '',
// 原始数据
originalData: JSON.stringify(job),
// 默认状态
applyStatus: 'pending',
chatStatus: 'none'
};
// 调用位置服务解析 location + companyName 获取坐标
if (jobInfo.location && jobInfo.companyName) {
const addressToParse = `${jobInfo.location.replaceAll(' ', '')}${jobInfo.companyName}`;
// 等待 1秒
await new Promise(resolve => setTimeout(resolve, 1000));
const location = await locationService.getLocationByAddress(addressToParse).catch(error => {
console.error(`[工作管理] 获取位置失败:`, error);
});
if (location) {
jobInfo.latitude = String(location.lat);
jobInfo.longitude = String(location.lng);
}
}
// 检查是否已存在(根据 jobId 和 sn_code
const existingJob = await job_postings.findOne({
where: {
jobId: jobInfo.jobId,
sn_code: sn_code
}
});
if (existingJob) {
// 更新现有职位
await job_postings.update(jobInfo, {
where: {
jobId: jobInfo.jobId,
sn_code: sn_code
}
});
console.log(`[工作管理] 职位已更新 - ${jobInfo.jobTitle} @ ${jobInfo.companyName}`);
} else {
// 创建新职位
await job_postings.create(jobInfo);
console.log(`[工作管理] 职位已创建 - ${jobInfo.jobTitle} @ ${jobInfo.companyName}`);
}
} catch (error) {
console.error(`[工作管理] 保存职位失败:`, error, job);
// 继续处理下一个职位
}
}
console.log(`[工作管理] 职位保存完成`);
}
/**
* 投递简历(单个职位)
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数
* @param {string} params.jobId - 职位ID必需
* @param {string} params.platform - 平台默认boss
* @param {string} params.encryptBossId - 加密的Boss ID可选
* @param {string} params.securityId - 安全ID可选
* @param {string} params.brandName - 公司名称(可选)
* @param {string} params.jobTitle - 职位标题(可选)
* @param {string} params.companyName - 公司名称(可选)
* @returns {Promise<object>} 投递结果
*/
async applyJob(sn_code, mqttClient, params = {}) {
const { platform = 'boss', jobId, encryptBossId, securityId, brandName, jobTitle, companyName } = params;
if (!jobId) {
throw new Error('jobId 参数不能为空请指定要投递的职位ID');
}
console.log(`[工作管理] 开始投递单个职位,设备: ${sn_code}, 职位: ${jobTitle || jobId}`);
const job_postings = db.getModel('job_postings');
const apply_records = db.getModel('apply_records');
try {
// 从数据库获取职位信息
const where = { sn_code, jobId, platform };
if (encryptBossId) where.encryptBossId = encryptBossId;
const job = await job_postings.findOne({ where });
if (!job) {
throw new Error(`未找到职位记录: ${jobId}`);
}
const jobData = job.toJSON();
// 检查是否已存在投递记录(避免重复投递同一职位)
const existingApply = await apply_records.findOne({ where: { sn_code, jobId: jobData.jobId } });
if (existingApply) {
console.log(`[工作管理] 跳过已投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`);
return {
success: false,
deliveredCount: 0,
failedCount: 1,
message: '该岗位已投递过',
deliveredJobs: [],
failedJobs: [{
jobId: jobData.jobId,
jobTitle: jobData.jobTitle,
companyName: jobData.companyName,
error: '该岗位已投递过'
}]
};
}
// 检查该公司是否在一个月内已投递过(避免连续投递同一公司)
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
const Sequelize = require('sequelize');
const recentCompanyApply = await apply_records.findOne({
where: {
sn_code: sn_code,
companyName: jobData.companyName,
applyTime: {
[Sequelize.Op.gte]: oneMonthAgo
}
},
order: [['applyTime', 'DESC']]
});
if (recentCompanyApply) {
const daysAgo = Math.floor((new Date() - new Date(recentCompanyApply.applyTime)) / (1000 * 60 * 60 * 24));
console.log(`[工作管理] 跳过一个月内已投递的公司: ${jobData.companyName} (${daysAgo}天前投递过)`);
return {
success: false,
deliveredCount: 0,
failedCount: 1,
message: `该公司在${daysAgo}天前已投递过,一个月内不重复投递`,
deliveredJobs: [],
failedJobs: [{
jobId: jobData.jobId,
jobTitle: jobData.jobTitle,
companyName: jobData.companyName,
error: `该公司在${daysAgo}天前已投递过,一个月内不重复投递`
}]
};
}
console.log(`[工作管理] 投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`);
// 通过MQTT指令投递简历
const response = await mqttClient.publishAndWait(sn_code, {
platform,
action: "deliver_resume",
data: {
encryptJobId: jobData.jobId,
securityId: jobData.securityId || securityId || '',
brandName: jobData.companyName || brandName || ''
}
});
if (response && response.code === 200) {
// 投递成功
await job_postings.update(
{ applyStatus: 'applied', applyTime: new Date() },
{ where: { id: jobData.id } }
);
// 计算距离和获取评分信息
let distance = null;
let locationScore = jobData.scoreDetails?.locationScore || 0;
try {
// 获取账号的经纬度
const pla_account = db.getModel('pla_account');
const account = await pla_account.findOne({
where: { sn_code, platform_type: platform }
});
// 如果账号和职位都有经纬度,计算实际距离
if (account && account.user_latitude && account.user_longitude &&
jobData.latitude && jobData.longitude) {
const userLat = parseFloat(account.user_latitude);
const userLng = parseFloat(account.user_longitude);
const jobLat = parseFloat(jobData.latitude);
const jobLng = parseFloat(jobData.longitude);
if (!isNaN(userLat) && !isNaN(userLng) && !isNaN(jobLat) && !isNaN(jobLng)) {
distance = locationService.calculateDistance(userLat, userLng, jobLat, jobLng);
console.log(`[工作管理] 计算距离: ${distance} 公里`);
}
}
} catch (error) {
console.warn(`[工作管理] 计算距离失败:`, error.message);
// 距离计算失败不影响投递记录保存
}
await apply_records.create({
sn_code: sn_code,
platform: platform,
jobId: jobData.jobId,
encryptBossId: jobData.encryptBossId || '',
jobTitle: jobData.jobTitle,
companyName: jobData.companyName,
companyId: jobData.companyId || '',
salary: jobData.salary || '',
location: jobData.location || '',
resumeId: jobData.resumeId || '',
resumeName: jobData.resumeName || '',
matchScore: jobData.matchScore || jobData.scoreDetails?.totalScore || 0,
isOutsourcing: jobData.isOutsourcing || false,
priority: jobData.priority || 5,
isAutoApply: true,
applyStatus: 'success',
applyTime: new Date(),
feedbackStatus: 'none',
taskId: jobData.taskId || '',
keyword: jobData.keyword || '',
originalData: JSON.stringify({
...(response.data || {}),
scoreDetails: {
...(jobData.scoreDetails || {}),
locationScore: locationScore,
distance: distance // 实际距离(公里)
}
})
});
console.log(`[工作管理] 投递成功: ${jobData.jobTitle}`);
return {
success: true,
deliveredCount: 1,
failedCount: 0,
deliveredJobs: [{
jobId: jobData.jobId,
jobTitle: jobData.jobTitle,
companyName: jobData.companyName
}],
failedJobs: []
};
} else {
// 投递失败
await job_postings.update(
{ applyStatus: 'failed' },
{ where: { id: jobData.id } }
);
await apply_records.create({
sn_code: sn_code,
platform: platform,
jobId: jobData.jobId,
encryptBossId: jobData.encryptBossId || '',
jobTitle: jobData.jobTitle,
companyName: jobData.companyName,
companyId: jobData.companyId || '',
salary: jobData.salary || '',
location: jobData.location || '',
resumeId: jobData.resumeId || '',
resumeName: jobData.resumeName || '',
matchScore: jobData.matchScore || 0,
isOutsourcing: jobData.isOutsourcing || false,
priority: jobData.priority || 5,
isAutoApply: true,
applyStatus: 'failed',
applyTime: new Date(),
feedbackStatus: 'none',
taskId: jobData.taskId || '',
keyword: jobData.keyword || '',
errorMessage: response?.message || '投递失败',
originalData: JSON.stringify(response || {})
});
throw new Error(response?.message || '投递失败');
}
} catch (error) {
console.error(`[工作管理] 投递异常: ${jobTitle || jobId}`, error);
throw error;
}
}
/**
* 根据规则过滤职位并评分
* 评分规则距离30% + 关键字10% + 薪资20% + 公司规模20% = 总分
* 只有总分 >= 60 的职位才会被投递
* @param {Array} jobs - 职位列表
* @param {Object} filterRules - 过滤规则(包含账号配置和简历信息)
* @returns {Promise<Array>} 过滤并评分后的职位列表(按总分降序排序)
*/
async filter_jobs_by_rules(jobs, filterRules) {
const {
minSalary = 0,
maxSalary = 0,
keywords = [],
excludeKeywords = [],
accountConfig = {}, // 账号配置
resumeInfo = {} // 简历信息
} = filterRules;
// 关键词从职位类型配置中获取,不再从账号配置中获取
let filterKeywords = keywords || [];
let excludeKeywordsList = excludeKeywords || [];
// 使用账号配置的薪资范围
const accountMinSalary = accountConfig.min_salary || minSalary || 0;
const accountMaxSalary = accountConfig.max_salary || maxSalary || 0;
// 对每个职位进行评分
const scoredJobs = jobs.map(job => {
const jobData = job.toJSON ? job.toJSON() : job;
// 1. 距离分数30%
const locationScore = this.calculate_location_score(
jobData.location,
resumeInfo.expectedLocation
);
// 2. 关键字分数10%
const keywordScore = this.calculate_keyword_score(
jobData,
filterKeywords
);
// 3. 薪资分数20%
const salaryScore = this.calculate_salary_score(
jobData.salary,
resumeInfo.expectedSalary,
accountMinSalary,
accountMaxSalary
);
// 4. 公司规模分数20%
const companySizeScore = this.calculate_company_size_score(
jobData.companySize
);
// 计算总分(累加)
const totalScore = Math.round(
locationScore * 0.3 +
keywordScore * 0.1 +
salaryScore * 0.2 +
companySizeScore * 0.2
);
return {
...jobData,
matchScore: totalScore,
scoreDetails: {
locationScore,
keywordScore,
salaryScore,
companySizeScore,
totalScore
}
};
});
// 过滤:排除关键词、最低分数、按总分排序
return scoredJobs
.filter(job => {
// 1. 排除关键词过滤
if (excludeKeywordsList && excludeKeywordsList.length > 0) {
const jobText = `${job.jobTitle} ${job.companyName} ${job.jobDescription || ''}`.toLowerCase();
const hasExcluded = excludeKeywordsList.some(kw => jobText.includes(kw.toLowerCase()));
if (hasExcluded) {
return false;
}
}
// 2. 最低分数过滤(总分 >= 60 才投递)
if (job.matchScore < 60) {
return false;
}
return true;
})
.sort((a, b) => b.matchScore - a.matchScore); // 按总分降序排序
}
/**
* 计算距离分数0-100分
* @param {string} jobLocation - 职位工作地点
* @param {string} expectedLocation - 期望工作地点
* @returns {number} 距离分数
*/
calculate_location_score(jobLocation, expectedLocation) {
if (!jobLocation) return 50; // 没有地点信息,给中等分数
if (!expectedLocation) return 70; // 没有期望地点,给较高分数
const jobLoc = jobLocation.toLowerCase().trim();
const expectedLoc = expectedLocation.toLowerCase().trim();
// 完全匹配
if (jobLoc === expectedLoc) {
return 100;
}
// 包含匹配(如:期望"北京",职位"北京朝阳区"
if (jobLoc.includes(expectedLoc) || expectedLoc.includes(jobLoc)) {
return 90;
}
// 城市匹配(提取城市名)
const jobCity = this.extract_city(jobLoc);
const expectedCity = this.extract_city(expectedLoc);
if (jobCity && expectedCity && jobCity === expectedCity) {
return 80;
}
// 部分匹配(如:都包含"北京"
if (jobLoc.includes('北京') && expectedLoc.includes('北京')) {
return 70;
}
if (jobLoc.includes('上海') && expectedLoc.includes('上海')) {
return 70;
}
if (jobLoc.includes('深圳') && expectedLoc.includes('深圳')) {
return 70;
}
if (jobLoc.includes('广州') && expectedLoc.includes('广州')) {
return 70;
}
// 不匹配
return 30;
}
/**
* 提取城市名
* @param {string} location - 地点字符串
* @returns {string|null} 城市名
*/
extract_city(location) {
const cities = ['北京', '上海', '深圳', '广州', '杭州', '成都', '南京', '武汉', '西安', '苏州'];
for (const city of cities) {
if (location.includes(city)) {
return city;
}
}
return null;
}
/**
* 计算关键字分数0-100分
* @param {object} jobData - 职位数据
* @param {Array} filterKeywords - 过滤关键词列表
* @returns {number} 关键字分数
*/
calculate_keyword_score(jobData, filterKeywords) {
if (!filterKeywords || filterKeywords.length === 0) {
return 70; // 没有关键词要求,给较高分数
}
const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''} ${jobData.keyword || ''}`.toLowerCase();
// 计算匹配的关键词数量
let matchedCount = 0;
filterKeywords.forEach(kw => {
if (jobText.includes(kw.toLowerCase())) {
matchedCount++;
}
});
// 计算匹配度百分比
const matchRate = matchedCount / filterKeywords.length;
return Math.round(matchRate * 100);
}
/**
* 计算薪资分数0-100分
* @param {string} jobSalary - 职位薪资
* @param {string} expectedSalary - 期望薪资
* @param {number} accountMinSalary - 账号配置最低薪资
* @param {number} accountMaxSalary - 账号配置最高薪资
* @returns {number} 薪资分数
*/
calculate_salary_score(jobSalary, expectedSalary, accountMinSalary, accountMaxSalary) {
if (!jobSalary) return 50; // 没有薪资信息,给中等分数
const salaryRange = this.parse_salary_range(jobSalary);
if (!salaryRange || salaryRange.min === 0) return 50;
const avgJobSalary = (salaryRange.min + salaryRange.max) / 2;
// 优先使用账号配置的薪资范围
if (accountMinSalary > 0 || accountMaxSalary > 0) {
if (accountMinSalary > 0 && salaryRange.max < accountMinSalary) {
return 20; // 职位最高薪资低于账号最低要求
}
if (accountMaxSalary > 0 && salaryRange.min > accountMaxSalary) {
return 20; // 职位最低薪资高于账号最高要求
}
// 在范围内,根据薪资水平给分
if (avgJobSalary >= accountMinSalary && (accountMaxSalary === 0 || avgJobSalary <= accountMaxSalary)) {
return 100; // 完全符合要求
}
}
// 如果有期望薪资,与期望薪资比较
if (expectedSalary) {
const expected = this.parse_expected_salary(expectedSalary);
if (expected) {
const ratio = expected / avgJobSalary;
if (ratio <= 0.8) {
return 100; // 期望薪资低于职位薪资,完全匹配
} else if (ratio <= 1.0) {
return 90; // 期望薪资略低于或等于职位薪资
} else if (ratio <= 1.2) {
return 70; // 期望薪资略高于职位薪资
} else {
return 50; // 期望薪资明显高于职位薪资
}
}
}
// 没有期望薪资和账号配置,给中等分数
return 60;
}
/**
* 解析期望薪资
* @param {string} expectedSalary - 期望薪资描述
* @returns {number|null} 期望薪资数值(元)
*/
parse_expected_salary(expectedSalary) {
if (!expectedSalary) return null;
// 匹配数字+K格式20K
const match = expectedSalary.match(/(\d+)[kK千]/);
if (match) {
return parseInt(match[1]) * 1000;
}
// 匹配纯数字20000
const numMatch = expectedSalary.match(/(\d+)/);
if (numMatch) {
return parseInt(numMatch[1]);
}
return null;
}
/**
* 计算公司规模分数0-100分
* @param {string} companySize - 公司规模
* @returns {number} 公司规模分数
*/
calculate_company_size_score(companySize) {
if (!companySize) return 60; // 没有规模信息,给中等分数
const size = companySize.toLowerCase();
// 大型公司1000人以上给高分
if (size.includes('1000') || size.includes('1000+') || size.includes('1000人以上')) {
return 100;
}
if (size.includes('500-1000') || size.includes('500人以上')) {
return 90;
}
// 中型公司100-500人给中高分
if (size.includes('100-500') || size.includes('100人以上')) {
return 80;
}
if (size.includes('50-100')) {
return 70;
}
// 小型公司50人以下给中低分
if (size.includes('20-50') || size.includes('20人以上')) {
return 60;
}
if (size.includes('0-20') || size.includes('20人以下')) {
return 50;
}
// 默认中等分数
return 60;
}
/**
* 解析薪资范围
* @param {string} salaryDesc - 薪资描述(如 "15-25K·14薪"
* @returns {object} 薪资范围 { min, max }
*/
parse_salary_range(salaryDesc) {
if (!salaryDesc) return { min: 0, max: 0 };
// 匹配常见格式15-25K, 15K-25K, 15-25k·14薪
const match = salaryDesc.match(/(\d+)[-~](\d+)[kK]/);
if (match) {
return {
min: parseInt(match[1]) * 1000,
max: parseInt(match[2]) * 1000
};
}
// 匹配单个数值25K
const singleMatch = salaryDesc.match(/(\d+)[kK]/);
if (singleMatch) {
const value = parseInt(singleMatch[1]) * 1000;
return { min: value, max: value };
}
return { min: 0, max: 0 };
}
/**
* 延迟函数
* @param {number} ms - 毫秒数
* @returns {Promise<void>}
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
module.exports = new JobManager();

View File

@@ -0,0 +1,722 @@
/**
* 职位文本匹配过滤服务
* 使用简单的文本匹配规则来过滤职位,替代 AI 分析(二期规划)
* 支持从数据库动态获取职位类型的技能关键词和排除关键词
*/
const db = require('../dbProxy.js');
const locationService = require('../../services/locationService');
class JobFilterService {
constructor() {
// 默认技能关键词(当没有职位类型或获取失败时使用)
this.defaultCommonSkills = [
'Vue', 'React', 'Angular', 'JavaScript', 'TypeScript', 'Node.js',
'Python', 'Java', 'C#', '.NET', 'Flutter', 'React Native',
'Webpack', 'Vite', 'Redux', 'MobX', 'Express', 'Koa',
'Django', 'Flask', 'MySQL', 'MongoDB', 'Redis',
'WebRTC', 'FFmpeg', 'Canvas', 'WebSocket', 'HTML5', 'CSS3',
'jQuery', 'Bootstrap', 'Element UI', 'Ant Design',
'Git', 'Docker', 'Kubernetes', 'AWS', 'Azure',
'Selenium', 'Jest', 'Mocha', 'Cypress'
];
// 默认排除关键词(当没有职位类型或获取失败时使用)
this.defaultExcludeKeywords = [
'外包', '外派', '驻场', '销售', '客服', '电话销售',
'地推', '推广', '市场', '运营', '行政', '文员'
];
// 缓存职位类型配置(避免频繁查询数据库)
this.jobTypeCache = new Map();
}
/**
* 根据职位类型ID获取技能关键词和排除关键词
* @param {number} jobTypeId - 职位类型ID
* @returns {Promise<{commonSkills: Array, excludeKeywords: Array}>}
*/
async getJobTypeConfig(jobTypeId) {
if (!jobTypeId) {
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
};
}
// 检查缓存
if (this.jobTypeCache.has(jobTypeId)) {
return this.jobTypeCache.get(jobTypeId);
}
try {
const job_types = db.getModel('job_types');
if (!job_types) {
console.warn('[职位过滤服务] job_types 模型不存在,使用默认配置');
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
};
}
const jobType = await job_types.findOne({
where: { id: jobTypeId, is_enabled: 1 }
});
if (!jobType) {
console.warn(`[职位过滤服务] 职位类型 ${jobTypeId} 不存在或已禁用,使用默认配置`);
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
};
}
const jobTypeData = jobType.toJSON();
// 解析 JSON 字段
let commonSkills = this.defaultCommonSkills;
let excludeKeywords = this.defaultExcludeKeywords;
if (jobTypeData.commonSkills) {
try {
const parsed = typeof jobTypeData.commonSkills === 'string'
? JSON.parse(jobTypeData.commonSkills)
: jobTypeData.commonSkills;
if (Array.isArray(parsed) && parsed.length > 0) {
commonSkills = parsed;
}
} catch (e) {
console.warn(`[职位过滤服务] 解析 commonSkills 失败:`, e);
}
}
if (jobTypeData.excludeKeywords) {
try {
const parsed = typeof jobTypeData.excludeKeywords === 'string'
? JSON.parse(jobTypeData.excludeKeywords)
: jobTypeData.excludeKeywords;
if (Array.isArray(parsed) && parsed.length > 0) {
excludeKeywords = parsed;
}
} catch (e) {
console.warn(`[职位过滤服务] 解析 excludeKeywords 失败:`, e);
}
}
const config = {
commonSkills,
excludeKeywords
};
// 缓存配置缓存5分钟
this.jobTypeCache.set(jobTypeId, config);
setTimeout(() => {
this.jobTypeCache.delete(jobTypeId);
}, 5 * 60 * 1000);
return config;
} catch (error) {
console.error(`[职位过滤服务] 获取职位类型配置失败:`, error);
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
};
}
}
/**
* 清除职位类型缓存
* @param {number} jobTypeId - 职位类型ID可选不传则清除所有缓存
*/
clearCache(jobTypeId = null) {
if (jobTypeId) {
this.jobTypeCache.delete(jobTypeId);
} else {
this.jobTypeCache.clear();
}
}
/**
* 使用文本匹配分析职位与简历的匹配度
* @param {object} jobInfo - 职位信息
* @param {object} resumeInfo - 简历信息(可选)
* @param {number} jobTypeId - 职位类型ID可选
* @returns {Promise<object>} 匹配度分析结果
*/
async analyzeJobMatch(jobInfo, resumeInfo = {}, jobTypeId = null) {
const jobText = this.buildJobText(jobInfo);
const resumeText = this.buildResumeText(resumeInfo);
// 获取职位类型配置
const { commonSkills, excludeKeywords } = await this.getJobTypeConfig(jobTypeId);
// 1. 技能匹配度0-100分
const skillScore = this.calculateSkillMatch(jobText, resumeText, commonSkills);
// 2. 经验匹配度0-100分
const experienceScore = this.calculateExperienceMatch(jobInfo, resumeInfo);
// 3. 薪资合理性0-100分
const salaryScore = this.calculateSalaryMatch(jobInfo, resumeInfo);
// 4. 是否为外包岗位
const isOutsourcing = this.checkOutsourcing(jobText);
// 5. 综合推荐指数(加权平均)
const overallScore = Math.round(
skillScore * 0.4 +
experienceScore * 0.3 +
salaryScore * 0.3
);
// 6. 匹配原因
const matchReasons = this.getMatchReasons(jobText, resumeText, skillScore, experienceScore, commonSkills);
// 7. 关注点
const concerns = this.getConcerns(jobInfo, resumeInfo, isOutsourcing, excludeKeywords);
// 8. 投递建议
const suggestion = this.getSuggestion(overallScore, isOutsourcing, concerns);
return {
skillMatch: skillScore,
experienceMatch: experienceScore,
salaryMatch: salaryScore,
isOutsourcing: isOutsourcing,
overallScore: overallScore,
matchReasons: matchReasons,
concerns: concerns,
suggestion: suggestion,
analysis: {
skillMatch: skillScore,
experienceMatch: experienceScore,
salaryMatch: salaryScore,
isOutsourcing: isOutsourcing,
overallScore: overallScore,
matchReasons: matchReasons,
concerns: concerns,
suggestion: suggestion
}
};
}
/**
* 构建职位文本(用于匹配)
* @param {object} jobInfo - 职位信息
* @returns {string} 职位文本
*/
buildJobText(jobInfo) {
const parts = [
jobInfo.jobTitle || '',
jobInfo.companyName || '',
jobInfo.description || '',
jobInfo.skills || '',
jobInfo.requirements || ''
];
return parts.join(' ').toLowerCase();
}
/**
* 构建简历文本(用于匹配)
* @param {object} resumeInfo - 简历信息
* @returns {string} 简历文本
*/
buildResumeText(resumeInfo) {
const parts = [
resumeInfo.skills || '',
resumeInfo.skillDescription || '',
resumeInfo.currentPosition || '',
resumeInfo.expectedPosition || ''
];
return parts.join(' ').toLowerCase();
}
/**
* 计算技能匹配度
* @param {string} jobText - 职位文本
* @param {string} resumeText - 简历文本
* @param {Array} commonSkills - 技能关键词列表
* @returns {number} 匹配度分数0-100
*/
calculateSkillMatch(jobText, resumeText, commonSkills = null) {
const skills = commonSkills || this.defaultCommonSkills;
if (!resumeText || resumeText.trim() === '') {
// 如果没有简历信息,基于职位关键词匹配
let matchedSkills = 0;
skills.forEach(skill => {
if (jobText.includes(skill.toLowerCase())) {
matchedSkills++;
}
});
return Math.min(100, matchedSkills * 10);
}
// 提取职位中的技能关键词
const jobSkills = [];
skills.forEach(skill => {
if (jobText.includes(skill.toLowerCase())) {
jobSkills.push(skill);
}
});
if (jobSkills.length === 0) {
return 50; // 如果没有明确技能要求,给中等分数
}
// 计算简历中匹配的技能数量
let matchedCount = 0;
jobSkills.forEach(skill => {
if (resumeText.includes(skill.toLowerCase())) {
matchedCount++;
}
});
// 计算匹配度百分比
const matchRate = matchedCount / jobSkills.length;
return Math.round(matchRate * 100);
}
/**
* 计算经验匹配度
* @param {object} jobInfo - 职位信息
* @param {object} resumeInfo - 简历信息
* @returns {number} 匹配度分数0-100
*/
calculateExperienceMatch(jobInfo, resumeInfo) {
if (!resumeInfo || !resumeInfo.workYears) {
return 60; // 默认中等分数
}
const jobText = this.buildJobText(jobInfo);
const resumeYears = parseInt(resumeInfo.workYears) || 0;
// 从职位描述中提取经验要求
const experienceKeywords = {
'应届': 0,
'1年': 1,
'2年': 2,
'3年': 3,
'5年': 5,
'10年': 10
};
let requiredYears = null;
for (const [keyword, years] of Object.entries(experienceKeywords)) {
if (jobText.includes(keyword)) {
requiredYears = years;
break;
}
}
if (requiredYears === null) {
return 70; // 没有明确要求,给较高分数
}
// 计算匹配度
if (resumeYears >= requiredYears) {
return 100; // 经验满足要求
} else if (resumeYears >= requiredYears - 1) {
return 80; // 经验略低于要求
} else {
return Math.max(30, 100 - (requiredYears - resumeYears) * 15); // 经验不足
}
}
/**
* 计算薪资合理性
* @param {object} jobInfo - 职位信息
* @param {object} resumeInfo - 简历信息
* @returns {number} 匹配度分数0-100
*/
calculateSalaryMatch(jobInfo, resumeInfo) {
if (!jobInfo.salary) {
return 60; // 没有薪资信息,给中等分数
}
const salaryRange = this.parseSalaryRange(jobInfo.salary);
if (!salaryRange) {
return 60;
}
if (!resumeInfo || !resumeInfo.expectedSalary) {
return 70; // 没有期望薪资,给较高分数
}
const expectedSalary = this.parseExpectedSalary(resumeInfo.expectedSalary);
if (!expectedSalary) {
return 70;
}
// 计算匹配度
const avgJobSalary = (salaryRange.min + salaryRange.max) / 2;
const ratio = expectedSalary / avgJobSalary;
if (ratio <= 0.8) {
return 100; // 期望薪资低于职位薪资,完全匹配
} else if (ratio <= 1.0) {
return 90; // 期望薪资略低于或等于职位薪资
} else if (ratio <= 1.2) {
return 70; // 期望薪资略高于职位薪资
} else {
return 50; // 期望薪资明显高于职位薪资
}
}
/**
* 检查是否为外包岗位
* @param {string} jobText - 职位文本
* @returns {boolean} 是否为外包
*/
checkOutsourcing(jobText) {
const outsourcingKeywords = ['外包', '外派', '驻场', '人力外包', '项目外包'];
return outsourcingKeywords.some(keyword => jobText.includes(keyword));
}
/**
* 获取匹配原因
* @param {string} jobText - 职位文本
* @param {string} resumeText - 简历文本
* @param {number} skillScore - 技能匹配度
* @param {number} experienceScore - 经验匹配度
* @param {Array} commonSkills - 技能关键词列表
* @returns {Array<string>} 匹配原因列表
*/
getMatchReasons(jobText, resumeText, skillScore, experienceScore, commonSkills = null) {
const skills = commonSkills || this.defaultCommonSkills;
const reasons = [];
if (skillScore >= 80) {
reasons.push('技能匹配度高');
} else if (skillScore >= 60) {
reasons.push('技能部分匹配');
}
if (experienceScore >= 80) {
reasons.push('工作经验符合要求');
}
// 检查是否有共同技能
const matchedSkills = [];
skills.forEach(skill => {
if (jobText.includes(skill.toLowerCase()) &&
resumeText.includes(skill.toLowerCase())) {
matchedSkills.push(skill);
}
});
if (matchedSkills.length > 0) {
reasons.push(`共同技能: ${matchedSkills.slice(0, 3).join(', ')}`);
}
return reasons.length > 0 ? reasons : ['基础匹配'];
}
/**
* 获取关注点
* @param {object} jobInfo - 职位信息
* @param {object} resumeInfo - 简历信息
* @param {boolean} isOutsourcing - 是否为外包
* @param {Array} excludeKeywords - 排除关键词列表
* @returns {Array<string>} 关注点列表
*/
getConcerns(jobInfo, resumeInfo, isOutsourcing, excludeKeywords = null) {
const keywords = excludeKeywords || this.defaultExcludeKeywords;
const concerns = [];
if (isOutsourcing) {
concerns.push('可能是外包岗位');
}
const jobText = this.buildJobText(jobInfo);
keywords.forEach(keyword => {
if (jobText.includes(keyword)) {
concerns.push(`包含排除关键词: ${keyword}`);
}
});
if (resumeInfo && resumeInfo.workYears) {
const resumeYears = parseInt(resumeInfo.workYears) || 0;
if (jobText.includes('5年') && resumeYears < 5) {
concerns.push('工作经验可能不足');
}
}
return concerns;
}
/**
* 获取投递建议
* @param {number} overallScore - 综合分数
* @param {boolean} isOutsourcing - 是否为外包
* @param {Array<string>} concerns - 关注点
* @returns {string} 投递建议
*/
getSuggestion(overallScore, isOutsourcing, concerns) {
if (isOutsourcing) {
return '谨慎投递:可能是外包岗位';
}
if (concerns.length > 2) {
return '不推荐投递:存在多个关注点';
}
if (overallScore >= 80) {
return '强烈推荐投递:匹配度很高';
} else if (overallScore >= 60) {
return '可以投递:匹配度中等';
} else {
return '谨慎投递:匹配度较低';
}
}
/**
* 解析薪资范围
* @param {string} salaryDesc - 薪资描述(如 "15-25K·14薪"
* @returns {object|null} 薪资范围 { min, max }
*/
parseSalaryRange(salaryDesc) {
if (!salaryDesc) return null;
// 匹配 "15-25K" 或 "15K-25K" 格式
const match = salaryDesc.match(/(\d+)[-~](\d+)[Kk千]/);
if (match) {
return {
min: parseInt(match[1]) * 1000,
max: parseInt(match[2]) * 1000
};
}
// 匹配单个数字 "20K"
const singleMatch = salaryDesc.match(/(\d+)[Kk千]/);
if (singleMatch) {
const value = parseInt(singleMatch[1]) * 1000;
return { min: value, max: value };
}
return null;
}
/**
* 解析期望薪资
* @param {string} expectedSalary - 期望薪资描述
* @returns {number|null} 期望薪资数值
*/
parseExpectedSalary(expectedSalary) {
if (!expectedSalary) return null;
// 匹配数字+K格式
const match = expectedSalary.match(/(\d+)[Kk千]/);
if (match) {
return parseInt(match[1]) * 1000;
}
// 匹配纯数字
const numMatch = expectedSalary.match(/(\d+)/);
if (numMatch) {
return parseInt(numMatch[1]);
}
return null;
}
/**
* 过滤职位列表(基于文本匹配)
* @param {Array} jobs - 职位列表
* @param {object} filterRules - 过滤规则
* @param {object} resumeInfo - 简历信息(可选)
* @param {number} jobTypeId - 职位类型ID可选
* @returns {Promise<Array>} 过滤后的职位列表(带匹配分数)
*/
async filterJobs(jobs, filterRules = {}, resumeInfo = {}, jobTypeId = null) {
const {
minScore = 60, // 最低匹配分数
excludeOutsourcing = true, // 是否排除外包
excludeKeywords = [] // 额外排除关键词
} = filterRules;
// 获取职位类型配置
const { excludeKeywords: typeExcludeKeywords } = await this.getJobTypeConfig(jobTypeId);
const allExcludeKeywords = [...typeExcludeKeywords, ...excludeKeywords];
const results = [];
for (const job of jobs) {
const jobData = job.toJSON ? job.toJSON() : job;
// 分析匹配度
const analysis = await this.analyzeJobMatch(jobData, resumeInfo, jobTypeId);
results.push({
...jobData,
matchScore: analysis.overallScore,
matchAnalysis: analysis
});
}
return results
.filter(job => {
// 1. 最低分数过滤
if (job.matchScore < minScore) {
return false;
}
// 2. 外包过滤
if (excludeOutsourcing && job.matchAnalysis.isOutsourcing) {
return false;
}
// 3. 排除关键词过滤
const jobText = this.buildJobText(job);
if (allExcludeKeywords.some(kw => jobText.includes(kw.toLowerCase()))) {
return false;
}
return true;
})
.sort((a, b) => b.matchScore - a.matchScore); // 按匹配分数降序排序
}
/**
* 根据自定义权重配置计算职位评分
* @param {Object} jobData - 职位数据
* @param {Object} resumeInfo - 简历信息
* @param {Object} accountConfig - 账号配置(包含 user_longitude, user_latitude
* @param {Object} jobTypeConfig - 职位类型配置(可选)
* @param {Array} priorityWeights - 权重配置 [{key: "distance", weight: 50}, ...]
* @returns {Object} 评分结果 {totalScore, scores}
*/
calculateJobScoreWithWeights(jobData, resumeInfo, accountConfig, jobTypeConfig, priorityWeights) {
const scores = {};
let totalScore = 0;
// 解析权重配置
const weights = {};
priorityWeights.forEach(item => {
weights[item.key] = item.weight / 100; // 转换为小数
});
// 1. 距离评分(基于经纬度)
if (weights.distance) {
let distanceScore = 50; // 默认分数
if (accountConfig.user_longitude && accountConfig.user_latitude &&
jobData.longitude && jobData.latitude) {
try {
const userLon = parseFloat(accountConfig.user_longitude);
const userLat = parseFloat(accountConfig.user_latitude);
const jobLon = parseFloat(jobData.longitude);
const jobLat = parseFloat(jobData.latitude);
if (!isNaN(userLon) && !isNaN(userLat) && !isNaN(jobLon) && !isNaN(jobLat)) {
const distance = locationService.calculateDistance(userLat, userLon, jobLat, jobLon);
// 距离越近分数越高0-5km=100分5-10km=90分10-20km=80分20-50km=60分50km以上=30分
if (distance <= 5) distanceScore = 100;
else if (distance <= 10) distanceScore = 90;
else if (distance <= 20) distanceScore = 80;
else if (distance <= 50) distanceScore = 60;
else distanceScore = 30;
}
} catch (e) {
console.warn(`[职位过滤服务] 计算距离失败:`, e);
}
}
scores.distance = distanceScore;
totalScore += distanceScore * weights.distance;
}
// 2. 薪资评分
if (weights.salary) {
let salaryScore = 50;
if (jobData.salary && resumeInfo.expectedSalary) {
const jobSalary = this.parseExpectedSalary(jobData.salary);
const expectedSalary = this.parseExpectedSalary(resumeInfo.expectedSalary);
if (jobSalary && expectedSalary) {
if (jobSalary >= expectedSalary) salaryScore = 100;
else if (jobSalary >= expectedSalary * 0.8) salaryScore = 80;
else if (jobSalary >= expectedSalary * 0.6) salaryScore = 60;
else salaryScore = 40;
}
}
scores.salary = salaryScore;
totalScore += salaryScore * weights.salary;
}
// 3. 工作年限评分
if (weights.work_years) {
let workYearsScore = 50;
if (jobData.experience && resumeInfo.workYears) {
const jobExp = this.parseWorkYears(jobData.experience);
const resumeExp = this.parseWorkYears(resumeInfo.workYears);
if (jobExp !== null && resumeExp !== null) {
if (resumeExp >= jobExp) workYearsScore = 100;
else if (resumeExp >= jobExp * 0.8) workYearsScore = 80;
else if (resumeExp >= jobExp * 0.6) workYearsScore = 60;
else workYearsScore = 40;
}
}
scores.work_years = workYearsScore;
totalScore += workYearsScore * weights.work_years;
}
// 4. 学历评分
if (weights.education) {
let educationScore = 50;
if (jobData.education && resumeInfo.education) {
const educationLevels = { '博士': 7, '硕士': 6, '本科': 5, '大专': 4, '高中': 3, '中专': 2, '初中': 1 };
const jobLevel = educationLevels[jobData.education] || 0;
const resumeLevel = educationLevels[resumeInfo.education] || 0;
if (resumeLevel >= jobLevel) educationScore = 100;
else if (resumeLevel >= jobLevel - 1) educationScore = 70;
else educationScore = 40;
}
scores.education = educationScore;
totalScore += educationScore * weights.education;
}
// 5. 技能匹配评分(基于职位类型的 commonSkills
if (jobTypeConfig && jobTypeConfig.commonSkills) {
let skillScore = 0;
try {
const commonSkills = typeof jobTypeConfig.commonSkills === 'string'
? JSON.parse(jobTypeConfig.commonSkills)
: jobTypeConfig.commonSkills;
const resumeSkills = typeof resumeInfo.skills === 'string'
? JSON.parse(resumeInfo.skills || '[]')
: (resumeInfo.skills || []);
if (Array.isArray(commonSkills) && Array.isArray(resumeSkills)) {
const matchedSkills = commonSkills.filter(skill =>
resumeSkills.some(rs => rs.toLowerCase().includes(skill.toLowerCase()) ||
skill.toLowerCase().includes(rs.toLowerCase()))
);
skillScore = (matchedSkills.length / commonSkills.length) * 100;
}
} catch (e) {
console.warn(`[职位过滤服务] 解析技能失败:`, e);
}
scores.skills = skillScore;
// 技能评分作为额外加分项权重10%
totalScore += skillScore * 0.1;
}
return {
totalScore: Math.round(totalScore),
scores
};
}
/**
* 解析工作年限字符串为数字
* @param {string} workYearsStr - 工作年限字符串
* @returns {number|null} 工作年限数字
*/
parseWorkYears(workYearsStr) {
if (!workYearsStr) return null;
const match = workYearsStr.match(/(\d+)/);
if (match) return parseInt(match[1]);
return null;
}
}
// 导出单例
module.exports = new JobFilterService();

View File

@@ -0,0 +1,555 @@
const aiService = require('./aiService');
const jobFilterService = require('./job_filter_service');
const logs = require('../logProxy');
const db = require('../dbProxy');
const { v4: uuidv4 } = require('uuid');
/**
* 简历管理模块
* 负责简历获取、分析、存储和匹配度计算
*/
class ResumeManager {
constructor() {
}
/**
* 获取用户在线简历
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端
* @param {object} params - 参数
* @returns {Promise<object>} 简历信息
*/
async get_online_resume(sn_code, mqttClient, params = {}) {
console.log(`[简历管理] 开始获取设备 ${sn_code} 的在线简历`);
const { platform = 'boss' } = params;
// 通过MQTT指令获取简历信息
const response = await mqttClient.publishAndWait(sn_code, {
platform: platform,
action: 'get_online_resume',
data: {}
});
if (!response || response.code !== 200) {
console.error(`[简历管理] 获取简历失败:`, response);
throw new Error(response?.message || '获取简历失败');
}
const resumeData = response.data;
console.log(`[简历管理] 成功获取简历数据:`, resumeData);
// 存储用户在线简历数据到数据库
try {
await this.save_resume_to_database(sn_code, platform, resumeData);
console.log(`[简历管理] 简历数据已保存到数据库`);
} catch (error) {
console.error(`[简历管理] 保存简历数据失败:`, error);
// 不抛出错误,继续返回数据
}
return resumeData;
}
/**
* 保存简历数据到数据库
* @param {string} sn_code - 设备SN码
* @param {string} platform - 平台名称
* @param {object} resumeData - 简历数据
* @returns {Promise<object>} 保存结果
*/
async save_resume_to_database(sn_code, platform, resumeData) {
const resume_info = db.getModel('resume_info');
const pla_account = db.getModel('pla_account');
// 通过 sn_code 查询对应的 pla_account 获取 account_id
const account = await pla_account.findOne({
where: { sn_code, platform_type: platform }
});
if (!account) {
throw new Error(`未找到设备 ${sn_code} 在平台 ${platform} 的账户信息`);
}
// 确保 account_id 是字符串类型(模型定义为 STRING(50)
const account_id = String(account.id || ''); // pla_account 的自增ID转换为字符串
// 提取基本信息
const baseInfo = resumeData.baseInfo || {};
const expectList = resumeData.expectList || [];
const workExpList = resumeData.workExpList || [];
const projectExpList = resumeData.projectExpList || [];
const educationExpList = resumeData.educationExpList || [];
const userDesc = resumeData.userDesc || '';
// 查找是否已存在简历
const existingResume = await resume_info.findOne({
where: { sn_code, platform, isActive: true }
});
// 如果已存在简历,使用数据库中的 resumeId可能是字符串或数字
// 如果不存在,生成新的 UUID 字符串
const resumeId = existingResume ? String(existingResume.resumeId || existingResume.id || uuidv4()) : uuidv4();
// 提取技能标签(从个人优势中提取)
const skills = this.extract_skills_from_desc(userDesc);
// 获取期望信息
const expectInfo = expectList[0] || {};
// 获取最新工作经验
const latestWork = workExpList[0] || {};
// 获取最高学历
const highestEdu = educationExpList[0] || {};
// 构建简历信息对象
const resumeInfo = {
sn_code,
account_id, // 使用 pla_account 的自增ID
platform,
resumeId: resumeId,
// 个人信息
fullName: String(baseInfo.name || ''),
gender: baseInfo.gender === 1 ? '男' : baseInfo.gender === 0 ? '女' : '',
age: parseInt(String(baseInfo.age || 0), 10) || 0, // 确保是整数
phone: String(baseInfo.account || ''),
email: String(baseInfo.emailBlur || ''),
location: String(expectInfo.locationName || ''),
// 教育背景
education: String(highestEdu.degreeName || baseInfo.degreeCategory || ''),
major: String(highestEdu.major || ''),
school: String(highestEdu.school || ''),
graduationYear: parseInt(String(highestEdu.endYear || 0), 10) || 0, // 确保是整数
// 工作经验
workYears: baseInfo.workYearDesc || `${baseInfo.workYears || 0}`,
currentPosition: latestWork.positionName || '',
currentCompany: latestWork.companyName || '',
currentSalary: '',
// 期望信息
expectedPosition: expectInfo.positionName || '',
expectedSalary: expectInfo.salaryDesc || '',
expectedLocation: expectInfo.locationName || '',
expectedIndustry: expectInfo.industryDesc || '',
// 技能和专长
skills: JSON.stringify(skills),
skillDescription: userDesc,
certifications: JSON.stringify(resumeData.certificationList || []),
// 项目经验
projectExperience: JSON.stringify(projectExpList.map(p => ({
name: p.name,
role: p.roleName,
startDate: p.startDate,
endDate: p.endDate,
description: p.projectDesc,
performance: p.performance
}))),
// 工作经历
workExperience: JSON.stringify(workExpList.map(w => ({
company: w.companyName,
position: w.positionName,
startDate: w.startDate,
endDate: w.endDate,
content: w.workContent,
industry: w.industry?.name
}))),
// 简历内容
resumeContent: userDesc,
// 原始数据
originalData: JSON.stringify(resumeData),
// 状态信息
isActive: true,
isPublic: true,
syncTime: new Date()
};
// 保存或更新简历
if (existingResume) {
await resume_info.update(resumeInfo, { where: { resumeId: resumeId } });
console.log(`[简历管理] 简历已更新 - ID: ${resumeId}`);
} else {
await resume_info.create(resumeInfo);
console.log(`[简历管理] 简历已创建 - ID: ${resumeId}`);
}
// 二期规划AI 分析暂时禁用,使用简单的文本匹配
console.log(`[简历管理] AI分析已禁用二期规划使用文本匹配过滤`);
return { resumeId, message: existingResume ? '简历更新成功' : '简历创建成功' };
}
/**
* 从描述中提取技能标签
* @param {string} description - 描述文本
* @returns {Array} 技能标签数组
*/
extract_skills_from_desc(description) {
const skills = [];
const commonSkills = [
'Vue', 'React', 'Angular', 'JavaScript', 'TypeScript', 'Node.js',
'Python', 'Java', 'C#', '.NET', 'Flutter', 'React Native',
'Webpack', 'Vite', 'Redux', 'MobX', 'Express', 'Koa',
'Django', 'Flask', 'MySQL', 'MongoDB', 'Redis',
'WebRTC', 'FFmpeg', 'Canvas', 'WebSocket', 'HTML5', 'CSS3',
'jQuery', 'Bootstrap', 'Element UI', 'Ant Design',
'Git', 'Docker', 'Kubernetes', 'AWS', 'Azure',
'Selenium', 'Jest', 'Mocha', 'Cypress'
];
commonSkills.forEach(skill => {
if (description.includes(skill)) {
skills.push(skill);
}
});
return [...new Set(skills)]; // 去重
}
/**
* 使用AI分析简历
* @param {string} resumeId - 简历ID
* @param {object} resumeInfo - 简历信息
* @returns {Promise<object>} 分析结果
*/
async analyze_resume_with_ai(resumeId, resumeInfo) {
console.log(`[简历管理] 开始AI分析简历 - ID: ${resumeId}`);
const resume_info = db.getModel('resume_info');
// 构建分析提示词
const prompt = `请分析以下简历,提供专业的评估:
姓名:${resumeInfo.fullName}
工作年限:${resumeInfo.workYears}
当前职位:${resumeInfo.currentPosition}
期望职位:${resumeInfo.expectedPosition}
期望薪资:${resumeInfo.expectedSalary}
学历:${resumeInfo.education}
技能:${resumeInfo.skills}
个人优势:
${resumeInfo.skillDescription}
请从以下几个方面进行分析:
1. 核心技能标签提取5-10个关键技能
2. 优势分析100字以内
3. 劣势分析100字以内
4. 职业建议150字以内
5. 竞争力评分0-100分`;
try {
// 调用AI服务进行分析
const aiAnalysis = await aiService.analyzeResume(prompt);
// 解析AI返回的结果
const analysis = this.parse_ai_analysis(aiAnalysis, resumeInfo);
// 更新简历的AI分析字段
await resume_info.update({
aiSkillTags: JSON.stringify(analysis.skillTags),
aiStrengths: analysis.strengths,
aiWeaknesses: analysis.weaknesses,
aiCareerSuggestion: analysis.careerSuggestion,
aiCompetitiveness: analysis.competitiveness
}, { where: { id: resumeId } });
console.log(`[简历管理] AI分析完成 - 竞争力评分: ${analysis.competitiveness}`);
return analysis;
} catch (error) {
console.error(`[简历管理] AI分析失败:`, error, {
resumeId: resumeId,
fullName: resumeInfo.fullName
});
// 如果AI分析失败使用基于规则的默认分析
const defaultAnalysis = this.get_default_analysis(resumeInfo);
return defaultAnalysis;
}
}
/**
* 解析AI分析结果
* @param {object} aiResponse - AI响应对象
* @param {object} resumeInfo - 简历信息
* @returns {object} 解析后的分析结果
*/
parse_ai_analysis(aiResponse, resumeInfo) {
try {
// 尝试从AI响应中解析JSON
const content = aiResponse.content || aiResponse.analysis?.content || '';
// 如果AI返回的是JSON格式
if (content.includes('{') && content.includes('}')) {
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
return {
skillTags: parsed.skillTags || parsed.技能标签 || [],
strengths: parsed.strengths || parsed.优势 || parsed.优势分析 || '',
weaknesses: parsed.weaknesses || parsed.劣势 || parsed.劣势分析 || '',
careerSuggestion: parsed.careerSuggestion || parsed.职业建议 || '',
competitiveness: parsed.competitiveness || parsed.竞争力评分 || 70
};
}
}
// 如果无法解析JSON尝试从文本中提取信息
const skillTagsMatch = content.match(/技能标签[:](.*?)(?:\n|$)/);
const strengthsMatch = content.match(/[]*[:](.*?)(?:\n|)/s);
const weaknessesMatch = content.match(/[]*[:](.*?)(?:\n|)/s);
const suggestionMatch = content.match(/[:](.*?)(?:\n|)/s);
const scoreMatch = content.match(/竞争力评分[:](\d+)/);
return {
skillTags: skillTagsMatch ? skillTagsMatch[1].split(/[,,、]/).map(s => s.trim()) : [],
strengths: strengthsMatch ? strengthsMatch[1].trim() : '',
weaknesses: weaknessesMatch ? weaknessesMatch[1].trim() : '',
careerSuggestion: suggestionMatch ? suggestionMatch[1].trim() : '',
competitiveness: scoreMatch ? parseInt(scoreMatch[1]) : 70
};
} catch (error) {
console.error(`[简历管理] 解析AI分析结果失败:`, error);
// 解析失败时使用默认分析
return this.get_default_analysis(resumeInfo);
}
}
/**
* 获取默认分析结果(基于规则)
* @param {object} resumeInfo - 简历信息
* @returns {object} 分析结果
*/
get_default_analysis(resumeInfo) {
const skills = JSON.parse(resumeInfo.skills || '[]');
const workYears = parseInt(resumeInfo.workYears) || 0;
// 计算竞争力评分
let competitiveness = 50; // 基础分
// 工作年限加分
if (workYears >= 10) competitiveness += 20;
else if (workYears >= 5) competitiveness += 15;
else if (workYears >= 3) competitiveness += 10;
// 技能数量加分
if (skills.length >= 10) competitiveness += 15;
else if (skills.length >= 5) competitiveness += 10;
// 学历加分
if (resumeInfo.education.includes('硕士')) competitiveness += 10;
else if (resumeInfo.education.includes('本科')) competitiveness += 5;
// 确保分数在0-100之间
competitiveness = Math.min(100, Math.max(0, competitiveness));
return {
skillTags: skills.slice(0, 10),
strengths: `拥有${resumeInfo.workYears}工作经验,技术栈全面,掌握${skills.slice(0, 5).join('、')}等主流技术。`,
weaknesses: '建议继续深化技术深度,关注行业前沿技术发展。',
careerSuggestion: `基于您的${resumeInfo.expectedPosition}职业目标和${resumeInfo.workYears}经验,建议重点关注中大型互联网公司的相关岗位,期望薪资${resumeInfo.expectedSalary}较为合理。`,
competitiveness: competitiveness
};
}
/**
* 分析简历要素
* @param {string} sn_code - 设备SN码
* @param {object} resumeData - 简历数据
* @returns {Promise<object>} 分析结果
*/
async analyze_resume(sn_code, resumeData) {
console.log(`[简历管理] 开始分析设备 ${sn_code} 的简历要素`);
console.log(`[简历管理] AI分析已禁用二期规划使用文本匹配过滤`);
// 构建简历文本
const resumeText = this.build_resume_text(resumeData);
// 二期规划AI 分析暂时禁用,使用简单的文本匹配
// const analysis = await aiService.analyzeResume(resumeText);
// 使用简单的文本匹配提取技能
const skills = this.extract_skills_from_desc(resumeData.skillDescription || resumeText);
const result = {
sn_code: sn_code,
resumeData: resumeData,
analysis: {
skills: skills,
content: '文本匹配分析AI分析已禁用二期规划'
},
timestamp: Date.now()
};
console.log(`[简历管理] 简历分析完成,提取技能: ${skills.join(', ')}`);
return result;
}
/**
* 构建简历文本
* @param {object} resumeData - 简历数据
* @returns {string} 简历文本
*/
build_resume_text(resumeData) {
const parts = [];
// 基本信息
if (resumeData.basicInfo) {
parts.push(`基本信息:${JSON.stringify(resumeData.basicInfo)}`);
}
// 技能标签
if (resumeData.skills && resumeData.skills.length > 0) {
parts.push(`技能标签:${resumeData.skills.join(', ')}`);
}
// 工作经验
if (resumeData.workExperience && resumeData.workExperience.length > 0) {
const workExp = resumeData.workExperience.map(exp =>
`${exp.company} - ${exp.position} (${exp.duration})`
).join('; ');
parts.push(`工作经验:${workExp}`);
}
// 项目经验
if (resumeData.projectExperience && resumeData.projectExperience.length > 0) {
const projectExp = resumeData.projectExperience.map(project =>
`${project.name} - ${project.description}`
).join('; ');
parts.push(`项目经验:${projectExp}`);
}
// 教育背景
if (resumeData.education) {
parts.push(`教育背景:${resumeData.education.school} - ${resumeData.education.major} - ${resumeData.education.degree}`);
}
// 期望信息
if (resumeData.expectations) {
parts.push(`期望薪资:${resumeData.expectations.salary}`);
parts.push(`期望地点:${resumeData.expectations.location}`);
}
return parts.join('\n');
}
/**
* 计算简历与岗位的匹配度
* @param {string} sn_code - 设备SN码
* @param {object} jobInfo - 岗位信息
* @returns {Promise<object>} 匹配度分析结果
*/
async calculate_match_score(sn_code, jobInfo) {
console.log(`[简历管理] 开始计算设备 ${sn_code} 与岗位的匹配度`);
console.log(`[简历管理] 使用文本匹配分析AI分析已禁用二期规划`);
// 获取简历分析结果
const resumeAnalysis = await this.get_resume_analysis(sn_code);
// 获取账号的职位类型ID
let jobTypeId = null;
try {
const db = require('../dbProxy.js');
const pla_account = db.getModel('pla_account');
if (pla_account) {
const account = await pla_account.findOne({
where: { sn_code, platform_type: jobInfo.platform || 'boss' }
});
if (account) {
jobTypeId = account.job_type_id;
}
}
} catch (error) {
console.warn(`[简历管理] 获取职位类型ID失败:`, error);
}
// 使用文本匹配进行岗位分析(替代 AI
const jobAnalysis = await jobFilterService.analyzeJobMatch(jobInfo, resumeAnalysis.analysis || {}, jobTypeId);
const result = {
sn_code: sn_code,
jobInfo: jobInfo,
resumeAnalysis: resumeAnalysis.analysis,
jobAnalysis: jobAnalysis,
timestamp: Date.now()
};
console.log(`[简历管理] 匹配度分析完成,综合分数: ${jobAnalysis.overallScore}`);
return result;
}
/**
* 获取简历分析结果
* @param {string} sn_code - 设备SN码
* @param {object} mqttClient - MQTT客户端可选
* @returns {Promise<object>} 简历分析结果
*/
async get_resume_analysis(sn_code, mqttClient = null) {
// 获取MQTT客户端
if (!mqttClient) {
const scheduleManager = require('../schedule');
if (scheduleManager && scheduleManager.mqttClient) {
mqttClient = scheduleManager.mqttClient;
} else {
throw new Error('MQTT客户端未初始化');
}
}
// 重新获取和分析
const resumeData = await this.get_online_resume(sn_code, mqttClient);
return await this.analyze_resume(sn_code, resumeData);
}
/**
* 更新简历信息
* @param {string} sn_code - 设备SN码
* @param {object} newResumeData - 新的简历数据
* @returns {Promise<object>} 更新结果
*/
async update_resume(sn_code, newResumeData) {
console.log(`[简历管理] 更新设备 ${sn_code} 的简历信息`);
// 重新分析简历
const analysis = await this.analyze_resume(sn_code, newResumeData);
return {
message: '简历更新成功',
analysis: analysis
};
}
/**
* 获取简历摘要
* @param {string} sn_code - 设备SN码
* @returns {Promise<object>} 简历摘要
*/
async get_resume_summary(sn_code) {
const analysis = await this.get_resume_analysis(sn_code);
const summary = {
sn_code: sn_code,
skills: analysis.analysis.skills || [],
experience: analysis.analysis.experience || '',
education: analysis.analysis.education || '',
expectedSalary: analysis.analysis.expectedSalary || '',
expectedLocation: analysis.analysis.expectedLocation || '',
lastUpdate: analysis.timestamp
};
return summary;
}
}
module.exports = new ResumeManager();

View File

@@ -0,0 +1,81 @@
const Framework = require('../../framework/node-core-framework');
/**
* Log代理模块
* 提供统一的日志接口延迟获取logsService
*/
class LogProxy {
constructor() {
this._logsService = null;
}
/**
* 获取logsService实例
* @returns {object} logsService实例
*/
get logsService() {
if (!this._logsService) {
try {
this._logsService = Framework.getServices().logsService;
} catch (error) {
console.error('无法获取logsService使用默认日志:', error.message);
}
}
return this._logsService;
}
/**
* 获取调用位置信息
* @returns {object} 包含文件路径和行号的对象
*/
getCallerInfo() {
const stack = new Error().stack;
const lines = stack.split('\n');
// 跳过前3行Error、getCallerInfo、当前方法
for (let i = 3; i < lines.length; i++) {
const line = lines[i];
// 匹配文件路径和行号
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
if (match) {
const [, functionName, filePath, lineNumber, columnNumber] = match;
return {
functionName: functionName || 'anonymous',
filePath: filePath,
lineNumber: parseInt(lineNumber),
columnNumber: parseInt(columnNumber)
};
}
}
return null;
}
/**
* 记录普通日志
* @param {...any} args 日志参数
*/
log(...args) {
this.logsService.log(...args);
}
/**
* 记录错误日志
* @param {...any} args 日志参数
*/
error(...args) {
this.logsService.error(...args);
}
/**
* 记录警告日志
* @param {...any} args 日志参数
*/
warn(...args) {
this.logsService.warn(...args);
}
}
// 导出单例实例
module.exports = new LogProxy();

View File

@@ -0,0 +1,176 @@
const mqtt = require('mqtt')
const { v4: uuidv4 } = require('uuid'); // 顶部添加
const Framework = require('../../../framework/node-core-framework');
const logs = require('../logProxy');
// 获取logsService
class MqttSyncClient {
constructor(brokerUrl, options = {}) {
this.client = mqtt.connect(brokerUrl, options)
this.isConnected = false
this.messageListeners = []
this.client.on('connect', () => {
this.isConnected = true
console.log('MQTT 服务端已连接')
})
this.client.on('message', (topic, message) => {
message = JSON.parse(message.toString())
console.log('MQTT 收到消息', topic, message)
this.messageListeners.forEach(listener => listener(topic, message))
})
this.client.on('error', (err) => {
console.warn('[MQTT] Error:', err.message)
})
}
waitForConnect(timeout = 5000) {
return new Promise((resolve, reject) => {
if (this.isConnected) return resolve()
const timer = setTimeout(() => {
reject(new Error('MQTT connect timeout'))
}, timeout)
const check = () => {
if (this.isConnected) {
clearTimeout(timer)
resolve()
} else {
setTimeout(check, 100)
}
}
check()
})
}
publish(topic, message, options = {}) {
return new Promise((resolve, reject) => {
this.client.publish(topic, message, options, (err) => {
if (err) reject(err)
else resolve()
})
})
}
subscribe(topic, callback = null, options = {}) {
return new Promise((resolve, reject) => {
this.client.subscribe(topic, options, (err, granted) => {
if (err) {
reject(err)
} else {
// 如果提供了回调函数,添加到消息监听器
if (callback && typeof callback === 'function') {
const messageHandler = (responseTopic, message) => {
if (responseTopic !== topic) return
try {
callback(topic, message)
} catch (error) {
console.warn(`[MQTT] 处理订阅消息失败: ${error.message}`)
}
}
// 添加到消息监听器
this.addMessageListener(messageHandler)
// 在订阅成功后返回订阅信息和消息处理器
resolve({ granted, messageHandler, topic })
} else {
resolve(granted)
}
}
1
})
})
}
/**
* 发布指令并等待某个主题返回模拟同步通过uuid确定唯一项
* @param {*} requestTopic
* @param {*} requestMessage
* @param {*} responseTopic
* @param {*} timeout
*/
async publishAndWait(sn_code, requestMessage, timeout = 60 * 3 * 1000) {
return new Promise((resolve, reject) => {
console.log(`[MQTT指令] 发送到主题: request_${sn_code}`, requestMessage);
const uuid = uuidv4();
// 将uuid加入消息体建议使用JSON格式
const msgObj = typeof requestMessage === 'object'
? { ...requestMessage, uuid }
: { data: requestMessage, uuid };
const sendMsg = JSON.stringify(msgObj);
const timer = setTimeout(() => {
this.removeMessageListener(onMessage);
reject(new Error('Timeout waiting for response'));
}, timeout);
const onMessage = (topic, message) => {
let { uuid: responseUuid } = message
if (topic === 'response' && responseUuid === uuid) {
clearTimeout(timer);
this.removeMessageListener(onMessage);
resolve(message)
}
}
this.addMessageListener(onMessage)
this.publish(`request_${sn_code}`, sendMsg).catch(err => {
clearTimeout(timer);
this.removeMessageListener(onMessage);
reject(err);
});
});
}
addMessageListener(fn) {
this.messageListeners.push(fn)
}
removeMessageListener(fn) {
this.messageListeners = this.messageListeners.filter(f => f !== fn)
}
/**
* 取消订阅主题
* @param {string} topic 要取消订阅的主题
* @param {object} subscriptionInfo 订阅时返回的订阅信息
*/
async unsubscribe(topic, subscriptionInfo = null) {
return new Promise((resolve, reject) => {
this.client.unsubscribe(topic, (err) => {
if (err) {
reject(err)
} else {
// 如果提供了订阅信息,移除对应的消息处理器
if (subscriptionInfo && subscriptionInfo.messageHandler) {
this.removeMessageListener(subscriptionInfo.messageHandler)
}
resolve()
}
})
})
}
end(force = false) {
this.client.end(force)
}
}
module.exports = MqttSyncClient

View File

@@ -0,0 +1,302 @@
const db = require('../dbProxy.js');
const deviceManager = require('../schedule/deviceManager.js');
/**
* MQTT 消息分发器
* 处理所有 MQTT 消息的分发和响应(包括请求、心跳、响应等)
*/
class MqttDispatcher {
constructor(components, mqttClient) {
this.taskQueue = components.taskQueue;
this.mqttClient = mqttClient;
this.actionHandlers = new Map();
this.subscribedTopics = new Set();
}
/**
* 启动 MQTT 分发器
*/
start() {
this.registerActionHandlers();
console.log('[MQTT分发器] 已启动');
}
/**
* 注册指令处理器
*/
registerActionHandlers() {
// 注册各种MQTT指令处理器
this.actionHandlers.set('manual_job', async (params) => {
return await this.handleManualJobRequest(params);
});
this.actionHandlers.set('get_status', async () => {
return this.getSystemStatus();
});
}
/**
* 处理手动任务请求
*/
async handleManualJobRequest(params) {
const { sn_code, data } = params;
const { keyword, taskType } = data;
const config = require('../schedule/config.js');
try {
// addTask 内部会检查账号是否启用,这里直接调用即可
const taskId = await this.taskQueue.addTask(sn_code, {
taskType: taskType,
taskName: `手动任务 - ${keyword || taskType}`,
taskParams: data,
priority: config.getTaskPriority(taskType, { urgent: true })
});
return {
success: true,
taskId: taskId,
message: '任务已添加到队列'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* 获取系统状态(供调度管理器使用)
*/
getSystemStatus() {
return {
systemStats: deviceManager.getSystemStats(),
allDevices: deviceManager.getAllDevicesStatus(),
taskQueues: this.taskQueue.getAllDeviceStatus()
};
}
/**
* 分发 MQTT 请求消息
*/
async dispatchMqttMessage(topic, messageStr, mqttClient) {
try {
const message = JSON.parse(messageStr);
const { action, data, uuid, platform, sn_code } = message;
const deviceCode = topic.replace('request_', '') || sn_code;
console.log(`[MQTT分发器] 收到指令 - 设备: ${deviceCode}, 动作: ${action}, UUID: ${uuid}`);
// 检查设备是否在线
if (!deviceManager.isDeviceOnline(deviceCode)) {
console.log(`[MQTT分发器] 设备 ${deviceCode} 离线,忽略指令`);
return this.sendMqttResponse(mqttClient, uuid, {
code: 500,
message: '设备离线',
data: null
});
}
// 获取对应的处理器
const handler = this.actionHandlers.get(action);
if (!handler) {
console.log(`[MQTT分发器] 未找到动作 ${action} 的处理器`);
return this.sendMqttResponse(mqttClient, uuid, {
code: 404,
message: `未找到动作 ${action} 的处理器`,
data: null
});
}
// 执行处理器
const result = await handler({
sn_code: deviceCode,
action,
data,
platform,
mqttClient,
uuid
});
// 发送响应
return this.sendMqttResponse(mqttClient, uuid, result);
} catch (error) {
console.error('[MQTT分发器] 分发指令时出错:', error);
try {
const message = JSON.parse(messageStr);
if (message.uuid) {
return this.sendMqttResponse(mqttClient, message.uuid, {
code: 500,
message: error.message,
data: null
});
}
} catch (e) {
console.error('[MQTT分发器] 解析消息失败');
}
}
}
/**
* 发送 MQTT 响应
*/
async sendMqttResponse(mqttClient, uuid, result) {
const response = {
uuid: uuid,
code: result.code || 200,
message: result.message || 'success',
data: result.data || null,
timestamp: Date.now()
};
try {
await mqttClient.publish('response', JSON.stringify(response));
console.log(`[MQTT分发器] 发送响应 - UUID: ${uuid}, Code: ${response.code}`);
} catch (error) {
console.error('[MQTT分发器] 发送响应失败:', error);
}
}
/**
* 创建成功响应
*/
static createSuccessResponse(data = null, message = 'success') {
return {
code: 200,
message: message,
data: data
};
}
/**
* 创建失败响应
*/
static createErrorResponse(message, code = 500) {
return {
code: code,
message: message,
data: null
};
}
/**
* 处理心跳消息
* @param {object|string} message - 心跳消息对象或JSON字符串
*/
async handleHeartbeat(message) {
try {
// 解析消息如果是字符串则解析JSON
let heartbeatData = message;
if (typeof message === 'string') {
heartbeatData = JSON.parse(message);
}
const { sn_code, timestamp, status, memory, platform_status, platform_login_status } = heartbeatData;
if (!sn_code) {
console.warn('[MQTT心跳] 心跳消息中未找到设备SN码');
return;
}
console.log(`[MQTT心跳] 收到设备 ${sn_code} 的心跳消息`);
const device_status = db.getModel('device_status');
// 检查设备是否存在
let device = await device_status.findByPk(sn_code);
const updateData = {
isOnline: true, // 收到心跳,设备在线
lastHeartbeatTime: timestamp ? new Date(timestamp) : new Date(),
missedHeartbeats: 0
};
// 更新内存信息
if (memory && memory.percent !== undefined) {
updateData.memoryUsage = memory.percent;
}
// 优先使用 platform_login_status新格式
const loginStatusData = platform_login_status ;
if (loginStatusData) {
let isLoggedIn = false;
let loggedInPlatform = null;
let loggedInUsername = null;
let loggedInUserId = null;
let loginTime = null;
// 判断是新格式还是旧格式
isLoggedIn = platform_login_status.login || false;
loggedInPlatform = platform_login_status.platform || null;
loggedInUsername = platform_login_status.username || '';
loggedInUserId = platform_login_status.user_id || null;
if (platform_login_status.timestamp) {
loginTime = new Date(platform_login_status.timestamp);
}
// 更新登录状态
const previousIsLoggedIn = device ? device.isLoggedIn : false;
updateData.isLoggedIn = isLoggedIn;
if (isLoggedIn) {
updateData.platform = loggedInPlatform;
updateData.accountName = loggedInUsername || '';
// 如果之前未登录,现在登录了,更新登录时间
if (!previousIsLoggedIn) {
updateData.loginTime = loginTime || updateData.lastHeartbeatTime;
}
console.log(`[MQTT心跳] 设备 ${sn_code} 已登录 - 平台: ${loggedInPlatform}, 用户: ${loggedInUsername}, ID: ${loggedInUserId}`);
} else {
// 如果之前已登录,现在未登录了,清空登录相关信息
if (previousIsLoggedIn) {
updateData.accountName = '';
// loginTime 保持不变,记录最后登录时间
}
console.log(`[MQTT心跳] 设备 ${sn_code} 未登录`);
}
}
// 更新或创建设备记录
if (device) {
await device_status.update(updateData, { where: { sn_code } });
console.log(`[MQTT心跳] 设备 ${sn_code} 状态已更新 - 在线: true, 登录: ${updateData.isLoggedIn}`);
} else {
// 创建新设备记录
await device_status.create({
sn_code,
deviceName: `设备_${sn_code}`,
deviceType: 'node_mqtt_client',
...updateData,
isRunning: false,
taskStatus: 'idle',
healthStatus: 'unknown',
healthScore: 0
});
console.log(`[MQTT心跳] 设备 ${sn_code} 记录已创建 - 在线: true, 登录: ${updateData.isLoggedIn}`);
}
// 记录心跳到设备管理器
await deviceManager.recordHeartbeat(sn_code, heartbeatData);
} catch (error) {
console.error('[MQTT心跳] 处理心跳消息失败:', error);
}
}
/**
* 处理响应消息
* @param {object|string} message - 响应消息对象或JSON字符串
*/
handleResponse(message) {
// 响应消息处理逻辑(目前为空,可根据需要扩展)
// 例如:记录响应日志、更新任务状态等
}
}
module.exports = MqttDispatcher;

View File

@@ -0,0 +1,127 @@
const MqttSyncClient = require('./mqttClient');
const Framework = require('../../../framework/node-core-framework');
const logs = require('../logProxy');
// action.js 已合并到 mqttDispatcher.js不再需要单独引入
/**
* MQTT管理器 - 单例模式
* 负责管理MQTT连接确保全局只有一个MQTT客户端实例
*/
class MqttManager {
constructor() {
this.client = null;
this.isInitialized = false;
this.config = {
brokerUrl: 'mqtt://192.144.167.231:1883', // MQTT Broker地址
options: {
clientId: `mqtt_server_${Math.random().toString(16).substr(2, 8)}`,
clean: true,
connectTimeout: 5000,
reconnectPeriod: 5000, // 自动重连间隔
keepalive: 10
}
};
}
/**
* 获取MQTT客户端实例单例模式
* @param {object} config - 可选的配置覆盖
* @returns {Promise<MqttSyncClient>} MQTT客户端实例
*/
async getInstance(config = {}) {
if (this.client && this.isInitialized) {
console.log('[MQTT管理器] 返回已存在的MQTT客户端实例');
return this.client;
}
// 合并配置
if (config.brokerUrl) {
this.config.brokerUrl = config.brokerUrl;
}
if (config.options) {
this.config.options = { ...this.config.options, ...config.options };
}
try {
console.log('[MQTT管理器] 创建新的MQTT客户端实例');
console.log(`[MQTT管理器] Broker地址: ${this.config.brokerUrl}`);
// 创建MQTT客户端
this.client = new MqttSyncClient(this.config.brokerUrl, this.config.options);
// 等待连接成功
await this.client.waitForConnect(10000);
// 注意:心跳和响应的订阅已移到 mqttDispatcher 中处理
// 这里不再直接订阅,由调度系统通过 mqttDispatcher 统一管理
console.log('[MQTT管理器] MQTT客户端连接成功等待分发器初始化订阅');
this.isInitialized = true;
console.log('[MQTT管理器] MQTT客户端初始化成功');
return this.client;
} catch (error) {
console.error('[MQTT管理器] MQTT客户端初始化失败:', error);
this.client = null;
this.isInitialized = false;
throw error;
}
}
/**
* 重置MQTT客户端用于重新连接
*/
async reset() {
if (this.client) {
try {
console.log('[MQTT管理器] 关闭现有MQTT连接');
this.client.end(true);
} catch (error) {
console.error('[MQTT管理器] 关闭MQTT连接时出错:', error);
}
}
this.client = null;
this.isInitialized = false;
console.log('[MQTT管理器] MQTT客户端已重置');
}
/**
* 检查MQTT客户端是否已初始化
* @returns {boolean}
*/
isReady() {
return this.isInitialized && this.client && this.client.isConnected;
}
/**
* 更新配置
* @param {object} config - 新的配置
*/
updateConfig(config) {
if (this.isInitialized) {
console.warn('[MQTT管理器] MQTT客户端已初始化配置更新需要重置连接');
}
if (config.brokerUrl) {
this.config.brokerUrl = config.brokerUrl;
}
if (config.options) {
this.config.options = { ...this.config.options, ...config.options };
}
console.log('[MQTT管理器] 配置已更新:', this.config);
}
/**
* 获取当前配置
* @returns {object}
*/
getConfig() {
return { ...this.config };
}
}
// 导出单例
const mqttManager = new MqttManager();
module.exports = mqttManager;

View File

@@ -0,0 +1,118 @@
const db = require('../dbProxy');
/**
* 统一错误处理模块
* 负责错误分类、记录、恢复决策
*/
class ErrorHandler {
/**
* 可重试的错误类型
*/
static RETRYABLE_ERRORS = [
'ETIMEDOUT',
'ECONNRESET',
'ENOTFOUND',
'NetworkError',
'MQTT客户端未初始化',
'设备离线',
'超时'
];
/**
* 处理错误
* @param {Error} error - 错误对象
* @param {Object} context - 上下文信息
* @returns {Object} 错误处理结果
*/
static async handleError(error, context = {}) {
const errorInfo = {
message: error.message || '未知错误',
stack: error.stack || '',
code: error.code || '',
context: {
taskId: context.taskId,
sn_code: context.sn_code,
taskType: context.taskType,
...context
},
timestamp: new Date().toISOString(),
isRetryable: this.isRetryableError(error)
};
// 记录到日志
console.error(`[错误处理] ${errorInfo.message}`, {
context: errorInfo.context,
isRetryable: errorInfo.isRetryable,
stack: errorInfo.stack
});
// 错误信息已通过 console.error 记录到控制台日志
return errorInfo;
}
/**
* 判断错误是否可重试
* @param {Error} error - 错误对象
* @returns {boolean}
*/
static isRetryableError(error) {
if (!error) return false;
const errorMessage = (error.message || '').toLowerCase();
const errorCode = error.code || '';
// 检查错误代码
if (this.RETRYABLE_ERRORS.some(code => errorCode === code)) {
return true;
}
// 检查错误消息
return this.RETRYABLE_ERRORS.some(code =>
errorMessage.includes(code.toLowerCase())
);
}
/**
* 计算重试延迟(指数退避)
* @param {number} retryCount - 当前重试次数
* @param {number} baseDelay - 基础延迟(毫秒)
* @param {number} maxDelay - 最大延迟(毫秒)
* @returns {number} 延迟时间(毫秒)
*/
static calculateRetryDelay(retryCount, baseDelay = 1000, maxDelay = 30000) {
const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), maxDelay);
return delay;
}
/**
* 创建可重试错误
* @param {string} message - 错误消息
* @param {Object} context - 上下文
* @returns {Error}
*/
static createRetryableError(message, context = {}) {
const error = new Error(message);
error.code = 'RETRYABLE';
error.context = context;
error.isRetryable = true;
return error;
}
/**
* 创建致命错误
* @param {string} message - 错误消息
* @param {Object} context - 上下文
* @returns {Error}
*/
static createFatalError(message, context = {}) {
const error = new Error(message);
error.code = 'FATAL';
error.context = context;
error.isRetryable = false;
return error;
}
}
module.exports = ErrorHandler;

View File

@@ -0,0 +1,215 @@
/**
* 优先级队列实现(使用最小堆)
* 优先级高的任务priority值大会优先出队
*/
class PriorityQueue {
constructor() {
this.heap = [];
}
/**
* 获取父节点索引
*/
parent(index) {
return Math.floor((index - 1) / 2);
}
/**
* 获取左子节点索引
*/
leftChild(index) {
return 2 * index + 1;
}
/**
* 获取右子节点索引
*/
rightChild(index) {
return 2 * index + 2;
}
/**
* 交换两个节点
*/
swap(i, j) {
[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
}
/**
* 上浮操作(插入时使用)
*/
bubbleUp(index) {
if (index === 0) return;
const parentIndex = this.parent(index);
const current = this.heap[index];
const parent = this.heap[parentIndex];
// 优先级高的在前priority值大如果优先级相同创建时间早的在前
if (
current.priority > parent.priority ||
(current.priority === parent.priority && current.createdAt < parent.createdAt)
) {
this.swap(index, parentIndex);
this.bubbleUp(parentIndex);
}
}
/**
* 下沉操作(删除时使用)
*/
bubbleDown(index) {
const leftIndex = this.leftChild(index);
const rightIndex = this.rightChild(index);
let largest = index;
const current = this.heap[index];
// 比较左子节点
if (leftIndex < this.heap.length) {
const left = this.heap[leftIndex];
if (
left.priority > current.priority ||
(left.priority === current.priority && left.createdAt < current.createdAt)
) {
largest = leftIndex;
}
}
// 比较右子节点
if (rightIndex < this.heap.length) {
const right = this.heap[rightIndex];
const largestNode = this.heap[largest];
if (
right.priority > largestNode.priority ||
(right.priority === largestNode.priority && right.createdAt < largestNode.createdAt)
) {
largest = rightIndex;
}
}
if (largest !== index) {
this.swap(index, largest);
this.bubbleDown(largest);
}
}
/**
* 添加任务到队列
* @param {Object} task - 任务对象,必须包含 priority 和 createdAt 属性
*/
push(task) {
if (!task.hasOwnProperty('priority')) {
task.priority = 5; // 默认优先级
}
if (!task.hasOwnProperty('createdAt')) {
task.createdAt = Date.now();
}
this.heap.push(task);
this.bubbleUp(this.heap.length - 1);
}
/**
* 取出优先级最高的任务
* @returns {Object|null} 任务对象或null
*/
pop() {
if (this.heap.length === 0) {
return null;
}
if (this.heap.length === 1) {
return this.heap.pop();
}
const top = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown(0);
return top;
}
/**
* 查看优先级最高的任务(不移除)
* @returns {Object|null} 任务对象或null
*/
peek() {
return this.heap.length > 0 ? this.heap[0] : null;
}
/**
* 获取队列大小
* @returns {number}
*/
size() {
return this.heap.length;
}
/**
* 检查队列是否为空
* @returns {boolean}
*/
isEmpty() {
return this.heap.length === 0;
}
/**
* 清空队列
*/
clear() {
this.heap = [];
}
/**
* 查找任务
* @param {Function} predicate - 查找条件函数
* @returns {Object|null} 任务对象或null
*/
find(predicate) {
return this.heap.find(predicate) || null;
}
/**
* 移除任务
* @param {Function} predicate - 查找条件函数
* @returns {boolean} 是否成功移除
*/
remove(predicate) {
const index = this.heap.findIndex(predicate);
if (index === -1) {
return false;
}
if (index === this.heap.length - 1) {
this.heap.pop();
return true;
}
// 将最后一个元素移到当前位置
this.heap[index] = this.heap.pop();
// 重新调整堆
const parentIndex = this.parent(index);
if (index > 0 && this.heap[parentIndex] &&
(this.heap[index].priority > this.heap[parentIndex].priority ||
(this.heap[index].priority === this.heap[parentIndex].priority &&
this.heap[index].createdAt < this.heap[parentIndex].createdAt))) {
this.bubbleUp(index);
} else {
this.bubbleDown(index);
}
return true;
}
/**
* 转换为数组(用于调试)
* @returns {Array}
*/
toArray() {
return [...this.heap];
}
}
module.exports = PriorityQueue;

View File

@@ -0,0 +1,308 @@
const logs = require('../logProxy');
const db = require('../dbProxy');
const jobManager = require('../job/index');
const ScheduleUtils = require('./utils');
const ScheduleConfig = require('./config');
/**
* 指令管理器
* 负责管理任务下的多个指令简化MQTT通信流程
*/
class CommandManager {
constructor() {
this.pendingCommands = new Map(); // 等待响应的指令 { commandId: { resolve, reject, timeout } }
}
/**
* 执行指令序列
* @param {Array} commands - 指令数组
* @param {object} mqttClient - MQTT客户端
* @param {object} options - 执行选项
* @returns {Promise<object>} 执行结果
*/
async executeCommands(taskId, commands, mqttClient, options = {}) {
// try {
if (!commands || commands.length === 0) {
throw new Error('没有找到要执行的指令');
}
const {
maxRetries = 1, // 最大重试次数
retryDelay = 1000 // 重试延迟(毫秒)
} = options;
console.log(`[指令管理] 开始执行 ${commands.length} 个指令`);
const results = [];
const errors = [];
// 顺序执行指令,失败时停止
for (let i = 0; i < commands.length; i++) {
const command = commands[i];
let retryCount = 0;
let commandResult = null;
// 重试逻辑
while (retryCount <= maxRetries) {
console.log(`[指令管理] 执行指令 ${i + 1}/${commands.length}: ${command.command_name || command.name} (尝试 ${retryCount + 1}/${maxRetries + 1})`);
commandResult = await this.executeCommand(taskId, command, mqttClient);
results. push(commandResult);
break; // 成功执行,跳出重试循环
}
}
const successCount = results.length;
const errorCount = errors.length;
console.log(`[指令管理] 指令执行完成: 成功 ${successCount}/${commands.length}, 失败 ${errorCount}`);
return {
success: errorCount === 0, // 只有全部成功才算成功
results: results,
errors: errors,
totalCommands: commands.length,
successCount: successCount,
errorCount: errorCount
};
// } catch (error) {
// console.error(`[指令管理] 执行指令序列失败:`, error);
// throw error;
// }
}
/**
* 执行单个指令
* @param {object} command - 指令对象
* @param {object} mqttClient - MQTT客户端
* @returns {Promise<object>} 执行结果
*/
async executeCommand(taskId, command, mqttClient) {
const startTime = new Date();
let commandRecord = null;
const task = await db.getModel('task_status').findByPk(taskId);
// 获取指令信息(支持两种格式)
const commandName = command.command_name;
const commandType = command.command_type;
const commandParams = command.command_params ? JSON.parse(command.command_params) : {};
// 创建指令记录
commandRecord = await db.getModel('task_commands').create({
task_id: taskId,
command_type: commandType,
command_name: commandName,
command_params: JSON.stringify(commandParams),
priority: command.priority || 1,
sequence: command.sequence || 1,
max_retries: command.maxRetries || command.max_retries || 3,
status: 'pending'
});
let commandId = commandRecord.id;
console.log(`[指令管理] 创建指令记录: ${commandName} (ID: ${commandId})`);
// 更新指令状态为运行中
await this.updateCommandStatus(commandId, 'running');
console.log(`[指令管理] 执行指令: ${commandName} (ID: ${commandId})`);
const sn_code = task.sn_code;
// 将驼峰命名转换为下划线命名getOnlineResume -> get_online_resume
const toSnakeCase = (str) => {
// 如果已经是下划线格式,直接返回
if (str.includes('_')) {
return str;
}
// 驼峰转下划线
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
};
const methodName = toSnakeCase(commandType);
// 获取指令超时时间从配置中获取默认5分钟
const timeout = ScheduleConfig.taskTimeouts[commandType] || ScheduleConfig.taskTimeouts[methodName] || 5 * 60 * 1000;
let result;
try {
// 使用超时机制包装指令执行
const commandPromise = (async () => {
if (commandType && jobManager[methodName]) {
return await jobManager[methodName](sn_code, mqttClient, commandParams);
} else {
// 如果转换后找不到,尝试直接使用原名称
if (jobManager[commandType]) {
return await jobManager[commandType](sn_code, mqttClient, commandParams);
} else {
throw new Error(`未知的指令类型: ${commandType} (尝试的方法名: ${methodName})`);
}
}
})();
// 使用超时机制
result = await ScheduleUtils.withTimeout(
commandPromise,
timeout,
`指令执行超时: ${commandName} (超时时间: ${timeout / 1000}秒)`
);
} catch (error) {
const endTime = new Date();
const duration = endTime - startTime;
// 如果是超时错误,更新指令状态为失败
const errorMessage = error.message || '指令执行失败';
await this.updateCommandStatus(commandId, 'failed', null, errorMessage);
throw error;
}
const endTime = new Date();
const duration = endTime - startTime;
// 更新指令状态为完成
await this.updateCommandStatus(commandId, 'completed', result);
return {
commandId: commandId,
commandName: commandName,
result: result,
duration: duration,
success: true
};
}
/**
* 更新指令状态
* @param {number} commandId - 指令ID
* @param {string} status - 状态
* @param {object} result - 结果
* @param {string} errorMessage - 错误信息
*/
async updateCommandStatus(commandId, status, result = null, errorMessage = null) {
try {
const updateData = {
status: status,
updated_at: new Date()
};
if (status === 'running') {
updateData.start_time = new Date();
} else if (status === 'completed' || status === 'failed') {
updateData.end_time = new Date();
if (result) {
// 将结果转换为JSON字符串并限制长度TEXT类型最大约65KB
let resultStr = JSON.stringify(result);
const maxLength = 60000; // 限制为60KB留一些余量
if (resultStr.length > maxLength) {
// 如果结果太长,尝试压缩或截断
try {
// 如果是对象,尝试只保存关键信息
if (typeof result === 'object' && result !== null) {
const summary = {
success: result.success !== undefined ? result.success : true,
message: result.message || '执行成功',
dataLength: resultStr.length,
truncated: true,
preview: resultStr.substring(0, 1000) // 保存前1000字符作为预览
};
resultStr = JSON.stringify(summary);
} else {
// 直接截断
resultStr = resultStr.substring(0, maxLength) + '...[数据已截断]';
}
} catch (e) {
// 如果处理失败,直接截断
resultStr = resultStr.substring(0, maxLength) + '...[数据已截断]';
}
}
updateData.result = resultStr;
updateData.progress = 100;
}
if (errorMessage) {
// 错误信息也限制长度
const maxErrorLength = 10000; // 错误信息限制10KB
updateData.error_message = errorMessage.length > maxErrorLength
? errorMessage.substring(0, maxErrorLength) + '...[错误信息已截断]'
: errorMessage;
}
// 计算执行时长
const command = await db.getModel('task_commands').findByPk(commandId);
if (command && command.start_time) {
const duration = new Date() - new Date(command.start_time);
updateData.duration = duration;
}
}
await db.getModel('task_commands').update(updateData, {
where: { id: commandId }
});
} catch (error) {
logs.error(`[指令管理] 更新指令状态失败:`, error, {
commandId: commandId,
status: status
});
// 如果是因为数据太长导致的错误,尝试只保存错误信息
if (error.message && error.message.includes('Data too long')) {
try {
await db.getModel('task_commands').update({
status: status,
error_message: '结果数据过长,无法保存完整结果',
end_time: new Date(),
updated_at: new Date()
}, {
where: { id: commandId }
});
} catch (e) {
console.error(`[指令管理] 保存截断结果也失败:`, e);
}
}
}
}
/**
* 清理过期的指令记录
* @param {number} days - 保留天数
*/
async cleanupExpiredCommands(days = 30) {
try {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const deletedCount = await db.getModel('task_commands').destroy({
where: {
create_time: {
[db.Sequelize.Op.lt]: cutoffDate
}
}
});
console.log(`[指令管理] 清理了 ${deletedCount} 条过期指令记录`);
return deletedCount;
} catch (error) {
logs.error(`[指令管理] 清理过期指令失败:`, error);
return 0;
}
}
}
module.exports = new CommandManager();

View File

@@ -0,0 +1,150 @@
const dayjs = require('dayjs');
const config = require('../../../config/config');
/**
* 调度系统配置中心
* 统一管理所有配置参数
*/
class ScheduleConfig {
constructor() {
// 工作时间配置
this.workHours = {
start: 9,
end: 18
};
// 频率限制配置(毫秒)
this.rateLimits = {
search: 30 * 60 * 1000, // 搜索间隔30分钟
apply: 5 * 60 * 1000, // 投递间隔5分钟
chat: 1 * 60 * 1000, // 聊天间隔1分钟
};
// 单日操作限制
this.dailyLimits = {
maxSearch: 20, // 每天最多搜索20次
maxApply: 50, // 每天最多投递50份简历
maxChat: 100, // 每天最多发送100条聊天
};
// 任务超时配置(毫秒)
this.taskTimeouts = {
get_login_qr_code: 30 * 1000, // 登录检查30秒
get_resume: 60 * 1000, // 获取简历1分钟
search_jobs: 5 * 60 * 1000, // 搜索岗位5分钟
chat: 30 * 1000, // 聊天30秒
apply: 30 * 1000 // 投递30秒
};
// 任务优先级配置
this.taskPriorities = {
get_login_qr_code: 10, // 最高优先级
get_resume: 9,
apply: 8,
auto_deliver: 7, // 自动投递任务
search_jobs: 6,
chat: 5,
cleanup: 1
};
// 监控配置
this.monitoring = {
heartbeatTimeout: 3 * 60 * 1000, // 心跳超时5分钟
taskFailureRate: 0.5, // 任务失败率50%
consecutiveFailures: 3, // 连续失败次数3次
alertCooldown: 5 * 60 * 1000, // 告警冷却5分钟
offlineThreshold: 24 * 60 * 60 * 1000 // 离线设备清理24小时
};
// 定时任务配置
this.schedules = {
dailyReset: '0 0 * * *', // 每天凌晨重置统计
jobFlowInterval: '0 */5 * * * *', // 每10秒执行一次找工作流程
monitoringInterval: '*/1 * * * *', // 监控检查间隔1分钟
autoDeliver: '0 */5 * * * *', // 自动投递任务每5分钟执行一次
// 监控检查间隔1分钟
};
// 测试配置覆盖
if (config.test) {
this.applyTestConfig(config.test);
}
}
/**
* 应用测试配置
* @param {object} testConfig - 测试配置
*/
applyTestConfig(testConfig) {
if (testConfig.skipWorkStartHour) {
this.workHours.start = 0;
this.workHours.end = 24;
}
// 测试模式下缩短所有间隔时间
if (testConfig.fastMode) {
this.rateLimits.search = 10 * 1000; // 10秒
this.rateLimits.apply = 5 * 1000; // 5秒
this.rateLimits.chat = 2 * 1000; // 2秒
}
}
/**
* 检查是否在工作时间
* @returns {boolean}
*/
isWorkingHours() {
const now = dayjs();
const hour = now.hour();
return hour >= this.workHours.start && hour < this.workHours.end;
}
/**
* 获取任务超时时间
* @param {string} taskType - 任务类型
* @returns {number} 超时时间(毫秒)
*/
getTaskTimeout(taskType) {
return this.taskTimeouts[taskType] || 30000; // 默认30秒
}
/**
* 获取任务优先级
* @param {string} taskType - 任务类型
* @param {object} options - 选项
* @returns {number} 优先级1-10
*/
getTaskPriority(taskType, options = {}) {
let priority = this.taskPriorities[taskType] || 5;
if (options.urgent) {
priority = Math.min(10, priority + 2);
}
return priority;
}
/**
* 获取操作频率限制
* @param {string} operation - 操作类型
* @returns {number} 间隔时间(毫秒)
*/
getRateLimit(operation) {
return this.rateLimits[operation] || 0;
}
/**
* 获取日限制
* @param {string} operation - 操作类型
* @returns {number} 日限制次数
*/
getDailyLimit(operation) {
return this.dailyLimits[`max${operation.charAt(0).toUpperCase() + operation.slice(1)}`] || Infinity;
}
}
// 导出单例
const scheduleConfig = new ScheduleConfig();
module.exports = scheduleConfig;

View File

@@ -0,0 +1,287 @@
const dayjs = require('dayjs');
const Sequelize = require('sequelize');
const db = require('../dbProxy');
const config = require('./config');
const utils = require('./utils');
/**
* 设备管理器(简化版)
* 合并了 Monitor 和 Strategy 的核心功能
*/
class DeviceManager {
constructor() {
// 设备状态 { sn_code: { isOnline, lastHeartbeat, lastSearch, lastApply, lastChat, dailyCounts } }
this.devices = new Map();
// 系统统计
this.stats = {
totalDevices: 0,
onlineDevices: 0,
totalTasks: 0,
completedTasks: 0,
failedTasks: 0,
startTime: new Date()
};
}
/**
* 初始化
*/
async init() {
console.log('[设备管理器] 初始化中...');
await this.loadStats();
console.log('[设备管理器] 初始化完成');
}
/**
* 加载统计数据
*/
async loadStats() {
try {
const devices = await db.getModel('pla_account').findAll();
this.stats.totalDevices = devices.length;
const completedCount = await db.getModel('task_status').count({
where: { status: 'completed' }
});
const failedCount = await db.getModel('task_status').count({
where: { status: 'failed' }
});
this.stats.completedTasks = completedCount;
this.stats.failedTasks = failedCount;
this.stats.totalTasks = completedCount + failedCount;
} catch (error) {
console.error('[设备管理器] 加载统计失败:', error);
}
}
/**
* 记录心跳
*/
async recordHeartbeat(sn_code, heartbeatData = {}) {
const now = Date.now();
if (!this.devices.has(sn_code)) {
this.devices.set(sn_code, {
isOnline: true,
lastHeartbeat: now,
dailyCounts: { date: utils.getTodayString(), searchCount: 0, applyCount: 0, chatCount: 0 }
});
}
const device = this.devices.get(sn_code);
device.isOnline = true;
device.lastHeartbeat = now;
}
/**
* 检查设备是否在线
*/
isDeviceOnline(sn_code) {
const device = this.devices.get(sn_code);
if (!device) return false;
const elapsed = Date.now() - device.lastHeartbeat;
if (elapsed > config.monitoring.heartbeatTimeout) {
device.isOnline = false;
return false;
}
return device.isOnline;
}
/**
* 检查是否可以执行操作
*/
canExecuteOperation(sn_code, operationType) {
// 检查工作时间
if (!config.isWorkingHours()) {
return { allowed: false, reason: '不在工作时间内' };
}
// 检查频率限制
const device = this.devices.get(sn_code);
if (device) {
const lastTime = device[`last${operationType.charAt(0).toUpperCase() + operationType.slice(1)}`] || 0;
const interval = config.getRateLimit(operationType);
if (Date.now() - lastTime < interval) {
return { allowed: false, reason: '操作过于频繁' };
}
}
// 检查日限制
if (device && device.dailyCounts) {
const today = utils.getTodayString();
if (device.dailyCounts.date !== today) {
device.dailyCounts = { date: today, searchCount: 0, applyCount: 0, chatCount: 0 };
}
const countKey = `${operationType}Count`;
const current = device.dailyCounts[countKey] || 0;
const max = config.getDailyLimit(operationType);
if (current >= max) {
return { allowed: false, reason: `今日${operationType}操作已达上限` };
}
}
return { allowed: true };
}
/**
* 记录操作
*/
recordOperation(sn_code, operationType) {
const device = this.devices.get(sn_code) || {};
device[`last${operationType.charAt(0).toUpperCase() + operationType.slice(1)}`] = Date.now();
if (device.dailyCounts) {
const countKey = `${operationType}Count`;
device.dailyCounts[countKey] = (device.dailyCounts[countKey] || 0) + 1;
}
this.devices.set(sn_code, device);
}
/**
* 记录任务开始
*/
recordTaskStart(sn_code, task) {
// 简化实现,只记录日志
console.log(`[设备管理器] 设备 ${sn_code} 开始执行任务: ${task.taskName}`);
}
/**
* 记录任务完成
*/
recordTaskComplete(sn_code, task, success, duration) {
if (success) {
this.stats.completedTasks++;
} else {
this.stats.failedTasks++;
}
this.stats.totalTasks++;
console.log(`[设备管理器] 设备 ${sn_code} 任务${success ? '成功' : '失败'}: ${task.taskName} (${duration}ms)`);
}
/**
* 获取系统统计
*/
getSystemStats() {
const onlineCount = Array.from(this.devices.values()).filter(d => d.isOnline).length;
return {
...this.stats,
onlineDevices: onlineCount,
uptime: utils.formatDuration(Date.now() - this.stats.startTime.getTime())
};
}
/**
* 获取所有设备状态
*/
getAllDevicesStatus() {
const result = {};
for (const [sn_code, device] of this.devices.entries()) {
result[sn_code] = {
isOnline: device.isOnline,
lastHeartbeat: device.lastHeartbeat,
dailyCounts: device.dailyCounts || {}
};
}
return result;
}
/**
* 检查心跳状态(异步更新数据库)
*/
async checkHeartbeatStatus() {
try {
const now = Date.now();
const device_status = db.getModel('device_status');
const offlineDevices = [];
for (const [sn_code, device] of this.devices.entries()) {
if (now - device.lastHeartbeat > config.monitoring.heartbeatTimeout) {
// 如果之前是在线状态,现在检测到离线,需要更新数据库
if (device.isOnline) {
device.isOnline = false;
offlineDevices.push(sn_code);
}
}
}
// 批量更新数据库中的离线设备状态
if (offlineDevices.length > 0) {
await device_status.update(
{ isOnline: false },
{
where: {
sn_code: {
[Sequelize.Op.in]: offlineDevices
},
isOnline: true // 只更新当前在线的设备,避免重复更新
}
}
);
console.log(`[设备管理器] 检测到 ${offlineDevices.length} 个设备心跳超时,已同步到数据库: ${offlineDevices.join(', ')}`);
}
// 同时检查数据库中的设备状态(处理内存中没有但数据库中有心跳超时的情况)
const heartbeatTimeout = config.monitoring.heartbeatTimeout;
const heartbeatThreshold = new Date(now - heartbeatTimeout);
const timeoutDevices = await device_status.findAll({
where: {
isOnline: true,
lastHeartbeatTime: {
[Sequelize.Op.lt]: heartbeatThreshold
}
},
attributes: ['sn_code', 'lastHeartbeatTime']
});
if (timeoutDevices.length > 0) {
const timeoutSnCodes = timeoutDevices.map(dev => dev.sn_code);
await device_status.update(
{ isOnline: false },
{
where: {
sn_code: {
[Sequelize.Op.in]: timeoutSnCodes
}
}
}
);
console.log(`[设备管理器] 从数据库检测到 ${timeoutSnCodes.length} 个心跳超时设备,已更新为离线: ${timeoutSnCodes.join(', ')}`);
}
} catch (error) {
console.error('[设备管理器] 检查心跳状态失败:', error);
}
}
/**
* 重置所有日计数器
*/
resetAllDailyCounters() {
const today = utils.getTodayString();
for (const device of this.devices.values()) {
if (device.dailyCounts && device.dailyCounts.date !== today) {
device.dailyCounts = { date: today, searchCount: 0, applyCount: 0, chatCount: 0 };
}
}
}
/**
* 清理离线设备
*/
cleanupOfflineDevices(threshold = 3600000) {
const now = Date.now();
for (const [sn_code, device] of this.devices.entries()) {
if (now - device.lastHeartbeat > threshold) {
this.devices.delete(sn_code);
}
}
}
}
// 导出单例
const deviceManager = new DeviceManager();
module.exports = deviceManager;

View File

@@ -0,0 +1,203 @@
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 TaskHandlers = require('./taskHandlers.js');
const MqttDispatcher = require('../mqtt/mqttDispatcher.js');
const ScheduledJobs = require('./scheduledJobs.js');
/**
* 调度系统管理器
* 统一管理整个调度系统的生命周期
*/
class ScheduleManager {
constructor() {
this.mqttClient = null;
this.isInitialized = false;
this.startTime = new Date();
// 子模块
this.taskHandlers = null;
this.mqttDispatcher = null;
this.scheduledJobs = null;
}
/**
* 初始化调度系统
*/
async init() {
try {
console.log('[调度管理器] 开始初始化...');
// 1. 初始化MQTT管理器
await this.initMqttClient();
console.log('[调度管理器] MQTT管理器已初始化');
// 2. 初始化各个组件
await this.initComponents();
console.log('[调度管理器] 组件已初始化');
// 3. 初始化子模块
this.initSubModules();
console.log('[调度管理器] 子模块已初始化');
// 4. 启动心跳监听
this.startHeartbeatListener();
console.log('[调度管理器] 心跳监听已启动');
// 5. 启动定时任务
this.scheduledJobs.start();
console.log('[调度管理器] 定时任务已启动');
this.isInitialized = true;
} catch (error) {
console.error('[调度管理器] 初始化失败:', error);
throw error;
}
}
/**
* 初始化MQTT客户端
*/
async initMqttClient() {
this.mqttClient = await mqttManager.getInstance();
}
/**
* 初始化各个组件(简化版)
*/
async initComponents() {
// 初始化设备管理器
await deviceManager.init();
// 初始化任务队列
await TaskQueue.init?.();
}
/**
* 初始化子模块
*/
initSubModules() {
// 初始化任务处理器
this.taskHandlers = new TaskHandlers(this.mqttClient);
this.taskHandlers.register(TaskQueue);
// 初始化 MQTT 分发器(简化:不再需要 components
this.mqttDispatcher = new MqttDispatcher({ deviceManager, taskQueue: TaskQueue }, this.mqttClient);
this.mqttDispatcher.start();
// 初始化定时任务管理器
this.scheduledJobs = new ScheduledJobs({ deviceManager, taskQueue: TaskQueue }, this.taskHandlers);
}
/**
* 启动心跳监听
*/
startHeartbeatListener() {
// 订阅心跳主题,使用 mqttDispatcher 处理
this.mqttClient.subscribe("heartbeat", async (topic, message) => {
try {
await this.mqttDispatcher.handleHeartbeat(message);
} catch (error) {
console.error('[调度管理器] 处理心跳消息失败:', error);
}
});
// 订阅响应主题
this.mqttClient.subscribe("response", async (topic, message) => {
try {
if (this.mqttDispatcher) {
this.mqttDispatcher.handleResponse(message);
}
} catch (error) {
console.error('[调度管理器] 处理响应消息失败:', error);
}
});
}
/**
* 手动执行找工作流程已废弃full_flow 不再使用)
* @deprecated 请使用其他任务类型,如 auto_deliver
*/
async manualExecuteJobFlow(sn_code, keyword = '前端') {
console.warn(`[手动执行] manualExecuteJobFlow 已废弃full_flow 不再使用`);
throw new Error('full_flow 任务类型已废弃,请使用其他任务类型');
}
/**
* 获取系统状态
*/
getSystemStatus() {
const status = this.mqttDispatcher ? this.mqttDispatcher.getSystemStatus() : {};
return {
isInitialized: this.isInitialized,
mqttConnected: this.mqttClient && this.mqttClient.isConnected,
systemStats: deviceManager.getSystemStats(),
allDevices: deviceManager.getAllDevicesStatus(),
taskQueues: TaskQueue.getAllDeviceStatus(),
uptime: utils.formatDuration(Date.now() - this.startTime.getTime()),
startTime: utils.formatTimestamp(this.startTime),
...status
};
}
/**
* 停止调度系统
*/
stop() {
console.log('[调度管理器] 正在停止...');
// 停止所有定时任务
if (this.scheduledJobs) {
this.scheduledJobs.stop();
}
// 停止任务队列扫描器
if (TaskQueue && typeof TaskQueue.stopQueueScanner === 'function') {
TaskQueue.stopQueueScanner();
}
// 关闭MQTT连接
if (this.mqttClient) {
this.mqttClient.end();
}
this.isInitialized = false;
console.log('[调度管理器] 已停止');
}
}
// 创建调度管理器实例
const scheduleManager = new ScheduleManager();
// 导出兼容的接口,保持与原有代码的一致性
module.exports = {
// 初始化方法
init: () => scheduleManager.init(),
// 手动执行任务
manualExecuteJobFlow: (sn_code, keyword) => scheduleManager.manualExecuteJobFlow(sn_code, keyword),
// 获取系统状态
getSystemStatus: () => scheduleManager.getSystemStatus(),
// 停止系统
stop: () => scheduleManager.stop(),
// 访问各个组件(为了兼容性)
get mqttClient() { return scheduleManager.mqttClient; },
get isInitialized() { return scheduleManager.isInitialized; },
// 访问各个组件实例(简化版)
get taskQueue() { return TaskQueue; },
get command() { return Command; },
get deviceManager() { return deviceManager; }
};

View File

@@ -0,0 +1,168 @@
const node_schedule = require("node-schedule");
const config = require('./config.js');
const deviceManager = require('./deviceManager.js');
const command = require('./command.js');
const db = require('../dbProxy');
/**
* 定时任务管理器(简化版)
* 管理所有定时任务的创建和销毁
*/
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 autoDeliverJob = node_schedule.scheduleJob(config.schedules.autoDeliver, () => {
this.autoDeliverTask();
});
this.jobs.push(autoDeliverJob);
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);
}
}
/**
* 自动投递任务
*/
async autoDeliverTask() {
const now = new Date();
console.log(`[自动投递] ${now.toLocaleString()} 开始执行自动投递任务`);
// 检查是否在工作时间
if (!config.isWorkingHours()) {
console.log(`[自动投递] 非工作时间,跳过执行`);
return;
}
try {
// 从 device_status 查询所有在线且已登录的设备
const models = db.models;
const { device_status, pla_account, op } = models;
const onlineDevices = await device_status.findAll({
where: {
isOnline: true,
isLoggedIn: true
},
attributes: ['sn_code', 'accountName', 'platform']
});
if (!onlineDevices || onlineDevices.length === 0) {
console.log('[自动投递] 没有在线且已登录的设备');
return;
}
// 获取这些在线设备对应的账号配置(只获取启用且开启自动投递的账号)
const snCodes = onlineDevices.map(device => device.sn_code);
const pla_users = await pla_account.findAll({
where: {
sn_code: { [op.in]: snCodes },
is_delete: 0,
is_enabled: 1, // 只获取启用的账号
auto_deliver: 1
}
});
if (!pla_users || pla_users.length === 0) {
console.log('[自动投递] 没有启用且开启自动投递的账号');
return;
}
console.log(`[自动投递] 找到 ${pla_users.length} 个可用账号`);
// 为每个设备添加自动投递任务到队列
for (const pl_user of pla_users) {
const userData = pl_user.toJSON();
// 检查设备调度策略
const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'deliver');
if (!canExecute.allowed) {
console.log(`[自动投递] 设备 ${userData.sn_code} 不满足执行条件: ${canExecute.reason}`);
continue;
}
// 添加自动投递任务到队列
await this.taskQueue.addTask(userData.sn_code, {
taskType: 'auto_deliver',
taskName: `自动投递 - ${userData.keyword || '默认关键词'}`,
taskParams: {
keyword: userData.keyword || '',
platform: userData.platform_type || 'boss',
pageCount: 3, // 默认值
maxCount: 10, // 默认值
filterRules: {
minSalary: userData.min_salary || 0,
maxSalary: userData.max_salary || 0,
keywords: [],
excludeKeywords: []
}
},
priority: config.getTaskPriority('auto_deliver') || 6
});
console.log(`[自动投递] 已为设备 ${userData.sn_code} 添加自动投递任务,关键词: ${userData.keyword || '默认'}`);
}
console.log('[自动投递] 任务添加完成');
} catch (error) {
console.error('[自动投递] 执行失败:', error);
}
}
}
module.exports = ScheduledJobs;

View File

@@ -0,0 +1,432 @@
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');
/**
* 任务处理器(简化版)
* 处理各种类型的任务
*/
class TaskHandlers {
constructor(mqttClient) {
this.mqttClient = mqttClient;
}
/**
* 注册任务处理器到任务队列
* @param {object} taskQueue - 任务队列实例
*/
register(taskQueue) {
taskQueue.registerHandler('get_resume', async (task) => {
return await this.handleGetResumeTask(task);
});
taskQueue.registerHandler('get_job_list', async (task) => {
return await this.handleGetJobListTask(task);
});
taskQueue.registerHandler('send_chat', async (task) => {
return await this.handleSendChatTask(task);
});
taskQueue.registerHandler('apply_job', async (task) => {
return await this.handleApplyJobTask(task);
});
taskQueue.registerHandler('auto_deliver', async (task) => {
return await this.handleAutoDeliverTask(task);
});
}
/**
* 处理获取简历任务
*/
async handleGetResumeTask(task) {
const { sn_code } = task;
console.log(`[任务处理器] 获取简历任务 - 设备: ${sn_code}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
const commands = [{
command_type: 'getOnlineResume',
command_name: '获取在线简历',
command_params: JSON.stringify({ sn_code }),
priority: config.getTaskPriority('get_resume')
}];
const result = await command.executeCommands(task.id, commands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
}
/**
* 处理获取岗位列表任务
*/
async handleGetJobListTask(task) {
const { sn_code, taskParams } = task;
const { keyword, platform } = taskParams;
console.log(`[任务处理器] 获取岗位列表任务 - 设备: ${sn_code}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
const commands = [{
command_type: 'getJobList',
command_name: '获取岗位列表',
command_params: JSON.stringify({ sn_code, keyword, platform }),
priority: config.getTaskPriority('search_jobs')
}];
const result = await command.executeCommands(task.id, commands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
}
/**
* 处理发送聊天任务
*/
async handleSendChatTask(task) {
const { sn_code, taskParams } = task;
console.log(`[任务处理器] 发送聊天任务 - 设备: ${sn_code}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
const commands = [{
command_type: 'sendChatMessage',
command_name: '发送聊天消息',
command_params: JSON.stringify(taskParams),
priority: config.getTaskPriority('chat')
}];
const result = await command.executeCommands(task.id, commands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
}
/**
* 处理投递简历任务
*/
async handleApplyJobTask(task) {
const { sn_code, taskParams } = task;
console.log(`[任务处理器] 投递简历任务 - 设备: ${sn_code}`);
deviceManager.recordTaskStart(sn_code, task);
const startTime = Date.now();
try {
const commands = [{
command_type: 'applyJob',
command_name: '投递简历',
command_params: JSON.stringify(taskParams),
priority: config.getTaskPriority('apply')
}];
const result = await command.executeCommands(task.id, commands, this.mqttClient);
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, true, duration);
return result;
} catch (error) {
const duration = Date.now() - startTime;
deviceManager.recordTaskComplete(sn_code, task, false, duration);
throw error;
}
}
/**
* 处理自动投递任务
*/
async handleAutoDeliverTask(task) {
const { sn_code, taskParams } = task;
const { keyword, platform, pageCount, maxCount } = taskParams;
console.log(`[任务处理器] 自动投递任务 - 设备: ${sn_code}, 关键词: ${keyword}`);
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');
// 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();
// 获取职位类型配置
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: (maxCount || 10) * 3 // 获取更多职位用于筛选
});
if (!pendingJobs || pendingJobs.length === 0) {
console.log(`[任务处理器] 没有待投递的职位`);
return {
success: true,
deliveredCount: 0,
message: '没有待投递的职位'
};
}
// 5. 根据简历信息、职位类型配置和权重配置进行评分和过滤
const scoredJobs = [];
const excludeKeywords = jobTypeConfig && jobTypeConfig.excludeKeywords
? (typeof jobTypeConfig.excludeKeywords === 'string'
? JSON.parse(jobTypeConfig.excludeKeywords)
: jobTypeConfig.excludeKeywords)
: [];
// 获取一个月内已投递的公司列表(用于过滤)
const apply_records = db.getModel('apply_records');
const Sequelize = require('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 (Array.isArray(excludeKeywords) && excludeKeywords.length > 0) {
const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase();
const hasExcluded = excludeKeywords.some(kw => jobText.includes(kw.toLowerCase()));
if (hasExcluded) {
continue;
}
}
// 检查该公司是否在一个月内已投递过
if (jobData.companyName && recentCompanyNames.has(jobData.companyName)) {
console.log(`[任务处理器] 跳过一个月内已投递的公司: ${jobData.companyName}`);
continue;
}
// 使用 job_filter_service 计算评分
const scoreResult = jobFilterService.calculateJobScoreWithWeights(
jobData,
resumeInfo,
accountConfig,
jobTypeConfig,
priorityWeights
);
// 只保留总分 >= 60 的职位
if (scoreResult.totalScore >= 60) {
scoredJobs.push({
...jobData,
matchScore: scoreResult.totalScore,
scoreDetails: scoreResult.scores
});
}
}
// 按总分降序排序
scoredJobs.sort((a, b) => b.matchScore - a.matchScore);
// 取前 maxCount 个职位
const jobsToDeliver = scoredJobs.slice(0, maxCount || 10);
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;
}
}
}
module.exports = TaskHandlers;

View File

@@ -0,0 +1,927 @@
const { v4: uuidv4 } = require('uuid');
const Sequelize = require('sequelize');
const logs = require('../logProxy');
const db = require('../dbProxy');
const command = require('./command');
const PriorityQueue = require('./PriorityQueue');
const ErrorHandler = require('./ErrorHandler');
const deviceManager = require('./deviceManager');
/**
* 任务队列管理器(重构版)
* - 使用优先级队列(堆)提升性能
* - 工作池模式:设备内串行执行,设备间并行执行
* - 统一重试机制
* - 统一MQTT管理
*/
class TaskQueue {
constructor(config = {}) {
// 设备任务队列映射 { sn_code: PriorityQueue }
this.deviceQueues = new Map();
// 设备执行状态 { sn_code: { isRunning, currentTask, runningCount } }
this.deviceStatus = new Map();
// 任务处理器映射 { taskType: handler }
this.taskHandlers = new Map();
// 工作池配置
this.config = {
maxConcurrency: config.maxConcurrency || 5, // 全局最大并发数(设备数)
deviceMaxConcurrency: config.deviceMaxConcurrency || 1, // 每个设备最大并发数(保持串行)
...config
};
// 全局运行中的任务数
this.globalRunningCount = 0;
// 全局任务队列(用于跨设备优先级调度,可选)
this.globalQueue = new PriorityQueue();
// 定期扫描定时器
this.scanInterval = null;
}
/**
* 初始化(从数据库恢复未完成的任务)
*/
async init() {
try {
console.log('[任务队列] 初始化中...');
// 从数据库加载pending和running状态的任务
const pendingTasks = await db.getModel('task_status').findAll({
where: {
status: ['pending', 'running']
},
order: [['priority', 'DESC'], ['id', 'ASC']]
});
// 获取所有启用的账号和设备在线状态
const pla_account = db.getModel('pla_account');
const device_status = db.getModel('device_status');
const enabledAccounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
const enabledSnCodes = new Set(enabledAccounts.map(acc => acc.sn_code));
// 检查设备在线状态需要同时满足isOnline = true 且心跳未超时)
const heartbeatTimeout = require('./config.js').monitoring.heartbeatTimeout; // 默认5分钟
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', 'lastHeartbeatTime']
});
const onlineSnCodes = new Set(onlineDevices.map(dev => dev.sn_code));
let restoredCount = 0;
let skippedCount = 0;
for (const taskRecord of pendingTasks) {
const task = taskRecord.toJSON();
const sn_code = task.sn_code;
// 检查账号是否启用
if (!enabledSnCodes.has(sn_code)) {
console.log(`[任务队列] 初始化时跳过任务 ${task.id}:账号 ${sn_code} 未启用`);
// 标记任务为已取消
await db.getModel('task_status').update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({ error: '账号未启用,任务已取消' })
},
{ where: { id: task.id } }
);
skippedCount++;
continue;
}
// 检查设备是否在线
if (!onlineSnCodes.has(sn_code)) {
console.log(`[任务队列] 初始化时跳过任务 ${task.id}:设备 ${sn_code} 不在线`);
// 不在线的任务仍然恢复,等待设备上线后执行
// 不取消任务,只是不立即执行
}
// 初始化设备队列
if (!this.deviceQueues.has(sn_code)) {
this.deviceQueues.set(sn_code, new PriorityQueue());
}
// 初始化设备状态(重要:确保设备状态存在)
if (!this.deviceStatus.has(sn_code)) {
this.deviceStatus.set(sn_code, {
isRunning: false,
currentTask: null,
runningCount: 0
});
}
// 恢复任务对象
const taskObj = {
id: task.id,
sn_code: task.sn_code,
taskType: task.taskType,
taskName: task.taskName,
taskParams: task.taskParams ? JSON.parse(task.taskParams) : {},
priority: task.priority || 5,
maxRetries: task.maxRetries || 3,
retryCount: task.retryCount || 0,
status: 'pending',
createdAt: task.create_time ? new Date(task.create_time).getTime() : Date.now()
};
// 添加到设备队列
this.deviceQueues.get(sn_code).push(taskObj);
restoredCount++;
// 如果状态是running重置为pending
if (task.status === 'running') {
await db.getModel('task_status').update(
{ status: 'pending' },
{ where: { id: task.id } }
);
}
}
// 恢复任务后,尝试执行所有设备的队列(只执行在线且启用的设备)
for (const sn_code of this.deviceQueues.keys()) {
// 只处理启用且在线的设备
if (enabledSnCodes.has(sn_code) && onlineSnCodes.has(sn_code)) {
this.processQueue(sn_code).catch(error => {
console.error(`[任务队列] 初始化后执行队列失败 (设备: ${sn_code}):`, error);
});
} else {
console.log(`[任务队列] 初始化时跳过设备 ${sn_code} 的队列执行:${!enabledSnCodes.has(sn_code) ? '账号未启用' : '设备不在线'}`);
}
}
console.log(`[任务队列] 初始化完成,恢复 ${restoredCount} 个任务,跳过 ${skippedCount} 个未启用账号的任务`);
// 启动定期扫描机制每10秒扫描一次
this.startQueueScanner();
} catch (error) {
console.error('[任务队列] 初始化失败:', error);
throw error;
}
}
/**
* 启动队列扫描器(定期检查并执行队列中的任务)
*/
startQueueScanner() {
// 如果已经启动,先清除
if (this.scanInterval) {
clearInterval(this.scanInterval);
}
// 每10秒扫描一次所有设备的队列
this.scanInterval = setInterval(() => {
this.scanAndProcessQueues();
}, 10000); // 10秒扫描一次
console.log('[任务队列] 队列扫描器已启动每10秒扫描一次');
}
/**
* 停止队列扫描器
*/
stopQueueScanner() {
if (this.scanInterval) {
clearInterval(this.scanInterval);
this.scanInterval = null;
console.log('[任务队列] 队列扫描器已停止');
}
}
/**
* 扫描所有设备的队列并尝试执行任务(过滤未启用的账号)
*/
async scanAndProcessQueues() {
try {
const deviceCount = this.deviceQueues.size;
if (deviceCount === 0) {
return;
}
// 获取所有启用的账号对应的设备SN码
const pla_account = db.getModel('pla_account');
const enabledAccounts = await pla_account.findAll({
where: {
is_delete: 0,
is_enabled: 1
},
attributes: ['sn_code']
});
const enabledSnCodes = new Set(enabledAccounts.map(acc => acc.sn_code));
let processedCount = 0;
let queuedCount = 0;
let skippedCount = 0;
// 遍历所有设备的队列,只处理启用账号的设备
for (const [sn_code, queue] of this.deviceQueues.entries()) {
// 跳过未启用的账号
if (!enabledSnCodes.has(sn_code)) {
skippedCount++;
continue;
}
const queueSize = queue.size();
if (queueSize > 0) {
queuedCount += queueSize;
// 尝试处理该设备的队列
this.processQueue(sn_code).catch(error => {
console.error(`[任务队列] 扫描执行队列失败 (设备: ${sn_code}):`, error);
});
processedCount++;
}
}
if (queuedCount > 0) {
console.log(`[任务队列] 扫描完成: ${processedCount} 个设备有任务,共 ${queuedCount} 个待执行任务`);
}
} catch (error) {
console.error('[任务队列] 扫描队列失败:', error);
}
}
/**
* 注册任务处理器
* @param {string} taskType - 任务类型
* @param {function} handler - 处理函数
*/
registerHandler(taskType, handler) {
this.taskHandlers.set(taskType, handler);
}
/**
* 查找设备是否已有相同类型的任务
* @param {string} sn_code - 设备SN码
* @param {string} taskType - 任务类型
* @returns {object|null} 现有任务或null
*/
findExistingTask(sn_code, taskType) {
// 检查当前正在执行的任务
const deviceStatus = this.deviceStatus.get(sn_code);
if (deviceStatus && deviceStatus.currentTask && deviceStatus.currentTask.taskType === taskType) {
return deviceStatus.currentTask;
}
// 检查队列中等待的任务
const queue = this.deviceQueues.get(sn_code);
if (queue) {
const existingTask = queue.find(task => task.taskType === taskType && task.status === 'pending');
if (existingTask) {
return existingTask;
}
}
return null;
}
/**
* 检查账号是否启用
* @param {string} sn_code - 设备SN码
* @returns {Promise<boolean>} 是否启用
*/
async checkAccountEnabled(sn_code) {
try {
const pla_account = db.getModel('pla_account');
const account = await pla_account.findOne({
where: {
sn_code: sn_code,
is_delete: 0
},
attributes: ['is_enabled']
});
if (!account) {
console.warn(`[任务队列] 设备 ${sn_code} 对应的账号不存在`);
return false;
}
const isEnabled = Boolean(account.is_enabled);
if (!isEnabled) {
console.log(`[任务队列] 设备 ${sn_code} 对应的账号未启用,跳过任务`);
}
return isEnabled;
} catch (error) {
console.error(`[任务队列] 检查账号启用状态失败:`, error);
return false;
}
}
/**
* 添加任务到队列
* @param {string} sn_code - 设备SN码
* @param {object} taskConfig - 任务配置
* @returns {Promise<string>} 任务ID
*/
async addTask(sn_code, taskConfig) {
// 检查账号是否启用
const isEnabled = await this.checkAccountEnabled(sn_code);
if (!isEnabled) {
throw new Error(`账号未启用,无法添加任务`);
}
// 检查是否已有相同类型的任务在队列中或正在执行
const existingTask = this.findExistingTask(sn_code, taskConfig.taskType);
if (existingTask) {
console.log(`[任务队列] 设备 ${sn_code} 已有 ${taskConfig.taskType} 任务在执行或等待中,跳过添加`);
return existingTask.id;
}
const task = {
sn_code,
taskType: taskConfig.taskType,
taskName: taskConfig.taskName || taskConfig.taskType,
taskParams: taskConfig.taskParams || {},
priority: taskConfig.priority || 5,
maxRetries: taskConfig.maxRetries || 3,
retryCount: 0,
status: 'pending',
createdAt: Date.now()
};
// 初始化设备队列
if (!this.deviceQueues.has(sn_code)) {
this.deviceQueues.set(sn_code, new PriorityQueue());
}
// 初始化设备状态
if (!this.deviceStatus.has(sn_code)) {
this.deviceStatus.set(sn_code, {
isRunning: false,
currentTask: null,
runningCount: 0
});
}
// 保存到数据库
let res = await db.getModel('task_status').create({
sn_code: task.sn_code,
taskType: task.taskType,
taskName: task.taskName,
taskParams: JSON.stringify(task.taskParams),
status: task.status,
priority: task.priority,
maxRetries: task.maxRetries,
retryCount: task.retryCount,
});
// 使用数据库返回的自增ID
task.id = res.id;
// 添加到优先级队列
const queue = this.deviceQueues.get(sn_code);
queue.push(task);
console.log(`[任务队列] 任务已添加到队列: ${task.taskName} (ID: ${task.id}, 优先级: ${task.priority}),等待扫描机制执行`);
// 不立即执行,等待扫描机制自动执行
// 扫描机制会定期检查队列并执行任务
return res.id;
}
/**
* 处理设备的任务队列(工作池模式)
* 设备内串行执行,设备间并行执行
* @param {string} sn_code - 设备SN码
*/
async processQueue(sn_code) {
try {
// 先检查账号是否启用
const isEnabled = await this.checkAccountEnabled(sn_code);
if (!isEnabled) {
// 如果账号未启用,从队列中移除所有待执行任务
const queue = this.deviceQueues.get(sn_code);
if (queue && queue.size() > 0) {
console.log(`[任务队列] 设备 ${sn_code} 账号未启用,清空队列中的 ${queue.size()} 个待执行任务`);
// 标记所有待执行任务为已取消
const queueArray = queue.toArray();
for (const task of queueArray) {
if (task.status === 'pending') {
try {
await db.getModel('task_status').update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({ error: '账号未启用,任务已取消' })
},
{ where: { id: task.id } }
);
} catch (error) {
console.error(`[任务队列] 更新任务状态失败:`, error);
}
}
}
queue.clear();
}
return;
}
const status = this.deviceStatus.get(sn_code);
if (!status) {
console.warn(`[任务队列] 设备 ${sn_code} 状态不存在,无法执行任务`);
return;
}
// 检查设备是否正在执行任务(设备内串行)
if (status.isRunning || status.runningCount >= this.config.deviceMaxConcurrency) {
console.log(`[任务队列] 设备 ${sn_code} 正在执行任务,等待中... (isRunning: ${status.isRunning}, runningCount: ${status.runningCount})`);
return;
}
// 检查全局并发限制(设备间并行控制)
if (this.globalRunningCount >= this.config.maxConcurrency) {
console.log(`[任务队列] 全局并发数已达上限 (${this.globalRunningCount}/${this.config.maxConcurrency}),等待中...`);
return;
}
const queue = this.deviceQueues.get(sn_code);
if (!queue || queue.isEmpty()) {
console.log(`[任务队列] 设备 ${sn_code} 队列为空,无任务可执行`);
return;
}
// 从优先级队列取出任务
const task = queue.pop();
if (!task) {
console.warn(`[任务队列] 设备 ${sn_code} 队列非空但无法取出任务`);
return;
}
console.log(`[任务队列] 开始执行任务: ${task.taskName} (ID: ${task.id}, 设备: ${sn_code})`);
// 更新状态
status.isRunning = true;
status.currentTask = task;
status.runningCount++;
this.globalRunningCount++;
// 异步执行任务(不阻塞)
this.executeTask(task).finally(() => {
// 任务完成后更新状态
status.isRunning = false;
status.currentTask = null;
status.runningCount--;
this.globalRunningCount--;
console.log(`[任务队列] 任务完成,设备 ${sn_code} 状态已重置,准备处理下一个任务`);
// 继续处理队列中的下一个任务(延迟一小段时间,确保状态已更新)
setTimeout(() => {
this.processQueue(sn_code).catch(error => {
console.error(`[任务队列] processQueue 执行失败 (设备: ${sn_code}):`, error);
});
}, 100); // 延迟100ms确保状态已更新
});
} catch (error) {
console.error(`[任务队列] processQueue 处理失败 (设备: ${sn_code}):`, error);
throw error;
}
}
/**
* 执行任务(统一重试机制)
* @param {object} task - 任务对象
*/
async executeTask(task) {
const startTime = Date.now();
task.status = 'running';
task.startTime = new Date();
try {
// 执行前再次检查账号是否启用(双重保险)
const isEnabled = await this.checkAccountEnabled(task.sn_code);
if (!isEnabled) {
// 更新任务状态为已取消
await db.getModel('task_status').update(
{
status: 'cancelled',
endTime: new Date(),
result: JSON.stringify({ error: '账号未启用,任务已取消' })
},
{ where: { id: task.id } }
);
throw new Error(`账号未启用,任务已取消`);
}
// 更新数据库状态
await db.getModel('task_status').update(
{
status: 'running',
startTime: task.startTime
},
{ where: { id: task.id } }
);
// 使用注册的任务处理器执行任务
const handler = this.taskHandlers.get(task.taskType);
if (!handler) {
throw new Error(`未找到任务类型 ${task.taskType} 的处理器,请先注册处理器`);
}
// 执行任务处理器
const result = await handler(task);
// 任务成功
task.status = 'completed';
task.endTime = new Date();
task.duration = Date.now() - startTime;
task.result = result;
// 更新数据库
await db.getModel('task_status').update(
{
status: 'completed',
endTime: task.endTime,
duration: task.duration,
result: JSON.stringify(task.result),
progress: 100
},
{ where: { id: task.id } }
);
console.log(`[任务队列] 设备 ${task.sn_code} 任务执行成功: ${task.taskName} (耗时: ${task.duration}ms)`);
} catch (error) {
// 使用统一错误处理
const errorInfo = await ErrorHandler.handleError(error, {
taskId: task.id,
sn_code: task.sn_code,
taskType: task.taskType,
taskName: task.taskName
});
// 直接标记为失败(重试已禁用)
task.status = 'failed';
task.endTime = new Date();
task.duration = Date.now() - startTime;
task.errorMessage = errorInfo.message || error.message || '未知错误';
task.errorStack = errorInfo.stack || error.stack || '';
console.error(`[任务队列] 任务执行失败: ${task.taskName} (ID: ${task.id}), 错误: ${task.errorMessage}`, {
errorStack: task.errorStack,
taskId: task.id,
sn_code: task.sn_code,
taskType: task.taskType
});
}
}
/**
* 取消任务
* @param {string} taskId - 任务ID
* @returns {Promise<boolean>} 是否成功取消
*/
async cancelTask(taskId) {
// 遍历所有设备队列查找任务
for (const [sn_code, queue] of this.deviceQueues.entries()) {
const removed = queue.remove(task => task.id === taskId);
if (removed) {
// 检查是否正在执行
const status = this.deviceStatus.get(sn_code);
if (status && status.currentTask && status.currentTask.id === taskId) {
// 正在执行的任务无法取消,只能标记
console.warn(`[任务队列] 任务 ${taskId} 正在执行,无法取消`);
return false;
}
// 更新数据库
await db.getModel('task_status').update(
{
status: 'cancelled',
endTime: new Date()
},
{ where: { id: taskId } }
);
console.log(`[任务队列] 任务已取消: ${taskId}`);
return true;
}
}
// 未找到可取消的任务
return false;
}
/**
* 取消设备的所有待执行任务
* @param {string} sn_code - 设备SN码
* @returns {Promise<number>} 取消的任务数量
*/
async cancelDeviceTasks(sn_code) {
let cancelledCount = 0;
// 1. 从队列中移除所有待执行任务
const queue = this.deviceQueues.get(sn_code);
if (queue) {
const pendingTasks = [];
// 获取所有待执行任务(不包括正在执行的)
const status = this.deviceStatus.get(sn_code);
const currentTaskId = status && status.currentTask ? status.currentTask.id : null;
// 遍历队列,收集待取消的任务
const queueArray = queue.toArray();
for (const task of queueArray) {
if (task.id !== currentTaskId && (task.status === 'pending' || !task.status)) {
pendingTasks.push(task);
}
}
// 从队列中移除这些任务
for (const task of pendingTasks) {
queue.remove(t => t.id === task.id);
cancelledCount++;
}
}
// 2. 更新数据库中的任务状态
try {
const taskStatusModel = db.getModel('task_status');
const status = this.deviceStatus.get(sn_code);
const currentTaskId = status && status.currentTask ? status.currentTask.id : null;
// 更新所有待执行或运行中的任务(除了当前正在执行的)
const whereCondition = {
sn_code: sn_code,
status: ['pending', 'running']
};
if (currentTaskId) {
whereCondition.id = { [Sequelize.Op.ne]: currentTaskId };
}
const updateResult = await taskStatusModel.update(
{
status: 'cancelled',
endTime: new Date()
},
{ where: whereCondition }
);
const dbCancelledCount = Array.isArray(updateResult) ? updateResult[0] : updateResult;
console.log(`[任务队列] 设备 ${sn_code} 已取消 ${cancelledCount} 个队列任务,${dbCancelledCount} 个数据库任务`);
} catch (error) {
console.error(`[任务队列] 更新数据库任务状态失败:`, error, {
sn_code: sn_code,
cancelledCount: cancelledCount
});
}
return cancelledCount;
}
/**
* 获取设备队列状态
* @param {string} sn_code - 设备SN码
* @returns {object} 队列状态
*/
getDeviceStatus(sn_code) {
const queue = this.deviceQueues.get(sn_code);
const status = this.deviceStatus.get(sn_code) || {
isRunning: false,
currentTask: null,
runningCount: 0
};
return {
sn_code,
isRunning: status.isRunning,
currentTask: status.currentTask,
queueLength: queue ? queue.size() : 0,
pendingTasks: queue ? queue.size() : 0,
runningCount: status.runningCount
};
}
/**
* 获取任务状态
* @param {string} taskId - 任务ID
* @returns {Promise<object|null>} 任务对象
*/
async getTaskStatus(taskId) {
// 先从内存中查找
for (const queue of this.deviceQueues.values()) {
const task = queue.find(t => t.id === taskId);
if (task) {
return task;
}
}
// 从正在执行的任务中查找
for (const status of this.deviceStatus.values()) {
if (status.currentTask && status.currentTask.id === taskId) {
return status.currentTask;
}
}
// 从数据库中查找
try {
const taskRecord = await db.getModel('task_status').findOne({
where: { id: taskId }
});
if (taskRecord) {
return taskRecord.toJSON();
}
} catch (error) {
console.error(`[任务队列] 查询任务状态失败:`, error, {
taskId: taskId
});
}
return null;
}
/**
* 清空设备队列
* @param {string} sn_code - 设备SN码
*/
clearQueue(sn_code) {
if (this.deviceQueues.has(sn_code)) {
const queue = this.deviceQueues.get(sn_code);
const count = queue.size();
queue.clear();
console.log(`[任务队列] 已清空设备 ${sn_code} 的队列,共移除 ${count} 个任务`);
}
}
/**
* 删除所有任务(从内存队列和数据库)
* @returns {Promise<object>} 删除结果
*/
async deleteAllTaskFromDatabase() {
try {
console.log('[任务队列] 开始删除所有任务...');
let totalQueued = 0;
let totalRunning = 0;
// 1. 清空所有设备的内存队列
for (const [sn_code, queue] of this.deviceQueues.entries()) {
const queueSize = queue.size();
totalQueued += queueSize;
queue.clear();
// 重置设备状态(但保留正在执行的任务信息,稍后处理)
const status = this.deviceStatus.get(sn_code);
if (status && status.currentTask) {
totalRunning++;
// 标记正在执行的任务,但不立即取消(让它们自然完成或失败)
console.warn(`[任务队列] 设备 ${sn_code} 有正在执行的任务,将在完成后清理`);
}
}
// 2. 使用 MCP MySQL 删除所有关联数据(先删除关联表,再删除主表)
// 注意MCP MySQL 是只读的,这里使用 Sequelize 执行删除操作
// 但移除数据库层面的外键关联,避免约束问题
const taskCommandsModel = db.getModel('task_commands');
const chatRecordsModel = db.getModel('chat_records');
const applyRecordsModel = db.getModel('apply_records');
const taskStatusModel = db.getModel('task_status');
// 删除任务指令记录(所有记录)
const commandsDeleted = await taskCommandsModel.destroy({
where: {},
truncate: false
});
console.log(`[任务队列] 已删除任务指令记录: ${commandsDeleted}`);
// 删除聊天记录中关联的任务记录(删除所有有 taskId 且不为空的记录)
const chatRecordsDeleted = await chatRecordsModel.destroy({
where: {
[Sequelize.Op.and]: [
{ taskId: { [Sequelize.Op.ne]: null } },
{ taskId: { [Sequelize.Op.ne]: '' } }
]
},
truncate: false
});
console.log(`[任务队列] 已删除聊天记录: ${chatRecordsDeleted}`);
// 删除投递记录中关联的任务记录(删除所有有 taskId 且不为空的记录)
const applyRecordsDeleted = await applyRecordsModel.destroy({
where: {
[Sequelize.Op.and]: [
{ taskId: { [Sequelize.Op.ne]: null } },
{ taskId: { [Sequelize.Op.ne]: '' } }
]
},
truncate: false
});
console.log(`[任务队列] 已删除投递记录: ${applyRecordsDeleted}`);
// 3. 删除数据库中的所有任务记录
const deleteResult = await taskStatusModel.destroy({
where: {},
truncate: false // 使用 DELETE 而不是 TRUNCATE保留表结构
});
console.log(`[任务队列] 已删除所有任务:`);
console.log(` - 内存队列任务: ${totalQueued}`);
console.log(` - 正在执行任务: ${totalRunning} 个(将在完成后清理)`);
console.log(` - 任务指令记录: ${commandsDeleted}`);
console.log(` - 聊天记录: ${chatRecordsDeleted}`);
console.log(` - 投递记录: ${applyRecordsDeleted}`);
console.log(` - 数据库任务记录: ${deleteResult}`);
return {
success: true,
memoryQueued: totalQueued,
memoryRunning: totalRunning,
commandsDeleted: commandsDeleted,
chatRecordsDeleted: chatRecordsDeleted,
applyRecordsDeleted: applyRecordsDeleted,
databaseDeleted: deleteResult,
message: `已删除所有任务及关联数据(任务: ${deleteResult} 条,指令: ${commandsDeleted} 条,聊天: ${chatRecordsDeleted} 条,投递: ${applyRecordsDeleted} 条)`
};
} catch (error) {
console.error('[任务队列] 删除所有任务失败:', error);
throw error;
}
}
/**
* 获取所有设备的队列状态
* @returns {array} 所有设备的队列状态
*/
getAllDeviceStatus() {
const allStatus = [];
for (const sn_code of this.deviceQueues.keys()) {
allStatus.push(this.getDeviceStatus(sn_code));
}
return allStatus;
}
/**
* 获取全局统计信息
* @returns {object} 统计信息
*/
getStatistics() {
let totalQueued = 0;
for (const queue of this.deviceQueues.values()) {
totalQueued += queue.size();
}
return {
globalRunningCount: this.globalRunningCount,
maxConcurrency: this.config.maxConcurrency,
totalDevices: this.deviceQueues.size,
totalQueuedTasks: totalQueued,
deviceStatuses: this.getAllDeviceStatus()
};
}
/**
* 获取MQTT客户端统一管理
* @returns {Promise<object|null>} MQTT客户端实例
*/
async getMqttClient() {
try {
// 首先尝试从调度系统获取已初始化的MQTT客户端
const scheduleManager = require('./index');
if (scheduleManager.mqttClient) {
return scheduleManager.mqttClient;
}
// 如果调度系统没有初始化,则直接创建
const mqttManager = require('../mqtt/mqttManager');
console.log('[任务队列] 创建新的MQTT客户端');
return await mqttManager.getInstance();
} catch (error) {
console.error(`[任务队列] 获取MQTT客户端失败:`, error);
return null;
}
}
}
// 导出单例
const taskQueue = new TaskQueue();
module.exports = taskQueue;

View File

@@ -0,0 +1,265 @@
const dayjs = require('dayjs');
/**
* 调度系统工具函数
* 提供通用的辅助功能
*/
class ScheduleUtils {
/**
* 生成唯一任务ID
* @returns {string} 任务ID
*/
static generateTaskId() {
return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 生成唯一指令ID
* @returns {string} 指令ID
*/
static generateCommandId() {
return `cmd_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 格式化时间戳
* @param {number} timestamp - 时间戳
* @returns {string} 格式化的时间
*/
static formatTimestamp(timestamp) {
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss');
}
/**
* 格式化持续时间
* @param {number} ms - 毫秒数
* @returns {string} 格式化的持续时间
*/
static formatDuration(ms) {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60 * 1000) {
return `${(ms / 1000).toFixed(1)}s`;
} else if (ms < 60 * 60 * 1000) {
return `${(ms / (60 * 1000)).toFixed(1)}min`;
} else {
return `${(ms / (60 * 60 * 1000)).toFixed(1)}h`;
}
}
/**
* 深度克隆对象
* @param {object} obj - 要克隆的对象
* @returns {object} 克隆后的对象
*/
static deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map(item => this.deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = this.deepClone(obj[key]);
}
}
return cloned;
}
/**
* 安全解析JSON
* @param {string} jsonString - JSON字符串
* @param {any} defaultValue - 默认值
* @returns {any} 解析结果或默认值
*/
static safeJsonParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
return defaultValue;
}
}
/**
* 安全序列化JSON
* @param {any} obj - 要序列化的对象
* @param {string} defaultValue - 默认值
* @returns {string} JSON字符串或默认值
*/
static safeJsonStringify(obj, defaultValue = '{}') {
try {
return JSON.stringify(obj);
} catch (error) {
return defaultValue;
}
}
/**
* 延迟执行
* @param {number} ms - 延迟毫秒数
* @returns {Promise} Promise对象
*/
static delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 重试执行函数
* @param {function} fn - 要执行的函数
* @param {number} maxRetries - 最大重试次数
* @param {number} delay - 重试延迟(毫秒)
* @returns {Promise} 执行结果
*/
static async retry(fn, maxRetries = 3, delay = 1000) {
let lastError;
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i < maxRetries) {
console.log(`[工具函数] 执行失败,${delay}ms后重试 (${i + 1}/${maxRetries + 1}):`, error.message);
await this.delay(delay);
}
}
}
throw lastError;
}
/**
* 限制并发执行数量
* @param {Array} tasks - 任务数组
* @param {number} concurrency - 并发数量
* @returns {Promise<Array>} 执行结果数组
*/
static async limitConcurrency(tasks, concurrency = 5) {
const results = [];
const executing = [];
for (const task of tasks) {
const promise = Promise.resolve(task()).then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});
results.push(promise);
if (tasks.length >= concurrency) {
executing.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
/**
* 创建带超时的Promise
* @param {Promise} promise - 原始Promise
* @param {number} timeout - 超时时间(毫秒)
* @param {string} timeoutMessage - 超时错误消息
* @returns {Promise} 带超时的Promise
*/
static withTimeout(promise, timeout, timeoutMessage = 'Operation timed out') {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => reject(new Error(timeoutMessage)), timeout);
})
]);
}
/**
* 获取今天的日期字符串
* @returns {string} 日期字符串 YYYY-MM-DD
*/
static getTodayString() {
return dayjs().format('YYYY-MM-DD');
}
/**
* 检查日期是否为今天
* @param {string|Date} date - 日期
* @returns {boolean} 是否为今天
*/
static isToday(date) {
return dayjs(date).format('YYYY-MM-DD') === this.getTodayString();
}
/**
* 获取随机延迟时间
* @param {number} min - 最小延迟(毫秒)
* @param {number} max - 最大延迟(毫秒)
* @returns {number} 随机延迟时间
*/
static getRandomDelay(min = 1000, max = 5000) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* 格式化文件大小
* @param {number} bytes - 字节数
* @returns {string} 格式化的文件大小
*/
static formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* 计算成功率
* @param {number} success - 成功次数
* @param {number} total - 总次数
* @returns {string} 百分比字符串
*/
static calculateSuccessRate(success, total) {
if (total === 0) return '0%';
return ((success / total) * 100).toFixed(2) + '%';
}
/**
* 验证设备SN码格式
* @param {string} sn_code - 设备SN码
* @returns {boolean} 是否有效
*/
static isValidSnCode(sn_code) {
return typeof sn_code === 'string' && sn_code.length > 0 && sn_code.length <= 50;
}
/**
* 清理对象中的空值
* @param {object} obj - 要清理的对象
* @returns {object} 清理后的对象
*/
static cleanObject(obj) {
const cleaned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] !== null && obj[key] !== undefined && obj[key] !== '') {
cleaned[key] = obj[key];
}
}
return cleaned;
}
}
module.exports = ScheduleUtils;

View File

@@ -0,0 +1,12 @@
const { querySql, user, op } = require("./baseModel");
class SqlUpdate {
async init() {
}
}
const sqlUpdate = new SqlUpdate();
module.exports = sqlUpdate;