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} 执行结果 */ 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;