11
This commit is contained in:
410
api/middleware/schedule/handlers/deliverHandler.js
Normal file
410
api/middleware/schedule/handlers/deliverHandler.js
Normal file
@@ -0,0 +1,410 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user