AI 分析推荐岗位信息
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package org.jiayunet.service;
|
package org.jiayunet.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jiayunet.mapper.ChinaRegionsCodeMapper;
|
import org.jiayunet.mapper.ChinaRegionsCodeMapper;
|
||||||
import org.jiayunet.mapper.IndustryMapper;
|
import org.jiayunet.mapper.IndustryMapper;
|
||||||
@@ -16,6 +17,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,13 +49,32 @@ public class DictCacheService {
|
|||||||
private List<ChinaRegionsCode> regionList;
|
private List<ChinaRegionsCode> regionList;
|
||||||
private List<MajorCategory> majorCategoryList;
|
private List<MajorCategory> majorCategoryList;
|
||||||
|
|
||||||
/** 岗位分类文本(叶子节点,带父级路径),供 AI prompt 使用 */
|
/** 岗位分类文本(叶子节点,带父级路径),供 AI prompt 使用
|
||||||
|
* -- GETTER --
|
||||||
|
* 获取岗位分类文本(三级叶子节点,带父级路径,逗号分隔)
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
private String jobCategoryText;
|
private String jobCategoryText;
|
||||||
/** 行业文本(叶子节点,带父级路径),供 AI prompt 使用 */
|
/** 行业文本(叶子节点,带父级路径),供 AI prompt 使用
|
||||||
|
* -- GETTER --
|
||||||
|
* 获取行业文本(二级叶子节点,带父级路径,逗号分隔)
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
private String industryText;
|
private String industryText;
|
||||||
/** 专业分类文本(三级叶子节点,带父级路径),供 AI prompt 使用 */
|
/** 专业分类文本(三级叶子节点,带父级路径),供 AI prompt 使用
|
||||||
|
* -- GETTER --
|
||||||
|
* 获取专业分类文本(三级叶子节点,带父级路径,逗号分隔)
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
private String majorCategoryText;
|
private String majorCategoryText;
|
||||||
|
|
||||||
|
/** 合法岗位分类叶子ID集合(三级),供 AI 返回结果白名单校验 */
|
||||||
|
@Getter
|
||||||
|
private Set<Long> jobCategoryLeafIds;
|
||||||
|
/** 合法行业叶子ID集合(二级),供 AI 返回结果白名单校验 */
|
||||||
|
@Getter
|
||||||
|
private Set<Long> industryLeafIds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动时加载全量字典数据
|
* 启动时加载全量字典数据
|
||||||
* <p>分类/行业/专业全量加载用于构建父级路径,文本只取叶子节点</p>
|
* <p>分类/行业/专业全量加载用于构建父级路径,文本只取叶子节点</p>
|
||||||
@@ -95,6 +116,16 @@ public class DictCacheService {
|
|||||||
})
|
})
|
||||||
.collect(Collectors.joining(", "));
|
.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(一级/二级)
|
// 构建专业分类文本:只取三级(叶子),格式 id:name(一级/二级)
|
||||||
Map<Long, String> majorNameMap = majorCategoryList.stream()
|
Map<Long, String> majorNameMap = majorCategoryList.stream()
|
||||||
.collect(Collectors.toMap(MajorCategory::getId, MajorCategory::getName));
|
.collect(Collectors.toMap(MajorCategory::getId, MajorCategory::getName));
|
||||||
@@ -117,11 +148,6 @@ public class DictCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** 获取专业分类文本(三级叶子节点,带父级路径,逗号分隔) */
|
|
||||||
public String getMajorCategoryText() {
|
|
||||||
return majorCategoryText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据城市名匹配地区编码
|
* 根据城市名匹配地区编码
|
||||||
* <p>模糊匹配,如"北京"匹配"北京市"</p>
|
* <p>模糊匹配,如"北京"匹配"北京市"</p>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户简历分析服务
|
* 用户简历分析服务
|
||||||
@@ -61,6 +62,9 @@ public class UserProfileAnalyzeService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserProfileSkillTagRelationMapper relationMapper;
|
private UserProfileSkillTagRelationMapper relationMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserJobIntentionMapper userJobIntentionMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步分析用户简历
|
* 异步分析用户简历
|
||||||
* <p>1. 查询用户完整简历 2. 第一次AI综合分析 3. 第二次AI专业归一化 4. 第三次AI技能提取</p>
|
* <p>1. 查询用户完整简历 2. 第一次AI综合分析 3. 第二次AI专业归一化 4. 第三次AI技能提取</p>
|
||||||
@@ -114,6 +118,20 @@ public class UserProfileAnalyzeService {
|
|||||||
log.warn("技能提取失败, userId={}", userId, e);
|
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);
|
log.info("用户简历分析完成, userId={}", userId);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} 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