添加AI推荐列表
This commit is contained in:
@@ -4,6 +4,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import org.jiayunet.pojo.PageResult;
|
import org.jiayunet.pojo.PageResult;
|
||||||
import org.jiayunet.pojo.dto.job.JobDetailDto;
|
import org.jiayunet.pojo.dto.job.JobDetailDto;
|
||||||
import org.jiayunet.pojo.dto.job.JobDto;
|
import org.jiayunet.pojo.dto.job.JobDto;
|
||||||
|
import org.jiayunet.pojo.dto.job.JobAgentRecommendDto;
|
||||||
import org.jiayunet.pojo.param.job.*;
|
import org.jiayunet.pojo.param.job.*;
|
||||||
import org.jiayunet.pojo.vo.JobApplyCountVo;
|
import org.jiayunet.pojo.vo.JobApplyCountVo;
|
||||||
import org.jiayunet.pojo.vo.JobFavoriteCountVo;
|
import org.jiayunet.pojo.vo.JobFavoriteCountVo;
|
||||||
@@ -139,4 +140,13 @@ public class JobController {
|
|||||||
Long userId = UserSecurityTool.getUserId();
|
Long userId = UserSecurityTool.getUserId();
|
||||||
return jobService.listAgentTasks(param, userId);
|
return jobService.listAgentTasks(param, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 求职助手岗位推荐(AI精筛)
|
||||||
|
*/
|
||||||
|
@PostMapping("/agent/recommend")
|
||||||
|
public JobAgentRecommendDto recommendJobs(@RequestBody JobAgentRecommendParam param) {
|
||||||
|
Long userId = UserSecurityTool.getUserId();
|
||||||
|
return jobService.recommendJobs(param, userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.jiayunet.pojo.dto.job;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 求职助手岗位推荐出参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class JobAgentRecommendDto {
|
||||||
|
|
||||||
|
/** 推荐说明(20字以内) */
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
/** 推荐的岗位列表(8-10个,完整列表数据) */
|
||||||
|
private List<JobDto> list;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.jiayunet.pojo.param.job;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 求职助手岗位推荐入参
|
||||||
|
*
|
||||||
|
* @author zk
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class JobAgentRecommendParam {
|
||||||
|
|
||||||
|
/** 用户偏好描述,如"更喜欢技术一点的产品" */
|
||||||
|
private String preference;
|
||||||
|
|
||||||
|
/** 排除已推荐过的岗位ID列表 */
|
||||||
|
private List<Long> excludeJobIds;
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@ public class JobQueryParam extends PageParam {
|
|||||||
/** 指定岗位ID列表(用于收藏列表) */
|
/** 指定岗位ID列表(用于收藏列表) */
|
||||||
private List<Long> jobIds;
|
private List<Long> jobIds;
|
||||||
|
|
||||||
|
/** 排除岗位ID列表(用于推荐时排除已推荐过的) */
|
||||||
|
private List<Long> excludeJobIds;
|
||||||
|
|
||||||
/** 岗位状态过滤(0=有效 1=已下架 2=已过期,可多选,null或空=查所有) */
|
/** 岗位状态过滤(0=有效 1=已下架 2=已过期,可多选,null或空=查所有) */
|
||||||
private List<Integer> statusFilter;
|
private List<Integer> statusFilter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.jiayunet.service;
|
package org.jiayunet.service;
|
||||||
|
|
||||||
|
import org.jiayunet.ai.AiChatAbility;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -8,11 +9,16 @@ import org.jiayunet.pojo.PageResult;
|
|||||||
import org.jiayunet.pojo.dto.job.JobDetailDto;
|
import org.jiayunet.pojo.dto.job.JobDetailDto;
|
||||||
import org.jiayunet.pojo.dto.job.JobDto;
|
import org.jiayunet.pojo.dto.job.JobDto;
|
||||||
import org.jiayunet.pojo.dto.job.JobMatchScoreDto;
|
import org.jiayunet.pojo.dto.job.JobMatchScoreDto;
|
||||||
|
import org.jiayunet.pojo.dto.job.JobAgentRecommendDto;
|
||||||
|
import org.jiayunet.pojo.param.job.JobAgentRecommendParam;
|
||||||
import org.jiayunet.pojo.param.job.JobAgentTaskQueryParam;
|
import org.jiayunet.pojo.param.job.JobAgentTaskQueryParam;
|
||||||
import org.jiayunet.pojo.param.job.JobApplyQueryParam;
|
import org.jiayunet.pojo.param.job.JobApplyQueryParam;
|
||||||
import org.jiayunet.pojo.param.job.JobFavoriteQueryParam;
|
import org.jiayunet.pojo.param.job.JobFavoriteQueryParam;
|
||||||
import org.jiayunet.pojo.param.job.JobQueryParam;
|
import org.jiayunet.pojo.param.job.JobQueryParam;
|
||||||
import org.jiayunet.pojo.po.*;
|
import org.jiayunet.pojo.po.*;
|
||||||
|
import org.jiayunet.tool.HttpTool;
|
||||||
|
import org.jiayunet.exception.BusinessException;
|
||||||
|
import org.jiayunet.exception.BusinessExpCodeEnum;
|
||||||
import org.jiayunet.pojo.vo.JobApplyCountVo;
|
import org.jiayunet.pojo.vo.JobApplyCountVo;
|
||||||
import org.jiayunet.pojo.vo.JobFavoriteCountVo;
|
import org.jiayunet.pojo.vo.JobFavoriteCountVo;
|
||||||
import org.jiayunet.pojo.vo.JobListItemVo;
|
import org.jiayunet.pojo.vo.JobListItemVo;
|
||||||
@@ -20,6 +26,7 @@ import org.springframework.beans.BeanUtils;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -66,6 +73,12 @@ public class JobService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private JobMatchService jobMatchService;
|
private JobMatchService jobMatchService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserJobIntentionMapper userJobIntentionMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AiChatAbility aiChatAbility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 岗位列表查询
|
* 岗位列表查询
|
||||||
* <p>方法逻辑流程:</p>
|
* <p>方法逻辑流程:</p>
|
||||||
@@ -96,7 +109,10 @@ public class JobService {
|
|||||||
List<UserJobDislike> dislikes = userJobDislikeMapper.selectList(new LambdaQueryWrapper<UserJobDislike>().eq(UserJobDislike::getUserId, userId));
|
List<UserJobDislike> dislikes = userJobDislikeMapper.selectList(new LambdaQueryWrapper<UserJobDislike>().eq(UserJobDislike::getUserId, userId));
|
||||||
|
|
||||||
// 3. 提取排除列表
|
// 3. 提取排除列表
|
||||||
List<Long> excludeJobIds = dislikes.stream().map(UserJobDislike::getJobId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
List<Long> excludeJobIds = new ArrayList<>(dislikes.stream().map(UserJobDislike::getJobId).filter(Objects::nonNull).distinct().collect(Collectors.toList()));
|
||||||
|
if (param.getExcludeJobIds() != null && !param.getExcludeJobIds().isEmpty()) {
|
||||||
|
excludeJobIds.addAll(param.getExcludeJobIds());
|
||||||
|
}
|
||||||
List<Long> excludeCompanyIds = dislikes.stream().map(UserJobDislike::getCompanyId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
List<Long> excludeCompanyIds = dislikes.stream().map(UserJobDislike::getCompanyId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||||
|
|
||||||
// 4. 提取不感兴趣的地区(直接使用,不扩展子级)
|
// 4. 提取不感兴趣的地区(直接使用,不扩展子级)
|
||||||
@@ -561,4 +577,104 @@ public class JobService {
|
|||||||
|
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 求职助手岗位推荐
|
||||||
|
* <p>1. 查求职意向构造筛选条件(无意向则不设条件) 2. 排除已推荐的+已投递的 3. 取前35条候选 4. AI精筛返回8-10个</p>
|
||||||
|
*/
|
||||||
|
public JobAgentRecommendDto recommendJobs(JobAgentRecommendParam param, Long userId) {
|
||||||
|
// 1. 查求职意向
|
||||||
|
UserJobIntention intention = userJobIntentionMapper.selectOne(new LambdaQueryWrapper<UserJobIntention>().eq(UserJobIntention::getUserId, userId));
|
||||||
|
|
||||||
|
// 2. 构造查询参数
|
||||||
|
JobQueryParam queryParam = new JobQueryParam();
|
||||||
|
queryParam.setPageNum(1);
|
||||||
|
queryParam.setPageSize(35);
|
||||||
|
if (intention != null) {
|
||||||
|
queryParam.setCategoryIds(intention.getCategoryIds());
|
||||||
|
queryParam.setRegionCodes(intention.getRegionCodes());
|
||||||
|
queryParam.setIndustryIds(intention.getIndustryIds());
|
||||||
|
queryParam.setEmploymentType(intention.getEmploymentType());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 合并排除列表:入参传的 + 已投递/待投递的
|
||||||
|
List<Long> excludeIds = new ArrayList<>();
|
||||||
|
if (param.getExcludeJobIds() != null) {
|
||||||
|
excludeIds.addAll(param.getExcludeJobIds());
|
||||||
|
}
|
||||||
|
List<UserJobApplication> applications = userJobApplicationMapper.selectList(new LambdaQueryWrapper<UserJobApplication>().eq(UserJobApplication::getUserId, userId));
|
||||||
|
applications.forEach(a -> excludeIds.add(a.getJobId()));
|
||||||
|
queryParam.setExcludeJobIds(excludeIds);
|
||||||
|
|
||||||
|
// 4. 查询候选岗位(完整列表数据)
|
||||||
|
PageResult<JobDto> candidates = listJobs(queryParam, userId);
|
||||||
|
if (candidates.getList().isEmpty()) {
|
||||||
|
JobAgentRecommendDto dto = new JobAgentRecommendDto();
|
||||||
|
dto.setSummary("暂无合适的岗位推荐");
|
||||||
|
dto.setList(Collections.emptyList());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 批量查岗位详情(title + description + requirement)
|
||||||
|
List<Long> candidateJobIds = candidates.getList().stream().map(JobDto::getId).collect(Collectors.toList());
|
||||||
|
List<Job> jobDetails = jobMapper.selectList(new LambdaQueryWrapper<Job>().in(Job::getId, candidateJobIds).select(Job::getId, Job::getTitle, Job::getDescription, Job::getRequirement));
|
||||||
|
Map<Long, Job> jobDetailMap = jobDetails.stream().collect(Collectors.toMap(Job::getId, j -> j));
|
||||||
|
|
||||||
|
// 6. 构造候选岗位映射(供AI返回后过滤使用)
|
||||||
|
Map<Long, JobDto> candidateMap = candidates.getList().stream().collect(Collectors.toMap(JobDto::getId, d -> d));
|
||||||
|
|
||||||
|
// 7. 构造AI输入
|
||||||
|
StringBuilder jobInfo = new StringBuilder();
|
||||||
|
for (JobDto dto : candidates.getList()) {
|
||||||
|
Job detail = jobDetailMap.get(dto.getId());
|
||||||
|
jobInfo.append("ID:").append(dto.getId())
|
||||||
|
.append("\n标题:").append(dto.getTitle())
|
||||||
|
.append("\n公司:").append(dto.getCompanyName())
|
||||||
|
.append("\n岗位职责:").append(detail != null ? detail.getDescription() : "")
|
||||||
|
.append("\n任职要求:").append(detail != null ? detail.getRequirement() : "")
|
||||||
|
.append("\n---\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
String preferenceInfo = param.getPreference() != null ? param.getPreference() : "无特殊偏好";
|
||||||
|
|
||||||
|
String systemPrompt = "你是一个求职推荐助手。根据用户的偏好,从候选岗位中选出8-10个最合适的岗位。" +
|
||||||
|
"返回JSON格式:{\"summary\":\"推荐说明(20字以内)\",\"jobIds\":[岗位ID列表]}。" +
|
||||||
|
"只返回JSON,不要其他内容。";
|
||||||
|
String userMessage = "【用户偏好】\n" + preferenceInfo + "\n\n【候选岗位】\n" + jobInfo;
|
||||||
|
|
||||||
|
// 8. 调用AI
|
||||||
|
String aiResponse = aiChatAbility.chat("job-recommend", systemPrompt, userMessage);
|
||||||
|
String json = cleanAiResponse(aiResponse);
|
||||||
|
|
||||||
|
// 9. 解析AI返回,过滤出选中的岗位
|
||||||
|
try {
|
||||||
|
JsonNode root = HttpTool.objectMapper.readTree(json);
|
||||||
|
String summary = root.path("summary").asText("为你精选了合适的岗位");
|
||||||
|
List<Long> selectedIds = new ArrayList<>();
|
||||||
|
root.path("jobIds").forEach(node -> selectedIds.add(node.asLong()));
|
||||||
|
|
||||||
|
List<JobDto> selectedJobs = selectedIds.stream()
|
||||||
|
.map(candidateMap::get)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
JobAgentRecommendDto result = new JobAgentRecommendDto();
|
||||||
|
result.setSummary(summary);
|
||||||
|
result.setList(selectedJobs);
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AI推荐结果解析失败, response={}", json, e);
|
||||||
|
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "AI推荐结果解析失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清理AI返回的markdown代码块和控制字符 */
|
||||||
|
private String cleanAiResponse(String response) {
|
||||||
|
String json = response.trim();
|
||||||
|
if (json.startsWith("```")) {
|
||||||
|
json = json.replaceAll("^```\\w*\\n?", "").replaceAll("\\n?```$", "").trim();
|
||||||
|
}
|
||||||
|
json = json.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", "");
|
||||||
|
return json;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ app:
|
|||||||
base-url: ${AI_BASE_URL:https://ark.cn-beijing.volces.com/api/v3}
|
base-url: ${AI_BASE_URL:https://ark.cn-beijing.volces.com/api/v3}
|
||||||
api-key: ${AI_API_KEY:fd065993-bee2-4f31-8bf2-56d5d3012c02}
|
api-key: ${AI_API_KEY:fd065993-bee2-4f31-8bf2-56d5d3012c02}
|
||||||
model: ${AI_MODEL:doubao-seed-2-0-lite-260215}
|
model: ${AI_MODEL:doubao-seed-2-0-lite-260215}
|
||||||
|
job-recommend:
|
||||||
|
base-url: ${AI_BASE_URL:https://ark.cn-beijing.volces.com/api/v3}
|
||||||
|
api-key: ${AI_API_KEY:fd065993-bee2-4f31-8bf2-56d5d3012c02}
|
||||||
|
model: ${AI_MODEL:doubao-1-5-pro-32k-250115}
|
||||||
|
|
||||||
|
|
||||||
# 岗位清洗配置
|
# 岗位清洗配置
|
||||||
job-clean:
|
job-clean:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class PageParam {
|
|||||||
|
|
||||||
/** 每页条数 */
|
/** 每页条数 */
|
||||||
@Min(value = 1, message = "每页条数最小为1")
|
@Min(value = 1, message = "每页条数最小为1")
|
||||||
@Max(value = 100, message = "每页条数最大为100")
|
@Max(value = 1000, message = "每页条数最大为100")
|
||||||
private Integer pageSize = 10;
|
private Integer pageSize = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user