This commit is contained in:
张成
2025-12-25 22:11:34 +08:00
parent c6c78d0c43
commit 7ee92b8905
26 changed files with 27282 additions and 1706 deletions

View File

@@ -104,10 +104,8 @@ module.exports = {
});
return ctx.success({
count: result.count,
total: result.count,
rows: result.rows,
list: result.rows
count: result.count
});
},

View File

@@ -150,8 +150,8 @@ module.exports = {
});
return ctx.success({
total: result.count,
list: list
rows: list,
count: result.count
});
},

View File

@@ -686,6 +686,86 @@ module.exports = {
end_date: end_date
}
});
},
/**
* @swagger
* /admin_api/pla_account/parse-resume:
* post:
* summary: 解析账号在线简历
* description: 获取指定账号的在线简历并进行AI分析返回简历详情
* tags: [后台-账号管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - id
* properties:
* id:
* type: integer
* description: 账号ID
* responses:
* 200:
* description: 解析成功返回简历ID
*/
'POST /pla_account/parse-resume': async (ctx) => {
const { id } = ctx.getBody();
const models = await Framework.getModels();
const { pla_account } = models;
const mqttClient = require('../middleware/mqtt/mqttClient');
const resumeManager = require('../middleware/job/resumeManager');
if (!id) {
return ctx.fail('账号ID不能为空');
}
// 获取账号信息
const account = await pla_account.findOne({ where: { id } });
if (!account) {
return ctx.fail('账号不存在');
}
const { sn_code, platform_type } = account;
if (!sn_code) {
return ctx.fail('该账号未绑定设备');
}
try {
// 调用简历管理器获取并保存简历
const resumeData = await resumeManager.get_online_resume(sn_code, mqttClient, {
platform: platform_type || 'boss'
});
// 从返回数据中获取 resumeId
// resumeManager 已经保存了简历到数据库,我们需要查询获取 resumeId
const resume_info = models.resume_info;
const savedResume = await resume_info.findOne({
where: {
sn_code,
platform: platform_type || 'boss',
isActive: true
},
order: [['syncTime', 'DESC']]
});
if (!savedResume) {
return ctx.fail('简历解析失败:未找到保存的简历记录');
}
return ctx.success({
message: '简历解析成功',
resumeId: savedResume.resumeId,
data: resumeData
});
} catch (error) {
console.error('[解析简历] 失败:', error);
return ctx.fail('解析简历失败: ' + (error.message || '未知错误'));
}
}
};

View File

