Files
autoAiWorkSys/api/middleware/schedule/handlers/deliverHandler.js
张成 ca8bbcd9cd 1
2026-04-08 13:29:22 +08:00

418 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const BaseHandler = require('./baseHandler');
const ConfigManager = require('../services/configManager');
const jobFilterEngine = require('../services/jobFilterEngine');
const command = require('../core/command');
const config = require('../infrastructure/config');
const db = require('../../dbProxy');
/**
* 自动投递处理器
* 负责职位搜索、过滤、评分和自动投递
*/
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. 获取职位类型配置(同时下发 get_job_listings 并保存到 resume_info.job_listings
const jobTypeConfig = await this.getJobTypeConfig(accountConfig.job_type_id, {
sn_code,
platform,
taskId: task.id,
mqttClient: this.mqttClient
});
// 6. 下发 get_job_list 拉取职位列表tabLabel 切换期望 tabjob_type_id 随指令下发供设备使用)
const tabLabel = resume.deliver_tab_label || '';
await this.getJobList(
sn_code,
platform,
pageCount,
task.id,
tabLabel,
accountConfig.job_type_id,
accountConfig.keyword
);
// 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. 过滤 + 评分 + 按 60 分阈值筛(入口在 jobFilterEngine便于阅读
const filteredJobs = await jobFilterEngine.filterAndScoreJobsForDeliver(
pendingJobs,
filterConfig,
resume,
accountConfig,
jobTypeConfig,
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: 'get_online_resume',
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;
}
/**
* 获取职位类型配置;若传入 options先下发 get_job_listings 获取 tab 列表并写入 resume_info.job_listings
* @param {number} jobTypeId - 职位类型 ID
* @param {object} options - 可选 { sn_code, platform, taskId, mqttClient },用于下发 get_job_listings 并保存
*/
async getJobTypeConfig(jobTypeId, options = {}) {
const { sn_code, platform = 'boss', taskId, mqttClient } = options;
if (sn_code && taskId && mqttClient) {
try {
const getListingsCommand = {
command_type: 'get_job_listings',
command_name: '获取投递标签列表',
command_params: JSON.stringify({ sn_code, platform }),
priority: config.getTaskPriority('auto_deliver') || 7
};
const ret = await command.executeCommands(taskId, [getListingsCommand], mqttClient);
const firstResult = ret.results && ret.results[0];
const list = firstResult && firstResult.result && Array.isArray(firstResult.result)
? firstResult.result
: [];
const job_listings = list.map((item) => (item && item.text != null ? String(item.text).trim() : '')).filter(Boolean);
if (job_listings.length > 0) {
const resume_info = db.getModel('resume_info');
const [updated] = await resume_info.update(
{ job_listings },
{ where: { sn_code, platform } }
);
if (updated) {
console.log(`[自动投递] job_listings 已保存,共 ${job_listings.length}`);
}
}
} catch (err) {
console.warn(`[自动投递] 下发 get_job_listings 或保存失败:`, err.message);
}
}
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;
}
}
/**
* 下发 get_job_list 命令拉取职位列表
* @param {string} tabLabel - 投递用期望标签文案,对应 resume_info.deliver_tab_labelget_job_list 会按此选择 tab
* @param {number} jobTypeId - 职位类型 ID随指令下发供设备使用
*/
async getJobList(sn_code, platform, pageCount, taskId, tabLabel = '', jobTypeId = null, accountKeyword = '') {
const label = tabLabel != null && String(tabLabel).trim() !== '' ? String(tabLabel).trim() : '';
const accKw = accountKeyword != null && String(accountKeyword).trim() !== '' ? String(accountKeyword).trim() : '';
// 与 jobManager 一致:优先期望职位文案,其次账户搜索词,用于 job_postings.keyword
const keyword = label || accKw;
const params = {
sn_code,
platform,
pageCount,
keyword
};
if (label) {
params.tabLabel = label;
}
if (jobTypeId != null && jobTypeId !== '') {
params.job_type_id = jobTypeId;
}
const getJobListCommand = {
command_type: 'get_job_list',
command_name: '获取职位列表',
command_params: JSON.stringify(params),
priority: config.getTaskPriority('auto_deliver') || 7
};
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 rawJobTypeExclude = jobTypeConfig?.excludeKeywords
? ConfigManager.parseConfig(jobTypeConfig.excludeKeywords, [])
: [];
const jobTypeExclude = Array.isArray(rawJobTypeExclude) ? rawJobTypeExclude : [];
const deliverExcludeRaw = ConfigManager.getExcludeKeywords(deliverConfig);
const deliverExclude = Array.isArray(deliverExcludeRaw) ? deliverExcludeRaw : [];
const filterExcludeRaw = filterRules.excludeKeywords || [];
const filterExclude = Array.isArray(filterExcludeRaw) ? filterExcludeRaw : [];
// 过滤关键词
const deliverFilterRaw = ConfigManager.getFilterKeywords(deliverConfig);
const deliverFilter = Array.isArray(deliverFilterRaw) ? deliverFilterRaw : [];
const filterKeywordsRaw = filterRules.keywords || [];
const filterKeywords = Array.isArray(filterKeywordsRaw) ? filterKeywordsRaw : [];
// 薪资范围
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));
}
/**
* 创建投递指令
*/
createDeliverCommands(jobs, sn_code, platform) {
return jobs.map(job => ({
command_type: 'deliver_resume',
command_name: '投递简历',
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;