411 lines
13 KiB
JavaScript
411 lines
13 KiB
JavaScript
const BaseHandler = require('./baseHandler');
|
||
const ConfigManager = require('../services/configManager');
|
||
const jobFilterEngine = require('../services/jobFilterEngine');
|
||
const command = require('../command');
|
||
const config = require('../config');
|
||
const db = require('../../dbProxy');
|
||
const jobFilterService = require('../../../services/job_filter_service');
|
||
|
||
/**
|
||
* 自动投递处理器
|
||
* 负责职位搜索、过滤、评分和自动投递
|
||
*/
|
||
class DeliverHandler extends BaseHandler {
|
||
/**
|
||
* 处理自动投递任务
|
||
* @param {object} task - 任务对象
|
||
* @returns {Promise<object>} 执行结果
|
||
*/
|
||
async handle(task) {
|
||
return await this.execute(task, async () => {
|
||
return await this.doDeliver(task);
|
||
}, {
|
||
checkAuth: true,
|
||
checkOnline: true,
|
||
recordDeviceMetrics: true
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 执行投递逻辑
|
||
*/
|
||
async doDeliver(task) {
|
||
const { sn_code, taskParams } = task;
|
||
const { keyword, platform = 'boss', pageCount = 3, maxCount = 10, filterRules = {} } = taskParams;
|
||
|
||
console.log(`[自动投递] 开始 - 设备: ${sn_code}, 关键词: ${keyword}`);
|
||
|
||
// 1. 检查每日投递限制
|
||
const dailyCheck = await this.checkDailyDeliverLimit(sn_code, platform);
|
||
if (!dailyCheck.allowed) {
|
||
return {
|
||
deliveredCount: 0,
|
||
message: dailyCheck.message
|
||
};
|
||
}
|
||
|
||
const actualMaxCount = dailyCheck.actualMaxCount;
|
||
|
||
// 2. 检查并获取简历
|
||
const resume = await this.getOrRefreshResume(sn_code, platform, task.id);
|
||
if (!resume) {
|
||
return {
|
||
deliveredCount: 0,
|
||
message: '未找到简历信息'
|
||
};
|
||
}
|
||
|
||
// 3. 获取账户配置
|
||
const accountConfig = await this.getAccountConfig(sn_code, [
|
||
'keyword', 'platform_type', 'deliver_config', 'job_type_id', 'is_salary_priority'
|
||
]);
|
||
|
||
if (!accountConfig) {
|
||
return {
|
||
deliveredCount: 0,
|
||
message: '未找到账户配置'
|
||
};
|
||
}
|
||
|
||
// 4. 检查投递时间范围
|
||
const deliverConfig = ConfigManager.parseDeliverConfig(accountConfig.deliver_config);
|
||
const timeRange = ConfigManager.getTimeRange(deliverConfig);
|
||
|
||
if (timeRange) {
|
||
const timeRangeValidator = require('../services/timeRangeValidator');
|
||
const timeCheck = timeRangeValidator.checkTimeRange(timeRange);
|
||
if (!timeCheck.allowed) {
|
||
return {
|
||
deliveredCount: 0,
|
||
message: timeCheck.reason
|
||
};
|
||
}
|
||
}
|
||
|
||
// 5. 获取职位类型配置
|
||
const jobTypeConfig = await this.getJobTypeConfig(accountConfig.job_type_id);
|
||
|
||
// 6. 搜索职位列表
|
||
await this.searchJobs(sn_code, platform, keyword || accountConfig.keyword, pageCount, task.id);
|
||
|
||
// 7. 从数据库获取待投递职位
|
||
const pendingJobs = await this.getPendingJobs(sn_code, platform, actualMaxCount * 3);
|
||
|
||
if (!pendingJobs || pendingJobs.length === 0) {
|
||
return {
|
||
deliveredCount: 0,
|
||
message: '没有待投递的职位'
|
||
};
|
||
}
|
||
|
||
// 8. 合并过滤配置
|
||
const filterConfig = this.mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig);
|
||
|
||
// 9. 过滤已投递的公司
|
||
const recentCompanies = await this.getRecentDeliveredCompanies(sn_code, 30);
|
||
|
||
// 10. 过滤、评分、排序职位
|
||
const filteredJobs = await this.filterAndScoreJobs(
|
||
pendingJobs,
|
||
resume,
|
||
accountConfig,
|
||
jobTypeConfig,
|
||
filterConfig,
|
||
recentCompanies
|
||
);
|
||
|
||
const jobsToDeliver = filteredJobs.slice(0, actualMaxCount);
|
||
|
||
console.log(`[自动投递] 职位筛选完成 - 原始: ${pendingJobs.length}, 符合条件: ${filteredJobs.length}, 将投递: ${jobsToDeliver.length}`);
|
||
|
||
if (jobsToDeliver.length === 0) {
|
||
return {
|
||
deliveredCount: 0,
|
||
message: '没有符合条件的职位'
|
||
};
|
||
}
|
||
|
||
// 11. 创建投递指令并执行
|
||
const deliverCommands = this.createDeliverCommands(jobsToDeliver, sn_code, platform);
|
||
const result = await command.executeCommands(task.id, deliverCommands, this.mqttClient);
|
||
|
||
console.log(`[自动投递] 完成 - 设备: ${sn_code}, 投递: ${deliverCommands.length} 个职位`);
|
||
|
||
return {
|
||
deliveredCount: deliverCommands.length,
|
||
...result
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 检查每日投递限制
|
||
*/
|
||
async checkDailyDeliverLimit(sn_code, platform) {
|
||
const apply_records = db.getModel('apply_records');
|
||
const dailyLimit = config.getDailyLimit('apply', platform);
|
||
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
const todayApplyCount = await apply_records.count({
|
||
where: {
|
||
sn_code,
|
||
platform,
|
||
applyTime: {
|
||
[db.models.op.gte]: today
|
||
}
|
||
}
|
||
});
|
||
|
||
console.log(`[自动投递] 今日已投递: ${todayApplyCount}/${dailyLimit}`);
|
||
|
||
if (todayApplyCount >= dailyLimit) {
|
||
return {
|
||
allowed: false,
|
||
message: `已达到每日投递上限(${dailyLimit}次)`
|
||
};
|
||
}
|
||
|
||
const remainingQuota = dailyLimit - todayApplyCount;
|
||
|
||
return {
|
||
allowed: true,
|
||
actualMaxCount: remainingQuota,
|
||
todayCount: todayApplyCount,
|
||
limit: dailyLimit
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取或刷新简历
|
||
*/
|
||
async getOrRefreshResume(sn_code, platform, taskId) {
|
||
const resume_info = db.getModel('resume_info');
|
||
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
|
||
|
||
let resume = await resume_info.findOne({
|
||
where: {
|
||
sn_code,
|
||
platform,
|
||
isActive: true
|
||
},
|
||
order: [['last_modify_time', 'DESC']]
|
||
});
|
||
|
||
const needRefresh = !resume ||
|
||
!resume.last_modify_time ||
|
||
new Date(resume.last_modify_time) < twoHoursAgo;
|
||
|
||
if (needRefresh) {
|
||
console.log(`[自动投递] 简历超过2小时未更新,重新获取`);
|
||
|
||
try {
|
||
await command.executeCommands(taskId, [{
|
||
command_type: 'getOnlineResume',
|
||
command_name: '获取在线简历',
|
||
command_params: JSON.stringify({ sn_code, platform }),
|
||
priority: config.getTaskPriority('get_resume') || 5
|
||
}], this.mqttClient);
|
||
|
||
// 重新查询
|
||
resume = await resume_info.findOne({
|
||
where: { sn_code, platform, isActive: true },
|
||
order: [['last_modify_time', 'DESC']]
|
||
});
|
||
} catch (error) {
|
||
console.warn(`[自动投递] 获取在线简历失败:`, error.message);
|
||
}
|
||
}
|
||
|
||
return resume ? resume.toJSON() : null;
|
||
}
|
||
|
||
/**
|
||
* 获取职位类型配置
|
||
*/
|
||
async getJobTypeConfig(jobTypeId) {
|
||
if (!jobTypeId) return null;
|
||
|
||
try {
|
||
const job_types = db.getModel('job_types');
|
||
const jobType = await job_types.findByPk(jobTypeId);
|
||
return jobType ? jobType.toJSON() : null;
|
||
} catch (error) {
|
||
console.error(`[自动投递] 获取职位类型配置失败:`, error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 搜索职位列表
|
||
*/
|
||
async searchJobs(sn_code, platform, keyword, pageCount, taskId) {
|
||
const getJobListCommand = {
|
||
command_type: 'getJobList',
|
||
command_name: '获取职位列表',
|
||
command_params: JSON.stringify({
|
||
sn_code,
|
||
keyword,
|
||
platform,
|
||
pageCount
|
||
}),
|
||
priority: config.getTaskPriority('search_jobs') || 5
|
||
};
|
||
|
||
await command.executeCommands(taskId, [getJobListCommand], this.mqttClient);
|
||
}
|
||
|
||
/**
|
||
* 获取待投递职位
|
||
*/
|
||
async getPendingJobs(sn_code, platform, limit) {
|
||
const job_postings = db.getModel('job_postings');
|
||
|
||
const jobs = await job_postings.findAll({
|
||
where: {
|
||
sn_code,
|
||
platform,
|
||
applyStatus: 'pending'
|
||
},
|
||
order: [['create_time', 'DESC']],
|
||
limit
|
||
});
|
||
|
||
return jobs.map(job => job.toJSON ? job.toJSON() : job);
|
||
}
|
||
|
||
/**
|
||
* 合并过滤配置
|
||
*/
|
||
mergeFilterConfig(deliverConfig, filterRules, jobTypeConfig) {
|
||
// 排除关键词
|
||
const jobTypeExclude = jobTypeConfig?.excludeKeywords
|
||
? ConfigManager.parseConfig(jobTypeConfig.excludeKeywords, [])
|
||
: [];
|
||
|
||
const deliverExclude = ConfigManager.getExcludeKeywords(deliverConfig);
|
||
const filterExclude = filterRules.excludeKeywords || [];
|
||
|
||
// 过滤关键词
|
||
const deliverFilter = ConfigManager.getFilterKeywords(deliverConfig);
|
||
const filterKeywords = filterRules.keywords || [];
|
||
|
||
// 薪资范围
|
||
const salaryRange = filterRules.minSalary || filterRules.maxSalary
|
||
? { min: filterRules.minSalary || 0, max: filterRules.maxSalary || 0 }
|
||
: ConfigManager.getSalaryRange(deliverConfig);
|
||
|
||
return {
|
||
exclude_keywords: [...jobTypeExclude, ...deliverExclude, ...filterExclude],
|
||
filter_keywords: filterKeywords.length > 0 ? filterKeywords : deliverFilter,
|
||
min_salary: salaryRange.min,
|
||
max_salary: salaryRange.max,
|
||
priority_weights: ConfigManager.getPriorityWeights(deliverConfig)
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取近期已投递的公司
|
||
*/
|
||
async getRecentDeliveredCompanies(sn_code, days = 30) {
|
||
const apply_records = db.getModel('apply_records');
|
||
const daysAgo = new Date();
|
||
daysAgo.setDate(daysAgo.getDate() - days);
|
||
|
||
const recentApplies = await apply_records.findAll({
|
||
where: {
|
||
sn_code,
|
||
applyTime: {
|
||
[db.models.op.gte]: daysAgo
|
||
}
|
||
},
|
||
attributes: ['companyName'],
|
||
group: ['companyName']
|
||
});
|
||
|
||
return new Set(recentApplies.map(apply => apply.companyName).filter(Boolean));
|
||
}
|
||
|
||
/**
|
||
* 过滤和评分职位
|
||
*/
|
||
async filterAndScoreJobs(jobs, resume, accountConfig, jobTypeConfig, filterConfig, recentCompanies) {
|
||
const scored = [];
|
||
|
||
for (const job of jobs) {
|
||
// 1. 过滤近期已投递的公司
|
||
if (job.companyName && recentCompanies.has(job.companyName)) {
|
||
console.log(`[自动投递] 跳过已投递公司: ${job.companyName}`);
|
||
continue;
|
||
}
|
||
|
||
// 2. 使用 jobFilterEngine 过滤和评分
|
||
const filtered = await jobFilterEngine.filterJobs([job], filterConfig, resume);
|
||
if (filtered.length === 0) {
|
||
continue; // 不符合过滤条件
|
||
}
|
||
|
||
// 3. 使用原有的评分系统(job_filter_service)计算详细分数
|
||
const scoreResult = jobFilterService.calculateJobScoreWithWeights(
|
||
job,
|
||
resume,
|
||
accountConfig,
|
||
jobTypeConfig,
|
||
accountConfig.is_salary_priority || []
|
||
);
|
||
|
||
// 4. 计算关键词奖励
|
||
const KeywordMatcher = require('../utils/keywordMatcher');
|
||
const keywordBonus = KeywordMatcher.calculateBonus(
|
||
`${job.jobTitle} ${job.companyName} ${job.jobDescription || ''}`,
|
||
filterConfig.filter_keywords,
|
||
{ baseScore: 5, maxBonus: 20 }
|
||
);
|
||
|
||
const finalScore = scoreResult.totalScore + keywordBonus.score;
|
||
|
||
// 5. 只保留评分 >= 60 的职位
|
||
if (finalScore >= 60) {
|
||
scored.push({
|
||
...job,
|
||
matchScore: finalScore,
|
||
scoreDetails: {
|
||
...scoreResult.scores,
|
||
keywordBonus: keywordBonus.score
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 按评分降序排序
|
||
scored.sort((a, b) => b.matchScore - a.matchScore);
|
||
|
||
return scored;
|
||
}
|
||
|
||
/**
|
||
* 创建投递指令
|
||
*/
|
||
createDeliverCommands(jobs, sn_code, platform) {
|
||
return jobs.map(job => ({
|
||
command_type: 'deliver_resume',
|
||
command_name: `投递简历 - ${job.jobTitle} @ ${job.companyName} (评分:${job.matchScore})`,
|
||
command_params: JSON.stringify({
|
||
sn_code,
|
||
platform,
|
||
jobId: job.jobId,
|
||
encryptBossId: job.encryptBossId || '',
|
||
securityId: job.securityId || '',
|
||
brandName: job.companyName,
|
||
jobTitle: job.jobTitle,
|
||
companyName: job.companyName,
|
||
matchScore: job.matchScore,
|
||
scoreDetails: job.scoreDetails
|
||
}),
|
||
priority: config.getTaskPriority('apply') || 6
|
||
}));
|
||
}
|
||
}
|
||
|
||
module.exports = DeliverHandler;
|