@@ -69,8 +69,8 @@ const result = await resume_info.findAndCountAll({
});
return ctx.success({
total: result.count,
list: result.rows
rows: result.rows,
count: result.count
});
},
@@ -153,24 +153,45 @@ return ctx.success({
* 200:
* description: 获取成功
*/
'GET /resume/detail': async (ctx) => {
'POST /resume/detail': async (ctx) => {
const models = Framework.getModels();
const { resume_info } = models;
const { resumeId } = ctx.query;
const { resumeId } = ctx.getBody();
if (!resumeId) {
return ctx.fail('简历ID不能为空');
}
const resume = await resume_info.findOne({ where: { resumeId } });
const resume = await resume_info.findOne({ where: { resumeId } });
if (!resume) {
return ctx.fail('简历不存在');
}
if (!resume) {
return ctx.fail('简历不存在');
}
return ctx.success(resume);
// 解析 JSON 字段
const resumeDetail = resume.toJSON();
const jsonFields = ['skills', 'certifications', 'projectExperience', 'workExperience', 'aiSkillTags'];
jsonFields.forEach(field => {
if (resumeDetail[field]) {
try {
resumeDetail[field] = JSON.parse(resumeDetail[field]);
} catch (e) {
console.error(`解析字段 ${field} 失败:`, e);
}
}
});
// 解析原始数据(如果存在)
if (resumeDetail.originalData) {
try {
resumeDetail.originalData = JSON.parse(resumeDetail.originalData);
} catch (e) {
console.error('解析原始数据失败:', e);
}
}
return ctx.success(resumeDetail);
},
/**
@@ -269,6 +290,74 @@ return ctx.success({ message: '简历删除成功' });
});
return ctx.success(resumeDetail);
},
/**
* @swagger
* /admin_api/resume/analyze-with-ai:
* post:
* summary: AI 分析简历
* description: 使用 AI 分析简历并更新 AI 字段
* tags: [后台-简历管理]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - resumeId
* properties:
* resumeId:
* type: string
* description: 简历ID
* responses:
* 200:
* description: 分析成功
*/
'POST /resume/analyze-with-ai': async (ctx) => {
const models = Framework.getModels();
const { resume_info } = models;
const { resumeId } = ctx.getBody();
if (!resumeId) {
return ctx.fail('简历ID不能为空');
}
const resume = await resume_info.findOne({ where: { resumeId } });
if (!resume) {
return ctx.fail('简历不存在');
}
try {
const resumeManager = require('../middleware/job/resumeManager');
const resumeData = resume.toJSON();
// 调用 AI 分析
await resumeManager.analyze_resume_with_ai(resumeId, resumeData);
// 重新获取更新后的数据
const updatedResume = await resume_info.findOne({ where: { resumeId } });
const resumeDetail = updatedResume.toJSON();
// 解析 JSON 字段
const jsonFields = ['skills', 'certifications', 'projectExperience', 'workExperience', 'aiSkillTags'];
jsonFields.forEach(field => {
if (resumeDetail[field]) {
try {
resumeDetail[field] = JSON.parse(resumeDetail[field]);
} catch (e) {
console.error(`解析字段 ${field} 失败:`, e);
}
}
});
return ctx.success(resumeDetail);
} catch (error) {
console.error('AI 分析失败:', error);
return ctx.fail('AI 分析失败: ' + error.message);
}
}
};

View File

@@ -180,34 +180,50 @@ class aiService {
*/
async analyzeResume(resumeText) {
const prompt = `
请分析以下简历内容,提取核心要素
请分析以下简历内容,并返回 JSON 格式的分析结果
简历内容:
${resumeText}
提取以下信息
1. 技能标签(编程语言、框架、工具等)
2. 工作经验(年限、行业、项目等)
3. 教育背景(学历、专业、证书等)
4. 期望薪资范围
5. 期望工作地点
6. 核心优势
7. 职业发展方向
按以下格式返回 JSON 结果
{
"skillTags": ["技能1", "技能2", "技能3"], // 技能标签数组(编程语言、框架、工具等)
"strengths": "核心优势描述", // 简历的优势和亮点
"weaknesses": "不足之处描述", // 简历的不足或需要改进的地方
"careerSuggestion": "职业发展建议", // 针对该简历的职业发展方向和建议
"competitiveness": 75 // 竞争力评分0-100的整数综合考虑工作年限、技能、经验等因素
}
请以JSON格式返回结果。
要求:
1. skillTags 必须是字符串数组
2. strengths、weaknesses、careerSuggestion 是字符串描述
3. competitiveness 必须是 0-100 之间的整数
4. 所有字段都必须返回,如果没有相关信息,使用空数组或空字符串
`;
const result = await this.callAPI(prompt, {
systemPrompt: '你是一个专业的简历分析师,擅长提取简历的核心要素和关键信息。',
temperature: 0.2
systemPrompt: '你是一个专业的简历分析师,擅长分析简历的核心要素、优势劣势、竞争力评分和职业发展建议。请以 JSON 格式返回分析结果,确保格式正确。',
temperature: 0.3,
maxTokens: 1500
});
try {
const analysis = JSON.parse(result.content);
// 尝试从返回内容中提取 JSON
let content = result.content.trim();
// 如果返回内容被代码块包裹,提取其中的 JSON
const jsonMatch = content.match(/```(?:json)?\s*(\{[\s\S]*\})\s*```/) || content.match(/(\{[\s\S]*\})/);
if (jsonMatch) {
content = jsonMatch[1];
}
const analysis = JSON.parse(content);
return {
analysis: analysis
};
} catch (parseError) {
console.error(`[AI服务] 简历分析结果解析失败:`, parseError);
console.error(`[AI服务] 原始内容:`, result.content);
return {
analysis: {
content: result.content,

View File

@@ -181,12 +181,103 @@ class ResumeManager {
console.log(`[简历管理] 简历已创建 - ID: ${resumeId}`);
}
// 二期规划AI 分析暂时禁用,使用简单的文本匹配
console.log(`[简历管理] AI分析已禁用二期规划使用文本匹配过滤`);
// 调用 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 - 描述文本
@@ -253,16 +344,23 @@ ${resumeInfo.skillDescription}
// 解析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 } });
// 确保所有字段都有值
const updateData = {
aiSkillTags: JSON.stringify(analysis.skillTags || []),
aiStrengths: analysis.strengths || '',
aiWeaknesses: analysis.weaknesses || '',
aiCareerSuggestion: analysis.careerSuggestion || '',
aiCompetitiveness: parseInt(analysis.competitiveness || 70, 10)
};
console.log(`[简历管理] AI分析完成 - 竞争力评分: ${analysis.competitiveness}`);
// 确保竞争力评分在 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) {
@@ -271,9 +369,26 @@ ${resumeInfo.skillDescription}
fullName: resumeInfo.fullName
});
// 如果AI分析失败使用基于规则的默认分析
// 如果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;
}
}
@@ -286,39 +401,62 @@ ${resumeInfo.skillDescription}
*/
parse_ai_analysis(aiResponse, resumeInfo) {
try {
// 尝试从AI响应中解析JSON
const content = aiResponse.content || aiResponse.analysis?.content || '';
// aiService.analyzeResume 返回格式: { analysis: {...} } 或 { analysis: { content: "...", parseError: true } }
const analysis = aiResponse.analysis;
// 如果AI返回的是JSON格式
if (content.includes('{') && content.includes('}')) {
// 如果解析失败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: parsed.competitiveness || parsed.竞争力评分 || 70
competitiveness: parseInt(parsed.competitiveness || parsed.竞争力评分 || 70, 10)
};
}
}
// 如果无法解析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+)/);
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]) : 70
};
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);
// 解析失败时使用默认分析
@@ -378,7 +516,7 @@ ${resumeInfo.skillDescription}
// 二期规划AI 分析暂时禁用,使用简单的文本匹配
// const analysis = await aiService.analyzeResume(resumeText);
// 使用简单的文本匹配提取技能
const skills = this.extract_skills_from_desc(resumeData.skillDescription || resumeText);
@@ -505,7 +643,7 @@ ${resumeInfo.skillDescription}
throw new Error('MQTT客户端未初始化');
}
}
// 重新获取和分析
const resumeData = await this.get_online_resume(sn_code, mqttClient);
return await this.analyze_resume(sn_code, resumeData);

View File

@@ -296,7 +296,7 @@ class MqttDispatcher {
isLoggedIn: updateData.isLoggedIn || false,
...heartbeatData
};
console.log(`[MQTT心跳] 传递给 deviceManager 的数据:`, { sn_code, isLoggedIn: heartbeatPayload.isLoggedIn });
await deviceManager.recordHeartbeat(sn_code, heartbeatPayload);
} catch (error) {
console.error('[MQTT心跳] 处理心跳消息失败:', error);

View File

@@ -54,8 +54,8 @@ class ScheduleManager {
console.log('[调度管理器] 心跳监听已启动');
// 5. 启动定时任务
this.scheduledJobs.start();
console.log('[调度管理器] 定时任务已启动');
// this.scheduledJobs.start();
// console.log('[调度管理器] 定时任务已启动');
this.isInitialized = true;

View File

@@ -264,5 +264,5 @@ module.exports = (db) => {
return job_postings
// job_postings.sync({ force: true
};