This commit is contained in:
张成
2026-04-16 14:01:52 +08:00
parent 7ef0c68ad1
commit df0aacc782
10 changed files with 531 additions and 22 deletions

View File

@@ -350,7 +350,11 @@ class JobManager {
if (notPassedIds.length > 0) {
try {
await job_postings.update(
{ applyStatus: 'filtered' },
{
applyStatus: 'filtered',
is_delivered: false,
deliver_failed_reason: '未通过搜索并投递筛选规则'
},
{
where: {
id: { [Op.in]: notPassedIds },
@@ -650,6 +654,13 @@ class JobManager {
// 检查是否已存在投递记录(避免重复投递同一职位)
const existingApply = await apply_records.findOne({ where: { sn_code, jobId: jobData.jobId } });
if (existingApply) {
await job_postings.update(
{
is_delivered: false,
deliver_failed_reason: '该岗位已投递过'
},
{ where: { id: jobData.id } }
);
console.log(`[工作管理] 跳过已投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`);
return {
success: false,
@@ -684,6 +695,14 @@ class JobManager {
if (recentCompanyApply) {
const daysAgo = Math.floor((new Date() - new Date(recentCompanyApply.applyTime)) / (1000 * 60 * 60 * 24));
const skip_reason = `该公司在${daysAgo}天前已投递过30天内不重复投递`;
await job_postings.update(
{
is_delivered: false,
deliver_failed_reason: skip_reason
},
{ where: { id: jobData.id } }
);
console.log(`[工作管理] 跳过30天内已投递的公司: ${jobData.companyName} (${daysAgo}天前投递过)`);
return {
success: false,
@@ -716,7 +735,12 @@ class JobManager {
if (response && response.code === 200) {
// 投递成功
await job_postings.update(
{ applyStatus: 'applied', applyTime: new Date() },
{
applyStatus: 'applied',
applyTime: new Date(),
is_delivered: true,
deliver_failed_reason: ''
},
{ where: { id: jobData.id } }
);
@@ -795,8 +819,13 @@ class JobManager {
};
} else {
// 投递失败
const fail_msg = String(response?.message || response?.msg || '投递失败').slice(0, 65000);
await job_postings.update(
{ applyStatus: 'failed' },
{
applyStatus: 'failed',
is_delivered: false,
deliver_failed_reason: fail_msg
},
{ where: { id: jobData.id } }
);

View File

@@ -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);
}

View File

@@ -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 };
}
/**

View File

@@ -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 = [];