Files
autoAiWorkSys/api/middleware/job/resumeManager.js
张成 7ee92b8905 1
2025-12-25 22:11:34 +08:00

693 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 分析简历并更新 AI 字段
try {
await this.analyze_resume_with_ai(resumeId, resumeInfo);
console.log(`[简历管理] AI 分析完成并已更新到数据库`);
} catch (error) {
console.error(`[简历管理] AI 分析失败:`, error);
// AI 分析失败不影响主流程,继续返回成功
}
return { resumeId, message: existingResume ? '简历更新成功' : '简历创建成功' };
}
/**
* 构建用于 AI 分析的简历文本
* @param {object} resumeInfo - 简历信息对象
* @returns {string} 简历文本内容
*/
build_resume_text_for_ai(resumeInfo) {
const parts = [];
// 基本信息
if (resumeInfo.fullName) parts.push(`姓名:${resumeInfo.fullName}`);
if (resumeInfo.gender) parts.push(`性别:${resumeInfo.gender}`);
if (resumeInfo.age) parts.push(`年龄:${resumeInfo.age}`);
if (resumeInfo.location) parts.push(`所在地:${resumeInfo.location}`);
// 教育背景
if (resumeInfo.education) parts.push(`学历:${resumeInfo.education}`);
if (resumeInfo.school) parts.push(`毕业院校:${resumeInfo.school}`);
if (resumeInfo.major) parts.push(`专业:${resumeInfo.major}`);
if (resumeInfo.graduationYear) parts.push(`毕业年份:${resumeInfo.graduationYear}`);
// 工作经验
if (resumeInfo.workYears) parts.push(`工作年限:${resumeInfo.workYears}`);
if (resumeInfo.currentPosition) parts.push(`当前职位:${resumeInfo.currentPosition}`);
if (resumeInfo.currentCompany) parts.push(`当前公司:${resumeInfo.currentCompany}`);
// 期望信息
if (resumeInfo.expectedPosition) parts.push(`期望职位:${resumeInfo.expectedPosition}`);
if (resumeInfo.expectedSalary) parts.push(`期望薪资:${resumeInfo.expectedSalary}`);
if (resumeInfo.expectedLocation) parts.push(`期望地点:${resumeInfo.expectedLocation}`);
// 技能描述
if (resumeInfo.skillDescription) parts.push(`技能描述:${resumeInfo.skillDescription}`);
// 工作经历
if (resumeInfo.workExperience) {
try {
const workExp = JSON.parse(resumeInfo.workExperience);
if (Array.isArray(workExp) && workExp.length > 0) {
parts.push('\n工作经历');
workExp.forEach(work => {
const workText = [
work.company && `公司:${work.company}`,
work.position && `职位:${work.position}`,
work.startDate && work.endDate && `时间:${work.startDate} - ${work.endDate}`,
work.content && `工作内容:${work.content}`
].filter(Boolean).join('');
if (workText) parts.push(workText);
});
}
} catch (e) {
// 解析失败,忽略
}
}
// 项目经验
if (resumeInfo.projectExperience) {
try {
const projectExp = JSON.parse(resumeInfo.projectExperience);
if (Array.isArray(projectExp) && projectExp.length > 0) {
parts.push('\n项目经验');
projectExp.forEach(project => {
const projectText = [
project.name && `项目名称:${project.name}`,
project.role && `角色:${project.role}`,
project.description && `描述:${project.description}`
].filter(Boolean).join('');
if (projectText) parts.push(projectText);
});
}
} catch (e) {
// 解析失败,忽略
}
}
// 简历完整内容
if (resumeInfo.resumeContent) {
parts.push('\n简历详细内容');
parts.push(resumeInfo.resumeContent);
}
return parts.join('\n');
}
/**
* 从描述中提取技能标签
* @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);
// 确保所有字段都有值
const updateData = {
aiSkillTags: JSON.stringify(analysis.skillTags || []),
aiStrengths: analysis.strengths || '',
aiWeaknesses: analysis.weaknesses || '',
aiCareerSuggestion: analysis.careerSuggestion || '',
aiCompetitiveness: parseInt(analysis.competitiveness || 70, 10)
};
// 确保竞争力评分在 0-100 范围内
if (updateData.aiCompetitiveness < 0) updateData.aiCompetitiveness = 0;
if (updateData.aiCompetitiveness > 100) updateData.aiCompetitiveness = 100;
// 更新简历的AI分析字段
await resume_info.update(updateData, { where: { resumeId: resumeId } });
console.log(`[简历管理] AI分析完成 - 竞争力评分: ${updateData.aiCompetitiveness}, 技能标签: ${updateData.aiSkillTags}`);
return analysis;
} catch (error) {
console.error(`[简历管理] AI分析失败:`, error, {
resumeId: resumeId,
fullName: resumeInfo.fullName
});
// 如果AI分析失败使用基于规则的默认分析并保存到数据库
const defaultAnalysis = this.get_default_analysis(resumeInfo);
// 保存默认分析结果到数据库
const updateData = {
aiSkillTags: JSON.stringify(defaultAnalysis.skillTags || []),
aiStrengths: defaultAnalysis.strengths || '',
aiWeaknesses: defaultAnalysis.weaknesses || '',
aiCareerSuggestion: defaultAnalysis.careerSuggestion || '',
aiCompetitiveness: parseInt(defaultAnalysis.competitiveness || 70, 10)
};
// 确保竞争力评分在 0-100 范围内
if (updateData.aiCompetitiveness < 0) updateData.aiCompetitiveness = 0;
if (updateData.aiCompetitiveness > 100) updateData.aiCompetitiveness = 100;
await resume_info.update(updateData, { where: { resumeId: resumeId } });
console.log(`[简历管理] 使用默认分析结果 - 竞争力评分: ${updateData.aiCompetitiveness}`);
return defaultAnalysis;
}
}
/**
* 解析AI分析结果
* @param {object} aiResponse - AI响应对象
* @param {object} resumeInfo - 简历信息
* @returns {object} 解析后的分析结果
*/
parse_ai_analysis(aiResponse, resumeInfo) {
try {
// aiService.analyzeResume 返回格式: { analysis: {...} } 或 { analysis: { content: "...", parseError: true } }
const analysis = aiResponse.analysis;
// 如果解析失败analysis 会有 parseError 标记
if (analysis && analysis.parseError) {
console.warn(`[简历管理] AI分析结果解析失败使用默认分析`);
return this.get_default_analysis(resumeInfo);
}
// 如果解析成功analysis 直接是解析后的对象
if (analysis && typeof analysis === 'object' && !analysis.parseError) {
return {
skillTags: analysis.skillTags || analysis.技能标签 || [],
strengths: analysis.strengths || analysis.优势 || analysis.优势分析 || '',
weaknesses: analysis.weaknesses || analysis.劣势 || analysis.劣势分析 || '',
careerSuggestion: analysis.careerSuggestion || analysis.职业建议 || '',
competitiveness: parseInt(analysis.competitiveness || analysis.竞争力评分 || 70, 10)
};
}
// 如果 analysis 是字符串,尝试解析
const content = analysis?.content || analysis || '';
if (typeof content === 'string' && 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: parseInt(parsed.competitiveness || parsed.竞争力评分 || 70, 10)
};
}
}
// 如果无法解析JSON尝试从文本中提取信息
if (typeof content === 'string') {
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], 10) : 70
};
}
// 如果所有解析都失败,使用默认分析
console.warn(`[简历管理] 无法解析AI分析结果使用默认分析`);
return this.get_default_analysis(resumeInfo);
} 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();