1
This commit is contained in:
424
api/middleware/job/managers/chatManager.js
Normal file
424
api/middleware/job/managers/chatManager.js
Normal file
@@ -0,0 +1,424 @@
|
||||
const aiServiceModule = require('../../../services/ai_service');
|
||||
const logs = require('../../logProxy');
|
||||
|
||||
// 实例化AI服务
|
||||
const aiService = aiServiceModule.getInstance();
|
||||
|
||||
/**
|
||||
* 智能聊天管理模块
|
||||
* 负责聊天内容生成、发送策略和效果监控
|
||||
*/
|
||||
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();
|
||||
13
api/middleware/job/managers/index.js
Normal file
13
api/middleware/job/managers/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Managers 模块统一导出
|
||||
*/
|
||||
|
||||
const jobManager = require('./jobManager');
|
||||
const resumeManager = require('./resumeManager');
|
||||
const chatManager = require('./chatManager');
|
||||
|
||||
module.exports = {
|
||||
jobManager,
|
||||
resumeManager,
|
||||
chatManager
|
||||
};
|
||||
1289
api/middleware/job/managers/jobManager.js
Normal file
1289
api/middleware/job/managers/jobManager.js
Normal file
File diff suppressed because it is too large
Load Diff
712
api/middleware/job/managers/resumeManager.js
Normal file
712
api/middleware/job/managers/resumeManager.js
Normal file
@@ -0,0 +1,712 @@
|
||||
const aiServiceModule = require('../../../services/ai_service');
|
||||
const { jobFilterService } = require('../services');
|
||||
const logs = require('../../logProxy');
|
||||
const db = require('../../dbProxy');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
// 实例化AI服务
|
||||
const aiService = aiServiceModule.getInstance();
|
||||
|
||||
/**
|
||||
* 简历管理模块
|
||||
* 负责简历获取、分析、存储和匹配度计算
|
||||
*/
|
||||
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');
|
||||
|
||||
// 解析 JSON 字段
|
||||
let skillsArray = [];
|
||||
let workExpArray = [];
|
||||
let projectExpArray = [];
|
||||
|
||||
try {
|
||||
if (resumeInfo.skills) {
|
||||
skillsArray = typeof resumeInfo.skills === 'string' ? JSON.parse(resumeInfo.skills) : resumeInfo.skills;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[简历管理] 解析技能字段失败:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (resumeInfo.workExperience) {
|
||||
workExpArray = typeof resumeInfo.workExperience === 'string' ? JSON.parse(resumeInfo.workExperience) : resumeInfo.workExperience;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[简历管理] 解析工作经历字段失败:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (resumeInfo.projectExperience) {
|
||||
projectExpArray = typeof resumeInfo.projectExperience === 'string' ? JSON.parse(resumeInfo.projectExperience) : resumeInfo.projectExperience;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[简历管理] 解析项目经历字段失败:`, e);
|
||||
}
|
||||
|
||||
// 构建工作经历文本
|
||||
const workExpText = workExpArray.map((work, index) => {
|
||||
return `${index + 1}. ${work.company || ''} - ${work.position || ''} (${work.startDate || ''} ~ ${work.endDate || '至今'})
|
||||
工作内容:${work.content || ''}
|
||||
行业:${work.industry || ''}`;
|
||||
}).join('\n\n');
|
||||
|
||||
// 构建项目经历文本
|
||||
const projectExpText = projectExpArray.map((project, index) => {
|
||||
return `${index + 1}. ${project.name || ''} - ${project.role || ''} (${project.startDate || ''} ~ ${project.endDate || '至今'})
|
||||
项目描述:${project.description || ''}
|
||||
项目成果:${project.performance || ''}`;
|
||||
}).join('\n\n');
|
||||
|
||||
// 构建分析提示词
|
||||
const prompt = `请分析以下简历,提供专业的评估:
|
||||
|
||||
【基本信息】
|
||||
姓名:${resumeInfo.fullName || ''}
|
||||
性别:${resumeInfo.gender || ''}
|
||||
年龄:${resumeInfo.age || ''}
|
||||
所在地:${resumeInfo.location || ''}
|
||||
工作年限:${resumeInfo.workYears || ''}
|
||||
|
||||
【教育背景】
|
||||
学历:${resumeInfo.education || ''}
|
||||
专业:${resumeInfo.major || ''}
|
||||
毕业院校:${resumeInfo.school || ''}
|
||||
毕业年份:${resumeInfo.graduationYear || ''}
|
||||
|
||||
【当前状态】
|
||||
当前职位:${resumeInfo.currentPosition || ''}
|
||||
当前公司:${resumeInfo.currentCompany || ''}
|
||||
当前薪资:${resumeInfo.currentSalary || ''}
|
||||
|
||||
【求职期望】
|
||||
期望职位:${resumeInfo.expectedPosition || ''}
|
||||
期望薪资:${resumeInfo.expectedSalary || ''}
|
||||
期望地点:${resumeInfo.expectedLocation || ''}
|
||||
期望行业:${resumeInfo.expectedIndustry || ''}
|
||||
|
||||
【技能标签】
|
||||
${skillsArray.length > 0 ? skillsArray.join('、') : '无'}
|
||||
|
||||
【个人优势描述】
|
||||
${resumeInfo.skillDescription || ''}
|
||||
|
||||
【工作经历】
|
||||
${workExpText || '无'}
|
||||
|
||||
【项目经历】
|
||||
${projectExpText || '无'}
|
||||
|
||||
请从以下几个方面进行专业分析,并返回 JSON 格式结果:
|
||||
1. skillTags: 核心技能标签数组(提取5-10个最关键的技术技能,如:Vue、React、Node.js等)
|
||||
2. strengths: 优势分析(100字以内,基于工作经历、项目经历、技能水平等综合评估)
|
||||
3. weaknesses: 劣势分析(100字以内,指出需要改进的地方或不足)
|
||||
4. careerSuggestion: 职业建议(150字以内,基于期望职位和当前能力给出职业发展建议)
|
||||
5. competitiveness: 竞争力评分(0-100的整数,综合考虑工作年限、技能深度、项目经验、学历等因素)
|
||||
|
||||
要求:
|
||||
- 竞争力评分要客观公正,基于实际能力评估
|
||||
- 优势分析要突出核心亮点和技术能力
|
||||
- 劣势分析要指出真实存在的问题
|
||||
- 职业建议要具有针对性和可操作性`;
|
||||
|
||||
// 调用AI服务进行分析
|
||||
const aiAnalysis = await aiService.analyzeResume(prompt);
|
||||
|
||||
// 解析AI返回的结果,如果AI没返回有效数据,直接抛出错误
|
||||
const analysis = this.parse_ai_analysis(aiAnalysis, resumeInfo);
|
||||
|
||||
if (!analysis) {
|
||||
throw new Error('AI分析结果为空,无法处理');
|
||||
}
|
||||
|
||||
// 确保所有字段都有值
|
||||
const updateData = {
|
||||
aiSkillTags: JSON.stringify(analysis.skillTags || []),
|
||||
aiStrengths: analysis.strengths || '',
|
||||
aiWeaknesses: analysis.weaknesses || '',
|
||||
aiCareerSuggestion: analysis.careerSuggestion || '',
|
||||
aiCompetitiveness: parseInt(analysis.competitiveness || 0, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析AI分析结果
|
||||
* @param {object} aiResponse - AI响应对象
|
||||
* @param {object} resumeInfo - 简历信息
|
||||
* @returns {object|null} 解析后的分析结果,如果AI没返回有效数据则返回null
|
||||
*/
|
||||
parse_ai_analysis(aiResponse, resumeInfo) {
|
||||
// aiService.analyzeResume 返回格式: { analysis: {...} } 或 { analysis: { content: "...", parseError: true } }
|
||||
const analysis = aiResponse?.analysis;
|
||||
|
||||
// 如果解析失败,analysis 会有 parseError 标记,直接返回 null
|
||||
if (!analysis || analysis.parseError) {
|
||||
console.warn(`[简历管理] AI分析结果解析失败或为空`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果解析成功,analysis 直接是解析后的对象
|
||||
if (typeof analysis === 'object' && !analysis.parseError) {
|
||||
// 检查是否有必要的字段
|
||||
if (analysis.competitiveness === undefined && analysis.竞争力评分 === undefined) {
|
||||
console.warn(`[简历管理] AI分析结果缺少竞争力评分字段`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
skillTags: analysis.skillTags || analysis.技能标签 || [],
|
||||
strengths: analysis.strengths || analysis.优势 || analysis.优势分析 || '',
|
||||
weaknesses: analysis.weaknesses || analysis.劣势 || analysis.劣势分析 || '',
|
||||
careerSuggestion: analysis.careerSuggestion || analysis.职业建议 || '',
|
||||
competitiveness: parseInt(analysis.competitiveness || analysis.竞争力评分 || 0, 10)
|
||||
};
|
||||
}
|
||||
|
||||
// 如果格式不对,返回 null
|
||||
console.warn(`[简历管理] AI分析结果格式不正确`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认分析结果(基于规则)
|
||||
* @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();
|
||||
Reference in New Issue
Block a user