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} 执行结果 */ 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. 搜索职位列表(从 resume_info 取 deliver_tab_label 作为 tabLabel 下发给 get_job_list 切换期望 tab) const tabLabel = resume.deliver_tab_label || ''; await this.searchJobs(sn_code, platform, keyword || accountConfig.keyword, pageCount, task.id, tabLabel); // 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: 'get_online_resume', 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; } } /** * 搜索职位列表 * @param {string} tabLabel - 投递用期望标签文案,对应 resume_info.deliver_tab_label,get_job_list 会按此选择 tab */ async searchJobs(sn_code, platform, keyword, pageCount, taskId, tabLabel = '') { const params = { sn_code, keyword, platform, pageCount }; if (tabLabel != null && String(tabLabel).trim() !== '') { params.tabLabel = String(tabLabel).trim(); } const getJobListCommand = { command_type: 'get_job_list', command_name: 'get_job_list', command_params: JSON.stringify(params), 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 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: 'deliver_resume', 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;