diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/ESJobSearch.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/ESJobSearch.java index e8c162c..8c671c8 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/ESJobSearch.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/domain/query/ESJobSearch.java @@ -1,7 +1,7 @@ package com.ruoyi.cms.domain.query; -import com.fasterxml.jackson.annotation.JsonFormat; import com.ruoyi.cms.domain.Job; +import com.ruoyi.common.core.domain.entity.UserWorkExperiences; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -63,4 +63,13 @@ public class ESJobSearch extends Job //结束时间 private Date endDate; + + /**岗位收藏集合*/ + private List collectNames; + + /**岗位申请集合*/ + private List applyNames; + + /**工作经历*/ + private List userWorkExperiences; } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/JobApplyMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/JobApplyMapper.java index 6927d6d..25182ea 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/JobApplyMapper.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/JobApplyMapper.java @@ -42,4 +42,6 @@ public interface JobApplyMapper extends BaseMapper int updateJobZphApply(JobApply jobApply); JobApply selectByJobIdAndUserId(@Param("jobId") Long jobId, @Param("userId") Long userId); + + List queryApplyNames(@Param("userId") Long userId); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/JobCollectionMapper.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/JobCollectionMapper.java index b8184aa..0672ae6 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/JobCollectionMapper.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/mapper/JobCollectionMapper.java @@ -5,6 +5,7 @@ import java.util.List; import com.ruoyi.cms.domain.Job; import com.ruoyi.cms.domain.JobCollection; +import org.apache.ibatis.annotations.Param; /** * 用户岗位收藏Mapper接口 @@ -24,4 +25,6 @@ public interface JobCollectionMapper extends BaseMapper List collectionJob(Long userId); public List selectJobCollectionListJob(JobCollection jobCollection); + + List queryCollectNames(@Param("userId") Long userId); } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/ESJobSearchImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/ESJobSearchImpl.java index 77b992b..037ca8c 100644 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/ESJobSearchImpl.java +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/ESJobSearchImpl.java @@ -2,21 +2,20 @@ package com.ruoyi.cms.service.impl; import com.alibaba.fastjson.JSON; import com.ruoyi.cms.domain.vo.CompanyVo; -import com.ruoyi.cms.mapper.AppUserBlockCompanyMapper; -import com.ruoyi.cms.mapper.CompanyMapper; +import com.ruoyi.cms.mapper.*; import com.ruoyi.cms.service.ICompanyService; +import com.ruoyi.cms.util.remommend.JobRecommendUtil; import com.ruoyi.common.core.domain.entity.AppUser; import com.ruoyi.cms.domain.ESJobDocument; import com.ruoyi.cms.domain.Job; import com.ruoyi.cms.domain.query.ESJobSearch; import com.ruoyi.cms.mapper.es.EsJobDocumentMapper; -import com.ruoyi.cms.mapper.JobMapper; -import com.ruoyi.cms.mapper.JobTitleMapper; import com.ruoyi.cms.service.IESJobSearchService; import com.ruoyi.common.core.domain.entity.JobTitle; import com.ruoyi.cms.util.ListUtil; import com.ruoyi.cms.util.StringUtil; import com.ruoyi.common.core.domain.entity.Company; +import com.ruoyi.common.core.domain.entity.UserWorkExperiences; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.core.text.Convert; import com.ruoyi.common.utils.DateUtils; @@ -62,6 +61,12 @@ public class ESJobSearchImpl implements IESJobSearchService private RedisCache redisCache; @Autowired private AppUserBlockCompanyMapper appUserBlockCompanyMapper; + @Autowired + private JobApplyMapper jobApplyMapper; + @Autowired + private JobCollectionMapper jobCollectionMapper; + @Autowired + private UserWorkExperiencesMapper userWorkExperiencesMapper; // 锁的key(唯一标识ES索引初始化) private static final String ES_INIT_LOCK_KEY = "es:job_document:init:lock"; @@ -245,15 +250,12 @@ public class ESJobSearchImpl implements IESJobSearchService //查询 if(SiteSecurityUtils.isLogin()){ AppUser appUser = appUserService.selectAppUserByUserId(SiteSecurityUtils.getUserId()); - List jobs=appUserBlockCompanyMapper.selectBlockCompanyIdsByUserIdJobs(appUser.getUserId()); - if (jobs != null) { - jobIds.addAll(jobs.stream().distinct().collect(Collectors.toList())); - } if (!StringUtil.isEmptyOrNull(esJobSearch.getCode())) { newSearch.setCode(esJobSearch.getCode()); } + //求职者 if (!isCompanyUser) { - setJobSeekerParams(appUser, esJobSearch, newSearch); + setJobSeekerParams(appUser, esJobSearch, newSearch,jobIds); } } @@ -327,7 +329,7 @@ public class ESJobSearchImpl implements IESJobSearchService * @param esJobSearch * @param newSearch */ - private void setJobSeekerParams(AppUser appUser, ESJobSearch esJobSearch, ESJobSearch newSearch) { + private void setJobSeekerParams(AppUser appUser, ESJobSearch esJobSearch, ESJobSearch newSearch,List jobIds) { if(!ListUtil.isEmptyOrNull(appUser.getJobTitle())){ List jobTitle = appUser.getJobTitle(); newSearch.setJobTitle(String.join(",", jobTitle)); @@ -348,9 +350,10 @@ public class ESJobSearchImpl implements IESJobSearchService if(!StringUtil.isEmptyOrNull(esJobSearch.getArea())){ newSearch.setArea(esJobSearch.getArea()); } - if(!StringUtil.isEmptyOrNull(appUser.getWorkExperience())){ - newSearch.setExperience(appUser.getWorkExperience()); - } + //影响后面按照经验打分,先注释 +// if(!StringUtil.isEmptyOrNull(appUser.getWorkExperience())){ +// newSearch.setExperience(appUser.getWorkExperience()); +// } if(!StringUtil.isEmptyOrNull(esJobSearch.getExperience())){ newSearch.setExperience(esJobSearch.getExperience()); } @@ -379,6 +382,22 @@ public class ESJobSearchImpl implements IESJobSearchService if(!StringUtil.isEmptyOrNull(esJobSearch.getCompanyNature())){ newSearch.setCompanyNature(esJobSearch.getCompanyNature()); } + //屏蔽企业集合 + List jobs=appUserBlockCompanyMapper.selectBlockCompanyIdsByUserIdJobs(appUser.getUserId()); + if (jobs != null) { + jobIds.addAll(jobs.stream().distinct().collect(Collectors.toList())); + } + //查询投递集合 + List applyNames=jobApplyMapper.queryApplyNames(appUser.getUserId()); + newSearch.setApplyNames(applyNames); + //查询收藏集合 + List collectNames=jobCollectionMapper.queryCollectNames(appUser.getUserId()); + newSearch.setCollectNames(collectNames); + //工作经历集合 + UserWorkExperiences expQuery = new UserWorkExperiences(); + expQuery.setUserId(appUser.getUserId()); + List userWorkExperiences=userWorkExperiencesMapper.getWorkExperiencesList(expQuery); + newSearch.setUserWorkExperiences(userWorkExperiences); } /** @@ -644,6 +663,13 @@ public class ESJobSearchImpl implements IESJobSearchService if(!StringUtil.isEmptyOrNull(esJobSearch.getCompanyName())){ wrapper.and(x->x.like(ESJobDocument::getCompanyName,esJobSearch.getCompanyName())); } + // 1.简历工作经历(最高权重) + JobRecommendUtil.appendWorkExpMatch(wrapper, esJobSearch.getUserWorkExperiences()); + //投递记录 + JobRecommendUtil.appendJobTitleMatch(wrapper, esJobSearch.getApplyNames(), JobRecommendUtil.APPLY_TITLE_BOOST); + //收藏记录 + JobRecommendUtil.appendJobTitleMatch(wrapper, esJobSearch.getCollectNames(), JobRecommendUtil.COLLECT_TITLE_BOOST); + //按时间段来查询数据 Date startDate = esJobSearch.getStartDate(); Date endDate = esJobSearch.getEndDate(); @@ -1030,6 +1056,17 @@ public class ESJobSearchImpl implements IESJobSearchService if(!StringUtil.isEmptyOrNull(appUser.getSalaryMin())){ newSearch.setMinSalary(Long.valueOf(appUser.getSalaryMin())); } + //查询投递集合 + List applyNames=jobApplyMapper.queryApplyNames(appUser.getUserId()); + newSearch.setApplyNames(applyNames); + //查询收藏集合 + List collectNames=jobCollectionMapper.queryCollectNames(appUser.getUserId()); + newSearch.setCollectNames(collectNames); + //工作经历集合 + UserWorkExperiences expQuery = new UserWorkExperiences(); + expQuery.setUserId(appUser.getUserId()); + List userWorkExperiences=userWorkExperiencesMapper.getWorkExperiencesList(expQuery); + newSearch.setUserWorkExperiences(userWorkExperiences); } // 请求参数始终优先于用户资料偏好 @@ -1263,4 +1300,19 @@ public class ESJobSearchImpl implements IESJobSearchService return -1; } } + + /** + * 岗位名称推荐 + * @param wrapper + * @param nameList + * @param boost + */ + /*private void appendJobTitleMatch(LambdaEsQueryWrapper wrapper, List nameList, Float boost) { + if (CollUtil.isEmpty(nameList)) { + return; + } + for (String title : nameList) { + wrapper.should(w -> w.match(ESJobDocument::getJobTitle, title, boost)); + } + }*/ } diff --git a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/remommend/JobRecommendUtil.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/remommend/JobRecommendUtil.java new file mode 100644 index 0000000..60916a7 --- /dev/null +++ b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/util/remommend/JobRecommendUtil.java @@ -0,0 +1,177 @@ +package com.ruoyi.cms.util.remommend; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.ruoyi.cms.domain.ESJobDocument; +import com.ruoyi.common.core.domain.entity.UserWorkExperiences; +import org.dromara.easyes.core.conditions.select.LambdaEsQueryWrapper; + +import java.time.LocalDate; +import java.time.Period; +import java.util.*; + +/** + * 岗位推荐查询条件工具类 + * 权重规则: + * 简历岗位名称(4/6/7) > 投递名称(3) > 收藏名称(2) + * 经验区间字典: + * 0=经验不限,1=实习生,2=应届毕业生,3=1年以下,4=1-3年,5=3-5年,6=5-10年,7=10年以上 + */ +public class JobRecommendUtil { + + // =====================权重常量,集中调参===================== + /** 工作经历岗位名称:任职不足1年 */ + public static final Float POS_BOOST_SHORT = 4F; + /** 工作经历岗位名称:1~3年 */ + public static final Float POS_BOOST_MID = 6F; + /** 工作经历岗位名称:>=3年 */ + public static final Float POS_BOOST_LONG = 7F; + + /** 投递岗位名称权重 */ + public static final Float APPLY_TITLE_BOOST = 3F; + /** 收藏岗位名称权重 */ + public static final Float COLLECT_TITLE_BOOST = 2F; + + /** 经验区间匹配权重 */ + public static final Float EXP_MATCH_SELF = 3F; + public static final Float EXP_MATCH_LOW1 = 2F; + public static final Float EXP_MATCH_LOWER = 1F; + + // =====================基础计算方法===================== + + /** + * 计算单条工作经历时长(月) + * @param startDateStr 开始年月 yyyy-MM + * @param endDateStr 结束年月 yyyy-MM;null代表至今在职 + */ + public static int calcWorkMonth(String startDateStr, String endDateStr) { + if (StrUtil.isBlank(startDateStr)) { + return 0; + } + LocalDate start = LocalDate.parse(startDateStr + "-01"); + LocalDate end; + if (StrUtil.isBlank(endDateStr)) { + end = LocalDate.now(); + } else { + end = LocalDate.parse(endDateStr + "-01"); + } + Period period = Period.between(start, end); + return period.getYears() * 12 + period.getMonths(); + } + + /** + * 计算全部工作经历总月数(简单累加,不处理时间重叠) + */ + public static int calcTotalWorkMonth(List expList) { + if (CollUtil.isEmpty(expList)) { + return 0; + } + return expList.stream() + .mapToInt(exp -> calcWorkMonth(exp.getStartDate(), exp.getEndDate())) + .sum(); + } + + /** + * 总工作月数 → 转换经验编码 + */ + public static Integer convertMonthToExpCode(int totalMonth) { + if (totalMonth <= 0) { + // 无工作经历:应届生 + return 2; + } + double year = totalMonth / 12.0; + if (year < 1) { + return 3; + } else if (year <= 3) { + return 4; + } else if (year <= 5) { + return 5; + } else if (year <= 10) { + return 6; + } else { + return 7; + } + } + + /** + * 追加简历工作经历匹配条件 + * 1.岗位名称匹配 + * 2.经验等级匹配 + */ + public static void appendWorkExpMatch(LambdaEsQueryWrapper wrapper, List expList) { + if (CollUtil.isEmpty(expList)) { + return; + } + + Map posBoostMap = new HashMap<>(); + for (UserWorkExperiences exp : expList) { + String position = exp.getPosition(); + if (StrUtil.isBlank(position)) { + continue; + } + int month = calcWorkMonth(exp.getStartDate(), exp.getEndDate()); + Float boost; + if (month >= 36) { + boost = POS_BOOST_LONG; + } else if (month >= 12) { + boost = POS_BOOST_MID; + } else { + boost = POS_BOOST_SHORT; + } + posBoostMap.compute(position, (k, old) -> old == null ? boost : Math.max(old, boost)); + } + + // 名称拼接,带上对应权重 + for (Map.Entry entry : posBoostMap.entrySet()) { + String title = entry.getKey(); + Float boost = entry.getValue(); + wrapper.should(w -> w.match("jobTitle", title, boost)); + } + + // 经验区间梯度加权 + int totalMonth = calcTotalWorkMonth(expList); + Integer userExpCode = convertMonthToExpCode(totalMonth); + appendExpCodeMatch(wrapper, userExpCode); + } + + /** + * 根据用户经验编码,追加经验等级should条件 + */ + private static void appendExpCodeMatch(LambdaEsQueryWrapper wrapper, Integer userExpCode) { + // 应届生 / 实习生 + if (userExpCode <= 2) { + wrapper.should(w -> w.match("experience_int", 0, EXP_MATCH_SELF)); + wrapper.should(w -> w.match("experience_int", 1, EXP_MATCH_SELF)); + wrapper.should(w -> w.match("experience_int", 2, EXP_MATCH_SELF)); + return; + } + int selfCode = userExpCode; + int lower1 = selfCode - 1; + List lowerAll = new ArrayList<>(); + for (int i = 0; i < lower1; i++) { + lowerAll.add(i); + } + // 完全匹配自身经验区间,最高权重 + wrapper.should(w -> w.match("experience_int", selfCode, EXP_MATCH_SELF)); + // 低一档经验,次权重 + wrapper.should(w -> w.match("experience_int", lower1, EXP_MATCH_LOW1)); + // 低两档及经验不限,最低权重 + for (Integer code : lowerAll) { + wrapper.should(w -> w.match("experience_int", code, EXP_MATCH_LOWER)); + } + } + + /** + * 批量追加岗位名称match(投递/收藏通用) + * @param nameList 岗位名称集合 + * @param boost 权重 + */ + public static void appendJobTitleMatch(LambdaEsQueryWrapper wrapper, List nameList, Float boost) { + if (CollUtil.isEmpty(nameList)) { + return; + } + for (String title : nameList) { + wrapper.should(w -> w.match(ESJobDocument::getJobTitle, title, boost)); + } + } +} \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/resources/mapper/app/JobApplyMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/app/JobApplyMapper.xml index b12efb0..5290572 100644 --- a/ruoyi-bussiness/src/main/resources/mapper/app/JobApplyMapper.xml +++ b/ruoyi-bussiness/src/main/resources/mapper/app/JobApplyMapper.xml @@ -200,4 +200,10 @@ + + \ No newline at end of file diff --git a/ruoyi-bussiness/src/main/resources/mapper/app/JobCollectionMapper.xml b/ruoyi-bussiness/src/main/resources/mapper/app/JobCollectionMapper.xml index da06995..5a12285 100644 --- a/ruoyi-bussiness/src/main/resources/mapper/app/JobCollectionMapper.xml +++ b/ruoyi-bussiness/src/main/resources/mapper/app/JobCollectionMapper.xml @@ -44,4 +44,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and a.user_id = #{userId} + + \ No newline at end of file