From 86ecb7723e428fb7d78b9a5d110646982501dcd8 Mon Sep 17 00:00:00 2001 From: zk Date: Thu, 2 Jul 2026 21:17:23 +0800 Subject: [PATCH] =?UTF-8?q?AI=20=E5=88=86=E6=9E=90=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E5=B2=97=E4=BD=8D=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jiayunet/service/DictCacheService.java | 42 +++++- .../service/UserProfileAnalyzeService.java | 137 ++++++++++++++++++ 2 files changed, 171 insertions(+), 8 deletions(-) diff --git a/manager/src/main/java/org/jiayunet/service/DictCacheService.java b/manager/src/main/java/org/jiayunet/service/DictCacheService.java index df52415..f0199c5 100644 --- a/manager/src/main/java/org/jiayunet/service/DictCacheService.java +++ b/manager/src/main/java/org/jiayunet/service/DictCacheService.java @@ -1,6 +1,7 @@ package org.jiayunet.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jiayunet.mapper.ChinaRegionsCodeMapper; import org.jiayunet.mapper.IndustryMapper; @@ -16,6 +17,7 @@ import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -47,13 +49,32 @@ public class DictCacheService { private List regionList; private List majorCategoryList; - /** 岗位分类文本(叶子节点,带父级路径),供 AI prompt 使用 */ + /** 岗位分类文本(叶子节点,带父级路径),供 AI prompt 使用 + * -- GETTER -- + * 获取岗位分类文本(三级叶子节点,带父级路径,逗号分隔) + */ + @Getter private String jobCategoryText; - /** 行业文本(叶子节点,带父级路径),供 AI prompt 使用 */ + /** 行业文本(叶子节点,带父级路径),供 AI prompt 使用 + * -- GETTER -- + * 获取行业文本(二级叶子节点,带父级路径,逗号分隔) + */ + @Getter private String industryText; - /** 专业分类文本(三级叶子节点,带父级路径),供 AI prompt 使用 */ + /** 专业分类文本(三级叶子节点,带父级路径),供 AI prompt 使用 + * -- GETTER -- + * 获取专业分类文本(三级叶子节点,带父级路径,逗号分隔) + */ + @Getter private String majorCategoryText; + /** 合法岗位分类叶子ID集合(三级),供 AI 返回结果白名单校验 */ + @Getter + private Set jobCategoryLeafIds; + /** 合法行业叶子ID集合(二级),供 AI 返回结果白名单校验 */ + @Getter + private Set industryLeafIds; + /** * 启动时加载全量字典数据 *

分类/行业/专业全量加载用于构建父级路径,文本只取叶子节点

@@ -95,6 +116,16 @@ public class DictCacheService { }) .collect(Collectors.joining(", ")); + // 构建合法叶子ID白名单:岗位分类三级、行业二级 + jobCategoryLeafIds = jobCategoryList.stream() + .filter(c -> c.getLevel() == 3) + .map(JobCategory::getId) + .collect(Collectors.toSet()); + industryLeafIds = industryList.stream() + .filter(i -> i.getLevel() == 2) + .map(Industry::getId) + .collect(Collectors.toSet()); + // 构建专业分类文本:只取三级(叶子),格式 id:name(一级/二级) Map majorNameMap = majorCategoryList.stream() .collect(Collectors.toMap(MajorCategory::getId, MajorCategory::getName)); @@ -117,11 +148,6 @@ public class DictCacheService { } - /** 获取专业分类文本(三级叶子节点,带父级路径,逗号分隔) */ - public String getMajorCategoryText() { - return majorCategoryText; - } - /** * 根据城市名匹配地区编码 *

模糊匹配,如"北京"匹配"北京市"

diff --git a/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java b/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java index 1e37fb7..aa3c536 100644 --- a/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java +++ b/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java @@ -17,6 +17,7 @@ import org.springframework.stereotype.Service; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; /** * 用户简历分析服务 @@ -61,6 +62,9 @@ public class UserProfileAnalyzeService { @Autowired private UserProfileSkillTagRelationMapper relationMapper; + @Autowired + private UserJobIntentionMapper userJobIntentionMapper; + /** * 异步分析用户简历 *

1. 查询用户完整简历 2. 第一次AI综合分析 3. 第二次AI专业归一化 4. 第三次AI技能提取

@@ -114,6 +118,20 @@ public class UserProfileAnalyzeService { log.warn("技能提取失败, userId={}", userId, e); } + // 6. 第四次AI:AI推荐岗位方向 + try { + analyzeAiJobCategory(userId, profileJson); + } catch (Exception e) { + log.warn("AI岗位方向推荐失败, userId={}", userId, e); + } + + // 7. 第五次AI:AI推荐行业方向 + try { + analyzeAiIndustry(userId, profileJson); + } catch (Exception e) { + log.warn("AI行业方向推荐失败, userId={}", userId, e); + } + log.info("用户简历分析完成, userId={}", userId); } catch (Exception e) { @@ -281,6 +299,125 @@ public class UserProfileAnalyzeService { } } + /** + * 第四次AI:AI推荐岗位方向 + *

传入简历和岗位分类列表,AI返回推荐岗位分类ID数组(最多3个)→ 写入 bg_user_job_intention.ai_category_ids

+ */ + private void analyzeAiJobCategory(Long userId, String profileJson) { + String systemPrompt = """ + 你是一个求职方向推荐助手。根据用户简历,从岗位分类列表中推荐最适合该用户的岗位方向。 + 返回JSON数组格式,如:[1001, 1002, 1003] + 规则: + 1. 只能从给定岗位分类列表中选择ID + 2. 最多推荐3个,按匹配度从高到低排序 + 3. 信息不足时返回空数组 [] + 4. 只返回JSON数组,不要其他内容 + """; + + String userMessage = "【用户简历】\n" + profileJson + "\n\n【岗位分类列表】\n" + dictCacheService.getJobCategoryText(); + String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); + String json = AiResponseCleanTool.clean(aiResponse); + + try { + JsonNode arrayNode = HttpTool.objectMapper.readTree(json); + List categoryIds = parseValidIds(arrayNode, dictCacheService.getJobCategoryLeafIds(), 3); + if (!categoryIds.isEmpty()) { + upsertAiCategoryIds(userId, categoryIds); + } + } catch (Exception e) { + log.warn("AI岗位方向推荐返回解析失败: {}", json, e); + } + } + + /** + * 第五次AI:AI推荐行业方向 + *

传入简历和行业列表,AI返回推荐行业ID数组(最多3个)→ 写入 bg_user_job_intention.ai_industry_ids

+ */ + private void analyzeAiIndustry(Long userId, String profileJson) { + String systemPrompt = """ + 你是一个求职方向推荐助手。根据用户简历,从行业列表中推荐最适合该用户的意向行业。 + 返回JSON数组格式,如:[2001, 2002, 2003] + 规则: + 1. 只能从给定行业列表中选择ID + 2. 最多推荐3个,按匹配度从高到低排序 + 3. 信息不足时返回空数组 [] + 4. 只返回JSON数组,不要其他内容 + """; + + String userMessage = "【用户简历】\n" + profileJson + "\n\n【行业列表】\n" + dictCacheService.getIndustryText(); + String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); + String json = AiResponseCleanTool.clean(aiResponse); + + try { + JsonNode arrayNode = HttpTool.objectMapper.readTree(json); + List industryIds = parseValidIds(arrayNode, dictCacheService.getIndustryLeafIds(), 3); + if (!industryIds.isEmpty()) { + upsertAiIndustryIds(userId, industryIds); + } + } catch (Exception e) { + log.warn("AI行业方向推荐返回解析失败: {}", json, e); + } + } + + /** + * 解析并校验AI返回的ID数组 + *

只保留白名单内的合法整数ID,去重,最多保留limit个(超出截断),过滤幻觉/非法ID

+ */ + private List parseValidIds(JsonNode arrayNode, Set validIds, int limit) { + if (arrayNode == null || !arrayNode.isArray()) { + return List.of(); + } + return StreamSupport.stream(arrayNode.spliterator(), false) + .filter(JsonNode::isIntegralNumber) + .map(JsonNode::asLong) + .filter(validIds::contains) + .distinct() + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * 写入AI推荐岗位类型(仅更新AI字段,不覆盖用户手填意向) + *

意向行不存在则新建,仅填 userId + ai_category_ids

+ */ + private void upsertAiCategoryIds(Long userId, List categoryIds) { + UserJobIntention po = userJobIntentionMapper.selectOne(new LambdaQueryWrapper().eq(UserJobIntention::getUserId, userId)); + Instant now = Instant.now(); + if (po == null) { + po = new UserJobIntention(); + po.setUserId(userId); + po.setAiCategoryIds(categoryIds); + po.setCreateTime(now); + po.setUpdateTime(now); + userJobIntentionMapper.insert(po); + } else { + po.setAiCategoryIds(categoryIds); + po.setUpdateTime(now); + userJobIntentionMapper.updateById(po); + } + } + + /** + * 写入AI推荐行业(仅更新AI字段,不覆盖用户手填意向) + *

意向行不存在则新建,仅填 userId + ai_industry_ids

+ */ + private void upsertAiIndustryIds(Long userId, List industryIds) { + UserJobIntention po = userJobIntentionMapper.selectOne(new LambdaQueryWrapper().eq(UserJobIntention::getUserId, userId)); + Instant now = Instant.now(); + if (po == null) { + po = new UserJobIntention(); + po.setUserId(userId); + po.setAiIndustryIds(industryIds); + po.setCreateTime(now); + po.setUpdateTime(now); + userJobIntentionMapper.insert(po); + } else { + po.setAiIndustryIds(industryIds); + po.setUpdateTime(now); + userJobIntentionMapper.updateById(po); + } + } + /** * 查找或创建技能标签(依靠数据库唯一索引保证并发安全) */