This commit is contained in:
张成
2025-12-30 16:18:28 +08:00
parent fa2dea3f04
commit c45ea21c83
14 changed files with 64 additions and 984 deletions

View File

@@ -0,0 +1,9 @@
/**
* Services 模块统一导出
*/
const jobFilterService = require('./jobFilterService');
module.exports = {
jobFilterService
};

View File

@@ -0,0 +1,884 @@
/**
* 职位文本匹配过滤服务
* 使用简单的文本匹配规则来过滤职位,替代 AI 分析(二期规划)
* 支持从数据库动态获取职位类型的技能关键词和排除关键词
*/
const db = require('../../dbProxy.js');
const locationService = require('../../../services/locationService');
class JobFilterService {
constructor() {
// 默认技能关键词(当没有职位类型或获取失败时使用)
this.defaultCommonSkills = [
'Vue', 'React', 'Angular', 'JavaScript', 'TypeScript', 'Node.js',
'Python', 'Java', 'C#', '.NET', 'Flutter', 'React Native',
'Webpack', 'Vite', 'Redux', 'MobX', 'Express', 'Koa',
'Django', 'Flask', 'MySQL', 'MongoDB', 'Redis',
'WebRTC', 'FFmpeg', 'Canvas', 'WebSocket', 'HTML5', 'CSS3',
'jQuery', 'Bootstrap', 'Element UI', 'Ant Design',
'Git', 'Docker', 'Kubernetes', 'AWS', 'Azure',
'Selenium', 'Jest', 'Mocha', 'Cypress'
];
// 默认排除关键词(当没有职位类型或获取失败时使用)
this.defaultExcludeKeywords = [
'外包', '外派', '驻场', '销售', '客服', '电话销售',
'地推', '推广', '市场', '运营', '行政', '文员'
];
// 缓存职位类型配置(避免频繁查询数据库)
this.jobTypeCache = new Map();
}
/**
* 根据职位类型ID获取技能关键词和排除关键词
* @param {number} jobTypeId - 职位类型ID
* @returns {Promise<{commonSkills: Array, excludeKeywords: Array}>}
*/
async getJobTypeConfig(jobTypeId) {
if (!jobTypeId) {
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
};
}
// 检查缓存
if (this.jobTypeCache.has(jobTypeId)) {
return this.jobTypeCache.get(jobTypeId);
}
try {
const job_types = db.getModel('job_types');
if (!job_types) {
console.warn('[职位过滤服务] job_types 模型不存在,使用默认配置');
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
};
}
const jobType = await job_types.findOne({
where: { id: jobTypeId, is_enabled: 1 }
});
if (!jobType) {
console.warn(`[职位过滤服务] 职位类型 ${jobTypeId} 不存在或已禁用,使用默认配置`);
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
};
}
const jobTypeData = jobType.toJSON();
// 解析 JSON 字段
let commonSkills = this.defaultCommonSkills;
let excludeKeywords = this.defaultExcludeKeywords;
if (jobTypeData.commonSkills) {
try {
const parsed = typeof jobTypeData.commonSkills === 'string'
? JSON.parse(jobTypeData.commonSkills)
: jobTypeData.commonSkills;
if (Array.isArray(parsed) && parsed.length > 0) {
commonSkills = parsed;
}
} catch (e) {
console.warn(`[职位过滤服务] 解析 commonSkills 失败:`, e);
}
}
if (jobTypeData.excludeKeywords) {
try {
const parsed = typeof jobTypeData.excludeKeywords === 'string'
? JSON.parse(jobTypeData.excludeKeywords)
: jobTypeData.excludeKeywords;
if (Array.isArray(parsed) && parsed.length > 0) {
excludeKeywords = parsed;
}
} catch (e) {
console.warn(`[职位过滤服务] 解析 excludeKeywords 失败:`, e);
}
}
const config = {
commonSkills,
excludeKeywords
};
// 缓存配置缓存5分钟
this.jobTypeCache.set(jobTypeId, config);
setTimeout(() => {
this.jobTypeCache.delete(jobTypeId);
}, 5 * 60 * 1000);
return config;
} catch (error) {
console.error(`[职位过滤服务] 获取职位类型配置失败:`, error);
return {
commonSkills: this.defaultCommonSkills,
excludeKeywords: this.defaultExcludeKeywords
};
}
}
/**
* 清除职位类型缓存
* @param {number} jobTypeId - 职位类型ID可选不传则清除所有缓存
*/
clearCache(jobTypeId = null) {
if (jobTypeId) {
this.jobTypeCache.delete(jobTypeId);
} else {
this.jobTypeCache.clear();
}
}
/**
* 保存匹配分析结果到数据库
* @param {string|number} jobPostingId - 职位IDjob_postings表的主键ID
* @param {object} analysisResult - 分析结果
* @returns {Promise<boolean>} 是否保存成功
*/
async saveMatchAnalysisToDatabase(jobPostingId, analysisResult) {
try {
const job_postings = db.getModel('job_postings');
if (!job_postings) {
console.warn('[职位过滤服务] job_postings 模型不存在,跳过保存');
return false;
}
const updateData = {
textMatchScore: analysisResult.overallScore || 0,
isOutsourcing: analysisResult.isOutsourcing || false,
matchSuggestion: analysisResult.suggestion || '',
matchConcerns: JSON.stringify(analysisResult.concerns || []),
textMatchAnalysis: JSON.stringify(analysisResult.analysis || analysisResult)
};
await job_postings.update(updateData, {
where: { id: jobPostingId }
});
console.log(`[职位过滤服务] 已保存职位 ${jobPostingId} 的匹配分析结果,综合评分: ${analysisResult.overallScore}`);
return true;
} catch (error) {
console.error(`[职位过滤服务] 保存匹配分析结果失败:`, error);
return false;
}
}
/**
* 使用文本匹配分析职位与简历的匹配度
* @param {object} jobInfo - 职位信息
* @param {object} resumeInfo - 简历信息(可选)
* @param {number} jobTypeId - 职位类型ID可选
* @param {object} options - 选项
* @param {number} options.jobPostingId - 职位IDjob_postings表的主键ID如果提供则自动保存到数据库
* @param {boolean} options.autoSave - 是否自动保存到数据库默认false
* @returns {Promise<object>} 匹配度分析结果
*/
async analyzeJobMatch(jobInfo, resumeInfo = {}, jobTypeId = null, options = {}) {
const jobText = this.buildJobText(jobInfo);
const resumeText = this.buildResumeText(resumeInfo);
// 获取职位类型配置
const { commonSkills, excludeKeywords } = await this.getJobTypeConfig(jobTypeId);
// 1. 技能匹配度0-100分
const skillScore = this.calculateSkillMatch(jobText, resumeText, commonSkills);
// 2. 经验匹配度0-100分
const experienceScore = this.calculateExperienceMatch(jobInfo, resumeInfo);
// 3. 薪资合理性0-100分
const salaryScore = this.calculateSalaryMatch(jobInfo, resumeInfo);
// 4. 是否为外包岗位
const isOutsourcing = this.checkOutsourcing(jobText);
// 5. 综合推荐指数(加权平均)
const overallScore = Math.round(
skillScore * 0.4 +
experienceScore * 0.3 +
salaryScore * 0.3
);
// 6. 匹配原因
const matchReasons = this.getMatchReasons(jobText, resumeText, skillScore, experienceScore, commonSkills);
// 7. 关注点
const concerns = this.getConcerns(jobInfo, resumeInfo, isOutsourcing, excludeKeywords);
// 8. 投递建议
const suggestion = this.getSuggestion(overallScore, isOutsourcing, concerns);
const result = {
skillMatch: skillScore,
experienceMatch: experienceScore,
salaryMatch: salaryScore,
isOutsourcing: isOutsourcing,
overallScore: overallScore,
matchReasons: matchReasons,
concerns: concerns,
suggestion: suggestion,
analysis: {
skillMatch: skillScore,
experienceMatch: experienceScore,
salaryMatch: salaryScore,
isOutsourcing: isOutsourcing,
overallScore: overallScore,
matchReasons: matchReasons,
concerns: concerns,
suggestion: suggestion
}
};
// 如果提供了 jobPostingId 且 autoSave 为 true自动保存到数据库
if (options.jobPostingId && options.autoSave !== false) {
await this.saveMatchAnalysisToDatabase(options.jobPostingId, result);
}
return result;
}
/**
* 构建职位文本(用于匹配)
* @param {object} jobInfo - 职位信息
* @returns {string} 职位文本
*/
buildJobText(jobInfo) {
const parts = [
jobInfo.jobTitle || '',
jobInfo.companyName || '',
jobInfo.description || '',
jobInfo.skills || '',
jobInfo.requirements || ''
];
return parts.join(' ').toLowerCase();
}
/**
* 构建简历文本(用于匹配)
* @param {object} resumeInfo - 简历信息
* @returns {string} 简历文本
*/
buildResumeText(resumeInfo) {
const parts = [
resumeInfo.skills || '',
resumeInfo.skillDescription || '',
resumeInfo.currentPosition || '',
resumeInfo.expectedPosition || ''
];
return parts.join(' ').toLowerCase();
}
/**
* 计算技能匹配度
* @param {string} jobText - 职位文本
* @param {string} resumeText - 简历文本
* @param {Array} commonSkills - 技能关键词列表
* @returns {number} 匹配度分数0-100
*/
calculateSkillMatch(jobText, resumeText, commonSkills = null) {
const skills = commonSkills || this.defaultCommonSkills;
if (!resumeText || resumeText.trim() === '') {
// 如果没有简历信息,基于职位关键词匹配
let matchedSkills = 0;
skills.forEach(skill => {
if (jobText.includes(skill.toLowerCase())) {
matchedSkills++;
}
});
return Math.min(100, matchedSkills * 10);
}
// 提取职位中的技能关键词
const jobSkills = [];
skills.forEach(skill => {
if (jobText.includes(skill.toLowerCase())) {
jobSkills.push(skill);
}
});
if (jobSkills.length === 0) {
return 50; // 如果没有明确技能要求,给中等分数
}
// 计算简历中匹配的技能数量
let matchedCount = 0;
jobSkills.forEach(skill => {
if (resumeText.includes(skill.toLowerCase())) {
matchedCount++;
}
});
// 计算匹配度百分比
const matchRate = matchedCount / jobSkills.length;
return Math.round(matchRate * 100);
}
/**
* 计算经验匹配度
* @param {object} jobInfo - 职位信息
* @param {object} resumeInfo - 简历信息
* @returns {number} 匹配度分数0-100
*/
calculateExperienceMatch(jobInfo, resumeInfo) {
if (!resumeInfo || !resumeInfo.workYears) {
return 60; // 默认中等分数
}
const jobText = this.buildJobText(jobInfo);
const resumeYears = parseInt(resumeInfo.workYears) || 0;
// 从职位描述中提取经验要求
const experienceKeywords = {
'应届': 0,
'1年': 1,
'2年': 2,
'3年': 3,
'5年': 5,
'10年': 10
};
let requiredYears = null;
for (const [keyword, years] of Object.entries(experienceKeywords)) {
if (jobText.includes(keyword)) {
requiredYears = years;
break;
}
}
if (requiredYears === null) {
return 70; // 没有明确要求,给较高分数
}
// 计算匹配度
if (resumeYears >= requiredYears) {
return 100; // 经验满足要求
} else if (resumeYears >= requiredYears - 1) {
return 80; // 经验略低于要求
} else {
return Math.max(30, 100 - (requiredYears - resumeYears) * 15); // 经验不足
}
}
/**
* 计算薪资合理性
* @param {object} jobInfo - 职位信息
* @param {object} resumeInfo - 简历信息
* @returns {number} 匹配度分数0-100
*/
calculateSalaryMatch(jobInfo, resumeInfo) {
if (!jobInfo.salary) {
return 60; // 没有薪资信息,给中等分数
}
const salaryRange = this.parseSalaryRange(jobInfo.salary);
if (!salaryRange) {
return 60;
}
if (!resumeInfo || !resumeInfo.expectedSalary) {
return 70; // 没有期望薪资,给较高分数
}
const expectedSalary = this.parseExpectedSalary(resumeInfo.expectedSalary);
if (!expectedSalary) {
return 70;
}
// 计算匹配度
const avgJobSalary = (salaryRange.min + salaryRange.max) / 2;
const ratio = expectedSalary / avgJobSalary;
if (ratio <= 0.8) {
return 100; // 期望薪资低于职位薪资,完全匹配
} else if (ratio <= 1.0) {
return 90; // 期望薪资略低于或等于职位薪资
} else if (ratio <= 1.2) {
return 70; // 期望薪资略高于职位薪资
} else {
return 50; // 期望薪资明显高于职位薪资
}
}
/**
* 检查是否为外包岗位
* @param {string} jobText - 职位文本
* @returns {boolean} 是否为外包
*/
checkOutsourcing(jobText) {
const outsourcingKeywords = ['外包', '外派', '驻场', '人力外包', '项目外包'];
return outsourcingKeywords.some(keyword => jobText.includes(keyword));
}
/**
* 获取匹配原因
* @param {string} jobText - 职位文本
* @param {string} resumeText - 简历文本
* @param {number} skillScore - 技能匹配度
* @param {number} experienceScore - 经验匹配度
* @param {Array} commonSkills - 技能关键词列表
* @returns {Array<string>} 匹配原因列表
*/
getMatchReasons(jobText, resumeText, skillScore, experienceScore, commonSkills = null) {
const skills = commonSkills || this.defaultCommonSkills;
const reasons = [];
if (skillScore >= 80) {
reasons.push('技能匹配度高');
} else if (skillScore >= 60) {
reasons.push('技能部分匹配');
}
if (experienceScore >= 80) {
reasons.push('工作经验符合要求');
}
// 检查是否有共同技能
const matchedSkills = [];
skills.forEach(skill => {
if (jobText.includes(skill.toLowerCase()) &&
resumeText.includes(skill.toLowerCase())) {
matchedSkills.push(skill);
}
});
if (matchedSkills.length > 0) {
reasons.push(`共同技能: ${matchedSkills.slice(0, 3).join(', ')}`);
}
return reasons.length > 0 ? reasons : ['基础匹配'];
}
/**
* 获取关注点
* @param {object} jobInfo - 职位信息
* @param {object} resumeInfo - 简历信息
* @param {boolean} isOutsourcing - 是否为外包
* @param {Array} excludeKeywords - 排除关键词列表
* @returns {Array<string>} 关注点列表
*/
getConcerns(jobInfo, resumeInfo, isOutsourcing, excludeKeywords = null) {
const keywords = excludeKeywords || this.defaultExcludeKeywords;
const concerns = [];
if (isOutsourcing) {
concerns.push('可能是外包岗位');
}
const jobText = this.buildJobText(jobInfo);
keywords.forEach(keyword => {
if (jobText.includes(keyword)) {
concerns.push(`包含排除关键词: ${keyword}`);
}
});
if (resumeInfo && resumeInfo.workYears) {
const resumeYears = parseInt(resumeInfo.workYears) || 0;
if (jobText.includes('5年') && resumeYears < 5) {
concerns.push('工作经验可能不足');
}
}
return concerns;
}
/**
* 获取投递建议
* @param {number} overallScore - 综合分数
* @param {boolean} isOutsourcing - 是否为外包
* @param {Array<string>} concerns - 关注点
* @returns {string} 投递建议
*/
getSuggestion(overallScore, isOutsourcing, concerns) {
const concernsCount = concerns ? concerns.length : 0;
// 不推荐:外包 或 关注点过多(>2
if (isOutsourcing) {
return '不推荐投递:外包岗位';
}
if (concernsCount > 2) {
return '不推荐投递:存在多个关注点';
}
// 不推荐:< 60 分
if (overallScore < 60) {
return '不推荐投递:匹配度较低';
}
// 优先级1必须投递≥ 80 分,且无关注点,非外包
if (overallScore >= 80 && concernsCount === 0) {
return '必须投递:匹配度很高,无关注点';
}
// 优先级2可以投递60-79 分,关注点 ≤ 2 个,非外包(优先处理无关注点情况)
if (overallScore >= 60 && overallScore < 80 && concernsCount === 0) {
return '可以投递:匹配度中等,无关注点';
}
// 优先级3谨慎考虑60-79 分但有关注点,或 ≥ 80 分但有关注点
if (overallScore >= 60 && concernsCount > 0 && concernsCount <= 2) {
return '谨慎考虑:存在关注点或特殊情况';
}
// 兜底情况
return '谨慎考虑:需要进一步评估';
}
/**
* 解析薪资范围
* @param {string} salaryDesc - 薪资描述(如 "15-25K·14薪"、"5000-6000元/月"
* @returns {object|null} 薪资范围 { min, max },单位:元
*/
parseSalaryRange(salaryDesc) {
if (!salaryDesc) return null;
// 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 null;
}
/**
* 解析期望薪资(返回平均值)
* @param {string} expectedSalary - 期望薪资描述(如 "20-30K"、"5000-6000元/月"
* @returns {number|null} 期望薪资数值(元),如果是范围则返回平均值
*/
parseExpectedSalary(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;
}
/**
* 过滤职位列表(基于文本匹配)
* @param {Array} jobs - 职位列表
* @param {object} filterRules - 过滤规则
* @param {object} resumeInfo - 简历信息(可选)
* @param {number} jobTypeId - 职位类型ID可选
* @param {object} options - 选项
* @param {boolean} options.autoSave - 是否自动保存评分结果到数据库默认false
* @returns {Promise<Array>} 过滤后的职位列表(带匹配分数)
*/
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 - 职位数据
* @param {Object} resumeInfo - 简历信息
* @param {Object} accountConfig - 账号配置(包含 user_longitude, user_latitude
* @param {Object} jobTypeConfig - 职位类型配置(可选)
* @param {Array} priorityWeights - 权重配置 [{key: "distance", weight: 50}, ...]
* @returns {Object} 评分结果 {totalScore, scores}
*/
calculateJobScoreWithWeights(jobData, resumeInfo, accountConfig, jobTypeConfig, priorityWeights) {
const scores = {};
let totalScore = 0;
// 解析权重配置根据排序优先级规范化权重确保总和为100
const weights = {};
if (Array.isArray(priorityWeights) && priorityWeights.length > 0) {
// 计算权重总和
const totalWeight = priorityWeights.reduce((sum, item) => sum + (Number(item.weight) || 0), 0);
// 如果总和大于0按比例规范化权重总和归一化为1即100%
if (totalWeight > 0) {
priorityWeights.forEach(item => {
weights[item.key] = (Number(item.weight) || 0) / totalWeight; // 规范化权重0-1之间
});
} else {
// 如果权重总和为0或未设置按照排序顺序分配权重第一个优先级最高
// 使用等差数列递减:第一个权重最高,依次递减
const n = priorityWeights.length;
const totalSequentialWeight = (n * (n + 1)) / 2; // 1+2+...+n
priorityWeights.forEach((item, index) => {
// 按照排序顺序,第一个权重最高,依次递减(权重比例为 n, n-1, n-2, ..., 1
const sequentialWeight = n - index;
weights[item.key] = sequentialWeight / totalSequentialWeight;
});
}
}
// 1. 距离评分(基于经纬度)
if (weights.distance) {
let distanceScore = 50; // 默认分数
if (accountConfig.user_longitude && accountConfig.user_latitude &&
jobData.longitude && jobData.latitude) {
try {
const userLon = parseFloat(accountConfig.user_longitude);
const userLat = parseFloat(accountConfig.user_latitude);
const jobLon = parseFloat(jobData.longitude);
const jobLat = parseFloat(jobData.latitude);
if (!isNaN(userLon) && !isNaN(userLat) && !isNaN(jobLon) && !isNaN(jobLat)) {
const distance = locationService.calculateDistance(userLat, userLon, jobLat, jobLon);
// 距离越近分数越高0-5km=100分5-10km=90分10-20km=80分20-50km=60分50km以上=30分
if (distance <= 5) distanceScore = 100;
else if (distance <= 10) distanceScore = 90;
else if (distance <= 20) distanceScore = 80;
else if (distance <= 50) distanceScore = 60;
else distanceScore = 30;
}
} catch (e) {
console.warn(`[职位过滤服务] 计算距离失败:`, e);
}
}
scores.distance = distanceScore;
totalScore += distanceScore * weights.distance;
}
// 2. 薪资评分
if (weights.salary) {
let salaryScore = 50;
if (jobData.salary && resumeInfo.expectedSalary) {
const jobSalary = this.parseExpectedSalary(jobData.salary);
const expectedSalary = this.parseExpectedSalary(resumeInfo.expectedSalary);
if (jobSalary && expectedSalary) {
if (jobSalary >= expectedSalary) salaryScore = 100;
else if (jobSalary >= expectedSalary * 0.8) salaryScore = 80;
else if (jobSalary >= expectedSalary * 0.6) salaryScore = 60;
else salaryScore = 40;
}
}
scores.salary = salaryScore;
totalScore += salaryScore * weights.salary;
}
// 3. 工作年限评分
if (weights.work_years) {
let workYearsScore = 50;
if (jobData.experience && resumeInfo.workYears) {
const jobExp = this.parseWorkYears(jobData.experience);
const resumeExp = this.parseWorkYears(resumeInfo.workYears);
if (jobExp !== null && resumeExp !== null) {
if (resumeExp >= jobExp) workYearsScore = 100;
else if (resumeExp >= jobExp * 0.8) workYearsScore = 80;
else if (resumeExp >= jobExp * 0.6) workYearsScore = 60;
else workYearsScore = 40;
}
}
scores.work_years = workYearsScore;
totalScore += workYearsScore * weights.work_years;
}
// 4. 学历评分
if (weights.education) {
let educationScore = 50;
if (jobData.education && resumeInfo.education) {
const educationLevels = { '博士': 7, '硕士': 6, '本科': 5, '大专': 4, '高中': 3, '中专': 2, '初中': 1 };
const jobLevel = educationLevels[jobData.education] || 0;
const resumeLevel = educationLevels[resumeInfo.education] || 0;
if (resumeLevel >= jobLevel) educationScore = 100;
else if (resumeLevel >= jobLevel - 1) educationScore = 70;
else educationScore = 40;
}
scores.education = educationScore;
totalScore += educationScore * weights.education;
}
// 5. 技能匹配评分(基于职位类型的 commonSkills
if (jobTypeConfig && jobTypeConfig.commonSkills) {
let skillScore = 0;
try {
const commonSkills = typeof jobTypeConfig.commonSkills === 'string'
? JSON.parse(jobTypeConfig.commonSkills)
: jobTypeConfig.commonSkills;
const resumeSkills = typeof resumeInfo.skills === 'string'
? JSON.parse(resumeInfo.skills || '[]')
: (resumeInfo.skills || []);
if (Array.isArray(commonSkills) && Array.isArray(resumeSkills)) {
const matchedSkills = commonSkills.filter(skill =>
resumeSkills.some(rs => rs.toLowerCase().includes(skill.toLowerCase()) ||
skill.toLowerCase().includes(rs.toLowerCase()))
);
skillScore = (matchedSkills.length / commonSkills.length) * 100;
}
} catch (e) {
console.warn(`[职位过滤服务] 解析技能失败:`, e);
}
scores.skills = skillScore;
// 技能评分作为额外加分项权重10%
totalScore += skillScore * 0.1;
}
return {
totalScore: Math.round(totalScore),
scores
};
}
/**
* 解析工作年限字符串为数字
* @param {string} workYearsStr - 工作年限字符串
* @returns {number|null} 工作年限数字
*/
parseWorkYears(workYearsStr) {
if (!workYearsStr) return null;
const match = workYearsStr.match(/(\d+)/);
if (match) return parseInt(match[1]);
return null;
}
}
// 导出单例
module.exports = new JobFilterService();