diff --git a/api/middleware/job/services/jobFilterService.js b/api/middleware/job/services/jobFilterService.js index edf6d6a..8ad6942 100644 --- a/api/middleware/job/services/jobFilterService.js +++ b/api/middleware/job/services/jobFilterService.js @@ -655,71 +655,6 @@ class JobFilterService { return null; } - /** - * 过滤职位列表(基于文本匹配) - * @param {Array} jobs - 职位列表 - * @param {object} filterRules - 过滤规则 - * @param {object} resumeInfo - 简历信息(可选) - * @param {number} jobTypeId - 职位类型ID(可选) - * @param {object} options - 选项 - * @param {boolean} options.autoSave - 是否自动保存评分结果到数据库(默认false) - * @returns {Promise} 过滤后的职位列表(带匹配分数) - */ - async filterJobs(jobs, filterRules = {}, resumeInfo = {}, jobTypeId = null, options = {}) { - const { - minScore = 60, // 最低匹配分数 - excludeOutsourcing = true, // 是否排除外包 - excludeKeywords = [] // 额外排除关键词 - } = filterRules; - - const { autoSave = false } = options; - - // 获取职位类型配置 - const { excludeKeywords: typeExcludeKeywords } = await this.getJobTypeConfig(jobTypeId); - const allExcludeKeywords = [...typeExcludeKeywords, ...excludeKeywords]; - - const results = []; - for (const job of jobs) { - const jobData = job.toJSON ? job.toJSON() : job; - - // 分析匹配度(如果 autoSave 为 true 且 job 有 id,则自动保存) - const analysisOptions = autoSave && jobData.id ? { - jobPostingId: jobData.id, - autoSave: true - } : {}; - - const analysis = await this.analyzeJobMatch(jobData, resumeInfo, jobTypeId, analysisOptions); - - results.push({ - ...jobData, - matchScore: analysis.overallScore, - matchAnalysis: analysis - }); - } - - return results - .filter(job => { - // 1. 最低分数过滤 - if (job.matchScore < minScore) { - return false; - } - - // 2. 外包过滤 - if (excludeOutsourcing && job.matchAnalysis.isOutsourcing) { - return false; - } - - // 3. 排除关键词过滤 - const jobText = this.buildJobText(job); - if (allExcludeKeywords.some(kw => jobText.includes(kw.toLowerCase()))) { - return false; - } - - return true; - }) - .sort((a, b) => b.matchScore - a.matchScore); // 按匹配分数降序排序 - } - /** * 根据自定义权重配置计算职位评分 * @param {Object} jobData - 职位数据 diff --git a/api/middleware/schedule/handlers/deliverHandler.js b/api/middleware/schedule/handlers/deliverHandler.js index 58eada0..9da37a4 100644 --- a/api/middleware/schedule/handlers/deliverHandler.js +++ b/api/middleware/schedule/handlers/deliverHandler.js @@ -4,7 +4,6 @@ const jobFilterEngine = require('../services/jobFilterEngine'); const command = require('../core/command'); const config = require('../infrastructure/config'); const db = require('../../dbProxy'); -const { jobFilterService } = require('../../job/services'); /** * 自动投递处理器 @@ -105,13 +104,13 @@ class DeliverHandler extends BaseHandler { // 9. 过滤已投递的公司 const recentCompanies = await this.getRecentDeliveredCompanies(sn_code, 30); - // 10. 过滤、评分、排序职位 - const filteredJobs = await this.filterAndScoreJobs( + // 10. 过滤 + 评分 + 按 60 分阈值筛(入口在 jobFilterEngine,便于阅读) + const filteredJobs = await jobFilterEngine.filterAndScoreJobsForDeliver( pendingJobs, + filterConfig, resume, accountConfig, jobTypeConfig, - filterConfig, recentCompanies ); @@ -338,72 +337,6 @@ class DeliverHandler extends BaseHandler { return new Set(recentApplies.map(apply => apply.companyName).filter(Boolean)); } - /** - * 过滤和评分职位 - */ - async filterAndScoreJobs(jobs, resume, accountConfig, jobTypeConfig, filterConfig, recentCompanies) { - const scored = []; - const jobDesc = (j) => `${j.companyName || '?'} / ${j.jobTitle || '?'}`; - - console.log(`[自动投递-过滤] 开始过滤与评分,待处理职位数: ${jobs.length}`); - - for (const job of jobs) { - // 1. 过滤近期已投递的公司 - if (job.companyName && recentCompanies.has(job.companyName)) { - console.log(`[自动投递-过滤] 步骤1-已投递公司 剔除: ${jobDesc(job)}`); - continue; - } - - // 2. 使用 jobFilterEngine 过滤和评分 - const filtered = await jobFilterEngine.filterJobs([job], filterConfig, resume); - if (filtered.length === 0) { - console.log(`[自动投递-过滤] 步骤2-jobFilterEngine 剔除: ${jobDesc(job)} (不满足过滤条件)`); - 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) { - console.log(`[自动投递-过滤] 步骤5-评分不足(>=60) 剔除: ${jobDesc(job)} | 总分=${finalScore.toFixed(1)} (基础=${scoreResult.totalScore?.toFixed(1)}, 关键词奖励=${keywordBonus.score})`); - continue; - } - - scored.push({ - ...job, - matchScore: finalScore, - scoreDetails: { - ...scoreResult.scores, - keywordBonus: keywordBonus.score - } - }); - console.log(`[自动投递-过滤] 通过: ${jobDesc(job)} | 总分=${finalScore.toFixed(1)}`); - } - - // 按评分降序排序 - scored.sort((a, b) => b.matchScore - a.matchScore); - - console.log(`[自动投递-过滤] 结束: 原始=${jobs.length}, 通过=${scored.length}, 剔除=${jobs.length - scored.length}`); - return scored; - } - /** * 创建投递指令 */ diff --git a/api/middleware/schedule/services/configManager.js b/api/middleware/schedule/services/configManager.js index ba5347b..1e0cc36 100644 --- a/api/middleware/schedule/services/configManager.js +++ b/api/middleware/schedule/services/configManager.js @@ -63,7 +63,11 @@ class ConfigManager { page_count: 3, // 搜索页数 keywords: [], // 搜索关键词 exclude_keywords: [], // 排除关键词 - time_range: null // 时间范围 + time_range: null, // 时间范围 + city: '', // 城市 + salary_range: '', // 薪资范围 + experience: '', // 经验 + education: '' // 学历 }; return this.parseConfig(searchConfig, defaultConfig); diff --git a/api/middleware/schedule/services/jobFilterEngine.js b/api/middleware/schedule/services/jobFilterEngine.js index 16f955b..ee11028 100644 --- a/api/middleware/schedule/services/jobFilterEngine.js +++ b/api/middleware/schedule/services/jobFilterEngine.js @@ -3,18 +3,19 @@ const KeywordMatcher = require('../utils/keywordMatcher'); const db = require('../../dbProxy'); /** - * 职位过滤引擎 - * 综合处理职位的过滤、评分和排序 + * 职位过滤引擎(schedule 自动投递用) + * 本文件集中:过滤(薪资/关键词/活跃度/去重)+ 评分(权重分+关键词奖励)+ 按分数阈值筛。 + * 自动投递只需调 filterAndScoreJobsForDeliver 一个方法。 */ class JobFilterEngine { /** - * 过滤职位列表 + * 过滤职位列表(薪资 → 关键词 → 活跃度 → 去重) * @param {Array} jobs - 职位列表 * @param {object} config - 过滤配置 - * @param {object} resumeInfo - 简历信息 + * @param {object} resumeInfo - 简历信息(未使用,兼容签名) * @returns {Promise} 过滤后的职位列表 */ - async filterJobs(jobs, config, resumeInfo = {}) { + async filterJobs(jobs, config) { if (!jobs || jobs.length === 0) { return []; } @@ -61,6 +62,59 @@ class JobFilterEngine { return filtered; } + /** + * 自动投递用:过滤 + 评分 + 按 60 分阈值筛,一次调用完成(便于阅读与维护) + */ + async filterAndScoreJobsForDeliver(jobs, filterConfig, resume, accountConfig, jobTypeConfig, recentCompanies) { + const scored = []; + const jobDesc = (j) => `${j.companyName || '?'} / ${j.jobTitle || '?'}`; + const { jobFilterService } = require('../../job/services'); + + console.log(`[jobFilterEngine] filterAndScoreJobsForDeliver 开始,待处理: ${jobs.length}`); + + for (const job of jobs) { + if (job.companyName && recentCompanies.has(job.companyName)) { + console.log(`[jobFilterEngine] 已投递公司 剔除: ${jobDesc(job)}`); + continue; + } + + const filtered = await this.filterJobs([job], filterConfig, resume); + if (filtered.length === 0) { + console.log(`[jobFilterEngine] 过滤条件不通过 剔除: ${jobDesc(job)}`); + continue; + } + + const scoreResult = jobFilterService.calculateJobScoreWithWeights( + job, + resume, + accountConfig, + jobTypeConfig, + accountConfig.is_salary_priority || [] + ); + const keywordBonus = KeywordMatcher.calculateBonus( + `${job.jobTitle || ''} ${job.companyName || ''} ${job.jobDescription || ''}`, + filterConfig.filter_keywords || [], + { baseScore: 5, maxBonus: 20 } + ); + const finalScore = scoreResult.totalScore + keywordBonus.score; + + if (finalScore < 60) { + console.log(`[jobFilterEngine] 评分不足(>=60) 剔除: ${jobDesc(job)} 总分=${finalScore.toFixed(1)}`); + continue; + } + + scored.push({ + ...job, + matchScore: finalScore, + scoreDetails: { ...scoreResult.scores, keywordBonus: keywordBonus.score } + }); + } + + scored.sort((a, b) => b.matchScore - a.matchScore); + console.log(`[jobFilterEngine] filterAndScoreJobsForDeliver 结束: 原始${jobs.length} 通过${scored.length}`); + return scored; + } + /** * 按薪资过滤 * @param {Array} jobs - 职位列表 @@ -99,14 +153,6 @@ class JobFilterEngine { return KeywordMatcher.filterJobs(jobs, { excludeKeywords: exclude_keywords, filterKeywords: filter_keywords - }, (job) => { - // 组合职位名称、描述、技能要求等 - return [ - job.name || job.jobName || '', - job.description || job.jobDescription || '', - job.skills || '', - job.welfare || '' - ].join(' '); }); } @@ -376,39 +422,6 @@ class JobFilterEngine { return sorted; } - - /** - * 综合处理:过滤 + 评分 + 排序 - * @param {Array} jobs - 职位列表 - * @param {object} config - 过滤配置 - * @param {object} resumeInfo - 简历信息 - * @param {object} options - 选项 - * @returns {Promise} 处理后的职位列表 - */ - async process(jobs, config, resumeInfo = {}, options = {}) { - const { - maxCount = 10, // 最大返回数量 - sortBy = 'score' // 排序方式 - } = options; - - // 1. 过滤 - let filtered = await this.filterJobs(jobs, config, resumeInfo); - - console.log(`[职位过滤] 原始: ${jobs.length} 个,过滤后: ${filtered.length} 个`); - - // 2. 评分 - const scored = this.scoreJobs(filtered, resumeInfo, config); - - // 3. 排序 - const sorted = this.sortJobs(scored, sortBy); - - // 4. 截取 - const result = sorted.slice(0, maxCount); - - console.log(`[职位过滤] 最终返回: ${result.length} 个职位`); - - return result; - } } // 导出单例 diff --git a/api/middleware/schedule/tasks/autoDeliverTask.js b/api/middleware/schedule/tasks/autoDeliverTask.js index 119f09d..f537746 100644 --- a/api/middleware/schedule/tasks/autoDeliverTask.js +++ b/api/middleware/schedule/tasks/autoDeliverTask.js @@ -2,6 +2,7 @@ const BaseTask = require('./baseTask'); const db = require('../../dbProxy'); const config = require('../infrastructure/config'); const authorizationService = require('../../../services/authorization_service'); +const ConfigManager = require('../services/configManager'); /** * 自动投递任务 @@ -54,7 +55,7 @@ class AutoDeliverTask extends BaseTask { } // 3. 获取投递配置 - const deliverConfig = this.parseDeliverConfig(account.deliver_config); + const deliverConfig = ConfigManager.parseDeliverConfig(account.deliver_config); // 4. 检查日投递限制 const dailyLimit = config.platformDailyLimits[account.platform_type] || 50; @@ -108,30 +109,6 @@ class AutoDeliverTask extends BaseTask { return account ? account.toJSON() : null; } - /** - * 解析投递配置 - */ - parseDeliverConfig(deliver_config) { - if (typeof deliver_config === 'string') { - try { - deliver_config = JSON.parse(deliver_config); - } catch (e) { - deliver_config = {}; - } - } - - return { - deliver_interval: deliver_config?.deliver_interval || 30, - min_salary: deliver_config?.min_salary || 0, - max_salary: deliver_config?.max_salary || 0, - page_count: deliver_config?.page_count || 3, - max_deliver: deliver_config?.max_deliver || 10, - filter_keywords: deliver_config?.filter_keywords || [], - exclude_keywords: deliver_config?.exclude_keywords || [], - time_range: deliver_config?.time_range || null - }; - } - /** * 获取今日已投递数量 */ @@ -217,7 +194,7 @@ class AutoDeliverTask extends BaseTask { } // 3. 获取投递配置 - const deliverConfig = this.parseDeliverConfig(account.deliver_config); + const deliverConfig = ConfigManager.parseDeliverConfig(account.deliver_config); // 4. 检查时间范围 if (deliverConfig.time_range) { diff --git a/api/middleware/schedule/tasks/autoSearchTask.js b/api/middleware/schedule/tasks/autoSearchTask.js index 0e8a08e..a41753f 100644 --- a/api/middleware/schedule/tasks/autoSearchTask.js +++ b/api/middleware/schedule/tasks/autoSearchTask.js @@ -1,6 +1,7 @@ const BaseTask = require('./baseTask'); const db = require('../../dbProxy'); const config = require('../infrastructure/config'); +const ConfigManager = require('../services/configManager'); /** * 自动搜索职位任务 @@ -53,7 +54,7 @@ class AutoSearchTask extends BaseTask { } // 2. 获取搜索配置 - const searchConfig = this.parseSearchConfig(account.search_config); + const searchConfig = ConfigManager.parseSearchConfig(account.search_config); // 3. 检查日搜索限制 const dailyLimit = config.dailyLimits.maxSearch || 20; @@ -93,29 +94,6 @@ class AutoSearchTask extends BaseTask { return account ? account.toJSON() : null; } - /** - * 解析搜索配置 - */ - parseSearchConfig(search_config) { - if (typeof search_config === 'string') { - try { - search_config = JSON.parse(search_config); - } catch (e) { - search_config = {}; - } - } - - return { - search_interval: search_config?.search_interval || 60, - page_count: search_config?.page_count || 3, - city: search_config?.city || '', - salary_range: search_config?.salary_range || '', - experience: search_config?.experience || '', - education: search_config?.education || '', - time_range: search_config?.time_range || null - }; - } - /** * 获取今日已搜索数量 */ @@ -162,7 +140,7 @@ class AutoSearchTask extends BaseTask { } // 3. 获取搜索配置 - const searchConfig = this.parseSearchConfig(account.search_config); + const searchConfig = ConfigManager.parseSearchConfig(account.search_config); // 4. 检查时间范围 if (searchConfig.time_range) { diff --git a/api/middleware/schedule/utils/keywordMatcher.js b/api/middleware/schedule/utils/keywordMatcher.js index 11c579d..e9bfb28 100644 --- a/api/middleware/schedule/utils/keywordMatcher.js +++ b/api/middleware/schedule/utils/keywordMatcher.js @@ -199,11 +199,13 @@ class KeywordMatcher { * @param {Function} textExtractor - 文本提取函数 (job) => string * @returns {Array} 匹配通过的职位(带匹配信息) */ - static filterJobs(jobs, config, textExtractor = (job) => `${job.name || ''} ${job.description || ''}`) { + static filterJobs(jobs, config) { if (!jobs || jobs.length === 0) { return []; } + const textExtractor=(job) => `${job.jobTitle || ''} ${job.companyIndustry || ''}`; + const filtered = []; for (const job of jobs) {