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

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();