From f8bf2727f4ad33e278e222b60c27f36f085ab837 Mon Sep 17 00:00:00 2001 From: zk Date: Fri, 24 Apr 2026 15:38:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0AI=E6=8E=A8=E8=8D=90=E5=88=97?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jiayunet/controller/JobController.java | 10 ++ .../pojo/dto/job/JobAgentRecommendDto.java | 20 +++ .../param/job/JobAgentRecommendParam.java | 20 +++ .../pojo/param/job/JobQueryParam.java | 3 + .../java/org/jiayunet/service/JobService.java | 118 +++++++++++++++++- .../src/main/resources/application-local.yml | 5 + .../java/org/jiayunet/pojo/PageParam.java | 2 +- 7 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 client-api/src/main/java/org/jiayunet/pojo/dto/job/JobAgentRecommendDto.java create mode 100644 client-api/src/main/java/org/jiayunet/pojo/param/job/JobAgentRecommendParam.java diff --git a/client-api/src/main/java/org/jiayunet/controller/JobController.java b/client-api/src/main/java/org/jiayunet/controller/JobController.java index a6658a2..f7d04b6 100644 --- a/client-api/src/main/java/org/jiayunet/controller/JobController.java +++ b/client-api/src/main/java/org/jiayunet/controller/JobController.java @@ -4,6 +4,7 @@ import lombok.AllArgsConstructor; import org.jiayunet.pojo.PageResult; import org.jiayunet.pojo.dto.job.JobDetailDto; import org.jiayunet.pojo.dto.job.JobDto; +import org.jiayunet.pojo.dto.job.JobAgentRecommendDto; import org.jiayunet.pojo.param.job.*; import org.jiayunet.pojo.vo.JobApplyCountVo; import org.jiayunet.pojo.vo.JobFavoriteCountVo; @@ -139,4 +140,13 @@ public class JobController { Long userId = UserSecurityTool.getUserId(); return jobService.listAgentTasks(param, userId); } + + /** + * 求职助手岗位推荐(AI精筛) + */ + @PostMapping("/agent/recommend") + public JobAgentRecommendDto recommendJobs(@RequestBody JobAgentRecommendParam param) { + Long userId = UserSecurityTool.getUserId(); + return jobService.recommendJobs(param, userId); + } } diff --git a/client-api/src/main/java/org/jiayunet/pojo/dto/job/JobAgentRecommendDto.java b/client-api/src/main/java/org/jiayunet/pojo/dto/job/JobAgentRecommendDto.java new file mode 100644 index 0000000..c93b7a5 --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/pojo/dto/job/JobAgentRecommendDto.java @@ -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 list; +} diff --git a/client-api/src/main/java/org/jiayunet/pojo/param/job/JobAgentRecommendParam.java b/client-api/src/main/java/org/jiayunet/pojo/param/job/JobAgentRecommendParam.java new file mode 100644 index 0000000..5300880 --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/pojo/param/job/JobAgentRecommendParam.java @@ -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 excludeJobIds; +} diff --git a/client-api/src/main/java/org/jiayunet/pojo/param/job/JobQueryParam.java b/client-api/src/main/java/org/jiayunet/pojo/param/job/JobQueryParam.java index 6c24795..ea5894d 100644 --- a/client-api/src/main/java/org/jiayunet/pojo/param/job/JobQueryParam.java +++ b/client-api/src/main/java/org/jiayunet/pojo/param/job/JobQueryParam.java @@ -30,6 +30,9 @@ public class JobQueryParam extends PageParam { /** 指定岗位ID列表(用于收藏列表) */ private List jobIds; + /** 排除岗位ID列表(用于推荐时排除已推荐过的) */ + private List excludeJobIds; + /** 岗位状态过滤(0=有效 1=已下架 2=已过期,可多选,null或空=查所有) */ private List statusFilter; } diff --git a/client-api/src/main/java/org/jiayunet/service/JobService.java b/client-api/src/main/java/org/jiayunet/service/JobService.java index bc16f38..9e386e6 100644 --- a/client-api/src/main/java/org/jiayunet/service/JobService.java +++ b/client-api/src/main/java/org/jiayunet/service/JobService.java @@ -1,5 +1,6 @@ package org.jiayunet.service; +import org.jiayunet.ai.AiChatAbility; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.JobDto; 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.JobApplyQueryParam; import org.jiayunet.pojo.param.job.JobFavoriteQueryParam; import org.jiayunet.pojo.param.job.JobQueryParam; 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.JobFavoriteCountVo; 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.stereotype.Service; +import com.fasterxml.jackson.databind.JsonNode; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @@ -66,6 +73,12 @@ public class JobService { @Autowired private JobMatchService jobMatchService; + @Autowired + private UserJobIntentionMapper userJobIntentionMapper; + + @Autowired + private AiChatAbility aiChatAbility; + /** * 岗位列表查询 *

方法逻辑流程:

@@ -96,7 +109,10 @@ public class JobService { List dislikes = userJobDislikeMapper.selectList(new LambdaQueryWrapper().eq(UserJobDislike::getUserId, userId)); // 3. 提取排除列表 - List excludeJobIds = dislikes.stream().map(UserJobDislike::getJobId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); + List 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 excludeCompanyIds = dislikes.stream().map(UserJobDislike::getCompanyId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); // 4. 提取不感兴趣的地区(直接使用,不扩展子级) @@ -561,4 +577,104 @@ public class JobService { return vo; } + + /** + * 求职助手岗位推荐 + *

1. 查求职意向构造筛选条件(无意向则不设条件) 2. 排除已推荐的+已投递的 3. 取前35条候选 4. AI精筛返回8-10个

+ */ + public JobAgentRecommendDto recommendJobs(JobAgentRecommendParam param, Long userId) { + // 1. 查求职意向 + UserJobIntention intention = userJobIntentionMapper.selectOne(new LambdaQueryWrapper().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 excludeIds = new ArrayList<>(); + if (param.getExcludeJobIds() != null) { + excludeIds.addAll(param.getExcludeJobIds()); + } + List applications = userJobApplicationMapper.selectList(new LambdaQueryWrapper().eq(UserJobApplication::getUserId, userId)); + applications.forEach(a -> excludeIds.add(a.getJobId())); + queryParam.setExcludeJobIds(excludeIds); + + // 4. 查询候选岗位(完整列表数据) + PageResult 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 candidateJobIds = candidates.getList().stream().map(JobDto::getId).collect(Collectors.toList()); + List jobDetails = jobMapper.selectList(new LambdaQueryWrapper().in(Job::getId, candidateJobIds).select(Job::getId, Job::getTitle, Job::getDescription, Job::getRequirement)); + Map jobDetailMap = jobDetails.stream().collect(Collectors.toMap(Job::getId, j -> j)); + + // 6. 构造候选岗位映射(供AI返回后过滤使用) + Map 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 selectedIds = new ArrayList<>(); + root.path("jobIds").forEach(node -> selectedIds.add(node.asLong())); + + List 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; + } } diff --git a/client-api/src/main/resources/application-local.yml b/client-api/src/main/resources/application-local.yml index e4346bd..04bab45 100644 --- a/client-api/src/main/resources/application-local.yml +++ b/client-api/src/main/resources/application-local.yml @@ -88,6 +88,11 @@ app: 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-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: diff --git a/common/src/main/java/org/jiayunet/pojo/PageParam.java b/common/src/main/java/org/jiayunet/pojo/PageParam.java index dbf7b5d..4c8a17b 100644 --- a/common/src/main/java/org/jiayunet/pojo/PageParam.java +++ b/common/src/main/java/org/jiayunet/pojo/PageParam.java @@ -20,7 +20,7 @@ public class PageParam { /** 每页条数 */ @Min(value = 1, message = "每页条数最小为1") - @Max(value = 100, message = "每页条数最大为100") + @Max(value = 1000, message = "每页条数最大为100") private Integer pageSize = 10; /**