From fcc13d34964176fc635c7c54ea1b2cda78d73445 Mon Sep 17 00:00:00 2001 From: zk Date: Thu, 26 Mar 2026 19:05:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=94=A8=E6=88=B7=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E8=B5=84=E6=96=99=E8=AF=86=E5=88=AB=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jiayunet/service/UserProfileService.java | 14 +- .../service/UserProfileAnalyzeService.java | 341 ++++++++++++++++++ .../service/UserSkillTagMatchService.java | 324 ----------------- 3 files changed, 348 insertions(+), 331 deletions(-) create mode 100644 manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java delete mode 100644 manager/src/main/java/org/jiayunet/service/UserSkillTagMatchService.java diff --git a/client-api/src/main/java/org/jiayunet/service/UserProfileService.java b/client-api/src/main/java/org/jiayunet/service/UserProfileService.java index f06144d..4b94258 100644 --- a/client-api/src/main/java/org/jiayunet/service/UserProfileService.java +++ b/client-api/src/main/java/org/jiayunet/service/UserProfileService.java @@ -45,7 +45,7 @@ public class UserProfileService { private UserProfileCompetitionMapper competitionMapper; @Autowired - private org.jiayunet.service.UserSkillTagMatchService userSkillTagMatchService; + private org.jiayunet.service.UserProfileAnalyzeService userProfileAnalyzeService; // ==================== 主表 ==================== @@ -78,7 +78,7 @@ public class UserProfileService { profile.setUpdateTime(now); userProfileMapper.insert(profile); } - userSkillTagMatchService.matchUserSkillTags(userId); + userProfileAnalyzeService.analyzeUserProfile(userId); } // ==================== 教育经历 ==================== @@ -114,7 +114,7 @@ public class UserProfileService { item.setUpdateTime(now); }); educationMapper.batchInsert(list); - userSkillTagMatchService.matchUserSkillTags(userId); + userProfileAnalyzeService.analyzeUserProfile(userId); } // ==================== 工作经历 ==================== @@ -151,7 +151,7 @@ public class UserProfileService { item.setUpdateTime(now); }); workMapper.batchInsert(list); - userSkillTagMatchService.matchUserSkillTags(userId); + userProfileAnalyzeService.analyzeUserProfile(userId); } // ==================== 实习经历 ==================== @@ -188,7 +188,7 @@ public class UserProfileService { item.setUpdateTime(now); }); internshipMapper.batchInsert(list); - userSkillTagMatchService.matchUserSkillTags(userId); + userProfileAnalyzeService.analyzeUserProfile(userId); } // ==================== 项目经历 ==================== @@ -225,7 +225,7 @@ public class UserProfileService { item.setUpdateTime(now); }); projectMapper.batchInsert(list); - userSkillTagMatchService.matchUserSkillTags(userId); + userProfileAnalyzeService.analyzeUserProfile(userId); } // ==================== 竞赛经历 ==================== @@ -262,7 +262,7 @@ public class UserProfileService { item.setUpdateTime(now); }); competitionMapper.batchInsert(list); - userSkillTagMatchService.matchUserSkillTags(userId); + userProfileAnalyzeService.analyzeUserProfile(userId); } // ==================== 内部方法 ==================== diff --git a/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java b/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java new file mode 100644 index 0000000..2c2b333 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/service/UserProfileAnalyzeService.java @@ -0,0 +1,341 @@ +package org.jiayunet.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.jiayunet.ai.AiChatAbility; +import org.jiayunet.mapper.*; +import org.jiayunet.pojo.po.*; +import org.jiayunet.pojo.vo.UserHonorsVo; +import org.jiayunet.tool.HttpTool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 用户简历分析服务 + *

主要功能:用户保存简历后异步分析,提取学校等级、经历质量、专业、技能等维度数据

+ *

依赖:AiChatAbility(AI调用)、DictCacheService(专业分类缓存)

+ *

使用表:bg_user_profile(主表,读取/更新)、bg_user_profile_*(5张子表,读取)、 + * bg_skill_tag(技能入库)、bg_user_profile_skill_tag_relation(关联表)

+ * + * @author zk + */ +@Slf4j +@Service +public class UserProfileAnalyzeService { + + @Autowired + private AiChatAbility aiChatAbility; + + @Autowired + private DictCacheService dictCacheService; + + @Autowired + private UserProfileMapper userProfileMapper; + + @Autowired + private UserProfileEducationMapper educationMapper; + + @Autowired + private UserProfileWorkMapper workMapper; + + @Autowired + private UserProfileInternshipMapper internshipMapper; + + @Autowired + private UserProfileProjectMapper projectMapper; + + @Autowired + private UserProfileCompetitionMapper competitionMapper; + + @Autowired + private SkillTagMapper skillTagMapper; + + @Autowired + private UserProfileSkillTagRelationMapper relationMapper; + + /** + * 异步分析用户简历 + *

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

+ */ + @Async("userProfileAsyncExecutor") + public void analyzeUserProfile(Long userId) { + try { + log.info("开始分析用户简历, userId={}", userId); + + // 1. 查询用户完整简历 + UserProfile profile = userProfileMapper.selectOne(new LambdaQueryWrapper().eq(UserProfile::getUserId, userId)); + if (profile == null) { + log.info("用户主表为空,清空技能标签, userId={}", userId); + clearSkillRelations(userId); + return; + } + + List educationList = educationMapper.selectList(new LambdaQueryWrapper().eq(UserProfileEducation::getUserId, userId)); + List workList = workMapper.selectList(new LambdaQueryWrapper().eq(UserProfileWork::getUserId, userId)); + List internshipList = internshipMapper.selectList(new LambdaQueryWrapper().eq(UserProfileInternship::getUserId, userId)); + List projectList = projectMapper.selectList(new LambdaQueryWrapper().eq(UserProfileProject::getUserId, userId)); + List competitionList = competitionMapper.selectList(new LambdaQueryWrapper().eq(UserProfileCompetition::getUserId, userId)); + + // 2. 数据有效性检查 + if (educationList.isEmpty() && workList.isEmpty() && internshipList.isEmpty() && projectList.isEmpty() && competitionList.isEmpty()) { + log.info("用户所有子表为空,清空技能标签, userId={}", userId); + clearSkillRelations(userId); + return; + } + + String profileJson = buildProfileJson(profile, educationList, workList, internshipList, projectList, competitionList); + + // 3. 第一次AI:简历综合分析(失败不影响后续) + try { + analyzeProfileDimensions(profile.getId(), profileJson); + } catch (Exception e) { + log.warn("简历综合分析失败, userId={}", userId, e); + } + + // 4. 第二次AI:专业归一化(失败不影响后续) + try { + analyzeMajor(profile.getId(), profileJson); + } catch (Exception e) { + log.warn("专业归一化失败, userId={}", userId, e); + } + + // 5. 第三次AI:技能提取 + try { + extractSkillTags(userId, profileJson); + } catch (Exception e) { + log.warn("技能提取失败, userId={}", userId, e); + } + + log.info("用户简历分析完成, userId={}", userId); + + } catch (Exception e) { + log.error("用户简历分析异常, userId={}", userId, e); + } + } + + /** + * 第一次AI:简历综合分析 + *

一次性识别学校等级、公司背书、经历时长、职责深度、量化产出、荣誉

+ */ + private void analyzeProfileDimensions(Long profileId, String profileJson) { + String systemPrompt = """ + 你是一个简历分析助手。根据用户简历,分析以下维度并返回JSON。 + 返回JSON格式: + { + "schoolRank": 1-4的数字(1=C9/985/QS前50 2=211/双一流/QS前200 3=普通一本/QS前500 4=其他,无教育经历则null), + "companyPrestige": 1-4的数字(1=名企(世界500强/互联网大厂/知名国企/独角兽) 2=普通实习 3=仅校内活动 4=无经历,取最高档), + "experienceDuration": 1-3的数字(1=实习≥3月 2=1-3月 3=≤1月,取最长的一段,无实习则null), + "roleDepth": 1-3的数字(1=主导/创新(主导、核心负责、从0到1、架构设计) 2=执行/应用(负责、开发、实现、维护) 3=辅助/学习(协助、参与、了解),取最高档), + "outputQuality": 1-3的数字(1=有量化结果(含数字比率如提升20%) 2=有具体产出(明确产出物但无数字) 3=纯描述,取最高档), + "honors": {"national":["具体奖项名"],"provincial":["具体奖项名"],"school":["具体奖项/证书名"],"paper":["具体论文名"]} + } + 规则: + 1. 每个维度独立判断,某维度信息不足则设为null + 2. 多段经历取最高档(最优表现) + 3. honors中只填简历中明确提到的,没有则空数组 + 4. 只返回JSON,不要其他内容 + """; + + String userMessage = "【用户简历】\n" + profileJson; + String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); + String json = cleanAiResponse(aiResponse); + + try { + JsonNode root = HttpTool.objectMapper.readTree(json); + + UserProfile update = new UserProfile(); + update.setId(profileId); + update.setSchoolRank(root.path("schoolRank").isNull() ? null : root.path("schoolRank").asInt()); + update.setCompanyPrestige(root.path("companyPrestige").isNull() ? null : root.path("companyPrestige").asInt()); + update.setExperienceDuration(root.path("experienceDuration").isNull() ? null : root.path("experienceDuration").asInt()); + update.setRoleDepth(root.path("roleDepth").isNull() ? null : root.path("roleDepth").asInt()); + update.setOutputQuality(root.path("outputQuality").isNull() ? null : root.path("outputQuality").asInt()); + + // 解析 honors + JsonNode honorsNode = root.path("honors"); + if (!honorsNode.isMissingNode() && !honorsNode.isNull()) { + UserHonorsVo honors = HttpTool.objectMapper.treeToValue(honorsNode, UserHonorsVo.class); + update.setHonors(honors); + } + + update.setUpdateTime(Instant.now()); + userProfileMapper.updateById(update); + } catch (Exception e) { + log.warn("简历综合分析AI返回解析失败: {}", json, e); + } + } + + /** + * 第二次AI:专业归一化 + *

传入教育经历和三级专业分类列表,AI返回用户专业ID数组 → 更新 bg_user_profile.major_ids

+ */ + private void analyzeMajor(Long profileId, String profileJson) { + String systemPrompt = """ + 你是一个专业归一化助手。根据用户的教育经历,从专业分类列表中匹配用户的专业。 + 返回JSON格式: + { + "majorIds": [专业ID数组,从专业列表中选择最匹配的,可多个(如本科+硕士不同专业),无法判断则空数组] + } + 规则: + 1. 只能从给定专业列表中选择ID + 2. 每段教育经历匹配一个最相关的专业 + 3. 无教育经历或专业信息不足时返回空数组 + 4. 只返回JSON,不要其他内容 + """; + + String userMessage = "【用户简历】\n" + profileJson + "\n\n【专业分类列表】\n" + dictCacheService.getMajorCategoryText(); + + String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); + String json = cleanAiResponse(aiResponse); + + try { + JsonNode root = HttpTool.objectMapper.readTree(json); + List majorIds = new ArrayList<>(); + JsonNode idsNode = root.path("majorIds"); + if (idsNode.isArray()) { + for (JsonNode node : idsNode) { + long id = node.asLong(0); + if (id > 0 && !majorIds.contains(id)) { + majorIds.add(id); + } + } + } + + UserProfile update = new UserProfile(); + update.setId(profileId); + update.setMajorIds(majorIds.isEmpty() ? null : majorIds); + update.setUpdateTime(Instant.now()); + userProfileMapper.updateById(update); + } catch (Exception e) { + log.warn("专业归一化AI返回解析失败: {}", json, e); + } + } + + /** + * 第三次AI:技能提取 + *

自由提取用户技能,prompt与岗位侧保持一致,INSERT IGNORE入bg_skill_tag → 写关联表

+ */ + private void extractSkillTags(Long userId, String profileJson) { + String systemPrompt = """ + 你是一个技能提取助手。根据用户简历,提取该用户具备的核心专业能力和工具技能。 + 返回JSON数组格式,如:["java", "spring boot", "mysql", "redis"] + 规则: + 1. 统一使用小写字母 + 2. 尽量简短,使用业界通用缩写(如 js 而非 javascript,k8s 而非 kubernetes) + 3. 提取范围包括:技术栈、专业领域知识、行业工具、专业资质能力等 + 4. 不提取纯软技能(如沟通能力、团队协作、学习能力、积极主动) + 5. 如果简历中完全没有专业能力体现,返回空数组 [] + 6. 最多15个,按熟练度排序 + 7. 只返回JSON数组,不要其他内容 + 示例1(技术岗): + 输入:3年Java开发经验,熟悉Spring Boot、MySQL、Redis + 输出:["java", "spring boot", "mysql", "redis"] + 示例2(财务岗): + 输入:负责费用管理与审核,月度经营利润分析,会计学专业 + 输出:["财务管理", "会计核算", "经营分析", "费用审核"] + 示例3(科研岗): + 输入:开展生物合成相关研究,发酵工程方向博士 + 输出:["生物合成", "发酵工程"] + 示例4(无专业能力): + 输入:具备较强的沟通能力和创新意识,积极主动 + 输出:[] + """; + + String userMessage = "【用户简历】\n" + profileJson; + String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); + String json = cleanAiResponse(aiResponse); + + try { + JsonNode arrayNode = HttpTool.objectMapper.readTree(json); + if (!arrayNode.isArray() || arrayNode.isEmpty()) { + clearSkillRelations(userId); + return; + } + + List skillTagIds = new ArrayList<>(); + for (JsonNode node : arrayNode) { + String skillName = node.asText("").trim().toLowerCase(); + if (skillName.isBlank() || skillName.length() > 50) { + continue; + } + Long tagId = findOrCreateSkillTag(skillName); + if (tagId != null && !skillTagIds.contains(tagId)) { + skillTagIds.add(tagId); + } + } + + replaceSkillRelations(userId, skillTagIds); + } catch (Exception e) { + log.warn("技能提取AI返回解析失败: {}", json, e); + } + } + + /** + * 查找或创建技能标签(依靠数据库唯一索引保证并发安全) + */ + private Long findOrCreateSkillTag(String name) { + skillTagMapper.insertIgnore(IdWorker.getId(), name, Instant.now()); + SkillTag tag = skillTagMapper.selectOne(new LambdaQueryWrapper().eq(SkillTag::getName, name).last("LIMIT 1")); + return tag != null ? tag.getId() : null; + } + + /** 构建用户简历JSON */ + private String buildProfileJson(UserProfile profile, List educationList, + List workList, List internshipList, + List projectList, List competitionList) { + Map data = new HashMap<>(); + data.put("name", profile.getName()); + data.put("skills", profile.getSkills()); + data.put("certificates", profile.getCertificates()); + data.put("education", educationList); + data.put("work", workList); + data.put("internship", internshipList); + data.put("project", projectList); + data.put("competition", competitionList); + try { + return HttpTool.objectMapper.writeValueAsString(data); + } catch (Exception e) { + log.error("用户资料序列化失败", e); + return "{}"; + } + } + + /** 清空用户技能标签关联 */ + private void clearSkillRelations(Long userId) { + relationMapper.delete(new LambdaQueryWrapper().eq(UserProfileSkillTagRelation::getUserId, userId)); + } + + /** 全量替换用户技能标签关联 */ + private void replaceSkillRelations(Long userId, List skillTagIds) { + relationMapper.delete(new LambdaQueryWrapper().eq(UserProfileSkillTagRelation::getUserId, userId)); + if (skillTagIds.isEmpty()) { + return; + } + Instant now = Instant.now(); + List relations = skillTagIds.stream().map(tagId -> { + UserProfileSkillTagRelation relation = new UserProfileSkillTagRelation(); + relation.setUserId(userId); + relation.setSkillTagId(tagId); + relation.setCreateTime(now); + return relation; + }).collect(Collectors.toList()); + relationMapper.batchInsert(relations); + } + + /** 清理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/manager/src/main/java/org/jiayunet/service/UserSkillTagMatchService.java b/manager/src/main/java/org/jiayunet/service/UserSkillTagMatchService.java deleted file mode 100644 index e251874..0000000 --- a/manager/src/main/java/org/jiayunet/service/UserSkillTagMatchService.java +++ /dev/null @@ -1,324 +0,0 @@ -package org.jiayunet.service; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.extern.slf4j.Slf4j; -import org.jiayunet.ai.AiChatAbility; -import org.jiayunet.mapper.*; -import org.jiayunet.pojo.po.*; -import org.jiayunet.tool.HttpTool; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -import java.time.Instant; -import java.util.*; -import java.util.stream.Collectors; - -/** - * 用户技能标签匹配服务 - *

主要功能:根据用户个人资料,AI识别匹配的技能标签

- *

依赖:AiChatAbility(AI调用)

- *

使用表:bg_user_profile(主表)、bg_user_profile_*(5张子表)、 - * bg_job_category(岗位分类)、bg_skill_tag(技能标签)、 - * bg_user_profile_skill_tag_relation(关联表)

- * - * @author zk - */ -@Slf4j -@Service -public class UserSkillTagMatchService { - - @Autowired - private AiChatAbility aiChatAbility; - - @Autowired - private UserProfileMapper userProfileMapper; - - @Autowired - private UserProfileEducationMapper educationMapper; - - @Autowired - private UserProfileWorkMapper workMapper; - - @Autowired - private UserProfileInternshipMapper internshipMapper; - - @Autowired - private UserProfileProjectMapper projectMapper; - - @Autowired - private UserProfileCompetitionMapper competitionMapper; - - @Autowired - private JobCategoryMapper jobCategoryMapper; - - @Autowired - private SkillTagMapper skillTagMapper; - - @Autowired - private UserProfileSkillTagRelationMapper relationMapper; - - /** - * 异步匹配用户技能标签 - *

1. 查询用户完整个人资料 2. 第一次AI调用识别二级分类 3. 查询三级分类 - * 4. 查询候选技能标签 5. 第二次AI调用匹配技能标签 6. 全量替换关联表

- */ - @Async("userProfileAsyncExecutor") - public void matchUserSkillTags(Long userId) { - try { - log.info("开始匹配用户技能标签, userId={}", userId); - - // 1. 查询用户完整个人资料 - UserProfile profile = userProfileMapper.selectOne(new LambdaQueryWrapper().eq(UserProfile::getUserId, userId)); - if (profile == null) { - log.info("用户主表为空,清空技能标签, userId={}", userId); - clearRelations(userId); - return; - } - - List educationList = educationMapper.selectList(new LambdaQueryWrapper().eq(UserProfileEducation::getUserId, userId)); - List workList = workMapper.selectList(new LambdaQueryWrapper().eq(UserProfileWork::getUserId, userId)); - List internshipList = internshipMapper.selectList(new LambdaQueryWrapper().eq(UserProfileInternship::getUserId, userId)); - List projectList = projectMapper.selectList(new LambdaQueryWrapper().eq(UserProfileProject::getUserId, userId)); - List competitionList = competitionMapper.selectList(new LambdaQueryWrapper().eq(UserProfileCompetition::getUserId, userId)); - - // 2. 数据有效性检查 - if (educationList.isEmpty() && workList.isEmpty() && internshipList.isEmpty() && projectList.isEmpty() && competitionList.isEmpty()) { - log.info("用户所有子表为空,清空技能标签, userId={}", userId); - clearRelations(userId); - return; - } - - // 3. 第一次 AI 调用(识别二级分类) - List categoryIds = identifyCategories(profile, educationList, workList, internshipList, projectList, competitionList); - if (categoryIds.isEmpty()) { - log.info("AI未识别出岗位分类,清空技能标签, userId={}", userId); - clearRelations(userId); - return; - } - - // 4. 查询三级分类(技能标签挂在三级分类下) - List level3Categories = jobCategoryMapper.selectList(new LambdaQueryWrapper().eq(JobCategory::getLevel, 3).in(JobCategory::getParentId, categoryIds)); - if (level3Categories.isEmpty()) { - log.info("二级分类下无三级分类,清空技能标签, userId={}", userId); - clearRelations(userId); - return; - } - - // 5. 查询候选技能标签(使用三级分类ID) - List level3CategoryIds = level3Categories.stream().map(JobCategory::getId).collect(Collectors.toList()); - List candidateTags = skillTagMapper.selectList(new LambdaQueryWrapper().in(SkillTag::getCategoryId, level3CategoryIds)); - if (candidateTags.isEmpty()) { - log.info("候选技能标签为空,清空技能标签, userId={}", userId); - clearRelations(userId); - return; - } - - // 6. 第二次 AI 调用(匹配技能标签) - List matchedTagIds = matchSkillTags(profile, educationList, workList, internshipList, projectList, competitionList, candidateTags); - - // 7. 全量替换关联表 - replaceRelations(userId, matchedTagIds); - - log.info("用户技能标签匹配完成, userId={}, 匹配{}个标签", userId, matchedTagIds.size()); - - } catch (Exception e) { - log.error("用户技能标签匹配异常, userId={}", userId, e); - } - } - - /** - * 第一次 AI 调用:识别用户所属的二级岗位分类 - */ - private List identifyCategories(UserProfile profile, List educationList, - List workList, List internshipList, - List projectList, List competitionList) { - // 构建用户资料 JSON - Map profileData = new HashMap<>(); - profileData.put("name", profile.getName()); - profileData.put("workYears", profile.getWorkYears()); - profileData.put("education", educationList); - profileData.put("work", workList); - profileData.put("internship", internshipList); - profileData.put("project", projectList); - profileData.put("competition", competitionList); - - String profileJson; - try { - profileJson = HttpTool.objectMapper.writeValueAsString(profileData); - } catch (Exception e) { - log.error("用户资料序列化失败", e); - return List.of(); - } - - // 查询所有二级分类 - List allCategories = jobCategoryMapper.selectList(new LambdaQueryWrapper().eq(JobCategory::getLevel, 2)); - - // 构建分类列表文本(带一级分类名称) - Map parentNameMap = new HashMap<>(); - List parentCategories = jobCategoryMapper.selectList(new LambdaQueryWrapper().eq(JobCategory::getLevel, 1)); - for (JobCategory parent : parentCategories) { - parentNameMap.put(parent.getId(), parent.getName()); - } - - StringBuilder categoryText = new StringBuilder(); - for (JobCategory category : allCategories) { - String parentName = parentNameMap.getOrDefault(category.getParentId(), "未知"); - categoryText.append(String.format("ID: %d, 名称: %s > %s\n", category.getId(), parentName, category.getName())); - } - - // 构建 Prompt - String systemPrompt = """ - 任务:根据用户个人资料,选择最匹配的二级岗位分类。 - - 规则: - 1. 选择 1-10 个最匹配的二级分类(优先核心岗位,按匹配度排序) - 2. 信息不足无法判断时返回空数组 [] - 3. 只返回纯数字数组 JSON,不要任何其他文字 - - 返回格式示例: - - 有分类:[23, 45, 67] - - 无法判断:[] - """; - - String userMessage = "【用户个人资料】\n" + profileJson + "\n\n【二级岗位分类列表(格式:一级分类 > 二级分类)】\n" + categoryText; - - try { - String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); - return parseIdArray(aiResponse, allCategories.stream().map(JobCategory::getId).collect(Collectors.toSet())); - } catch (Exception e) { - log.error("AI识别岗位分类失败", e); - return List.of(); - } - } - - /** - * 第二次 AI 调用:从候选标签中匹配用户技能标签 - */ - private List matchSkillTags(UserProfile profile, List educationList, - List workList, List internshipList, - List projectList, List competitionList, - List candidateTags) { - // 构建用户资料 JSON - Map profileData = new HashMap<>(); - profileData.put("name", profile.getName()); - profileData.put("workYears", profile.getWorkYears()); - profileData.put("education", educationList); - profileData.put("work", workList); - profileData.put("internship", internshipList); - profileData.put("project", projectList); - profileData.put("competition", competitionList); - - String profileJson; - try { - profileJson = HttpTool.objectMapper.writeValueAsString(profileData); - } catch (Exception e) { - log.error("用户资料序列化失败", e); - return List.of(); - } - - // 构建候选标签文本(按分类分组) - Map categoryNameMap = new HashMap<>(); - Set categoryIds = candidateTags.stream().map(SkillTag::getCategoryId).collect(Collectors.toSet()); - List categories = jobCategoryMapper.selectList(new LambdaQueryWrapper().in(JobCategory::getId, categoryIds)); - for (JobCategory category : categories) { - categoryNameMap.put(category.getId(), category.getName()); - } - - StringBuilder tagText = new StringBuilder(); - Map> groupedTags = candidateTags.stream().collect(Collectors.groupingBy(SkillTag::getCategoryId)); - for (Map.Entry> entry : groupedTags.entrySet()) { - String categoryName = categoryNameMap.getOrDefault(entry.getKey(), "未知"); - tagText.append("【").append(categoryName).append("】\n"); - for (SkillTag tag : entry.getValue()) { - tagText.append(String.format("ID: %d, 名称: %s\n", tag.getId(), tag.getName())); - } - tagText.append("\n"); - } - - // 构建 Prompt - String systemPrompt = """ - 任务:根据用户个人资料,从候选技能标签中选出匹配的标签。 - - 规则: - 1. 只选择用户明确具备或可推断的技能 - 2. 无法匹配返回空数组 [] - 3. 只返回纯数字数组 JSON,不要任何其他文字 - - 返回格式示例: - - 有匹配:[12, 34, 56] - - 无匹配:[] - """; - - String userMessage = "【用户个人资料】\n" + profileJson + "\n\n【候选技能标签(按分类分组)】\n" + tagText; - - try { - String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); - return parseIdArray(aiResponse, candidateTags.stream().map(SkillTag::getId).collect(Collectors.toSet())); - } catch (Exception e) { - log.error("AI匹配技能标签失败", e); - return List.of(); - } - } - - /** - * 解析 AI 返回的 ID 数组 - */ - private List parseIdArray(String aiResponse, Set validIds) { - String json = aiResponse.trim(); - if (json.startsWith("```")) { - json = json.replaceAll("^```\\w*\\n?", "").replaceAll("\\n?```$", "").trim(); - } - - try { - JsonNode arrayNode = HttpTool.objectMapper.readTree(json); - List result = new ArrayList<>(); - if (arrayNode.isArray()) { - for (JsonNode node : arrayNode) { - long id = node.asLong(0); - if (id > 0 && validIds.contains(id) && !result.contains(id)) { - result.add(id); - } - } - } - return result; - } catch (Exception e) { - log.warn("AI返回解析失败: {}", json, e); - return List.of(); - } - } - - /** - * 清空用户技能标签关联 - */ - public void clearRelations(Long userId) { - relationMapper.delete(new LambdaQueryWrapper().eq(UserProfileSkillTagRelation::getUserId, userId)); - } - - /** - * 全量替换用户技能标签关联 - */ - - public void replaceRelations(Long userId, List skillTagIds) { - // 先删除 - relationMapper.delete(new LambdaQueryWrapper().eq(UserProfileSkillTagRelation::getUserId, userId)); - - // 结果为空则只删除 - if (skillTagIds.isEmpty()) { - return; - } - - // 批量插入 - Instant now = Instant.now(); - List relations = skillTagIds.stream().map(tagId -> { - UserProfileSkillTagRelation relation = new UserProfileSkillTagRelation(); - relation.setUserId(userId); - relation.setSkillTagId(tagId); - relation.setCreateTime(now); - return relation; - }).collect(Collectors.toList()); - - relationMapper.batchInsert(relations); - } -}