修改评分计算规则
This commit is contained in:
@@ -54,7 +54,7 @@ public class JobDto {
|
||||
/** 岗位状态(0=有效 1=已下架 2=已过期) */
|
||||
private Integer status;
|
||||
|
||||
/** 匹配总分(0-90) */
|
||||
/** 匹配总分(0-100) */
|
||||
private Integer matchScore;
|
||||
|
||||
/** 匹配度详情 */
|
||||
|
||||
@@ -14,12 +14,12 @@ import lombok.NoArgsConstructor;
|
||||
@AllArgsConstructor
|
||||
public class JobMatchScoreDto {
|
||||
|
||||
/** 行业得分(0-100,百分制) */
|
||||
private Integer industryScore;
|
||||
/** 教育得分(0-100,百分制) */
|
||||
private Integer educationScore;
|
||||
|
||||
/** 技能得分(0-100,百分制) */
|
||||
private Integer skillScore;
|
||||
|
||||
/** 经验得分(0-100,百分制) */
|
||||
/** 经历得分(0-100,百分制) */
|
||||
private Integer experienceScore;
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ public class JobService {
|
||||
dto.setStatus(vo.getStatus());
|
||||
Map<String, Integer> scoreMap = matchScoreMap.get(vo.getId());
|
||||
if (scoreMap != null) {
|
||||
JobMatchScoreDto matchScore = new JobMatchScoreDto(scoreMap.get("industryScore"), scoreMap.get("skillScore"), scoreMap.get("experienceScore"));
|
||||
JobMatchScoreDto matchScore = new JobMatchScoreDto(scoreMap.get("educationScore"), scoreMap.get("skillScore"), scoreMap.get("experienceScore"));
|
||||
dto.setMatchScore(scoreMap.get("totalScore"));
|
||||
dto.setMatchDetail(matchScore);
|
||||
} else {
|
||||
@@ -228,6 +228,8 @@ public class JobService {
|
||||
item.setId(job.getId());
|
||||
item.setRequiredIndustryId(job.getRequiredIndustryId());
|
||||
item.setMinExperience(job.getMinExperience());
|
||||
item.setRequiredMajorIds(job.getRequiredMajorIds());
|
||||
item.setMajorSensitivity(job.getMajorSensitivity());
|
||||
jobList.add(item);
|
||||
Map<Long, Map<String, Integer>> matchScoreMap = jobMatchService.batchCalculateMatchScore(jobList, userId);
|
||||
Map<String, Integer> scoreMap = matchScoreMap.get(jobId);
|
||||
@@ -290,7 +292,7 @@ public class JobService {
|
||||
dto.setIsFavorite(count > 0);
|
||||
if (scoreMap != null) {
|
||||
dto.setMatchScore(scoreMap.get("totalScore"));
|
||||
dto.setMatchDetail(new JobMatchScoreDto(scoreMap.get("industryScore"), scoreMap.get("skillScore"), scoreMap.get("experienceScore")));
|
||||
dto.setMatchDetail(new JobMatchScoreDto(scoreMap.get("educationScore"), scoreMap.get("skillScore"), scoreMap.get("experienceScore")));
|
||||
} else {
|
||||
dto.setMatchScore(0);
|
||||
dto.setMatchDetail(new JobMatchScoreDto(0, 0, 0));
|
||||
|
||||
@@ -63,6 +63,13 @@ public class JobListItemVo {
|
||||
/** 最低工作年限 */
|
||||
private Integer minExperience;
|
||||
|
||||
/** 要求专业ID数组(JSON) */
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<Long> requiredMajorIds;
|
||||
|
||||
/** 专业敏感度 0=不限 1=优先 2=强制 */
|
||||
private Integer majorSensitivity;
|
||||
|
||||
/** 岗位状态(0=有效 1=已下架 2=已过期) */
|
||||
private Integer status;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.jiayunet.mapper.*;
|
||||
import org.jiayunet.pojo.po.*;
|
||||
import org.jiayunet.pojo.vo.JobListItemVo;
|
||||
import org.jiayunet.pojo.vo.UserHonorsVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -13,8 +14,10 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 岗位匹配度计算服务
|
||||
* <p>主要功能:根据用户简历和岗位信息,计算教育/经历/技能三维度匹配分</p>
|
||||
* <p>依赖:无</p>
|
||||
* <p>使用表:bg_user_profile(查询用户简历)、bg_job_skill_tag_relation(查询岗位技能)、bg_user_profile_skill_tag_relation(查询用户技能)、bg_industry(查询行业父级)</p>
|
||||
* <p>使用表:bg_user_profile(查询用户简历维度数据)、bg_job_skill_tag_relation(查询岗位技能)、
|
||||
* bg_user_profile_skill_tag_relation(查询用户技能)、bg_major_category(专业树形匹配)</p>
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@@ -32,11 +35,11 @@ public class JobMatchService {
|
||||
private UserProfileSkillTagRelationMapper userProfileSkillTagRelationMapper;
|
||||
|
||||
@Autowired
|
||||
private IndustryMapper industryMapper;
|
||||
private MajorCategoryMapper majorCategoryMapper;
|
||||
|
||||
/**
|
||||
* 批量计算岗位匹配度
|
||||
* <p>1. 查询用户简历和技能 2. 批量查询岗位技能 3. 批量查询行业信息 4. 逐个计算匹配度(百分制)5. 加权计算总分</p>
|
||||
* <p>1. 查询用户简历 2. 查询用户技能 3. 批量查询岗位技能 4. 批量查询专业信息 5. 逐个计算三维度分数 6. 加权计算总分</p>
|
||||
*/
|
||||
public Map<Long, Map<String, Integer>> batchCalculateMatchScore(List<JobListItemVo> jobs, Long userId) {
|
||||
if (jobs == null || jobs.isEmpty()) {
|
||||
@@ -58,124 +61,241 @@ public class JobMatchService {
|
||||
List<JobSkillTagRelation> jobSkillRelations = jobSkillTagRelationMapper.selectList(new LambdaQueryWrapper<JobSkillTagRelation>().in(JobSkillTagRelation::getJobId, jobIds));
|
||||
Map<Long, List<Long>> jobSkillMap = jobSkillRelations.stream().collect(Collectors.groupingBy(JobSkillTagRelation::getJobId, Collectors.mapping(JobSkillTagRelation::getSkillTagId, Collectors.toList())));
|
||||
|
||||
// 4. 批量查询行业信息(用于父级匹配)
|
||||
Set<Long> allIndustryIds = new HashSet<>();
|
||||
if (profile != null && profile.getExperienceIndustryIds() != null) {
|
||||
allIndustryIds.addAll(profile.getExperienceIndustryIds());
|
||||
}
|
||||
jobs.stream().map(JobListItemVo::getRequiredIndustryId).filter(Objects::nonNull).forEach(allIndustryIds::add);
|
||||
Map<Long, Industry> industryMap = Collections.emptyMap();
|
||||
if (!allIndustryIds.isEmpty()) {
|
||||
List<Industry> industries = industryMapper.selectBatchIds(allIndustryIds);
|
||||
industryMap = industries.stream().collect(Collectors.toMap(Industry::getId, i -> i));
|
||||
}
|
||||
// 4. 批量查询专业信息(用于树形匹配)
|
||||
Map<Long, MajorCategory> majorMap = loadMajorMap(profile, jobs);
|
||||
|
||||
// 5. 逐个计算匹配度
|
||||
Map<Long, Map<String, Integer>> result = new HashMap<>();
|
||||
for (JobListItemVo job : jobs) {
|
||||
int industryScore = calculateIndustryScore(job.getRequiredIndustryId(), profile, industryMap);
|
||||
int skillScore = calculateSkillScore(job.getId(), jobSkillMap.get(job.getId()), userSkillTagSet);
|
||||
int experienceScore = calculateExperienceScore(job.getMinExperience(), profile);
|
||||
int educationScore = calculateEducationScore(profile, job, majorMap);
|
||||
int experienceScore = calculateExperienceScore(profile);
|
||||
int skillScore = calculateSkillScore(jobSkillMap.get(job.getId()), userSkillTagSet);
|
||||
|
||||
// 加权计算总分:行业30% + 技能50% + 经验20%
|
||||
int totalScore = (int) Math.round(industryScore * 0.3 + skillScore * 0.5 + experienceScore * 0.2);
|
||||
// 加权计算总分:教育30% + 经历40% + 技能30%
|
||||
int totalScore = (int) Math.round(educationScore * 0.3 + experienceScore * 0.4 + skillScore * 0.3);
|
||||
|
||||
result.put(job.getId(), createScoreMap(industryScore, skillScore, experienceScore, totalScore));
|
||||
Map<String, Integer> map = new HashMap<>();
|
||||
map.put("educationScore", educationScore);
|
||||
map.put("experienceScore", experienceScore);
|
||||
map.put("skillScore", skillScore);
|
||||
map.put("totalScore", totalScore);
|
||||
result.put(job.getId(), map);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算行业匹配得分(百分制)
|
||||
* <p>岗位无要求→100分,用户无简历→0分,完全匹配→100分,父级匹配→75分,不匹配→0分</p>
|
||||
* 批量加载专业信息Map
|
||||
*/
|
||||
private int calculateIndustryScore(Long jobIndustryId, UserProfile profile, Map<Long, Industry> industryMap) {
|
||||
// 岗位无要求
|
||||
if (jobIndustryId == null) {
|
||||
return 100;
|
||||
private Map<Long, MajorCategory> loadMajorMap(UserProfile profile, List<JobListItemVo> jobs) {
|
||||
Set<Long> allMajorIds = new HashSet<>();
|
||||
if (profile != null && profile.getMajorIds() != null) {
|
||||
allMajorIds.addAll(profile.getMajorIds());
|
||||
}
|
||||
for (JobListItemVo job : jobs) {
|
||||
if (job.getRequiredMajorIds() != null) {
|
||||
allMajorIds.addAll(job.getRequiredMajorIds());
|
||||
}
|
||||
}
|
||||
if (allMajorIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<MajorCategory> majors = majorCategoryMapper.selectBatchIds(allMajorIds);
|
||||
return majors.stream().collect(Collectors.toMap(MajorCategory::getId, m -> m));
|
||||
}
|
||||
|
||||
// 用户无简历或无行业经验
|
||||
if (profile == null || profile.getExperienceIndustryIds() == null || profile.getExperienceIndustryIds().isEmpty()) {
|
||||
/**
|
||||
* 计算教育维度得分(百分制)
|
||||
* <p>根据 majorSensitivity 决定学校分和专业分的权重</p>
|
||||
*/
|
||||
private int calculateEducationScore(UserProfile profile, JobListItemVo job, Map<Long, MajorCategory> majorMap) {
|
||||
if (profile == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<Long> userIndustryIds = profile.getExperienceIndustryIds();
|
||||
// 学校等级分
|
||||
int schoolScore = mapSchoolRank(profile.getSchoolRank());
|
||||
|
||||
// 完全匹配
|
||||
if (userIndustryIds.contains(jobIndustryId)) {
|
||||
return 100;
|
||||
// 专业敏感度
|
||||
Integer sensitivity = job.getMajorSensitivity();
|
||||
if (sensitivity == null) sensitivity = 0;
|
||||
|
||||
// 不限专业 → 教育分 = 学校分
|
||||
if (sensitivity == 0 || job.getRequiredMajorIds() == null || job.getRequiredMajorIds().isEmpty()) {
|
||||
return schoolScore;
|
||||
}
|
||||
|
||||
// 父级匹配
|
||||
Industry jobIndustry = industryMap.get(jobIndustryId);
|
||||
if (jobIndustry != null && jobIndustry.getParentId() != null && jobIndustry.getParentId() != 0) {
|
||||
for (Long userIndustryId : userIndustryIds) {
|
||||
Industry userIndustry = industryMap.get(userIndustryId);
|
||||
if (userIndustry != null && userIndustry.getParentId() != null && userIndustry.getParentId().equals(jobIndustry.getParentId())) {
|
||||
return 75;
|
||||
}
|
||||
// 计算专业相关度
|
||||
int majorScore = calculateMajorScore(profile.getMajorIds(), job.getRequiredMajorIds(), majorMap);
|
||||
|
||||
int educationScore;
|
||||
if (sensitivity == 2) {
|
||||
// 强制要求:学校40% + 专业60%
|
||||
educationScore = (int) Math.round(schoolScore * 0.4 + majorScore * 0.6);
|
||||
// 专业分<40 → 强制降权
|
||||
if (majorScore < 40) {
|
||||
educationScore = (int) Math.round(educationScore * 0.5);
|
||||
}
|
||||
} else {
|
||||
// 优先要求:学校60% + 专业40%
|
||||
educationScore = (int) Math.round(schoolScore * 0.6 + majorScore * 0.4);
|
||||
}
|
||||
|
||||
return educationScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算专业相关度(百分制)
|
||||
* <p>遍历用户专业 × 岗位要求专业,取最高匹配分</p>
|
||||
*/
|
||||
private int calculateMajorScore(List<Long> userMajorIds, List<Long> jobMajorIds, Map<Long, MajorCategory> majorMap) {
|
||||
if (userMajorIds == null || userMajorIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int maxScore = 0;
|
||||
for (Long userMajorId : userMajorIds) {
|
||||
MajorCategory userMajor = majorMap.get(userMajorId);
|
||||
if (userMajor == null) continue;
|
||||
|
||||
for (Long jobMajorId : jobMajorIds) {
|
||||
MajorCategory jobMajor = majorMap.get(jobMajorId);
|
||||
if (jobMajor == null) continue;
|
||||
|
||||
int score = matchMajorPair(userMajor, jobMajor);
|
||||
maxScore = Math.max(maxScore, score);
|
||||
if (maxScore == 100) return 100; // 已满分,提前退出
|
||||
}
|
||||
}
|
||||
return maxScore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配一对专业的相关度
|
||||
* <p>完全匹配→100,同二级→70,同一级→30,不相关→0</p>
|
||||
*/
|
||||
private int matchMajorPair(MajorCategory userMajor, MajorCategory jobMajor) {
|
||||
// 完全匹配
|
||||
if (userMajor.getId().equals(jobMajor.getId())) {
|
||||
return 100;
|
||||
}
|
||||
// 同二级专业类(parentId 相同)
|
||||
if (userMajor.getParentId() != null && userMajor.getParentId().equals(jobMajor.getParentId())) {
|
||||
return 70;
|
||||
}
|
||||
// 同一级学科门类(rootId 相同)
|
||||
if (userMajor.getRootId() != null && userMajor.getRootId().equals(jobMajor.getRootId())) {
|
||||
return 30;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 学校等级映射 */
|
||||
private int mapSchoolRank(Integer rank) {
|
||||
if (rank == null) return 0;
|
||||
return switch (rank) {
|
||||
case 1 -> 100;
|
||||
case 2 -> 80;
|
||||
case 3 -> 60;
|
||||
case 4 -> 40;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算经历维度得分(百分制)
|
||||
* <p>Sexp = 公司背书×30% + 时长×10% + 经历深度×60%</p>
|
||||
* <p>经历深度 = 职责深度×40% + 量化产出×30% + 荣誉×30%</p>
|
||||
*/
|
||||
private int calculateExperienceScore(UserProfile profile) {
|
||||
if (profile == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int companyScore = mapCompanyPrestige(profile.getCompanyPrestige());
|
||||
int durationScore = mapExperienceDuration(profile.getExperienceDuration());
|
||||
|
||||
// 经历深度
|
||||
int roleScore = mapRoleDepth(profile.getRoleDepth());
|
||||
int outputScore = mapOutputQuality(profile.getOutputQuality());
|
||||
int honorsScore = calculateHonorsScore(profile.getHonors());
|
||||
int depthScore = (int) Math.round(roleScore * 0.4 + outputScore * 0.3 + honorsScore * 0.3);
|
||||
|
||||
return (int) Math.round(companyScore * 0.3 + durationScore * 0.1 + depthScore * 0.6);
|
||||
}
|
||||
|
||||
/** 公司背书映射 */
|
||||
private int mapCompanyPrestige(Integer prestige) {
|
||||
if (prestige == null) return 0;
|
||||
return switch (prestige) {
|
||||
case 1 -> 100;
|
||||
case 2 -> 60;
|
||||
case 3 -> 30;
|
||||
case 4 -> 0;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
/** 经历时长映射 */
|
||||
private int mapExperienceDuration(Integer duration) {
|
||||
if (duration == null) return 0;
|
||||
return switch (duration) {
|
||||
case 1 -> 100;
|
||||
case 2 -> 60;
|
||||
case 3 -> 30;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
/** 职责深度映射 */
|
||||
private int mapRoleDepth(Integer depth) {
|
||||
if (depth == null) return 0;
|
||||
return switch (depth) {
|
||||
case 1 -> 100;
|
||||
case 2 -> 80;
|
||||
case 3 -> 40;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
/** 量化产出映射 */
|
||||
private int mapOutputQuality(Integer quality) {
|
||||
if (quality == null) return 0;
|
||||
return switch (quality) {
|
||||
case 1 -> 100;
|
||||
case 2 -> 70;
|
||||
case 3 -> 40;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算荣誉得分(封顶累加制,总分不超过100)
|
||||
* <p>national每项20、provincial每项10、school每项5、paper每项15</p>
|
||||
*/
|
||||
private int calculateHonorsScore(UserHonorsVo honors) {
|
||||
if (honors == null) return 0;
|
||||
int score = 0;
|
||||
if (honors.getNational() != null) score += honors.getNational().size() * 20;
|
||||
if (honors.getProvincial() != null) score += honors.getProvincial().size() * 10;
|
||||
if (honors.getSchool() != null) score += honors.getSchool().size() * 5;
|
||||
if (honors.getPaper() != null) score += honors.getPaper().size() * 15;
|
||||
return Math.min(score, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算技能匹配得分(百分制)
|
||||
* <p>岗位无要求→100分,用户无技能→0分,匹配公式:(匹配数量 / 岗位要求数量) * 100</p>
|
||||
*/
|
||||
private int calculateSkillScore(Long jobId, List<Long> jobSkillTagIds, Set<Long> userSkillTagSet) {
|
||||
// 岗位无要求
|
||||
private int calculateSkillScore(List<Long> jobSkillTagIds, Set<Long> userSkillTagSet) {
|
||||
if (jobSkillTagIds == null || jobSkillTagIds.isEmpty()) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
// 用户无技能
|
||||
if (userSkillTagSet == null || userSkillTagSet.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 计算匹配数量(Set.contains 是 O(1),性能优于 List.contains 的 O(n))
|
||||
long matchedCount = jobSkillTagIds.stream().filter(userSkillTagSet::contains).count();
|
||||
double ratio = (double) matchedCount / jobSkillTagIds.size();
|
||||
|
||||
return (int) Math.round(ratio * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算经验匹配得分(百分制)
|
||||
* <p>岗位无要求→100分,用户无简历→0分,计算公式:min(max((workYears - minExp) / minExp * 0.8 + 0.2, 0), 1.0) * 100</p>
|
||||
*/
|
||||
private int calculateExperienceScore(Integer minExperience, UserProfile profile) {
|
||||
// 岗位无要求
|
||||
if (minExperience == null || minExperience == 0) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
// 用户无简历或无工作年限
|
||||
if (profile == null || profile.getWorkYears() == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int workYears = profile.getWorkYears();
|
||||
double ratio = (double) (workYears - minExperience) / minExperience * 0.8 + 0.2;
|
||||
// 确保分数在 0-1 之间
|
||||
ratio = Math.max(0, Math.min(ratio, 1.0));
|
||||
|
||||
return (int) Math.round(ratio * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建得分Map
|
||||
*/
|
||||
private Map<String, Integer> createScoreMap(int industryScore, int skillScore, int experienceScore, int totalScore) {
|
||||
Map<String, Integer> map = new HashMap<>();
|
||||
map.put("industryScore", industryScore);
|
||||
map.put("skillScore", skillScore);
|
||||
map.put("experienceScore", experienceScore);
|
||||
map.put("totalScore", totalScore);
|
||||
return map;
|
||||
return (int) Math.round((double) matchedCount / jobSkillTagIds.size() * 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
<result column="source_url" property="sourceUrl"/>
|
||||
<result column="required_industry_id" property="requiredIndustryId"/>
|
||||
<result column="min_experience" property="minExperience"/>
|
||||
<result column="required_major_ids" property="requiredMajorIds" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
<result column="major_sensitivity" property="majorSensitivity"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="company_id" property="companyId"/>
|
||||
<result column="company_name" property="companyName"/>
|
||||
@@ -31,6 +33,8 @@
|
||||
j.source_url,
|
||||
j.required_industry_id,
|
||||
j.min_experience,
|
||||
j.required_major_ids,
|
||||
j.major_sensitivity,
|
||||
j.status,
|
||||
c.id AS company_id,
|
||||
c.name AS company_name,
|
||||
|
||||
Reference in New Issue
Block a user