11
This commit is contained in:
@@ -111,7 +111,7 @@ class DeliverHandler extends BaseHandler {
|
||||
const recentCompanies = await this.getRecentDeliveredCompanies(sn_code, repeatDeliverDays);
|
||||
|
||||
// 10. 过滤 + 评分 + 按 60 分阈值筛(入口在 jobFilterEngine,便于阅读)
|
||||
const filteredJobs = await jobFilterEngine.filterAndScoreJobsForDeliver(
|
||||
const { scored: filteredJobs, skipReasonByJobId } = await jobFilterEngine.filterAndScoreJobsForDeliver(
|
||||
pendingJobs,
|
||||
filterConfig,
|
||||
resume,
|
||||
@@ -121,7 +121,7 @@ class DeliverHandler extends BaseHandler {
|
||||
);
|
||||
|
||||
// 本轮未进入「可投递」列表的待投递记录,标记为已过滤,避免长期停留在 pending
|
||||
await this.markFilteredJobsNotPassed(pendingJobs, filteredJobs, sn_code, platform);
|
||||
await this.markFilteredJobsNotPassed(pendingJobs, filteredJobs, sn_code, platform, skipReasonByJobId);
|
||||
|
||||
const jobsToDeliver = filteredJobs.slice(0, actualMaxCount);
|
||||
|
||||
@@ -302,7 +302,7 @@ class DeliverHandler extends BaseHandler {
|
||||
* @param {Array} pendingJobs - 本批拉取的待投递
|
||||
* @param {Array} filteredJobs - filterAndScoreJobsForDeliver 通过的结果(含 matchScore)
|
||||
*/
|
||||
async markFilteredJobsNotPassed(pendingJobs, filteredJobs, sn_code, platform) {
|
||||
async markFilteredJobsNotPassed(pendingJobs, filteredJobs, sn_code, platform, skipReasonByJobId = {}) {
|
||||
if (!pendingJobs || pendingJobs.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -316,20 +316,30 @@ class DeliverHandler extends BaseHandler {
|
||||
return;
|
||||
}
|
||||
const job_postings = db.getModel('job_postings');
|
||||
const { op } = db.models;
|
||||
const default_reason = '未通过自动投递筛选';
|
||||
try {
|
||||
const [n] = await job_postings.update(
|
||||
{ applyStatus: 'filtered' },
|
||||
{
|
||||
where: {
|
||||
id: { [op.in]: notPassedIds },
|
||||
sn_code,
|
||||
platform,
|
||||
applyStatus: 'pending'
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
notPassedIds.map((id) => {
|
||||
const reason = (skipReasonByJobId && skipReasonByJobId[id]) || default_reason;
|
||||
const text = String(reason).slice(0, 65000);
|
||||
return job_postings.update(
|
||||
{
|
||||
applyStatus: 'filtered',
|
||||
is_delivered: false,
|
||||
deliver_failed_reason: text
|
||||
},
|
||||
{
|
||||
where: {
|
||||
id,
|
||||
sn_code,
|
||||
platform,
|
||||
applyStatus: 'pending'
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
console.log(`[自动投递] 不符合条件已标记 filtered: ${notPassedIds.length} 条(更新行数 ${n})`);
|
||||
console.log(`[自动投递] 不符合条件已标记 filtered: ${notPassedIds.length} 条(含原因)`);
|
||||
} catch (e) {
|
||||
console.warn('[自动投递] 标记 filtered 失败:', e.message);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const db = require('../../dbProxy');
|
||||
/**
|
||||
* 职位过滤引擎(schedule 自动投递用)
|
||||
* 本文件集中:过滤(薪资/关键词/活跃度/去重)+ 评分(权重分+关键词奖励)+ 按分数阈值筛。
|
||||
* 自动投递只需调 filterAndScoreJobsForDeliver 一个方法。
|
||||
* 自动投递调用 filterAndScoreJobsForDeliver,返回 { scored, skipReasonByJobId }。
|
||||
*/
|
||||
class JobFilterEngine {
|
||||
getJobKey(job) {
|
||||
@@ -89,24 +89,82 @@ class JobFilterEngine {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条职位:判断 filterJobs 中哪一步未通过(用于写入 job_postings.deliver_failed_reason)
|
||||
*/
|
||||
async get_single_job_filter_fail_reason(job, config) {
|
||||
const j = job;
|
||||
const after_salary = this.filterBySalary([j], config);
|
||||
if (after_salary.length === 0) {
|
||||
return `薪资不在设定范围(${config.min_salary ?? 0}-${config.max_salary ?? 0}K)`;
|
||||
}
|
||||
const after_title = this.filterByTitleIncludeKeywords(after_salary, config);
|
||||
if (after_title.length === 0) {
|
||||
const kws = (config.title_include_keywords || []).join('、') || '无';
|
||||
return `职位标题须包含以下关键词之一:${kws}`;
|
||||
}
|
||||
const after_kw = this.filterByKeywords(after_title, config);
|
||||
if (after_kw.length === 0) {
|
||||
const match_text = `${j.jobTitle || ''}`;
|
||||
const match_result = KeywordMatcher.match(match_text, {
|
||||
excludeKeywords: Array.isArray(config.exclude_keywords) ? config.exclude_keywords : [],
|
||||
filterKeywords: Array.isArray(config.filter_keywords) ? config.filter_keywords : [],
|
||||
bonusKeywords: []
|
||||
});
|
||||
if (match_result && match_result.details && match_result.details.exclude && match_result.details.exclude.matched) {
|
||||
const hit_keywords = match_result.details.exclude.keywords || [];
|
||||
if (hit_keywords.length) {
|
||||
return `命中排除关键词: ${hit_keywords.join('、')}`;
|
||||
}
|
||||
}
|
||||
if (match_result && match_result.details && match_result.details.filter && !match_result.details.filter.matched) {
|
||||
const needed = Array.isArray(config.filter_keywords) ? config.filter_keywords : [];
|
||||
if (needed.length) {
|
||||
return `未命中包含关键词: ${needed.join('、')}`;
|
||||
}
|
||||
}
|
||||
return '命中排除关键词或未满足包含词规则';
|
||||
}
|
||||
if (config.filter_inactive_companies) {
|
||||
const after_act = await this.filterByCompanyActivity(after_kw, config.company_active_days || 7);
|
||||
if (after_act.length === 0) {
|
||||
return '公司活跃度不满足配置';
|
||||
}
|
||||
}
|
||||
if (config.deduplicate) {
|
||||
const after_dedup = this.deduplicateJobs(after_kw);
|
||||
if (after_dedup.length === 0) {
|
||||
return '与列表内职位重复(公司+岗位名)';
|
||||
}
|
||||
}
|
||||
return '未通过职位过滤';
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动投递用:过滤 + 评分 + 按 60 分阈值筛,一次调用完成(便于阅读与维护)
|
||||
* @returns {{ scored: Array, skipReasonByJobId: Record<number|string, string> }}
|
||||
*/
|
||||
async filterAndScoreJobsForDeliver(jobs, filterConfig, resume, accountConfig, jobTypeConfig, recentCompanies) {
|
||||
const scored = [];
|
||||
const skipReasonByJobId = {};
|
||||
const jobDesc = (j) => `${j.companyName || '?'} / ${j.jobTitle || '?'}`;
|
||||
const { jobFilterService } = require('../../job/services');
|
||||
|
||||
console.log(`[jobFilterEngine] filterAndScoreJobsForDeliver 开始,待处理: ${jobs.length}`);
|
||||
|
||||
for (const job of jobs) {
|
||||
const job_id = job.id;
|
||||
if (job.companyName && recentCompanies.has(job.companyName)) {
|
||||
const msg = '近期已在配置天数内投递过该公司';
|
||||
if (job_id != null) skipReasonByJobId[job_id] = msg;
|
||||
console.log(`[jobFilterEngine] 已投递公司 剔除: ${jobDesc(job)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const filtered = await this.filterJobs([job], filterConfig, resume);
|
||||
if (filtered.length === 0) {
|
||||
const reason = await this.get_single_job_filter_fail_reason(job, filterConfig);
|
||||
if (job_id != null) skipReasonByJobId[job_id] = reason;
|
||||
console.log(`[jobFilterEngine] 过滤条件不通过 剔除: ${jobDesc(job)}`);
|
||||
continue;
|
||||
}
|
||||
@@ -126,6 +184,8 @@ class JobFilterEngine {
|
||||
const finalScore = scoreResult.totalScore + keywordBonus.score;
|
||||
|
||||
if (finalScore < 60) {
|
||||
const msg = `评分不足(总分${finalScore.toFixed(1)},需>=60)`;
|
||||
if (job_id != null) skipReasonByJobId[job_id] = msg;
|
||||
console.log(`[jobFilterEngine] 评分不足(>=60) 剔除: ${jobDesc(job)} 总分=${finalScore.toFixed(1)}`);
|
||||
continue;
|
||||
}
|
||||
@@ -139,7 +199,7 @@ class JobFilterEngine {
|
||||
|
||||
scored.sort((a, b) => b.matchScore - a.matchScore);
|
||||
console.log(`[jobFilterEngine] filterAndScoreJobsForDeliver 结束: 原始${jobs.length} 通过${scored.length}`);
|
||||
return scored;
|
||||
return { scored, skipReasonByJobId };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -172,7 +172,7 @@ class KeywordMatcher {
|
||||
if (filterKeywords.length > 0 && !filterResult.matched) {
|
||||
return {
|
||||
pass: false,
|
||||
reason: '不包含任何必需关键词',
|
||||
reason: `未命中包含关键词: ${filterKeywords.join(', ')}`,
|
||||
score: 0,
|
||||
details: { filter: filterResult }
|
||||
};
|
||||
@@ -204,7 +204,7 @@ class KeywordMatcher {
|
||||
return [];
|
||||
}
|
||||
|
||||
const textExtractor=(job) => `${job.jobTitle || ''} ${job.companyIndustry || ''}`;
|
||||
const textExtractor=(job) => `${job.jobTitle || ''}`;
|
||||
|
||||
const filtered = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user