AI 分析推荐岗位信息
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找或创建技能标签(依靠数据库唯一索引保证并发安全)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user