修改评分计算规则

This commit is contained in:
zk
2026-03-26 19:51:00 +08:00
parent fcc13d3496
commit e3dcfde190
6 changed files with 223 additions and 90 deletions
@@ -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,