diff --git a/.gitignore b/.gitignore index a177b4d..ebdeb04 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,7 @@ nbdist/ local.sh .claude -/test-reports/ \ No newline at end of file +/test-reports/ +ruoyi-admin/src/main/resources/application-local.yml +ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/ESJobSearchImpl.java +ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/JobTitleSuggestServiceImpl.java \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/application-local.yml b/ruoyi-admin/src/main/resources/application-local.yml deleted file mode 100644 index 6fa3ae7..0000000 --- a/ruoyi-admin/src/main/resources/application-local.yml +++ /dev/null @@ -1,158 +0,0 @@ -# 项目相关配置 -ruoyi: - # 名称 - name: RuoYi - # 版本 - version: 3.8.8 - # 版权年份 - copyrightYear: 2024 - # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) - profile: /Users/chenyanchang/logs/ruoyi/uploadPath - # 获取ip地址开关 - addressEnabled: false - # 验证码类型 math 数字计算 char 字符验证 - captchaType: math - -# 数据源配置 -spring: - datasource: - type: com.alibaba.druid.pool.DruidDataSource - driverClassName: com.highgo.jdbc.Driver - druid: - # 主库数据源 - # master: - # url: jdbc:highgo://124.243.245.42:5866/shz?useUnicode=true&characterEncoding=utf8¤tSchema=shz&stringtype=unspecified - # #username: syssso - # username: sysdba - # password: ZKR2024@comzkr - # 正式环境 - # master: - # url: jdbc:highgo://39.98.44.136:6023/highgo?useUnicode=true&characterEncoding=utf8¤tSchema=shz&stringtype=unspecified - # username: sysdba - # password: Hello@2026 - # 测试环境 - master: - url: jdbc:highgo://47.111.103.66:5866/highgo?useUnicode=true&characterEncoding=utf8¤tSchema=shz&stringtype=unspecified - username: sysdba - password: Hello@2026 - # 从库数据源 - slave: - # 从数据源开关/默认关闭 - enabled: false - url: - username: - password: - # 初始连接数 - initialSize: 10 - # 最小连接池数量 - minIdle: 30 - # 最大连接池数量 - maxActive: 50 - # 配置获取连接等待超时的时间 - maxWait: 60000 - # 配置连接超时时间 - connectTimeout: 30000 - # 配置网络超时时间 - socketTimeout: 60000 - # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 - timeBetweenEvictionRunsMillis: 60000 - # 配置一个连接在池中最小生存的时间,单位是毫秒 - minEvictableIdleTimeMillis: 300000 - # 配置一个连接在池中最大生存的时间,单位是毫秒 - maxEvictableIdleTimeMillis: 900000 - # 配置检测连接是否有效 - validationQuery: SELECT version() - testWhileIdle: true - testOnBorrow: false - testOnReturn: false - webStatFilter: - enabled: true - statViewServlet: - enabled: true - # 设置白名单,不填则允许所有访问 - allow: - url-pattern: /druid/* - # 控制台管理用户名和密码 - login-username: ruoyi - login-password: 123456 - filter: - stat: - enabled: true - # 慢SQL记录 - log-slow-sql: true - slow-sql-millis: 1000 - merge-sql: true - wall: - config: - multi-statement-allow: true - redis: - # 本地 - #host: 127.0.0.1 - #port: 6379 - host: 124.243.245.42 - port: 5379 - # 数据库索引 - database: 0 - # 密码(本地通常无密码) - password: - # 连接超时时间 - timeout: 100s - lettuce: - pool: - # 连接池中的最小空闲连接 - min-idle: 0 - # 连接池中的最大空闲连接 - max-idle: 8 - # 连接池的最大数据库连接数 - max-active: 8 - # #连接池最大阻塞等待时间(使用负值表示没有限制) - max-wait: -1ms - -# easy-es -easy-es: - enable: true - banner: false - address: 124.243.245.42:9200 - global-config: - process-index-mode: manual - db-config: - refresh-policy: immediate - username: elastic - password: zkr2024@@.com - -#ai -chat: - baseUrl: http://39.98.44.136:8082 - chatUrl: /v1/chat/completions - chatDetailUrl: /core/chat/getPaginationRecords - chatHistoryUrl: /core/chat/getHistories - updateNameUrl: /core/chat/updateHistory - stickChatUrl: /core/chat/updateHistory - delChatUrl: /core/chat/delHistory - delAllChatUrl: /core/chat/clearHistories - guestUrl: /v1/chat/completions - praiseUrl: /core/chat/feedback/updateUserFeedback - appId: 67cd49095e947ae0ca7fadd8 - apiKey: fastgpt-qMl63276wPZvKAxEkW77bur0sSJpmuC6Ngg9lzyEjufLhsBAurjT55j - model: qd-job-turbo - -audioText: - asr: http://127.0.0.1:8000/asr/file - tts: http://127.0.0.1:19527/synthesize - -#浪潮单点登录相关 -#互联网端: -lc_web_auth: - appId: cloud-out-2fb6330e9c0843e1a1424efda5d604c0 - appSecret: x14lueHbtLQL7Pz2G7gE4wcGCV6TDblO5xfeu9V2wGk= - getTokenUrl: http://218.31.252.15:9081/prod-psout-api/auth/token - getUserInfoUrl: http://218.31.252.15:9081/prod-psout-api/system/app/authorize/user/info - editUserInfoUrl: http://218.31.252.15:9081/prod-psout-api/auth/user/edit -#监管端: -lc_cms_auth: - appId: cloud-9793ee8a8c3d47b8871007ffc4128502 - appSecret: Yi+NACK70UPg8rFvsnnfBUq1wcLD4nm6ilC4II/4C4k= - getTokenUrl: http://218.31.252.15:9081/prod-api/auth/token - getUserInfoUrl: http://218.31.252.15:9081/prod-api/system/app/authorize/user/info - resrtPwdUrl: http://218.31.252.15:9081/prod-api/ps/user/resetPwd/zkr - changeStatusUrl: http://218.31.252.15:9081/prod-api/ps/user/change/status/zkr 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 deleted file mode 100644 index 037ca8c..0000000 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/ESJobSearchImpl.java +++ /dev/null @@ -1,1318 +0,0 @@ -package com.ruoyi.cms.service.impl; - -import com.alibaba.fastjson.JSON; -import com.ruoyi.cms.domain.vo.CompanyVo; -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.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; -import com.ruoyi.common.utils.SiteSecurityUtils; -import com.ruoyi.common.utils.StringUtils; -import com.ruoyi.common.utils.bean.BeanUtils; -import org.dromara.easyes.core.biz.EsPageInfo; -import org.dromara.easyes.core.conditions.select.LambdaEsQueryWrapper; -import org.dromara.easyes.core.conditions.update.LambdaEsUpdateWrapper; -import org.elasticsearch.common.geo.GeoPoint; -import org.elasticsearch.common.unit.DistanceUnit; -import org.elasticsearch.search.sort.SortBuilders; -import org.elasticsearch.search.sort.SortOrder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.PostConstruct; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -/** - * 实体模板Service业务层处理 - * - * @author ruoyi - */ -@Service -public class ESJobSearchImpl implements IESJobSearchService -{ - @Autowired - private EsJobDocumentMapper esJobDocumentMapper; - - @Autowired - private JobMapper jobMapper; - - @Autowired - private AppUserServiceImpl appUserService; - @Autowired - private ICompanyService iCompanyService; - @Autowired - 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"; - // 锁过期时间(30分钟,确保初始化完成) - private static final Integer LOCK_EXPIRE_SECONDS = 1800; - // 等待锁时间(5分钟,避免无限等待) - private static final Integer WAIT_LOCK_SECONDS = 300; - - - @Autowired - private BussinessDictDataServiceImpl bussinessDictDataServicel; - @Autowired - private JobTitleMapper jobTitleMapper; - Logger logger = LoggerFactory.getLogger(JobServiceImpl.class); - @Autowired - private CompanyMapper companyMapper; - - /** - * 项目启动时,初始化索引及数据 - */ - @PostConstruct - public void init() - { - boolean isLockAcquired = false; - try { - isLockAcquired = acquireDistributedLock(); - if (isLockAcquired) { - resetTextCache(); - } else { - logger.info("其他节点正在初始化ES索引,直接复用,无需重复执行"); - } - } catch (InterruptedException e) { - logger.error("ES索引初始化等待锁异常", e); - Thread.currentThread().interrupt(); - } finally { - if (isLockAcquired) { - releaseDistributedLock(); - } - } - } - - /** - * 基于自定义RedisCache实现分布式锁(无getRedisTemplate适配版) - */ - private boolean acquireDistributedLock() throws InterruptedException { - long start = System.currentTimeMillis(); - while (System.currentTimeMillis() - start < WAIT_LOCK_SECONDS * 1000) { - boolean success = redisCache.setIfAbsent(ES_INIT_LOCK_KEY,"es_init_locked", - LOCK_EXPIRE_SECONDS,TimeUnit.SECONDS - ); - if (success) { - logger.info("成功获取ES初始化分布式锁,key:{}", ES_INIT_LOCK_KEY); - return true; - } - TimeUnit.MILLISECONDS.sleep(500); - } - logger.warn("等待{}秒未获取到ES初始化锁,放弃执行", WAIT_LOCK_SECONDS); - return false; - } - - /** - * 释放分布式锁(用自定义RedisCache的deleteObject方法) - */ - private void releaseDistributedLock() { - try { - redisCache.deleteObject(ES_INIT_LOCK_KEY); - logger.info("已释放ES初始化分布式锁,key:{}", ES_INIT_LOCK_KEY); - } catch (Exception e) { - logger.error("释放ES初始化锁异常", e); - } - } - - @Override - public void checkEsAndFix() { - try { - esJobDocumentMapper.getIndex(); - }catch (Exception e){ - logger.warn("es索引丢失,正在重新刷新"); - this.init(); - } - } - - /** - * 重置全文检索数据 - */ - //todo 定时任务 - @Override - public void resetTextCache() { - logger.info("正在重新刷新es"); - // 删除并重新创建索引 - /*esJobDocumentMapper.deleteIndex("job_document"); - esJobDocumentMapper.createIndex();*/ - if (esJobDocumentMapper.existsIndex("job_document")) { - esJobDocumentMapper.deleteIndex("job_document"); - logger.info("已删除原有job_document索引"); - try { - TimeUnit.MILLISECONDS.sleep(500); - } catch (InterruptedException e) { - logger.error("删除索引后休眠异常", e); - } - } - - if (!esJobDocumentMapper.existsIndex("job_document")) { - esJobDocumentMapper.createIndex(); - logger.info("已创建job_document索引"); - } else { - logger.info("索引已被其他节点创建,直接复用"); - } - - // 分批次处理数据 - int batchSize = 1000; // 每批次处理的数据量 - int offset = 0; // 起始偏移量 - boolean hasMoreData = true; // 是否还有更多数据 - - while (hasMoreData) { - // 分页查询数据 - Map params = new HashMap<>(); - params.put("batchSize", batchSize); - params.put("offset", offset*batchSize); - List jobList = jobMapper.selectAllJob(params); - - if (jobList.isEmpty()) { - hasMoreData = false; // 没有更多数据,退出循环 - } else { - // 转换数据并插入到 Elasticsearch - List esJobDocumentList = new ArrayList<>(); - for (Job job : jobList) { - ESJobDocument esJobDocument = new ESJobDocument(); - BeanUtils.copyBeanProp(esJobDocument, job); - esJobDocument.setJobCategory(resolveJobCategoryLabel(job.getJobCategory())); - //类型转换导致赋值问题,重新赋值 - esJobDocument.setScale(org.apache.commons.lang3.StringUtils.isNotEmpty(job.getScale()) ? Integer.parseInt(job.getScale()) : 0); - //企业信息 - CompanyVo vo=job.getCompanyVo(); - esJobDocument.setCompanyVoJson(JSON.toJSONString(vo)); - //岗位联系人 - esJobDocument.setJobContactVoJson(JSON.toJSONString(job.getJobContactList())); - esJobDocument.setAppJobUrl("https://www.xjksly.cn/app#/packageA/pages/post/post?jobId="+ job.getJobId()); - if(!StringUtil.isEmptyOrNull(job.getScale())){ - esJobDocument.setScale(Integer.valueOf(job.getScale())); - }else { - esJobDocument.setScale(8); - } - if(!StringUtil.isEmptyOrNull(job.getExperience())){ - esJobDocument.setExperience_int(Integer.valueOf(job.getExperience())); - }else { - esJobDocument.setExperience("0"); - esJobDocument.setEducation_int(0); - } - if(!StringUtil.isEmptyOrNull(job.getEducation())){ - esJobDocument.setEducation_int(Integer.valueOf(job.getEducation())); - }else { - esJobDocument.setEducation("-1"); - esJobDocument.setExperience_int(-1); - } - if (esJobDocument.getLatitude() != null) { - esJobDocument.setLatAndLon(esJobDocument.getLatitude().toString() + "," + esJobDocument.getLongitude().toString()); - } - if (StringUtils.isNotEmpty(job.getPostingDate())) { - Date date = DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS,job.getPostingDate()); - esJobDocument.setPostingDate(date); - } - esJobDocumentList.add(esJobDocument); - } - - // 批量插入当前批次的数据 - esJobDocumentMapper.insertBatch(esJobDocumentList); - - // 更新偏移量 - offset = offset+1; - } - } - } - - @Override - public List selectTextListExceptJobId(ESJobSearch esJobSearch, List jobIds) { - - ESJobSearch newSearch = new ESJobSearch(); - BeanUtils.copyProperties(esJobSearch,newSearch); - boolean isCompanyUser = StringUtil.IS_COMPANY_USER.equals(esJobSearch.getUserType()); - //查询 - if(SiteSecurityUtils.isLogin()){ - AppUser appUser = appUserService.selectAppUserByUserId(SiteSecurityUtils.getUserId()); - if (!StringUtil.isEmptyOrNull(esJobSearch.getCode())) { - newSearch.setCode(esJobSearch.getCode()); - } - //求职者 - if (!isCompanyUser) { - setJobSeekerParams(appUser, esJobSearch, newSearch,jobIds); - } - } - - LambdaEsQueryWrapper wrapper = getWrapper(newSearch,jobIds); - - //todo 暂时 - if (isCompanyUser) { - int current = esJobSearch.getCurrent() == null ? 0 : esJobSearch.getCurrent(); - current = Math.max(current, 0); // 非负校验 - int pageSize = esJobSearch.getPageSize() == null ? 10 : esJobSearch.getPageSize(); - pageSize = Math.min(pageSize, 50); // 限制最大条数 - int from = current * pageSize; - wrapper.orderByAsc(ESJobDocument::getId); - wrapper.limit(from, pageSize); - }else{ - wrapper.limit(esJobSearch.getPageSize()); - } - - List esJobDocuments = esJobDocumentMapper.selectList(wrapper); - - if (!isCompanyUser &&esJobDocuments.size() < esJobSearch.getPageSize()) { - // 定义要逐步放宽的搜索条件字段(不放松核心文本搜索条件) - List relaxConditions = new ArrayList<>(); - relaxConditions.add(() -> newSearch.setArea(null)); - relaxConditions.add(() -> newSearch.setExperience(null)); - relaxConditions.add(() -> newSearch.setMaxSalary(null)); - relaxConditions.add(() -> newSearch.setMinSalary(null)); - relaxConditions.add(() -> newSearch.setEducation(null)); - // 保存所有查询到的文档 - List allDocuments = new ArrayList<>(esJobDocuments); - - // 记录已经放宽的条件数量 - int relaxedConditions = 0; - - // 继续放宽条件直到满足分页或所有条件都已放宽 - while (allDocuments.size() < esJobSearch.getPageSize() - && relaxedConditions < relaxConditions.size()) { - // 放宽下一个条件 - relaxConditions.get(relaxedConditions).run(); - relaxedConditions++; - - // 查询新增的文档(不包含之前已经查询到的) - wrapper = getWrapper(newSearch, jobIds); - wrapper.limit(esJobSearch.getPageSize() - allDocuments.size()); - if (!allDocuments.isEmpty()) { - // 排除已经查询到的文档ID - Set existingIds = allDocuments.stream() - .map(ESJobDocument::getId) - .collect(Collectors.toSet()); - wrapper.not().in(ESJobDocument::getId, existingIds); - } - - List newDocuments = esJobDocumentMapper.selectList(wrapper); - allDocuments.addAll(newDocuments); - } - - // 如果总数超过分页大小,截取所需数量 - if (allDocuments.size() > esJobSearch.getPageSize()) { - esJobDocuments = allDocuments.subList(0, esJobSearch.getPageSize()); - } else { - esJobDocuments = allDocuments; - } - } - - return esJobDocuments; - } - - /** - * 拼装参数 - * @param appUser - * @param esJobSearch - * @param 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)); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getJobTitle())){ - newSearch.setJobTitle(esJobSearch.getJobTitle()); - } - if(!StringUtil.isEmptyOrNull(appUser.getEducation())){ - newSearch.setEducation(appUser.getEducation()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getEducation())){ - newSearch.setEducation(esJobSearch.getEducation()); - } - - if(!StringUtil.isEmptyOrNull(appUser.getArea())){ - newSearch.setArea(appUser.getArea()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getArea())){ - newSearch.setArea(esJobSearch.getArea()); - } - //影响后面按照经验打分,先注释 -// if(!StringUtil.isEmptyOrNull(appUser.getWorkExperience())){ -// newSearch.setExperience(appUser.getWorkExperience()); -// } - if(!StringUtil.isEmptyOrNull(esJobSearch.getExperience())){ - newSearch.setExperience(esJobSearch.getExperience()); - } - if(!StringUtil.isEmptyOrNull(appUser.getSalaryMax())){ - newSearch.setMaxSalary(Long.valueOf(appUser.getSalaryMax())); - } - if(!StringUtil.isEmptyOrNull(appUser.getSalaryMin())){ - newSearch.setMinSalary(Long.valueOf(appUser.getSalaryMin())); - } - // 请求参数覆盖用户资料中的薪资偏好 - if(Objects.nonNull(esJobSearch.getSalaryMin()) || Objects.nonNull(esJobSearch.getMinSalary())){ - newSearch.setMinSalary(Objects.nonNull(esJobSearch.getSalaryMin()) ? esJobSearch.getSalaryMin() : esJobSearch.getMinSalary()); - } - if(Objects.nonNull(esJobSearch.getSalaryMax()) || Objects.nonNull(esJobSearch.getMaxSalary())){ - newSearch.setMaxSalary(Objects.nonNull(esJobSearch.getSalaryMax()) ? esJobSearch.getSalaryMax() : esJobSearch.getMaxSalary()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getJobType())){ - newSearch.setJobType(esJobSearch.getJobType()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getType())){ - newSearch.setType(esJobSearch.getType()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getDescription())){ - newSearch.setDescription(esJobSearch.getDescription()); - } - 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); - } - - /** - * 新增全文检索数据 - */ - @Override - public Integer addESJobDocument(ESJobDocument esJobDocument) - { - return esJobDocumentMapper.insert(esJobDocument); - } - - /** - * 修改全文检索数据 - */ - @Override - public Integer updateESJobDocument(ESJobDocument esJobDocument) - { - - LambdaEsUpdateWrapper wrapper = new LambdaEsUpdateWrapper<>(); - wrapper.eq(ESJobDocument::getJobId, esJobDocument.getJobId()); - return esJobDocumentMapper.update(esJobDocument, wrapper); - } - - /** - * 根据ID查询全文检索数据 - */ - @Override - public ESJobDocument getESJobDocument(Long jobId) - { - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper(); - wrapper.eq(ESJobDocument::getJobId, jobId); - return esJobDocumentMapper.selectOne(wrapper); - } - - /** - * 根据ID删除全文检索数据 - */ - @Override - public void deleteESJobDocument(String ids) - { - for (Long jobId : Convert.toLongArray(ids)) - { - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper(); - wrapper.eq(ESJobDocument::getJobId, jobId); - esJobDocumentMapper.delete(wrapper); - } - } - - @Override - public EsPageInfo nearJob(ESJobSearch jobQuery) { - Integer pageNum = jobQuery.getCurrent(); - Integer pageSize = jobQuery.getPageSize(); - //添加屏蔽的企业 - List jobIds=null; - if(SiteSecurityUtils.isLogin()) { - AppUser appUser = appUserService.selectAppUserByUserId(SiteSecurityUtils.getUserId()); - jobIds = appUserBlockCompanyMapper.selectBlockCompanyIdsByUserIdJobs(appUser.getUserId()); - } - LambdaEsQueryWrapper wrapper = getWrapper(jobQuery,jobIds); - EsPageInfo esJobDocumentEsPageInfo = esJobDocumentMapper.pageQuery(wrapper, pageNum, pageSize); - return esJobDocumentEsPageInfo; - } - - @Override - public EsPageInfo countyJobList(ESJobSearch jobQuery) { - Integer pageNum = jobQuery.getCurrent(); - Integer pageSize = jobQuery.getPageSize(); - LambdaEsQueryWrapper wrapper = getWrapper(jobQuery, null); - //添加屏蔽的企业 - List jobIds=null; - if(SiteSecurityUtils.isLogin()) { - AppUser appUser = appUserService.selectAppUserByUserId(SiteSecurityUtils.getUserId()); - jobIds = appUserBlockCompanyMapper.selectBlockCompanyIdsByUserIdJobs(appUser.getUserId()); - wrapper.not().in(ESJobDocument::getJobId, jobIds); - } - if(jobQuery.getCountyIds()!=null){ - wrapper.and(x->x.in(ESJobDocument::getJobLocationAreaCode,jobQuery.getCountyIds())); - } - EsPageInfo esJobDocumentEsPageInfo = esJobDocumentMapper.pageQuery(wrapper, pageNum, pageSize); - return esJobDocumentEsPageInfo; - } - - @Override - public EsPageInfo subway(ESJobSearch jobQuery) { - Integer pageNum = jobQuery.getCurrent(); - Integer pageSize = jobQuery.getPageSize(); - List jobIds=null; - if(SiteSecurityUtils.isLogin()) { - AppUser appUser = appUserService.selectAppUserByUserId(SiteSecurityUtils.getUserId()); - jobIds = appUserBlockCompanyMapper.selectBlockCompanyIdsByUserIdJobs(appUser.getUserId()); - } - LambdaEsQueryWrapper wrapper = getWrapper(jobQuery,jobIds); - EsPageInfo esJobDocumentEsPageInfo = esJobDocumentMapper.pageQuery(wrapper, pageNum, pageSize); - return esJobDocumentEsPageInfo; - } - - @Override - public EsPageInfo commercialArea(ESJobSearch jobQuery) { - Integer pageNum = jobQuery.getCurrent(); - Integer pageSize = jobQuery.getPageSize(); - List jobIds=null; - if(SiteSecurityUtils.isLogin()) { - AppUser appUser = appUserService.selectAppUserByUserId(SiteSecurityUtils.getUserId()); - jobIds = appUserBlockCompanyMapper.selectBlockCompanyIdsByUserIdJobs(appUser.getUserId()); - } - LambdaEsQueryWrapper wrapper = getWrapper(jobQuery,jobIds); - if(jobQuery.getLongitude()!=null){ - wrapper.geoDistance(ESJobDocument::getLatAndLon,Double.valueOf(jobQuery.getRadius()), DistanceUnit.KILOMETERS,new GeoPoint(Double.parseDouble(jobQuery.getLatitude().toString()), Double.parseDouble(jobQuery.getLongitude().toString()))); - } - EsPageInfo esJobDocumentEsPageInfo = esJobDocumentMapper.pageQuery(wrapper, pageNum, pageSize); - return esJobDocumentEsPageInfo; - } - - @Override - public List littleVideo(ESJobSearch esJobSearch) { - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); - wrapper.eq(ESJobDocument::getIsExplain, 1); - wrapper.limit(esJobSearch.getPageSize()); - return esJobDocumentMapper.selectList(wrapper); - } - - @Override - public void updateEs() { - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); - esJobDocumentMapper.selectList(wrapper).forEach(esJobDocument -> { - esJobDocument.setPostingDate(new Date()); - this.updateESJobDocument(esJobDocument); - }); - } - - @Override - public List littleVideoRandom(List cacheList, Integer count,String jobTitle) { - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); - wrapper.eq(ESJobDocument::getIsExplain, 1); - if(!ListUtil.isListEmptyOrNull(cacheList)){ - wrapper.not().in(ESJobDocument::getJobId,cacheList); - } - if(count==null){ - count = 1; - } - if(StringUtil.isEmptyOrNull(jobTitle)){ - wrapper.or(a -> a.like(ESJobDocument::getJobTitle, jobTitle, 5.0f) - .or() - .like(ESJobDocument::getDescription, jobTitle, 1.0f) - ); - } - wrapper.limit(count); - return esJobDocumentMapper.selectList(wrapper); - } - - private LambdaEsQueryWrapper getWrapper(ESJobSearch esJobSearch,List jobIds){ - - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); - /*if(!StringUtil.isEmptyOrNull(esJobSearch.getJobTitle())){ - for (String keyWord:esJobSearch.getJobTitle().split(",")) { - wrapper.and(a -> a.match(ESJobDocument::getJobTitle, keyWord, 5.0f) - ); - } - - }*/ - //优化岗位名称查询,只有命中岗位名称或者分类就行 - String titleStr = esJobSearch.getJobTitle(); - String cateStr = esJobSearch.getJobCategory(); - boolean hasTitle = !StringUtil.isEmptyOrNull(titleStr); - boolean hasCate = !StringUtil.isEmptyOrNull(cateStr); - - if (hasTitle || hasCate) { - wrapper.and(sub -> { - if (hasTitle) { - String[] words = titleStr.split(","); - for (String w : words) { - String word = w.trim(); - if (StringUtil.isEmptyOrNull(word)) { - continue; - } - sub.or(inner -> inner - .match(ESJobDocument::getJobTitle, word, 5.0f) - .or() - .match(ESJobDocument::getJobCategory, word, 5.0f) - ); - } - } - if (hasCate) { - String[] words = cateStr.split(","); - for (String w : words) { - String word = w.trim(); - if (StringUtil.isEmptyOrNull(word)) { - continue; - } - sub.or(inner -> inner - .match(ESJobDocument::getJobTitle, word, 5.0f) - .or() - .match(ESJobDocument::getJobCategory, word, 5.0f) - ); - } - } - }); - } - - if(esJobSearch.getLongitude()!=null){ - wrapper.geoDistance(ESJobDocument::getLatAndLon,Double.valueOf(esJobSearch.getRadius()), DistanceUnit.KILOMETERS,new GeoPoint(Double.parseDouble(esJobSearch.getLatitude().toString()), Double.parseDouble(esJobSearch.getLongitude().toString()))); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getEducation())){ - Integer maxValue = StringUtil.findMaxValue(esJobSearch.getEducation()); - wrapper.and(x->x.eq(ESJobDocument::getEducation_int,maxValue)); - } - // 薪资范围筛选:兼容前端字段 salaryMin/salaryMax 和 minSalary/maxSalary - Long userMinSalary = Objects.nonNull(esJobSearch.getSalaryMin()) ? esJobSearch.getSalaryMin() : esJobSearch.getMinSalary(); - Long userMaxSalary = Objects.nonNull(esJobSearch.getSalaryMax()) ? esJobSearch.getSalaryMax() : esJobSearch.getMaxSalary(); - - // 岗位最高薪资必须在用户筛选范围内 - // 条件:岗位最大薪资 <= 用户最大薪资 AND 岗位最大薪资 >= 用户最小薪资 - if(Objects.nonNull(userMinSalary) && Objects.nonNull(userMaxSalary)){ - wrapper.and(x->x.le(ESJobDocument::getMaxSalary,userMaxSalary) - .ge(ESJobDocument::getMaxSalary,userMinSalary)); - } else if(Objects.nonNull(userMinSalary)){ - // 只有最小薪资:岗位最大薪资 >= 用户最小薪资 - wrapper.and(x->x.ge(ESJobDocument::getMaxSalary,userMinSalary)); - } else if(Objects.nonNull(userMaxSalary)){ - // 只有最大薪资:岗位最大薪资 <= 用户最大薪资 - wrapper.and(x->x.le(ESJobDocument::getMaxSalary,userMaxSalary)); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getExperience())){ - Integer maxValue = StringUtil.findMaxValue(esJobSearch.getExperience()); - wrapper.and(x->x.eq(ESJobDocument::getExperience_int,maxValue)); - } -// if(!StringUtil.isEmptyOrNull(esJobSearch.getJobCategory())){ -// String targetValue = esJobSearch.getJobCategory(); -// wrapper.and(x -> x.eq(ESJobDocument::getJobCategory, targetValue)); -// } - if(!StringUtil.isEmptyOrNull(esJobSearch.getScale())){ - Integer maxValue = StringUtil.findMaxValue(esJobSearch.getScale()); - wrapper.and(x->x.le(ESJobDocument::getScale,maxValue)); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getArea())){ - List integers = StringUtil.convertStringToIntegerList(esJobSearch.getArea()); - wrapper.and(x->x.in(ESJobDocument::getJobLocationAreaCode,integers)); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getJobType())){ - wrapper.and(x->x.eq(ESJobDocument::getJobType,esJobSearch.getJobType())); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getType())){ - wrapper.and(x->x.eq(ESJobDocument::getType,esJobSearch.getType())); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getDescription())){ - wrapper.and(x->x.like(ESJobDocument::getDescription,esJobSearch.getDescription())); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getCompanyNature())){ - wrapper.and(x->x.eq(ESJobDocument::getCompanyNature,esJobSearch.getCompanyNature())); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getCode())){ - wrapper.and(x->x.eq(ESJobDocument::getCode,esJobSearch.getCode())); - } - if(esJobSearch.getJobId()!=null){ - wrapper.and(x->x.eq(ESJobDocument::getJobId,esJobSearch.getJobId())); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getJobAddress())){ - wrapper.and(x->x.like(ESJobDocument::getJobAddress,esJobSearch.getJobAddress())); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getJobLocation())){ - wrapper.and(x->x.like(ESJobDocument::getJobLocation,esJobSearch.getJobLocation())); - } - 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(); - if (Objects.nonNull(startDate) && Objects.nonNull(endDate)) { - String startStr = DateUtils.ES_DATE_FORMATTER.format(startDate.toInstant()); - String endStr = DateUtils.ES_DATE_FORMATTER.format(endDate.toInstant()); - wrapper.between(ESJobDocument::getPostingDate, startStr, endStr); - } else if (Objects.nonNull(startDate)) { - wrapper.ge(ESJobDocument::getPostingDate, DateUtils.ES_DATE_FORMATTER.format(startDate.toInstant())); - } else if (Objects.nonNull(endDate)) { - wrapper.le(ESJobDocument::getPostingDate, DateUtils.ES_DATE_FORMATTER.format(endDate.toInstant())); - } - // 先开启打分、优先按匹配分数排序(关键词前台命中越高越靠前) - wrapper.trackScores(); - wrapper.sortByScore(SortOrder.DESC); - if(Objects.nonNull(esJobSearch.getOrder())){ - switch (esJobSearch.getOrder()) { - case 0: // 推荐:权重 → 最新发布 - wrapper.sort(SortBuilders.fieldSort("postingDate").order(SortOrder.DESC).missing("_last")); - break; - case 1: // 最热:权重 → 热度 - wrapper.orderByDesc(ESJobDocument::getIsHot); - wrapper.orderByDesc(ESJobDocument::getApplyNum); - wrapper.orderByDesc(ESJobDocument::getView); - break; - case 2: // 最新发布:权重 → 最新发布 - wrapper.sort(SortBuilders.fieldSort("postingDate").order(SortOrder.DESC).missing("_last")); - break; - case 3: // 最大薪资:权重 → 最高薪资 - wrapper.orderByDesc(ESJobDocument::getMaxSalary); - break; - default: // 默认:权重 → 最新发布 - wrapper.sort(SortBuilders.fieldSort("postingDate").order(SortOrder.DESC).missing("_last")); - } - } - //企业用户排除es去除jobIds - boolean isCompanyUser = StringUtil.IS_COMPANY_USER.equals(esJobSearch.getUserType()); - if (!isCompanyUser && !ListUtil.isListEmptyOrNull(jobIds)) { - wrapper.not().in(ESJobDocument::getJobId, jobIds); - } - return wrapper; - } - - //todo 存储用户搜索 - @Override - public EsPageInfo appList(ESJobSearch jobQuery) { - Integer pageNum = jobQuery.getCurrent(); - Integer pageSize = jobQuery.getPageSize(); - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); - if(SiteSecurityUtils.isLogin()){ - //AppUser appUser = appUserService.selectAppUserByUserId(SiteSecurityUtils.getUserId()); - List jobs=appUserBlockCompanyMapper.selectBlockCompanyIdsByUserIdJobs(SiteSecurityUtils.getUserId()); - if (!ListUtil.isListEmptyOrNull(jobs)) { - wrapper.not().in(ESJobDocument::getJobId, jobs); - } -// if(!ListUtil.isEmptyOrNull(appUser.getJobTitle())){ -// List jobTitle = appUser.getJobTitle(); -// jobQuery.setJobTitle(String.join(",", jobTitle)); -// } - } - if(hasText(jobQuery.getJobTitle())){ - wrapper.and(a->a.match(ESJobDocument::getJobTitle,jobQuery.getJobTitle().trim(),5.0f)); - } - if(hasText(jobQuery.getEducation())){ - // 支持逗号分隔的多值筛选(如 "3,4") - String eduStr = jobQuery.getEducation().trim(); - if (eduStr.contains(",")) { - List eduList = StringUtil.convertStringToStringList(eduStr); - wrapper.and(a->a.in(ESJobDocument::getEducation,eduList)); - } else { - wrapper.and(a->a.eq(ESJobDocument::getEducation,eduStr)); - } - } - if(hasText(jobQuery.getArea())){ - List integers = StringUtil.convertStringToIntegerList(jobQuery.getArea().trim()); - wrapper.and(x->x.in(ESJobDocument::getJobLocationAreaCode,integers)); - } - if(!StringUtil.isEmptyOrNull(jobQuery.getExperience())){ - List integers = StringUtil.convertStringToIntegerList(jobQuery.getExperience()); - wrapper.and(a->a.in(ESJobDocument::getExperience,integers)); - } - Long userMinSalary = Objects.nonNull(jobQuery.getSalaryMin()) ? jobQuery.getSalaryMin() : jobQuery.getMinSalary(); - Long userMaxSalary = Objects.nonNull(jobQuery.getSalaryMax()) ? jobQuery.getSalaryMax() : jobQuery.getMaxSalary(); - if (Objects.nonNull(userMinSalary)) { - wrapper.filter(x -> x.ge(ESJobDocument::getMaxSalary, userMinSalary)); - } - if (Objects.nonNull(userMaxSalary)) { - wrapper.filter(x -> x.le(ESJobDocument::getMinSalary, userMaxSalary)); - } - if(hasText(jobQuery.getScaleDictCode())){ - wrapper.and(a->a.eq(ESJobDocument::getScaleDictCode,jobQuery.getScaleDictCode().trim())); - } - if(hasText(jobQuery.getJobCategory())){ - wrapper.and(a->a.eq(ESJobDocument::getJobCategory,jobQuery.getJobCategory().trim())); - } - if(hasText(jobQuery.getIndustry())){ - wrapper.and(a->a.eq(ESJobDocument::getIndustry,jobQuery.getIndustry().trim())); - } - if(jobQuery.getLongitude()!=null){ - wrapper.geoDistance(ESJobDocument::getLatAndLon,Double.valueOf(jobQuery.getRadius()), DistanceUnit.KILOMETERS,new GeoPoint(Double.parseDouble(jobQuery.getLatitude().toString()), Double.parseDouble(jobQuery.getLongitude().toString()))); - } - if(jobQuery.getJobLocationAreaCode()!=null){ - wrapper.and(a->a.eq(ESJobDocument::getJobLocationAreaCode,jobQuery.getJobLocationAreaCode())); - } - if(hasText(jobQuery.getScale())){ - wrapper.and(a->a.eq(ESJobDocument::getScale,jobQuery.getScale().trim())); - } - if(hasText(jobQuery.getSearchValue())){ - wrapper.and(a->a.match(ESJobDocument::getDescription,jobQuery.getSearchValue().trim(),4.0f)); - } - if(hasText(jobQuery.getCompanyNature())){ - wrapper.and(a->a.eq(ESJobDocument::getCompanyNature,jobQuery.getCompanyNature().trim())); - } - if(hasText(jobQuery.getType())){ - wrapper.and(a->a.eq(ESJobDocument::getType,jobQuery.getType().trim())); - } - //开始时间 - Date startDate = jobQuery.getStartDate(); - Date endDate = jobQuery.getEndDate(); - if (Objects.nonNull(startDate) && Objects.nonNull(endDate)) { - String startStr = DateUtils.ES_DATE_FORMATTER.format(startDate.toInstant()); - String endStr = DateUtils.ES_DATE_FORMATTER.format(endDate.toInstant()); - wrapper.between(ESJobDocument::getPostingDate, startStr, endStr); - } else if (Objects.nonNull(startDate)) { - wrapper.ge(ESJobDocument::getPostingDate, DateUtils.ES_DATE_FORMATTER.format(startDate.toInstant())); - } else if (Objects.nonNull(endDate)) { - wrapper.le(ESJobDocument::getPostingDate, DateUtils.ES_DATE_FORMATTER.format(endDate.toInstant())); - } - // 先开启打分、优先按匹配分数排序(关键词前台命中越高越靠前) -// wrapper.trackScores(); -// wrapper.sortByScore(SortOrder.DESC); - if(Objects.nonNull(jobQuery.getOrder())){ - switch (jobQuery.getOrder()) { - case 0: // 推荐:权重 → 最新发布 - wrapper.sort(SortBuilders.fieldSort("postingDate").order(SortOrder.DESC).missing("_last")); - break; - case 1: // 最热:权重 → 热度 - wrapper.orderByDesc(ESJobDocument::getIsHot); - wrapper.orderByDesc(ESJobDocument::getApplyNum); - wrapper.orderByDesc(ESJobDocument::getView); - break; - case 2: // 最新发布:权重 → 最新发布 - wrapper.sort(SortBuilders.fieldSort("postingDate").order(SortOrder.DESC).missing("_last")); - break; - case 3: // 最大薪资:权重 → 最高薪资 - wrapper.orderByDesc(ESJobDocument::getMaxSalary); - break; - default: // 默认:权重 → 最新发布 - wrapper.sort(SortBuilders.fieldSort("postingDate").order(SortOrder.DESC).missing("_last")); - } - } - EsPageInfo esJobDocumentEsPageInfo = esJobDocumentMapper.pageQuery(wrapper, pageNum, pageSize); - return esJobDocumentEsPageInfo; - } - @Override - public EsPageInfo aiList(ESJobSearch jobQuery) { - Integer pageNum = jobQuery.getCurrent(); - Integer pageSize = jobQuery.getPageSize(); - - // 第一次查询:包含区域条件 - EsPageInfo result = doAiListQuery(jobQuery, pageNum, pageSize, true); - - // 降级查询:如果没有找到岗位且有区域条件,忽略区域重新查询 - if ((result.getList() == null || result.getList().isEmpty()) - && (hasText(jobQuery.getArea()) || jobQuery.getJobLocationAreaCode() != null)) { - logger.info("AI岗位查询降级:忽略区域条件重新查询"); - result = doAiListQuery(jobQuery, pageNum, pageSize, false); - } - - return result; - } - - /** - * AI岗位查询核心逻辑 - * @param jobQuery 查询条件 - * @param pageNum 页码 - * @param pageSize 每页大小 - * @param includeArea 是否包含区域条件 - */ - private EsPageInfo doAiListQuery(ESJobSearch jobQuery, Integer pageNum, Integer pageSize, boolean includeArea) { - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); - - if(hasText(jobQuery.getJobTitle())){ - wrapper.and(a->a.match(ESJobDocument::getJobTitle,jobQuery.getJobTitle().trim(),5.0f) - .or() - .match(ESJobDocument::getDescription,jobQuery.getJobTitle().trim(),1.0f)); - } - if(hasText(jobQuery.getEducation())){ - wrapper.and(a->a.le(ESJobDocument::getEducation,jobQuery.getEducation().trim())); - } - // 区域条件:根据 includeArea 决定是否加入 - if(includeArea && hasText(jobQuery.getArea())){ - List integers = StringUtil.convertStringToIntegerList(jobQuery.getArea().trim()); - wrapper.and(x->x.in(ESJobDocument::getJobLocationAreaCode,integers)); - } - if(hasText(jobQuery.getExperience())){ - wrapper.and(a->a.le(ESJobDocument::getExperience,jobQuery.getExperience().trim())); - } - // 薪资范围:岗位最高薪资必须在用户筛选范围内 - if(Objects.nonNull(jobQuery.getMaxSalary())){ - wrapper.and(x->x.le(ESJobDocument::getMaxSalary,jobQuery.getMaxSalary())); - } - if(Objects.nonNull(jobQuery.getMinSalary())){ - wrapper.and(x->x.ge(ESJobDocument::getMaxSalary,jobQuery.getMinSalary())); - } - if(hasText(jobQuery.getScaleDictCode())){ - wrapper.and(a->a.eq(ESJobDocument::getScaleDictCode,jobQuery.getScaleDictCode().trim())); - } - if(hasText(jobQuery.getIndustry())){ - wrapper.and(a->a.eq(ESJobDocument::getIndustry,jobQuery.getIndustry().trim())); - } - if(jobQuery.getLongitude()!=null){ - wrapper.geoDistance(ESJobDocument::getLatAndLon,Double.valueOf(jobQuery.getRadius()), DistanceUnit.KILOMETERS,new GeoPoint(Double.parseDouble(jobQuery.getLatitude().toString()), Double.parseDouble(jobQuery.getLongitude().toString()))); - } - // 区域编码条件:根据 includeArea 决定是否加入 - if(includeArea && jobQuery.getJobLocationAreaCode()!=null){ - wrapper.and(a->a.eq(ESJobDocument::getJobLocationAreaCode,jobQuery.getJobLocationAreaCode())); - } - if(hasText(jobQuery.getScale())){ - wrapper.and(a->a.eq(ESJobDocument::getScale,jobQuery.getScale().trim())); - } - if(hasText(jobQuery.getSearchValue())){ - wrapper.and(a->a.match(ESJobDocument::getDescription,jobQuery.getSearchValue().trim(),4.0f)); - } - if(hasText(jobQuery.getCompanyNature())){ - wrapper.and(a->a.eq(ESJobDocument::getCompanyNature,jobQuery.getCompanyNature().trim())); - } - if(hasText(jobQuery.getType())){ - wrapper.and(a->a.eq(ESJobDocument::getType,jobQuery.getType().trim())); - } - if(Objects.nonNull(jobQuery.getOrder())){ - if (jobQuery.getOrder()==2){ - wrapper.orderByDesc(ESJobDocument::getPostingDate); - } - if (jobQuery.getOrder()==1){ - wrapper.orderByDesc(ESJobDocument::getIsHot); - wrapper.orderByDesc(ESJobDocument::getApplyNum); - wrapper.orderByDesc(ESJobDocument::getView); - } - } - return esJobDocumentMapper.pageQuery(wrapper, pageNum, pageSize); - } - /** - * 判断字符串是否有实际内容(非null、非空、非纯空白) - */ - private boolean hasText(String str) { - return str != null && !str.trim().isEmpty(); - } - - @Override - public ESJobDocument selectById(Long jobId) { - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); - wrapper.eq(ESJobDocument::getJobId,jobId); - return esJobDocumentMapper.selectOne(wrapper); - } - - @Override - public void fix() { - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); - wrapper.eq(ESJobDocument::getIsExplain,1); - List esJobDocuments = esJobDocumentMapper.selectList(wrapper); - for (ESJobDocument esJobDocument : esJobDocuments) { - Job job = new Job(); - BeanUtils.copyProperties(esJobDocument,job); - job.setJobId(null); - jobMapper.insert(job); - } - } - - @Override - public void deleteJob(Long jobId) { - ensureJobDocumentIndexExists(); - LambdaEsQueryWrapper lambdaEsQueryWrapper = new LambdaEsQueryWrapper(); - lambdaEsQueryWrapper.eq(ESJobDocument::getJobId,jobId); - esJobDocumentMapper.delete(lambdaEsQueryWrapper); - } - - @Override - public void updateJob(Long jobId) { - ESJobDocument esJobDocument = new ESJobDocument(); - Job job = jobMapper.selectById(jobId); - if(job!=null){ - if(job.getCompanyId()!=null){ - Company company=iCompanyService.selectCompanyByCompanyId(job.getCompanyId()); - if (company != null) { - job.setCode(company.getCode()); - job.setCompanyNature(company.getCompanyNature()); - } - } - } - - BeanUtils.copyBeanProp(esJobDocument, job); - esJobDocument.setJobCategory(resolveJobCategoryLabel(job.getJobCategory())); - esJobDocument.setAppJobUrl("https://xjshzly.longbiosphere.com/app#/packageA/pages/post/post?jobId="+ Base64.getEncoder().encodeToString(String.valueOf(job.getJobId()).getBytes())); - if(!StringUtil.isEmptyOrNull(job.getScale())){ - esJobDocument.setScale(Integer.valueOf(job.getScale())); - }else { - esJobDocument.setScale(0); - } - if(!StringUtil.isEmptyOrNull(job.getExperience())){ - esJobDocument.setExperience_int(Integer.valueOf(job.getExperience())); - }else { - esJobDocument.setExperience("0"); - esJobDocument.setExperience_int(0); - } - if(!StringUtil.isEmptyOrNull(job.getEducation())){ - esJobDocument.setEducation_int(Integer.valueOf(job.getEducation())); - }else { - esJobDocument.setEducation("-1"); - esJobDocument.setEducation_int(-1); - } - if (esJobDocument.getLatitude() != null) { - esJobDocument.setLatAndLon(esJobDocument.getLatitude().toString() + "," + esJobDocument.getLongitude().toString()); - } - ensureJobDocumentIndexExists(); - - LambdaEsQueryWrapper lambdaEsQueryWrapper = new LambdaEsQueryWrapper(); - lambdaEsQueryWrapper.eq(ESJobDocument::getJobId,job.getJobId()); - esJobDocumentMapper.delete(lambdaEsQueryWrapper); - esJobDocumentMapper.insert(esJobDocument); - } - - /** - * 确保job_document索引存在;缺失时懒创建(仅建空索引与映射,不重建全量数据)。 - * process-index-mode: manual 下easy-es不会自动建索引,缺失时delete/update等写操作 - * 会抛 index_not_found_exception 导致业务接口500并回滚事务,故在写操作前调用以自愈。 - */ - private void ensureJobDocumentIndexExists() { - try { - if (!esJobDocumentMapper.existsIndex("job_document")) { - esJobDocumentMapper.createIndex(); - logger.info("job_document索引不存在,已自动创建"); - } - } catch (Exception e) { - // 并发场景下其他请求可能已创建索引,忽略“索引已存在”类异常 - logger.warn("自动创建job_document索引异常(可能已被其他请求创建,忽略)", e); - } - } - - public List selectByIds(Long[] jobIds) { - LambdaEsQueryWrapper wrapper = new LambdaEsQueryWrapper<>(); - wrapper.in(ESJobDocument::getJobId,jobIds); - return esJobDocumentMapper.selectList(wrapper); - } - - /** - * pc端查询es - * @param esJobSearch - * @param jobIds - * @param appUser - * @return - */ - @Override - public List selectSysTextListExceptJobId(ESJobSearch esJobSearch, List jobIds,AppUser appUser) { - - ESJobSearch newSearch = new ESJobSearch(); - BeanUtils.copyProperties(esJobSearch,newSearch); - - // 判断是否为用户主动搜索(有明确的搜索条件),而非纯推荐场景 - boolean isExplicitSearch = !StringUtil.isEmptyOrNull(esJobSearch.getJobTitle()) - || !StringUtil.isEmptyOrNull(esJobSearch.getJobCategory()) - || !StringUtil.isEmptyOrNull(esJobSearch.getCompanyName()) - || !StringUtil.isEmptyOrNull(esJobSearch.getDescription()); - - // 用户资料偏好仅在"纯推荐"场景下作为个性化过滤条件; - // 用户主动搜索时不应被资料中的薪资/学历/经验/区域等偏好缩小结果范围 - if(appUser != null && !isExplicitSearch){ - if(!ListUtil.isEmptyOrNull(appUser.getJobTitle())){ - List jobTitle = appUser.getJobTitle(); - newSearch.setJobTitle(String.join(",", jobTitle)); - } - if(!StringUtil.isEmptyOrNull(appUser.getEducation())){ - newSearch.setEducation(appUser.getEducation()); - } - if(!StringUtil.isEmptyOrNull(appUser.getArea())){ - newSearch.setArea(appUser.getArea()); - } - if(!StringUtil.isEmptyOrNull(appUser.getWorkExperience())){ - newSearch.setExperience(appUser.getWorkExperience()); - } - if(!StringUtil.isEmptyOrNull(appUser.getSalaryMax())){ - newSearch.setMaxSalary(Long.valueOf(appUser.getSalaryMax())); - } - 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); - } - - // 请求参数始终优先于用户资料偏好 - if(Objects.nonNull(esJobSearch.getSalaryMin()) || Objects.nonNull(esJobSearch.getMinSalary())){ - newSearch.setMinSalary(Objects.nonNull(esJobSearch.getSalaryMin()) ? esJobSearch.getSalaryMin() : esJobSearch.getMinSalary()); - } - if(Objects.nonNull(esJobSearch.getSalaryMax()) || Objects.nonNull(esJobSearch.getMaxSalary())){ - newSearch.setMaxSalary(Objects.nonNull(esJobSearch.getSalaryMax()) ? esJobSearch.getSalaryMax() : esJobSearch.getMaxSalary()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getArea())){ - newSearch.setArea(esJobSearch.getArea()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getEducation())){ - newSearch.setEducation(esJobSearch.getEducation()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getExperience())){ - newSearch.setExperience(esJobSearch.getExperience()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getJobTitle())){ - newSearch.setJobTitle(esJobSearch.getJobTitle()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getJobType())){ - newSearch.setJobType(esJobSearch.getJobType()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getType())){ - newSearch.setType(esJobSearch.getType()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getDescription())){ - newSearch.setDescription(esJobSearch.getDescription()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getCompanyNature())){ - newSearch.setCompanyNature(esJobSearch.getCompanyNature()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getCode())){ - newSearch.setCode(esJobSearch.getCode()); - } - if(!StringUtil.isEmptyOrNull(esJobSearch.getCompanyName())){ - newSearch.setCompanyName(esJobSearch.getCompanyName()); - } - - LambdaEsQueryWrapper wrapper = getWrapper(newSearch,jobIds); - - //todo 暂时 - wrapper.limit(esJobSearch.getPageSize()); - List esJobDocuments = esJobDocumentMapper.selectList(wrapper); - - if (esJobDocuments.size() < esJobSearch.getPageSize()) { - // 定义要逐步放宽的搜索条件字段(不放松核心文本搜索条件) - List relaxConditions = new ArrayList<>(); - relaxConditions.add(() -> newSearch.setArea(null)); - relaxConditions.add(() -> newSearch.setExperience(null)); - relaxConditions.add(() -> newSearch.setMaxSalary(null)); - relaxConditions.add(() -> newSearch.setMinSalary(null)); - relaxConditions.add(() -> newSearch.setEducation(null)); - // 保存所有查询到的文档 - List allDocuments = new ArrayList<>(esJobDocuments); - - // 记录已经放宽的条件数量 - int relaxedConditions = 0; - - // 继续放宽条件直到满足分页或所有条件都已放宽 - while (allDocuments.size() < esJobSearch.getPageSize() - && relaxedConditions < relaxConditions.size()) { - // 放宽下一个条件 - relaxConditions.get(relaxedConditions).run(); - relaxedConditions++; - - // 查询新增的文档(不包含之前已经查询到的) - wrapper = getWrapper(newSearch, jobIds); - wrapper.limit(esJobSearch.getPageSize() - allDocuments.size()); - if (!allDocuments.isEmpty()) { - // 排除已经查询到的文档ID - Set existingIds = allDocuments.stream() - .map(ESJobDocument::getId) - .collect(Collectors.toSet()); - wrapper.not().in(ESJobDocument::getId, existingIds); - } - - List newDocuments = esJobDocumentMapper.selectList(wrapper); - allDocuments.addAll(newDocuments); - } - - // 如果总数超过分页大小,截取所需数量 - if (allDocuments.size() > esJobSearch.getPageSize()) { - esJobDocuments = allDocuments.subList(0, esJobSearch.getPageSize()); - } else { - esJobDocuments = allDocuments; - } - } - - return esJobDocuments; - } - - /** - * 将 jobCategory 从数字ID解析为标签文字(如 "45" → "销售顾问") - * 如果已经是文字标签或解析失败,返回原值 - */ - private String resolveJobCategoryLabel(String jobCategory) { - if (StringUtils.isEmpty(jobCategory)) { - return jobCategory; - } - try { - Long categoryId = Long.parseLong(jobCategory); - JobTitle jobTitle = jobTitleMapper.selectById(categoryId); - if (jobTitle != null && StringUtils.isNotEmpty(jobTitle.getJobName())) { - return jobTitle.getJobName(); - } - } catch (NumberFormatException e) { - // 已经是文字标签,直接返回原值 - } - return jobCategory; - } - - /** - * 根据岗位id批量更新es - * @param jobIdList - */ - @Override - public void batchUpdateJob(List jobIdList) { - if (jobIdList == null || jobIdList.isEmpty()) { - return; - } - //索引初始化 - ensureJobDocumentIndexExists(); - //查询所有岗位数据 - List allJobList = jobMapper.selectBatchIds(jobIdList); - if (allJobList == null || allJobList.isEmpty()) { - LambdaEsQueryWrapper delWrapper = new LambdaEsQueryWrapper<>(); - delWrapper.in(ESJobDocument::getJobId, jobIdList); - esJobDocumentMapper.delete(delWrapper); - return; - } - //查询所有企业数据 - List companyIdList = allJobList.stream() - .map(Job::getCompanyId) - .filter(id -> id != null) - .distinct() - .collect(Collectors.toList()); - List companyList = new ArrayList<>(); - if (!companyIdList.isEmpty()) { - companyList = companyMapper.selectBatchIds(companyIdList); - } - Map companyMap = companyList.stream() - .collect(Collectors.toMap(Company::getCompanyId, c -> c)); - - // 批量转换ES - List esDocList = allJobList.stream() - .map(job -> convertToEsDoc(job, companyMap)) - .collect(Collectors.toList()); - - try { - // 批量删除旧索引 - LambdaEsQueryWrapper delWrapper = new LambdaEsQueryWrapper<>(); - delWrapper.in(ESJobDocument::getJobId, jobIdList); - esJobDocumentMapper.delete(delWrapper); - // 批量插入新索引 - esJobDocumentMapper.insertBatch(esDocList); - } catch (Exception e) { - throw new RuntimeException("ES批量更新失败", e); - } - } - - /** - * Job 转 ESJobDocument 抽取单独方法 - */ - private ESJobDocument convertToEsDoc(Job job, Map companyMap) { - ESJobDocument esDoc = new ESJobDocument(); - BeanUtils.copyBeanProp(esDoc, job); - // 企业编码赋值 - if (job.getCompanyId() != null) { - Company company = companyMap.get(job.getCompanyId()); - if (company != null) { - esDoc.setCode(company.getCode()); - esDoc.setCompanyNature(company.getCompanyNature()); - } - } - - esDoc.setJobCategory(resolveJobCategoryLabel(job.getJobCategory())); - esDoc.setAppJobUrl("https://www.xjksly.cn/app#/packageA/pages/post/post?jobId="+ job.getJobId()); - esDoc.setScale(parseIntDefaultZero(job.getScale())); - //工作经验 - if (StringUtil.isEmptyOrNull(job.getExperience())) { - esDoc.setExperience("0"); - esDoc.setExperience_int(0); - } else { - Integer expInt = parseIntDefaultZero(job.getExperience()); - esDoc.setExperience_int(expInt); - } - - // 学历字段统一处理 - if (StringUtil.isEmptyOrNull(job.getEducation())) { - esDoc.setEducation("-1"); - esDoc.setEducation_int(-1); - } else { - Integer eduInt = parseIntDefaultMinusOne(job.getEducation()); - esDoc.setEducation_int(eduInt); - } - - // 经纬度拼接 - if (esDoc.getLatitude() != null && esDoc.getLongitude() != null) { - esDoc.setLatAndLon(esDoc.getLatitude() + "," + esDoc.getLongitude()); - } - - return esDoc; - } - - /** - * 字符串转数字,空/非法返回0 - */ - private Integer parseIntDefaultZero(String str) { - if (StringUtil.isEmptyOrNull(str)) { - return 0; - } - try { - return Integer.valueOf(str); - } catch (NumberFormatException e) { - return 0; - } - } - - /** - * 字符串转数字,空/非法返回-1 - */ - private Integer parseIntDefaultMinusOne(String str) { - if (StringUtil.isEmptyOrNull(str)) { - return -1; - } - try { - return Integer.valueOf(str); - } catch (NumberFormatException e) { - 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/service/impl/JobTitleSuggestServiceImpl.java b/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/JobTitleSuggestServiceImpl.java deleted file mode 100644 index a21ed2f..0000000 --- a/ruoyi-bussiness/src/main/java/com/ruoyi/cms/service/impl/JobTitleSuggestServiceImpl.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.ruoyi.cms.service.impl; - -import com.ruoyi.cms.mapper.JobMapper; -import com.ruoyi.cms.service.IJobTitleSuggestService; -import com.ruoyi.cms.util.JobTitleSuggestTokenizer; -import com.ruoyi.common.core.redis.RedisCache; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.PostConstruct; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -/** - * 岗位名称 IK 分词联想服务实现。 - */ -@Service -public class JobTitleSuggestServiceImpl implements IJobTitleSuggestService { - - private static final Logger logger = LoggerFactory.getLogger(JobTitleSuggestServiceImpl.class); - - /** - * Redis Hash:field 是 IK 分词,value 是包含该分词的完整岗位名称列表。 - */ - private static final String CACHE_KEY = "app:job:title:suggest"; - private static final String BUILDING_CACHE_KEY = CACHE_KEY + ":building"; - private static final String LOCK_KEY = CACHE_KEY + ":rebuild:lock"; - - private static final int DEFAULT_LIMIT = 10; - private static final int MAX_LIMIT = 20; - private static final int LOCK_EXPIRE_MINUTES = 30; - - @Autowired - private JobMapper jobMapper; - - @Autowired - private RedisCache redisCache; - - /** - * 确保服务启动后 Redis 中已有一份缓存;后续由 Quartz 定时任务周期性重建。 - */ - @PostConstruct - public void init() { - rebuildCache(); - } - - @Override - public List suggest(String keyword, Integer limit) { - String normalizedKeyword = JobTitleSuggestTokenizer.normalize(keyword); - if (normalizedKeyword.isEmpty()) { - return Collections.emptyList(); - } - - int resultLimit = normalizeLimit(limit); - try { - Map tokenTitles = redisCache.getCacheMap(CACHE_KEY); - if (tokenTitles == null || tokenTitles.isEmpty()) { - return Collections.emptyList(); - } - - // Hash field 只来自 IK 分词;这里仅对已有词项做前缀过滤,不生成新的前缀词。 - List> matchedTokens = tokenTitles.entrySet().stream() - .filter(entry -> entry.getKey() != null) - .filter(entry -> entry.getKey().startsWith(normalizedKeyword)) - .sorted(Map.Entry.comparingByKey()) - .collect(Collectors.toList()); - - Set result = new LinkedHashSet<>(); - for (Map.Entry entry : matchedTokens) { - if (result.size() >= resultLimit) { - break; - } - - // 先返回匹配到的 IK 分词本身。 - result.add(entry.getKey()); - - // 再返回包含该 IK 分词的完整岗位名称。 - appendTitles(result, entry.getValue(), resultLimit); - } - return new ArrayList<>(result).subList(0, Math.min(result.size(), resultLimit)); - } catch (Exception e) { - // 联想属于非核心能力,Redis 异常时返回空列表,不影响岗位主搜索。 - logger.warn("查询岗位名称 IK 分词缓存失败,keyword={}", keyword, e); - return Collections.emptyList(); - } - } - - @Override - public void rebuildCache() { - String lockValue = UUID.randomUUID().toString(); - boolean lockAcquired = false; - try { - lockAcquired = redisCache.setIfAbsent( - LOCK_KEY, lockValue, LOCK_EXPIRE_MINUTES, TimeUnit.MINUTES); - if (!lockAcquired) { - logger.info("其他节点正在重建岗位名称 IK 分词缓存,本节点跳过"); - return; - } - - List sourceList = jobMapper.selectValidJobTitles(); - Map> cacheData = buildTokenTitles(sourceList); - - // 先写临时 Hash,再原子替换正式 Hash,避免接口读到半成品。 - redisCache.deleteObject(BUILDING_CACHE_KEY); - if (cacheData.isEmpty()) { - redisCache.deleteObject(CACHE_KEY); - } else { - redisCache.setCacheMap(BUILDING_CACHE_KEY, cacheData); - redisCache.redisTemplate.rename(BUILDING_CACHE_KEY, CACHE_KEY); - } - logger.info("岗位名称 IK 分词缓存重建完成,有效岗位名称{}个,IK 词{}个", - sourceList == null ? 0 : sourceList.size(), cacheData.size()); - } catch (Exception e) { - logger.error("重建岗位名称 IK 分词缓存失败", e); - } finally { - if (lockAcquired) { - redisCache.deleteObject(LOCK_KEY); - } - } - } - - private Map> buildTokenTitles(List sourceList) { - Map> tokenTitles = new TreeMap<>(); - if (sourceList == null) { - return Collections.emptyMap(); - } - - for (String title : sourceList) { - String displayTitle = normalizeDisplayTitle(title); - if (displayTitle.isEmpty()) { - continue; - } - - Set tokens = JobTitleSuggestTokenizer.tokenize(displayTitle); - for (String token : tokens) { - tokenTitles.computeIfAbsent(token, key -> new TreeSet<>()).add(displayTitle); - } - } - - Map> result = new LinkedHashMap<>(); - for (Map.Entry> entry : tokenTitles.entrySet()) { - result.put(entry.getKey(), new ArrayList<>(entry.getValue())); - } - return result; - } - - private void appendTitles(Set result, Object cachedTitles, int resultLimit) { - if (!(cachedTitles instanceof Iterable)) { - return; - } - for (Object cachedTitle : (Iterable) cachedTitles) { - if (cachedTitle == null || result.size() >= resultLimit) { - break; - } - String title = String.valueOf(cachedTitle); - if (!title.isEmpty()) { - result.add(title); - } - } - } - - private String normalizeDisplayTitle(String title) { - if (title == null) { - return ""; - } - return title.trim().replaceAll("\\s+", " "); - } - - private int normalizeLimit(Integer limit) { - if (limit == null) { - return DEFAULT_LIMIT; - } - return Math.max(1, Math.min(limit, MAX_LIMIT)); - } -}