555 lines
21 KiB
JavaScript
555 lines
21 KiB
JavaScript
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();
|