本鼠鼠干JAVA也有些年头了,平常也只会写写CRUD。突然有一天,产品出了一个需求给本鼠鼠给整懵了,本想直接三连告诉他做不了,找别人吧,告辞。奈何产品好说歹说,心软了,于是就有了下面的一系列内容。第一次写文章,写这篇文章也主要是为了做个记录。
首先先说一下需求:
展示工作推荐列表,可下拉刷新,需满足以下条件
1、数据来源:【更新时间】在一个月内的招聘信息
2、排序规则:置顶>更新时间(3.1),随机(3.2)
3、排序顺序
3.1 与当前用户【简历-求职职位】相符的招聘信息
3.2 按以下顺序展示:职位所属招聘子版块>招聘求职(随机推荐,在UI展示上与3.1区别开)
4、 数据上限:100条
说实话,看到这个排序,然后还要随机,脑壳就一阵疼。一开始的想法是先把符合条件的数据分段从数据库查出来,然后再拼接100条进行返回,但是产品要求了,必须分页,而且由于该表在生产环境是经常被访问的,所以针对这表的操作在性能上是必须慎之又慎。中间就是绕了挺多弯的,后来想到这表的数据都存在ES中,那是否可以直接用ES查询来实现呢。废话不多说,先上代码:
@Override
public ResResult<Page<FrontJobsRecruittPageListVo>> selectRecommandRandList(RecommandJobsQueryParam params) {
//获取随机种子
int seed = getSeedNum(params.getCurrent());
Long positionId = params.getPositionId();
if (null == positionId){
FrontUserResume userResume = userResumeMapper.selectOne(new QueryWrapper<FrontUserResume>().eq("user_id",LoginUtil.getUserId()).last("limit 1"));
positionId = null != userResume ? userResume.getPositionId() : null;
}
if (params.getCurrent() * params.getSize() > 100){
// 封装结果
Page<FrontJobsRecruittPageListVo> result = new Page<>();
result.setCurrent(params.getCurrent());
result.setSize(params.getSize());
result.setTotal(100);
result.setRecords(new ArrayList<>());
return ResUtil.yes(result);
}
FrontModulesCategory modulesCategory = frontModulesCategoryMapper.selectById(positionId);
List<FrontModulesCategory> modulesCategoryList = frontModulesCategoryMapper.selectList(
new QueryWrapper<FrontModulesCategory>().eq("pid",modulesCategory.getPid())
.ne("id",positionId));
List<Long> categoryIds = modulesCategoryList.stream().map(FrontModulesCategory :: getId).collect(Collectors.toList());
List<Long> notExitsIds = new ArrayList<>();
notExitsIds.add(positionId);
notExitsIds.addAll(categoryIds);
List<FrontModulesCategory> otherCategoryList = frontModulesCategoryMapper.selectList(
new QueryWrapper<FrontModulesCategory>().eq("types","position")
.likeRight("modules_code","recruit_")
.notIn("id",notExitsIds));
List<Long> otherCategoryIds = otherCategoryList.stream().map(FrontModulesCategory :: getId).collect(Collectors.toList());
List<FrontJobsRecruittPageListVo> results=new ArrayList<>();
// 设置日期格式为 basic_date_time
Date now = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(now);
cal.add(Calendar.MONTH, -1);
Date oneMonthAgoDate = cal.getTime();
// 设置日期格式为 basic_date_time
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSSZ");
// sdf.setTimeZone(TimeZone.getTimeZone(DateUtils.getXTimezone())); // 确保使用 UTC 时间格式化
String oneMonthAgo = sdf.format(oneMonthAgoDate);
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//先选择公共的查询条件
BoolQueryBuilder boolQueryBuilder2 = boolQuery()
.must(termQuery("flag", "1"))
.must(wildcardQuery("modulesCode", "recruit_*"))
.must(rangeQuery("sortTime").gte(oneMonthAgo))
.must(termsQuery("auditStatus", AppConstant.SHOW_AUDIT_STATUS));
//里面多写了一次公共条件,之前有试着整合公共条件,但不知道为啥根据城市查询出的结果会有问题所以就多写了
if (params.getCitiesId() != null && !params.getCitiesId().equals("0")){
boolQueryBuilder2.mustNot(termQuery("categoryId", positionId));
BoolQueryBuilder boolQueryBuilder1 = boolQuery()
.must(termQuery("flag", "1"))
.must(wildcardQuery("modulesCode", "recruit_*"))
.must(rangeQuery("sortTime").gte(oneMonthAgo))
.must(termsQuery("auditStatus", AppConstant.SHOW_AUDIT_STATUS))
.must(termQuery("categoryId", positionId))
.must(termQuery("citiesIds", params.getCitiesId()));
boolQueryBuilder.should(boolQueryBuilder1);
}
boolQueryBuilder.should(boolQueryBuilder2);
//上面查询条件转换为sql为where (flag = 1 and modulesCode like 'recruit_%' and sortTime >= 时间 and auditStatus int(1,2))
// or (flag = 1 and modulesCode like 'recruit_%' and sortTime >= 时间 and auditStatus int(1,2) and categoryId = 职位ID and citiesIds = 城市ID)
//重点来了,根据符合条件的结果进行匹配得分,得分越高的排在越前面,因为查询的结果要分三块排序,精准职位匹配>同类职位匹配(随机)>同模块职位匹配的(随机)
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(
boolQueryBuilder,
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.boolQuery()
.must(params.getCitiesId() != null && !params.getCitiesId().equals("0") ?
QueryBuilders.termQuery("citiesIds", params.getCitiesId()) :
QueryBuilders.matchAllQuery()) // 仅过滤 citiesId,不影响得分
.must(QueryBuilders.termQuery("categoryId", positionId)),
ScoreFunctionBuilders.weightFactorFunction(99.0f) // 提高匹配 positionId 的文档得分
),
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("categoryId", categoryIds)),
ScoreFunctionBuilders.randomFunction().seed(seed).setWeight(10.0f) // 使用种子值增加随机排序
),
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.boolQuery().must(QueryBuilders.termsQuery("categoryId", otherCategoryIds)),
ScoreFunctionBuilders.randomFunction().seed(seed) // 使用种子值增加随机排序
)
}
);
Query searchQuery = new NativeSearchQueryBuilder()
.withQuery(functionScoreQueryBuilder)
.withSort(SortBuilders.scoreSort().order(SortOrder.DESC)) // 首先根据得
.withSort(SortBuilders.fieldSort("ifTop").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("sortTime").order(SortOrder.DESC))
.withPageable(PageRequest.of(params.getCurrent()-1, params.getSize()))
.build();
SearchHits<ModulesMessageDocument> searchHits = elasticsearchTemplate.search(searchQuery, ModulesMessageDocument.class);
List<ModulesMessageDocument> primaryResults = searchHits.getSearchHits().stream()
.map(hit -> hit.getContent())
.collect(Collectors.toList());
processDataNew(primaryResults,results,positionId,categoryIds);
// 封装结果
Page<FrontJobsRecruittPageListVo> result = new Page<>();
result.setCurrent(params.getCurrent());
result.setSize(params.getSize());
result.setTotal(100);
result.setRecords(results);
return ResUtil.yes(result);
}
由上面可看出,先设置查询条件,然后再根据匹配结果的得分来进行排序。由于需要确保大部分设备看到的数据是不一样的,所以每次请求进来,判断如果是第一页,则重新生成一个随机种子,以下是生成种子的方法
/**
* 获得随机种子,在ES查询的时候使用,确保每台设备查询的数据是随机的
**/
public Integer getSeedNum(Integer current){
Integer code;
String key = String.format(AppConstant.RECOMMAND_JOBS_KEY,LoginUtil.getUserId());
//判断如果是第一页,则重新生成一个随机种子
if (null != redisTemplate.opsForValue().get(key) && current != 1){
code = Integer.parseInt(redisTemplate.opsForValue().get(key));
}else{
Random random = new Random();
code = random.nextInt(9000) + 1000;
redisTemplate.opsForValue().set(key,code+"",1, TimeUnit.DAYS);
}
return code;
}
最后则是对结果数据进行整理,倒是没什么好看的,随手贴上来了。
public void processDataNew(List<ModulesMessageDocument> documentList,List<FrontJobsRecruittPageListVo> resultList,Long positionId , List<Long>positionIdList){
if(CollectionUtil.isEmpty(documentList)){
return;
}
List<ModulesMessageDocument> matchedDocuments = documentList.stream()
.filter(modulesMessageDocument -> positionId.equals(modulesMessageDocument.getCategoryId()))
.sorted(Comparator.comparing(ModulesMessageDocument::getIfTop)
.thenComparing(ModulesMessageDocument::getSortTime).reversed()).collect(Collectors.toList());
// 取出子模块的元素
List<ModulesMessageDocument> ifTopOneDocuments = documentList.stream()
.filter(modulesMessageDocument ->positionIdList.contains(modulesMessageDocument.getCategoryId()))
.sorted(Comparator.comparing(ModulesMessageDocument::getIfTop).reversed()
.thenComparing(ModulesMessageDocument::getSortTime).reversed())
.collect(Collectors.toList());
// 取出其它招聘的元素
positionIdList.add(positionId);
List<ModulesMessageDocument> randomDocuments = documentList.stream()
.filter(modulesMessageDocument ->!positionIdList.contains(modulesMessageDocument.getCategoryId()) )
.collect(Collectors.toList());
// Collections.shuffle(randomDocuments);
// 合并所有结果
matchedDocuments.addAll(ifTopOneDocuments);
matchedDocuments.addAll(randomDocuments);
List<FrontJobsRecruittPageListVo> parallelProcessed = matchedDocuments.parallelStream().map(document -> {
FrontJobsRecruittPageListVo vo=new FrontJobsRecruittPageListVo();
BeanUtils.copyProperties(document,vo);
vo.setCitiesIdChineseText(frontUsCitiesService.getCitiesChineseNameJoinStr(document.getCitiesIds()," "));
if(ObjectUtil.isNotNull(document.getCategoryId())){
vo.setPositionId(document.getCategoryId());
FrontModulesCategory frontModulesCategory = frontModulesCategoryService.getModulesCategoryById(document.getCategoryId());
FrontModulesCategory parentCategory = frontModulesCategoryService.getModulesCategoryById(frontModulesCategory.getPid());
if(parentCategory != null){
vo.setPositionIdText(parentCategory.getName()+"-"+frontModulesCategory.getName());
}else{
vo.setPositionIdText(frontModulesCategory.getName());
}
}
vo.setMoney(com.wf.user.common.util.StringUtils.getMoney(document.getMoney()));
JSONObject sourceData= JSON.parseObject(document.getSourceData());
if (null != sourceData.getString("ifTax")) {
FrontModulesCategory ifTax = frontModulesCategoryService.getModulesCategoryById(sourceData.getString("ifTax"));
if (null != ifTax && ifTax.getId() != null) {
if (AppConstant.IF_TAX_ID.contains(ifTax.getId().toString())) {
vo.setIsShowTaxText(false);
} else {
vo.setIsShowTaxText(true);
}
vo.setIfTaxText(ifTax.getName());
}
} else {
vo.setIsShowTaxText(false);
}
if (null != sourceData.getInteger("authType")) {
vo.setAuthType(sourceData.getInteger("authType"));
}
vo.setIfTop(ObjectUtil.isNull(document.getIfTop())? false :document.getIfTop());
vo.setRecommandType(positionId.equals(document.getCategoryId()) ? 0:1);
return vo;
}).collect(Collectors.toList());
resultList.addAll(parallelProcessed);
List<Long> ids = resultList.stream().map(FrontJobsRecruittPageListVo ::getId).collect(Collectors.toList());
List<FrontJobsRecruitt> jobsRecruittList = baseMapper.selectList( new QueryWrapper<FrontJobsRecruitt>()
.in("id", ids));
for (FrontJobsRecruittPageListVo recruittPageListVo : resultList){
for (FrontJobsRecruitt jobs : jobsRecruittList){
if (jobs.getId().equals(recruittPageListVo.getId())){
recruittPageListVo.setAuthType(jobs.getAuthType());
FrontModulesCategory fmc2 = frontModulesCategoryService.getModulesCategoryById(jobs.getWorkWay());
recruittPageListVo.setWorkWayText(fmc2.getName());
}
}
}
}
以上就是所有内容了,感觉在代码上还是有挺多优化的空间,如果有比较好的建议,可以在评论区说出来