// const aiService = require('./aiService'); // 二期规划:AI 服务暂时禁用 const jobFilterService = require('./job_filter_service'); // 使用文本匹配过滤服务 const locationService = require('../../services/location_service'); // 位置服务 const logs = require('../logProxy'); const db = require('../dbProxy'); const { v4: uuidv4 } = require('uuid'); /** * 工作管理模块 * 负责简历获取、分析、存储和匹配度计算 */ class JobManager { constructor() { } // 启动客户端那个平台 用户信息,心跳机制 async set_user_info(sn_code, mqttClient, user_info) { const response = await mqttClient.publishAndWait(sn_code, { platform: platform, action: 'set_user_info', data: { user_info: user_info } }); } /** * 获取登录二维码 * @param {string} sn_code - 设备SN码 * @param {object} mqttClient - MQTT客户端 * @param {object} params - 参数对象 * @returns {Promise} 二维码信息 */ async get_login_qr_code(sn_code, mqttClient, params = {}) { console.log(`[工作管理] 开始获取设备 ${sn_code} 的登录二维码`); const { platform = 'boss' } = params; // 通过MQTT指令获取登录二维码 const response = await mqttClient.publishAndWait(sn_code, { platform: platform, action: 'get_login_qr_code', data: {} }); if (!response || response.code !== 200) { console.error(`[工作管理] 获取登录二维码失败:`, response); throw new Error(response?.message || '获取登录二维码失败'); } const qrCodeData = response.data; console.log(`[工作管理] 成功获取登录二维码数据:`, qrCodeData); return qrCodeData; } /** * 打开机器人检测测试页 * @param {string} sn_code - 设备SN码 * @param {object} mqttClient - MQTT客户端 * @param {object} params - 参数对象 * @returns {Promise} 执行结果 */ async open_bot_detection(sn_code, mqttClient, params = {}) { console.log(`[工作管理] 开始打开设备 ${sn_code} 的机器人检测测试页`); const { platform = 'boss' } = params; // 通过MQTT指令打开机器人检测测试页 const response = await mqttClient.publishAndWait(sn_code, { platform: platform, action: 'open_bot_detection', data: {} }); if (!response || response.code !== 200) { console.error(`[工作管理] 打开机器人检测测试页失败:`, response); throw new Error(response?.message || '打开机器人检测测试页失败'); } const result = response.data; console.log(`[工作管理] 成功打开机器人检测测试页:`, result); return result; } /** * 获取用户信息 * @param {string} sn_code - 设备SN码 * @param {object} mqttClient - MQTT客户端 * @param {object} params - 参数对象 * @returns {Promise} 用户信息 */ async get_user_info(sn_code, mqttClient, params = {}) { console.log(`[工作管理] 开始获取设备 ${sn_code} 的用户信息`); const { platform = 'boss' } = params; // 通过MQTT指令获取用户信息 const response = await mqttClient.publishAndWait(sn_code, { platform: platform, action: 'get_user_info', data: {} }); if (!response || response.code !== 200) { console.error(`[工作管理] 获取用户信息失败:`, response); throw new Error(response?.message || '获取用户信息失败'); } const userInfo = response.data; console.log(`[工作管理] 成功获取用户信息:`, userInfo); return userInfo; } /** * 搜索岗位 * @param {string} sn_code - 设备SN码 * @param {object} mqttClient - MQTT客户端 * @param {object} params - 参数 * @returns {Promise} 搜索结果 */ async search_jobs(sn_code, mqttClient, params = {}) { const { keyword = '前端', platform = 'boss' } = params; console.log(`[工作管理] 开始搜索设备 ${sn_code} 的岗位,关键词: ${keyword}`); // 通过MQTT指令搜索岗位 const response = await mqttClient.publishAndWait(sn_code, { platform, action: "search_jobs", data: { keyword } }); if (!response || response.code !== 200) { console.error(`[工作管理] 搜索岗位失败:`, response); throw new Error('搜索岗位失败'); } console.log(`[工作管理] 成功搜索岗位`); return response.data; } /** * 多条件搜索职位列表(新指令,使用新的MQTT action) * @param {string} sn_code - 设备SN码 * @param {object} mqttClient - MQTT客户端 * @param {object} params - 搜索参数 * @returns {Promise} 搜索结果 */ async search_jobs_with_params(sn_code, mqttClient, params = {}) { const { keyword = '前端', platform = 'boss', city = '', cityName = '', salary = '', experience = '', education = '', industry = '', companySize = '', financingStage = '', page = 1, pageSize = 20, pageCount = 3 } = params; console.log(`[工作管理] 开始多条件搜索设备 ${sn_code} 的职位,关键词: ${keyword}, 城市: ${cityName || city}`); // 构建完整的搜索参数对象 const searchData = { keyword, pageCount }; // 添加可选搜索条件 if (city) searchData.city = city; if (cityName) searchData.cityName = cityName; if (salary) searchData.salary = salary; if (experience) searchData.experience = experience; if (education) searchData.education = education; if (industry) searchData.industry = industry; if (companySize) searchData.companySize = companySize; if (financingStage) searchData.financingStage = financingStage; if (page) searchData.page = page; if (pageSize) searchData.pageSize = pageSize; // 通过MQTT指令获取岗位列表(使用新的action) const response = await mqttClient.publishAndWait(sn_code, { platform, action: "search_job_list", // 新的搜索action data: searchData }); if (!response || response.code !== 200) { console.error(`[工作管理] 多条件搜索职位失败:`, response); throw new Error('多条件搜索职位失败'); } // 处理职位列表数据 let jobs = []; if (Array.isArray(response.data)) { for (const item of response.data) { if (item.data?.zpData?.jobList && Array.isArray(item.data.zpData.jobList)) { jobs = jobs.concat(item.data.zpData.jobList); } } } else if (response.data?.data?.zpData?.jobList) { jobs = response.data.data.zpData.jobList || []; } else if (response.data?.zpData?.jobList) { jobs = response.data.zpData.jobList || []; } console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`); // 保存职位到数据库 try { await this.saveJobsToDatabase(sn_code, platform, keyword, jobs); } catch (error) { console.error(`[工作管理] 保存职位到数据库失败:`, error); } return { jobs: jobs, keyword: keyword, platform: platform, count: jobs.length }; } /** * 搜索并投递职位(新指令) * @param {string} sn_code - 设备SN码 * @param {object} mqttClient - MQTT客户端 * @param {object} params - 参数 * @returns {Promise} 执行结果 */ async search_and_deliver(sn_code, mqttClient, params = {}) { const { keyword, searchParams = {}, pageCount = 3, filterRules = {}, maxCount = 10, platform = 'boss' } = params; console.log(`[工作管理] 开始搜索并投递职位,设备: ${sn_code}, 关键词: ${keyword}`); // 1. 先执行搜索(使用search_jobs_with_params,新的搜索指令) const searchResult = await this.search_jobs_with_params(sn_code, mqttClient, { keyword, platform, ...searchParams, pageCount }); if (!searchResult || searchResult.count === 0) { return { success: true, jobCount: 0, deliveredCount: 0, message: '未找到职位' }; } // 2. 等待数据保存完成 await new Promise(resolve => setTimeout(resolve, 2000)); // 3. 从数据库获取刚搜索到的职位 const job_postings = db.getModel('job_postings'); const searchedJobs = await job_postings.findAll({ where: { sn_code: sn_code, platform: platform, applyStatus: 'pending', keyword: keyword }, order: [['create_time', 'DESC']], limit: 1000 }); if (searchedJobs.length === 0) { return { success: true, jobCount: searchResult.count, deliveredCount: 0, message: '未找到待投递的职位' }; } // 4. 获取简历信息用于匹配 const resume_info = db.getModel('resume_info'); const resume = await resume_info.findOne({ where: { sn_code: sn_code, platform: platform, isActive: true }, order: [['last_modify_time', 'DESC']] }); if (!resume) { return { success: true, jobCount: searchResult.count, deliveredCount: 0, message: '未找到活跃简历,无法投递' }; } // 5. 获取账号配置 const pla_account = db.getModel('pla_account'); const account = await pla_account.findOne({ where: { sn_code, platform_type: platform } }); if (!account) { throw new Error('账号不存在'); } const accountConfig = account.toJSON(); const resumeData = resume.toJSON(); // 6. 使用过滤方法进行职位匹配 const matchedJobs = await this.filter_jobs_by_rules(searchedJobs, { minSalary: filterRules.minSalary || 0, maxSalary: filterRules.maxSalary || 0, keywords: filterRules.keywords || [], excludeKeywords: filterRules.excludeKeywords || [], accountConfig: accountConfig, resumeInfo: resumeData }); // 7. 限制投递数量 const jobsToDeliver = matchedJobs.slice(0, maxCount); console.log(`[工作管理] 匹配到 ${matchedJobs.length} 个职位,将投递 ${jobsToDeliver.length} 个`); // 8. 执行投递 let deliveredCount = 0; const apply_records = db.getModel('apply_records'); for (let i = 0; i < jobsToDeliver.length; i++) { const job = jobsToDeliver[i]; const jobData = job.toJSON ? job.toJSON() : job; try { // 从原始数据中获取 securityId let securityId = jobData.securityId || ''; try { if (jobData.originalData) { const originalData = typeof jobData.originalData === 'string' ? JSON.parse(jobData.originalData) : jobData.originalData; securityId = originalData.securityId || securityId; } } catch (e) { console.warn(`[工作管理] 解析职位原始数据失败:`, e); } // 执行投递(使用新的deliver_resume_search action) const deliverResult = await this.deliver_resume(sn_code, mqttClient, { jobId: jobData.jobId, encryptBossId: jobData.encryptBossId || '', securityId: securityId, brandName: jobData.companyName || '', jobTitle: jobData.jobTitle || '', companyName: jobData.companyName || '', platform: platform, action: 'deliver_resume_search' // 搜索并投递使用新的action }); if (deliverResult && deliverResult.success) { deliveredCount++; } // 投递间隔控制 if (i < jobsToDeliver.length - 1) { await new Promise(resolve => setTimeout(resolve, 3000)); } } catch (error) { console.error(`[工作管理] 投递职位失败:`, error); // 继续投递下一个职位 } } return { success: true, jobCount: searchResult.count, deliveredCount: deliveredCount, message: `搜索完成,找到 ${searchResult.count} 个职位,成功投递 ${deliveredCount} 个` }; } /** * 获取岗位列表(支持多条件搜索) * @param {string} sn_code - 设备SN码 * @param {object} mqttClient - MQTT客户端 * @param {object} params - 参数 * @returns {Promise} 岗位列表 */ async get_job_list(sn_code, mqttClient, params = {}) { const { keyword = '前端', platform = 'boss', pageCount = 3, city = '', cityName = '', salary = '', experience = '', education = '', industry = '', companySize = '', financingStage = '', page = 1, pageSize = 20 } = params; // 判断是否是多条件搜索(如果包含多条件参数,使用多条件搜索逻辑) const hasMultiParams = city || cityName || salary || experience || education || industry || companySize || financingStage || page || pageSize; if (hasMultiParams) { // 使用多条件搜索逻辑 console.log(`[工作管理] 开始多条件搜索设备 ${sn_code} 的职位,关键词: ${keyword}, 城市: ${cityName || city}`); // 构建完整的搜索参数对象 const searchData = { keyword, pageCount }; // 添加可选搜索条件 if (city) searchData.city = city; if (cityName) searchData.cityName = cityName; if (salary) searchData.salary = salary; if (experience) searchData.experience = experience; if (education) searchData.education = education; if (industry) searchData.industry = industry; if (companySize) searchData.companySize = companySize; if (financingStage) searchData.financingStage = financingStage; if (page) searchData.page = page; if (pageSize) searchData.pageSize = pageSize; // 通过MQTT指令获取岗位列表(保持action不变,前端已使用) const response = await mqttClient.publishAndWait(sn_code, { platform, action: "get_job_list", // 保持与原有get_job_list相同的action,前端已使用 data: searchData }); if (!response || response.code !== 200) { console.error(`[工作管理] 多条件搜索职位失败:`, response); throw new Error('多条件搜索职位失败'); } // 处理职位列表数据 let jobs = []; if (Array.isArray(response.data)) { for (const item of response.data) { if (item.data?.zpData?.jobList && Array.isArray(item.data.zpData.jobList)) { jobs = jobs.concat(item.data.zpData.jobList); } } } else if (response.data?.data?.zpData?.jobList) { jobs = response.data.data.zpData.jobList || []; } else if (response.data?.zpData?.jobList) { jobs = response.data.zpData.jobList || []; } console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`); // 保存职位到数据库 try { await this.saveJobsToDatabase(sn_code, platform, keyword, jobs); } catch (error) { console.error(`[工作管理] 保存职位到数据库失败:`, error); } return { jobs: jobs, keyword: keyword, platform: platform, count: jobs.length }; } // 简单搜索逻辑(保持原有逻辑) console.log(`[工作管理] 开始获取设备 ${sn_code} 的岗位列表,关键词: ${keyword}`); // 通过MQTT指令获取岗位列表 const response = await mqttClient.publishAndWait(sn_code, { platform, action: "get_job_list", data: { keyword, pageCount } }); if (!response || response.code !== 200) { console.error(`[工作管理] 获取岗位列表失败:`, response); throw new Error('获取岗位列表失败'); } // 处理职位列表数据:response.data 可能是数组(职位列表.json 格式)或单个对象 let jobs = []; if (Array.isArray(response.data)) { // 如果是数组格式(职位列表.json),遍历每个元素提取岗位数据 for (const item of response.data) { if (item.data?.zpData?.jobList && Array.isArray(item.data.zpData.jobList)) { jobs = jobs.concat(item.data.zpData.jobList); } } console.log(`[工作管理] 从 ${response.data.length} 个响应中提取岗位数据`); } else if (response.data?.data?.zpData?.jobList) { // 如果是单个对象格式,从 data.zpData.jobList 获取 jobs = response.data.data.zpData.jobList || []; } else if (response.data?.zpData?.jobList) { // 兼容旧格式:直接从 zpData.jobList 获取 jobs = response.data.zpData.jobList || []; } console.log(`[工作管理] 成功获取岗位数据,共 ${jobs.length} 个岗位`); // 保存职位到数据库 try { await this.saveJobsToDatabase(sn_code, platform, keyword, jobs); } catch (error) { console.error(`[工作管理] 保存职位到数据库失败:`, error); // 不影响主流程,继续返回数据 } const result = { jobs: jobs, keyword: keyword, platform: platform, count: jobs.length }; return result; } /** * 保存职位列表到数据库 * @param {string} sn_code - 设备SN码 * @param {string} platform - 平台 * @param {string} keyword - 搜索关键词 * @param {Array} jobs - 职位列表 */ async saveJobsToDatabase(sn_code, platform, keyword, jobs) { const job_postings = db.getModel('job_postings'); console.log(`[工作管理] 开始保存 ${jobs.length} 个职位到数据库`); for (const job of jobs) { try { // 构建职位信息对象 const jobInfo = { sn_code, platform, keyword, // Boss直聘字段映射 encryptBossId: job.encryptBossId || '', jobId: job.encryptJobId || '', jobTitle: job.jobName || '', companyId: job.encryptBrandId || '', companyName: job.brandName || '', companySize: job.brandScaleName || '', companyIndustry: job.brandIndustry || '', salary: job.salaryDesc || '', // 岗位要求(从 jobLabels 和 skills 提取) jobRequirements: JSON.stringify({ experience: job.jobExperience || '', education: job.jobDegree || '', labels: job.jobLabels || [], skills: job.skills || [] }), // 工作地点 location: [job.cityName, job.areaDistrict, job.businessDistrict] .filter(Boolean).join(' '), experience: job.jobExperience || '', education: job.jobDegree || '', // 原始数据 originalData: JSON.stringify(job), // 默认状态 applyStatus: 'pending', chatStatus: 'none' }; // 调用位置服务解析 location + companyName 获取坐标 if (jobInfo.location && jobInfo.companyName) { const addressToParse = `${jobInfo.location.replaceAll(' ', '')}${jobInfo.companyName}`; // 等待 1秒 await new Promise(resolve => setTimeout(resolve, 1000)); const location = await locationService.getLocationByAddress(addressToParse).catch(error => { console.error(`[工作管理] 获取位置失败:`, error); }); if (location) { jobInfo.latitude = String(location.lat); jobInfo.longitude = String(location.lng); } } // 检查是否已存在(根据 jobId 和 sn_code) const existingJob = await job_postings.findOne({ where: { jobId: jobInfo.jobId, sn_code: sn_code } }); if (existingJob) { // 更新现有职位 await job_postings.update(jobInfo, { where: { jobId: jobInfo.jobId, sn_code: sn_code } }); console.log(`[工作管理] 职位已更新 - ${jobInfo.jobTitle} @ ${jobInfo.companyName}`); } else { // 创建新职位 await job_postings.create(jobInfo); console.log(`[工作管理] 职位已创建 - ${jobInfo.jobTitle} @ ${jobInfo.companyName}`); } } catch (error) { console.error(`[工作管理] 保存职位失败:`, error, job); // 继续处理下一个职位 } } console.log(`[工作管理] 职位保存完成`); } /** * 投递简历(单个职位) * @param {string} sn_code - 设备SN码 * @param {object} mqttClient - MQTT客户端 * @param {object} params - 参数 * @param {string} params.jobId - 职位ID(必需) * @param {string} params.platform - 平台(默认boss) * @param {string} params.encryptBossId - 加密的Boss ID(可选) * @param {string} params.securityId - 安全ID(可选) * @param {string} params.brandName - 公司名称(可选) * @param {string} params.jobTitle - 职位标题(可选) * @param {string} params.companyName - 公司名称(可选) * @param {string} params.action - MQTT Action(默认:deliver_resume,可选:deliver_resume_search) * @returns {Promise} 投递结果 */ async deliver_resume(sn_code, mqttClient, params = {}) { const { platform = 'boss', jobId, encryptBossId, securityId, brandName, jobTitle, companyName, action = 'deliver_resume' } = params; if (!jobId) { throw new Error('jobId 参数不能为空,请指定要投递的职位ID'); } console.log(`[工作管理] 开始投递单个职位,设备: ${sn_code}, 职位: ${jobTitle || jobId}`); const job_postings = db.getModel('job_postings'); const apply_records = db.getModel('apply_records'); try { // 从数据库获取职位信息 const where = { sn_code, jobId, platform }; if (encryptBossId) where.encryptBossId = encryptBossId; const job = await job_postings.findOne({ where }); if (!job) { throw new Error(`未找到职位记录: ${jobId}`); } const jobData = job.toJSON(); // 检查是否已存在投递记录(避免重复投递同一职位) const existingApply = await apply_records.findOne({ where: { sn_code, jobId: jobData.jobId } }); if (existingApply) { console.log(`[工作管理] 跳过已投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`); return { success: false, deliveredCount: 0, failedCount: 1, message: '该岗位已投递过', deliveredJobs: [], failedJobs: [{ jobId: jobData.jobId, jobTitle: jobData.jobTitle, companyName: jobData.companyName, error: '该岗位已投递过' }] }; } // 检查该公司是否在一个月内已投递过(避免连续投递同一公司) const oneMonthAgo = new Date(); oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); const Sequelize = require('sequelize'); const recentCompanyApply = await apply_records.findOne({ where: { sn_code: sn_code, companyName: jobData.companyName, applyTime: { [Sequelize.Op.gte]: oneMonthAgo } }, order: [['applyTime', 'DESC']] }); if (recentCompanyApply) { const daysAgo = Math.floor((new Date() - new Date(recentCompanyApply.applyTime)) / (1000 * 60 * 60 * 24)); console.log(`[工作管理] 跳过一个月内已投递的公司: ${jobData.companyName} (${daysAgo}天前投递过)`); return { success: false, deliveredCount: 0, failedCount: 1, message: `该公司在${daysAgo}天前已投递过,一个月内不重复投递`, deliveredJobs: [], failedJobs: [{ jobId: jobData.jobId, jobTitle: jobData.jobTitle, companyName: jobData.companyName, error: `该公司在${daysAgo}天前已投递过,一个月内不重复投递` }] }; } console.log(`[工作管理] 投递职位: ${jobData.jobTitle} @ ${jobData.companyName}`); // 通过MQTT指令投递简历(支持自定义action) const response = await mqttClient.publishAndWait(sn_code, { platform, action: action, // 使用传入的action参数,默认为"deliver_resume" data: { encryptJobId: jobData.jobId, securityId: jobData.securityId || securityId || '', brandName: jobData.companyName || brandName || '' } }); if (response && response.code === 200) { // 投递成功 await job_postings.update( { applyStatus: 'applied', applyTime: new Date() }, { where: { id: jobData.id } } ); // 计算距离和获取评分信息 let distance = null; let locationScore = jobData.scoreDetails?.locationScore || 0; try { // 获取账号的经纬度 const pla_account = db.getModel('pla_account'); const account = await pla_account.findOne({ where: { sn_code, platform_type: platform } }); // 如果账号和职位都有经纬度,计算实际距离 if (account && account.user_latitude && account.user_longitude && jobData.latitude && jobData.longitude) { const userLat = parseFloat(account.user_latitude); const userLng = parseFloat(account.user_longitude); const jobLat = parseFloat(jobData.latitude); const jobLng = parseFloat(jobData.longitude); if (!isNaN(userLat) && !isNaN(userLng) && !isNaN(jobLat) && !isNaN(jobLng)) { distance = locationService.calculateDistance(userLat, userLng, jobLat, jobLng); console.log(`[工作管理] 计算距离: ${distance} 公里`); } } } catch (error) { console.warn(`[工作管理] 计算距离失败:`, error.message); // 距离计算失败不影响投递记录保存 } await apply_records.create({ sn_code: sn_code, platform: platform, jobId: jobData.jobId, encryptBossId: jobData.encryptBossId || '', jobTitle: jobData.jobTitle, companyName: jobData.companyName, companyId: jobData.companyId || '', salary: jobData.salary || '', location: jobData.location || '', resumeId: jobData.resumeId || '', resumeName: jobData.resumeName || '', matchScore: jobData.matchScore || jobData.scoreDetails?.totalScore || 0, isOutsourcing: jobData.isOutsourcing || false, priority: jobData.priority || 5, isAutoApply: true, applyStatus: 'success', applyTime: new Date(), feedbackStatus: 'none', taskId: jobData.taskId || '', keyword: jobData.keyword || '', originalData: JSON.stringify({ ...(response.data || {}), scoreDetails: { ...(jobData.scoreDetails || {}), locationScore: locationScore, distance: distance // 实际距离(公里) } }) }); console.log(`[工作管理] 投递成功: ${jobData.jobTitle}`); return { success: true, deliveredCount: 1, failedCount: 0, deliveredJobs: [{ jobId: jobData.jobId, jobTitle: jobData.jobTitle, companyName: jobData.companyName }], failedJobs: [] }; } else { // 投递失败 await job_postings.update( { applyStatus: 'failed' }, { where: { id: jobData.id } } ); await apply_records.create({ sn_code: sn_code, platform: platform, jobId: jobData.jobId, encryptBossId: jobData.encryptBossId || '', jobTitle: jobData.jobTitle, companyName: jobData.companyName, companyId: jobData.companyId || '', salary: jobData.salary || '', location: jobData.location || '', resumeId: jobData.resumeId || '', resumeName: jobData.resumeName || '', matchScore: jobData.matchScore || 0, isOutsourcing: jobData.isOutsourcing || false, priority: jobData.priority || 5, isAutoApply: true, applyStatus: 'failed', applyTime: new Date(), feedbackStatus: 'none', taskId: jobData.taskId || '', keyword: jobData.keyword || '', errorMessage: response?.message || '投递失败', originalData: JSON.stringify(response || {}) }); throw new Error(response?.message || '投递失败'); } } catch (error) { console.error(`[工作管理] 投递异常: ${jobTitle || jobId}`, error); throw error; } } /** * 根据规则过滤职位并评分 * 评分规则:距离30% + 关键字10% + 薪资20% + 公司规模20% = 总分 * 只有总分 >= 60 的职位才会被投递 * @param {Array} jobs - 职位列表 * @param {Object} filterRules - 过滤规则(包含账号配置和简历信息) * @returns {Promise} 过滤并评分后的职位列表(按总分降序排序) */ async filter_jobs_by_rules(jobs, filterRules) { const { minSalary = 0, maxSalary = 0, keywords = [], excludeKeywords = [], accountConfig = {}, // 账号配置 resumeInfo = {} // 简历信息 } = filterRules; // 关键词从职位类型配置中获取,不再从账号配置中获取 let filterKeywords = keywords || []; let excludeKeywordsList = excludeKeywords || []; // 使用账号配置的薪资范围 const accountMinSalary = accountConfig.min_salary || minSalary || 0; const accountMaxSalary = accountConfig.max_salary || maxSalary || 0; // 对每个职位进行评分 const scoredJobs = jobs.map(job => { const jobData = job.toJSON ? job.toJSON() : job; // 1. 距离分数(30%) const locationScore = this.calculate_location_score( jobData.location, resumeInfo.expectedLocation ); // 2. 关键字分数(10%) const keywordScore = this.calculate_keyword_score( jobData, filterKeywords ); // 3. 薪资分数(20%) const salaryScore = this.calculate_salary_score( jobData.salary, resumeInfo.expectedSalary, accountMinSalary, accountMaxSalary ); // 4. 公司规模分数(20%) const companySizeScore = this.calculate_company_size_score( jobData.companySize ); // 计算总分(累加) const totalScore = Math.round( locationScore * 0.3 + keywordScore * 0.1 + salaryScore * 0.2 + companySizeScore * 0.2 ); return { ...jobData, matchScore: totalScore, scoreDetails: { locationScore, keywordScore, salaryScore, companySizeScore, totalScore } }; }); // 过滤:排除关键词、最低分数、按总分排序 return scoredJobs .filter(job => { // 1. 排除关键词过滤 if (excludeKeywordsList && excludeKeywordsList.length > 0) { const jobText = `${job.jobTitle} ${job.companyName} ${job.jobDescription || ''}`.toLowerCase(); const hasExcluded = excludeKeywordsList.some(kw => jobText.includes(kw.toLowerCase())); if (hasExcluded) { return false; } } // 2. 最低分数过滤(总分 >= 60 才投递) if (job.matchScore < 60) { return false; } return true; }) .sort((a, b) => b.matchScore - a.matchScore); // 按总分降序排序 } /** * 计算距离分数(0-100分) * @param {string} jobLocation - 职位工作地点 * @param {string} expectedLocation - 期望工作地点 * @returns {number} 距离分数 */ calculate_location_score(jobLocation, expectedLocation) { if (!jobLocation) return 50; // 没有地点信息,给中等分数 if (!expectedLocation) return 70; // 没有期望地点,给较高分数 const jobLoc = jobLocation.toLowerCase().trim(); const expectedLoc = expectedLocation.toLowerCase().trim(); // 完全匹配 if (jobLoc === expectedLoc) { return 100; } // 包含匹配(如:期望"北京",职位"北京朝阳区") if (jobLoc.includes(expectedLoc) || expectedLoc.includes(jobLoc)) { return 90; } // 城市匹配(提取城市名) const jobCity = this.extract_city(jobLoc); const expectedCity = this.extract_city(expectedLoc); if (jobCity && expectedCity && jobCity === expectedCity) { return 80; } // 部分匹配(如:都包含"北京") if (jobLoc.includes('北京') && expectedLoc.includes('北京')) { return 70; } if (jobLoc.includes('上海') && expectedLoc.includes('上海')) { return 70; } if (jobLoc.includes('深圳') && expectedLoc.includes('深圳')) { return 70; } if (jobLoc.includes('广州') && expectedLoc.includes('广州')) { return 70; } // 不匹配 return 30; } /** * 提取城市名 * @param {string} location - 地点字符串 * @returns {string|null} 城市名 */ extract_city(location) { const cities = ['北京', '上海', '深圳', '广州', '杭州', '成都', '南京', '武汉', '西安', '苏州']; for (const city of cities) { if (location.includes(city)) { return city; } } return null; } /** * 计算关键字分数(0-100分) * @param {object} jobData - 职位数据 * @param {Array} filterKeywords - 过滤关键词列表 * @returns {number} 关键字分数 */ calculate_keyword_score(jobData, filterKeywords) { if (!filterKeywords || filterKeywords.length === 0) { return 70; // 没有关键词要求,给较高分数 } const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''} ${jobData.keyword || ''}`.toLowerCase(); // 计算匹配的关键词数量 let matchedCount = 0; filterKeywords.forEach(kw => { if (jobText.includes(kw.toLowerCase())) { matchedCount++; } }); // 计算匹配度百分比 const matchRate = matchedCount / filterKeywords.length; return Math.round(matchRate * 100); } /** * 计算薪资分数(0-100分) * @param {string} jobSalary - 职位薪资 * @param {string} expectedSalary - 期望薪资 * @param {number} accountMinSalary - 账号配置最低薪资 * @param {number} accountMaxSalary - 账号配置最高薪资 * @returns {number} 薪资分数 */ calculate_salary_score(jobSalary, expectedSalary, accountMinSalary, accountMaxSalary) { if (!jobSalary) return 50; // 没有薪资信息,给中等分数 const salaryRange = this.parse_salary_range(jobSalary); if (!salaryRange || salaryRange.min === 0) return 50; const avgJobSalary = (salaryRange.min + salaryRange.max) / 2; // 优先使用账号配置的薪资范围 if (accountMinSalary > 0 || accountMaxSalary > 0) { if (accountMinSalary > 0 && salaryRange.max < accountMinSalary) { return 20; // 职位最高薪资低于账号最低要求 } if (accountMaxSalary > 0 && salaryRange.min > accountMaxSalary) { return 20; // 职位最低薪资高于账号最高要求 } // 在范围内,根据薪资水平给分 if (avgJobSalary >= accountMinSalary && (accountMaxSalary === 0 || avgJobSalary <= accountMaxSalary)) { return 100; // 完全符合要求 } } // 如果有期望薪资,与期望薪资比较 if (expectedSalary) { const expected = this.parse_expected_salary(expectedSalary); if (expected) { const ratio = expected / avgJobSalary; if (ratio <= 0.8) { return 100; // 期望薪资低于职位薪资,完全匹配 } else if (ratio <= 1.0) { return 90; // 期望薪资略低于或等于职位薪资 } else if (ratio <= 1.2) { return 70; // 期望薪资略高于职位薪资 } else { return 50; // 期望薪资明显高于职位薪资 } } } // 没有期望薪资和账号配置,给中等分数 return 60; } /** * 解析期望薪资(返回平均值) * @param {string} expectedSalary - 期望薪资描述(如 "20-30K"、"5000-6000元/月") * @returns {number|null} 期望薪资数值(元),如果是范围则返回平均值 */ parse_expected_salary(expectedSalary) { if (!expectedSalary) return null; // 1. 匹配K格式范围:20-30K const kRangeMatch = expectedSalary.match(/(\d+)[-~](\d+)[kK千]/); if (kRangeMatch) { const min = parseInt(kRangeMatch[1]) * 1000; const max = parseInt(kRangeMatch[2]) * 1000; return (min + max) / 2; // 返回平均值 } // 2. 匹配单个K值:25K const kMatch = expectedSalary.match(/(\d+)[kK千]/); if (kMatch) { return parseInt(kMatch[1]) * 1000; } // 3. 匹配元/月格式范围:5000-6000元/月 const yuanRangeMatch = expectedSalary.match(/(\d+)[-~](\d+)[元万]/); if (yuanRangeMatch) { const min = parseInt(yuanRangeMatch[1]); const max = parseInt(yuanRangeMatch[2]); if (expectedSalary.includes('万')) { return ((min + max) / 2) * 10000; } else { return (min + max) / 2; // 返回平均值 } } // 4. 匹配单个元/月值:5000元/月 const yuanMatch = expectedSalary.match(/(\d+)[元万]/); if (yuanMatch) { const value = parseInt(yuanMatch[1]); if (expectedSalary.includes('万')) { return value * 10000; } else { return value; } } // 5. 匹配纯数字范围(如:20000-30000) const numRangeMatch = expectedSalary.match(/(\d+)[-~](\d+)/); if (numRangeMatch) { return (parseInt(numRangeMatch[1]) + parseInt(numRangeMatch[2])) / 2; // 返回平均值 } // 6. 匹配纯数字(如:20000) const numMatch = expectedSalary.match(/(\d+)/); if (numMatch) { return parseInt(numMatch[1]); } return null; } /** * 计算公司规模分数(0-100分) * @param {string} companySize - 公司规模 * @returns {number} 公司规模分数 */ calculate_company_size_score(companySize) { if (!companySize) return 60; // 没有规模信息,给中等分数 const size = companySize.toLowerCase(); // 大型公司(1000人以上)给高分 if (size.includes('1000') || size.includes('1000+') || size.includes('1000人以上')) { return 100; } if (size.includes('500-1000') || size.includes('500人以上')) { return 90; } // 中型公司(100-500人)给中高分 if (size.includes('100-500') || size.includes('100人以上')) { return 80; } if (size.includes('50-100')) { return 70; } // 小型公司(50人以下)给中低分 if (size.includes('20-50') || size.includes('20人以上')) { return 60; } if (size.includes('0-20') || size.includes('20人以下')) { return 50; } // 默认中等分数 return 60; } /** * 解析薪资范围 * @param {string} salaryDesc - 薪资描述(如 "15-25K·14薪"、"5000-6000元/月") * @returns {object} 薪资范围 { min, max },单位:元 */ parse_salary_range(salaryDesc) { if (!salaryDesc) return { min: 0, max: 0 }; // 1. 匹配K格式:40-60K, 30-40K·18薪(忽略后面的薪数) const kMatch = salaryDesc.match(/(\d+)[-~](\d+)[kK千]/); if (kMatch) { return { min: parseInt(kMatch[1]) * 1000, max: parseInt(kMatch[2]) * 1000 }; } // 2. 匹配单个K值:25K const singleKMatch = salaryDesc.match(/(\d+)[kK千]/); if (singleKMatch) { const value = parseInt(singleKMatch[1]) * 1000; return { min: value, max: value }; } // 3. 匹配元/月格式:5000-6000元/月 const yuanMatch = salaryDesc.match(/(\d+)[-~](\d+)[元万]/); if (yuanMatch) { const min = parseInt(yuanMatch[1]); const max = parseInt(yuanMatch[2]); // 判断单位(万或元) if (salaryDesc.includes('万')) { return { min: min * 10000, max: max * 10000 }; } else { return { min, max }; } } // 4. 匹配单个元/月值:5000元/月 const singleYuanMatch = salaryDesc.match(/(\d+)[元万]/); if (singleYuanMatch) { const value = parseInt(singleYuanMatch[1]); if (salaryDesc.includes('万')) { return { min: value * 10000, max: value * 10000 }; } else { return { min: value, max: value }; } } // 5. 匹配纯数字格式(如:20000-30000) const numMatch = salaryDesc.match(/(\d+)[-~](\d+)/); if (numMatch) { return { min: parseInt(numMatch[1]), max: parseInt(numMatch[2]) }; } return { min: 0, max: 0 }; } /** * 延迟函数 * @param {number} ms - 毫秒数 * @returns {Promise} */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } module.exports = new JobManager();