AI 分析推荐岗位信息

This commit is contained in:
zk
2026-07-02 21:17:23 +08:00
parent f626d3def0
commit 86ecb7723e
2 changed files with 171 additions and 8 deletions
@@ -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<ChinaRegionsCode> regionList;
private List<MajorCategory> 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<Long> jobCategoryLeafIds;
/** 合法行业叶子ID集合(二级),供 AI 返回结果白名单校验 */
@Getter
private Set<Long> industryLeafIds;
/**
* 启动时加载全量字典数据
* <p>分类/行业/专业全量加载用于构建父级路径,文本只取叶子节点</p>
@@ -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<Long, String> majorNameMap = majorCategoryList.stream()
.collect(Collectors.toMap(MajorCategory::getId, MajorCategory::getName));
@@ -117,11 +148,6 @@ public class DictCacheService {
}
/** 获取专业分类文本(三级叶子节点,带父级路径,逗号分隔) */
public String getMajorCategoryText() {
return majorCategoryText;
}
/**
* 根据城市名匹配地区编码
* <p>模糊匹配,如"北京"匹配"北京市"</p>
@@ -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;
/**
* 异步分析用户简历
* <p>1. 查询用户完整简历 2. 第一次AI综合分析 3. 第二次AI专业归一化 4. 第三次AI技能提取</p>
@@ -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推荐岗位方向
* <p>传入简历和岗位分类列表,AI返回推荐岗位分类ID数组(最多3个)→ 写入 bg_user_job_intention.ai_category_ids</p>
*/
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<Long> categoryIds = parseValidIds(arrayNode, dictCacheService.getJobCategoryLeafIds(), 3);
if (!categoryIds.isEmpty()) {
upsertAiCategoryIds(userId, categoryIds);
}
} catch (Exception e) {
log.warn("AI岗位方向推荐返回解析失败: {}", json, e);
}
}
/**
* 第五次AI:AI推荐行业方向
* <p>传入简历和行业列表,AI返回推荐行业ID数组(最多3个)→ 写入 bg_user_job_intention.ai_industry_ids</p>
*/
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<Long> industryIds = parseValidIds(arrayNode, dictCacheService.getIndustryLeafIds(), 3);
if (!industryIds.isEmpty()) {
upsertAiIndustryIds(userId, industryIds);
}
} catch (Exception e) {
log.warn("AI行业方向推荐返回解析失败: {}", json, e);
}
}
/**
* 解析并校验AI返回的ID数组
* <p>只保留白名单内的合法整数ID,去重,最多保留limit个(超出截断),过滤幻觉/非法ID</p>
*/
private List<Long> parseValidIds(JsonNode arrayNode, Set<Long> 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字段,不覆盖用户手填意向)
* <p>意向行不存在则新建,仅填 userId + ai_category_ids</p>
*/
private void upsertAiCategoryIds(Long userId, List<Long> categoryIds) {
UserJobIntention po = userJobIntentionMapper.selectOne(new LambdaQueryWrapper<UserJobIntention>().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字段,不覆盖用户手填意向)
* <p>意向行不存在则新建,仅填 userId + ai_industry_ids</p>
*/
private void upsertAiIndustryIds(Long userId, List<Long> industryIds) {
UserJobIntention po = userJobIntentionMapper.selectOne(new LambdaQueryWrapper<UserJobIntention>().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);
}
}
/**
* 查找或创建技能标签(依靠数据库唯一索引保证并发安全)
*/