458 lines
12 KiB
JavaScript
458 lines
12 KiB
JavaScript
/**
|
||
* 简历信息管理API - 后台管理
|
||
* 提供简历信息的查询和管理功能
|
||
*/
|
||
|
||
const Framework = require("../../framework/node-core-framework.js");
|
||
|
||
module.exports = {
|
||
/**
|
||
* @swagger
|
||
* /admin_api/resume/list:
|
||
* post:
|
||
* summary: 获取简历列表
|
||
* description: 分页获取所有简历信息
|
||
* tags: [后台-简历管理]
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* properties:
|
||
* page:
|
||
* type: integer
|
||
* description: 页码
|
||
* pageSize:
|
||
* type: integer
|
||
* description: 每页数量
|
||
* minCompetitiveness:
|
||
* type: integer
|
||
* description: 最小竞争力评分(可选)
|
||
* responses:
|
||
* 200:
|
||
* description: 获取成功
|
||
*/
|
||
'POST /resume/list': async (ctx) => {
|
||
const models = Framework.getModels();
|
||
const { resume_info, op } = models;
|
||
const body = ctx.getBody();
|
||
const { minCompetitiveness, searchText} = ctx.getBody();
|
||
|
||
// 获取分页参数
|
||
const { limit, offset } = ctx.getPageSize();
|
||
|
||
|
||
const where = {};
|
||
|
||
// 竞争力评分筛选
|
||
if (minCompetitiveness !== undefined) {
|
||
where.aiCompetitiveness = { [op.gte]: minCompetitiveness };
|
||
}
|
||
|
||
// 支持搜索姓名或技能
|
||
if (searchText) {
|
||
where[op.or] = [
|
||
{ fullName: { [op.like]: `%${searchText}%` } },
|
||
{ skills: { [op.like]: `%${searchText}%` } }
|
||
];
|
||
}
|
||
|
||
const result = await resume_info.findAndCountAll({
|
||
where,
|
||
limit,
|
||
offset,
|
||
order: [
|
||
['aiCompetitiveness', 'DESC'],
|
||
['id', 'DESC']
|
||
]
|
||
});
|
||
|
||
return ctx.success({
|
||
rows: result.rows,
|
||
count: result.count
|
||
});
|
||
|
||
},
|
||
|
||
/**
|
||
* @swagger
|
||
* /admin_api/resume/statistics:
|
||
* get:
|
||
* summary: 获取简历统计
|
||
* description: 获取简历信息的统计数据
|
||
* tags: [后台-简历管理]
|
||
* responses:
|
||
* 200:
|
||
* description: 获取成功
|
||
*/
|
||
'GET /resume/statistics': async (ctx) => {
|
||
const models = Framework.getModels();
|
||
const { resume_info } = models;
|
||
|
||
|
||
const totalResumes = await resume_info.count();
|
||
|
||
// 平均竞争力
|
||
const avgCompetitiveness = await resume_info.findAll({
|
||
attributes: [
|
||
[models.sequelize.fn('AVG', models.sequelize.col('aiCompetitiveness')), 'avgScore']
|
||
],
|
||
raw: true
|
||
});
|
||
|
||
// 按工作年限统计
|
||
const workYearsStats = await resume_info.findAll({
|
||
attributes: [
|
||
'workYears',
|
||
[models.sequelize.fn('COUNT', models.sequelize.col('*')), 'count']
|
||
],
|
||
group: ['workYears'],
|
||
raw: true
|
||
});
|
||
|
||
// 竞争力分布
|
||
const [
|
||
highCompetitiveness,
|
||
mediumCompetitiveness,
|
||
lowCompetitiveness
|
||
] = await Promise.all([
|
||
resume_info.count({ where: { aiCompetitiveness: { [models.op.gte]: 80 } } }),
|
||
resume_info.count({ where: { aiCompetitiveness: { [models.op.gte]: 60, [models.op.lt]: 80 } } }),
|
||
resume_info.count({ where: { aiCompetitiveness: { [models.op.lt]: 60 } } })
|
||
]);
|
||
|
||
return ctx.success({
|
||
totalResumes,
|
||
averageCompetitiveness: parseFloat(avgCompetitiveness[0]?.avgScore || 0).toFixed(2),
|
||
workYearsStats,
|
||
competitivenessDistribution: {
|
||
high: highCompetitiveness,
|
||
medium: mediumCompetitiveness,
|
||
low: lowCompetitiveness
|
||
}
|
||
});
|
||
|
||
},
|
||
|
||
/**
|
||
* @swagger
|
||
* /admin_api/resume/detail:
|
||
* get:
|
||
* summary: 获取简历详情
|
||
* description: 根据简历ID获取详细信息
|
||
* tags: [后台-简历管理]
|
||
* parameters:
|
||
* - in: query
|
||
* name: resumeId
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* description: 简历ID
|
||
* responses:
|
||
* 200:
|
||
* description: 获取成功
|
||
*/
|
||
'POST /resume/detail': 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('简历不存在');
|
||
}
|
||
|
||
// 解析 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);
|
||
},
|
||
|
||
/**
|
||
* @swagger
|
||
* /admin_api/resume/delete:
|
||
* post:
|
||
* summary: 删除简历
|
||
* description: 删除指定的简历信息
|
||
* tags: [后台-简历管理]
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required:
|
||
* - resumeId
|
||
* properties:
|
||
* resumeId:
|
||
* type: string
|
||
* description: 简历ID
|
||
* responses:
|
||
* 200:
|
||
* description: 删除成功
|
||
*/
|
||
'POST /resume/delete': async (ctx) => {
|
||
const models = Framework.getModels();
|
||
const { resume_info } = models;
|
||
const body = ctx.getBody();
|
||
const { resumeId } = body;
|
||
|
||
if (!resumeId) {
|
||
return ctx.fail('简历ID不能为空');
|
||
}
|
||
|
||
|
||
const result = await resume_info.destroy({ where: { resumeId } });
|
||
|
||
if (result === 0) {
|
||
return ctx.fail('简历不存在');
|
||
}
|
||
|
||
return ctx.success({ message: '简历删除成功' });
|
||
|
||
},
|
||
|
||
/**
|
||
* @swagger
|
||
* /admin_api/resume/get-by-device:
|
||
* get:
|
||
* summary: 根据设备获取简历
|
||
* description: 获取指定设备和平台的活跃简历
|
||
* tags: [后台-简历管理]
|
||
* parameters:
|
||
* - in: query
|
||
* name: sn_code
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* description: 设备SN码
|
||
* - in: query
|
||
* name: platform
|
||
* required: true
|
||
* schema:
|
||
* type: string
|
||
* description: 平台
|
||
* responses:
|
||
* 200:
|
||
* description: 获取成功
|
||
*/
|
||
'GET /resume/get-by-device': async (ctx) => {
|
||
const models = Framework.getModels();
|
||
const { resume_info } = models;
|
||
const { sn_code, platform } = ctx.query;
|
||
|
||
if (!sn_code || !platform) {
|
||
return ctx.fail('设备SN码和平台不能为空');
|
||
}
|
||
|
||
const resume = await resume_info.findOne({
|
||
where: { sn_code, platform, isActive: true },
|
||
order: [['syncTime', 'DESC']]
|
||
});
|
||
|
||
if (!resume) {
|
||
return ctx.fail('未找到活跃简历');
|
||
}
|
||
|
||
const resumeDetail = resume.toJSON();
|
||
const jsonFields = ['skills', 'certifications', 'projectExperience', 'workExperience', 'aiSkillTags'];
|
||
|
||
jsonFields.forEach(field => {
|
||
if (resumeDetail[field]) {
|
||
resumeDetail[field] = JSON.parse(resumeDetail[field]);
|
||
}
|
||
});
|
||
|
||
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);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* @swagger
|
||
* /admin_api/resume/sync-online:
|
||
* post:
|
||
* summary: 同步在线简历
|
||
* description: 通过MQTT指令获取用户在线简历并更新到数据库
|
||
* tags: [后台-简历管理]
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required:
|
||
* - resumeId
|
||
* properties:
|
||
* resumeId:
|
||
* type: string
|
||
* description: 简历ID
|
||
* responses:
|
||
* 200:
|
||
* description: 同步成功
|
||
*/
|
||
'POST /resume/sync-online': 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('简历不存在');
|
||
}
|
||
|
||
const { sn_code, platform } = resume;
|
||
|
||
if (!sn_code) {
|
||
return ctx.fail('该简历未绑定设备SN码');
|
||
}
|
||
|
||
try {
|
||
const scheduleManager = require('../middleware/schedule');
|
||
const resumeManager = require('../middleware/job/resumeManager');
|
||
|
||
// 检查 MQTT 客户端是否已初始化
|
||
if (!scheduleManager.mqttClient) {
|
||
return ctx.fail('MQTT客户端未初始化,请检查调度系统是否正常启动');
|
||
}
|
||
|
||
// 检查设备是否在线
|
||
// const deviceManager = require('../middleware/schedule/deviceManager');
|
||
// if (!deviceManager.isDeviceOnline(sn_code)) {
|
||
// return ctx.fail('设备离线,无法同步在线简历');
|
||
// }
|
||
|
||
// 调用简历管理器获取并保存简历
|
||
const resumeData = await resumeManager.get_online_resume(sn_code, scheduleManager.mqttClient, {
|
||
platform: platform || 'boss'
|
||
});
|
||
|
||
// 重新获取更新后的简历数据
|
||
const updatedResume = await resume_info.findOne({ where: { resumeId } });
|
||
if (!updatedResume) {
|
||
return ctx.fail('同步成功但未找到更新后的简历记录');
|
||
}
|
||
|
||
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({
|
||
message: '同步在线简历成功',
|
||
data: resumeDetail
|
||
});
|
||
} catch (error) {
|
||
console.error('同步在线简历失败:', error);
|
||
return ctx.fail('同步在线简历失败: ' + (error.message || '未知错误'));
|
||
}
|
||
}
|
||
};
|
||
|