Compare commits
5
Commits
d9239a2b68
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3cb4dead | ||
|
|
0a4b97e40e | ||
|
|
251552ea08 | ||
|
|
18f6a19a11 | ||
|
|
ff04496117 |
@@ -16,9 +16,13 @@ import java.util.List;
|
||||
import org.jiayunet.pojo.vo.JobFavoriteCountVo;
|
||||
import org.jiayunet.service.JobService;
|
||||
import org.jiayunet.tool.UserSecurityTool;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 岗位接口
|
||||
*
|
||||
@@ -50,6 +54,52 @@ public class JobController {
|
||||
return jobService.listJobs(param, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位列表查询(按匹配度排序)
|
||||
* <p>入参与 /job/list 一致。匹配度是内存计算的,无法在 SQL 层排序,因此先取候选池全量算分排序
|
||||
* 并整体缓存,再在本方法内对缓存结果切片分页</p>
|
||||
* <p>排序仅在候选池范围内有效,候选池之外的岗位不参与排序</p>
|
||||
* <p>未登录用户没有简历和求职意向,匹配度几乎全部相同,排序无意义,直接退回 /job/list 的最新排序</p>
|
||||
*/
|
||||
@PostMapping("/list/match")
|
||||
@FuncPermission(value = "job_list", key = "#param.pageNum")
|
||||
public PageResult<JobDto> listJobsByMatch(@Validated @RequestBody JobQueryParam param) {
|
||||
|
||||
Long userId = 0L;
|
||||
try {
|
||||
userId = UserSecurityTool.getUserId();
|
||||
} catch (Exception e) {
|
||||
// 接口允许不登录,不处理
|
||||
}
|
||||
|
||||
// 未登录:匹配度无区分度,走原列表逻辑,省掉候选池全量算分
|
||||
if (userId == 0L) {
|
||||
return jobService.listJobs(param, userId);
|
||||
}
|
||||
|
||||
Integer pageNum = param.getPageNum();
|
||||
Integer pageSize = param.getPageSize();
|
||||
|
||||
// 归一化候选池查询条件:pageNum/pageSize 参与缓存 key,必须固定,否则每页各自成为独立缓存条目
|
||||
JobQueryParam poolParam = new JobQueryParam();
|
||||
BeanUtils.copyProperties(param, poolParam);
|
||||
poolParam.setPageNum(1);
|
||||
poolParam.setPageSize(JobService.CANDIDATE_POOL_SIZE);
|
||||
|
||||
List<JobDto> sortedList = jobService.listJobsSortedByMatch(poolParam, userId);
|
||||
|
||||
// 手动分页:缓存中的列表为共享对象,切片后另建集合返回,避免污染缓存
|
||||
long total = sortedList.size();
|
||||
int fromIndex = (pageNum - 1) * pageSize;
|
||||
if (fromIndex >= sortedList.size()) {
|
||||
return new PageResult<>(pageNum.longValue(), pageSize.longValue(), total, Collections.emptyList());
|
||||
}
|
||||
int toIndex = Math.min(fromIndex + pageSize, sortedList.size());
|
||||
List<JobDto> pageList = new ArrayList<>(sortedList.subList(fromIndex, toIndex));
|
||||
|
||||
return new PageResult<>(pageNum.longValue(), pageSize.longValue(), total, pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位列表总数查询
|
||||
* <p>筛选条件与 /job/list 一致,仅返回符合条件的岗位总数,接口允许不登录访问</p>
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户个人资料接口
|
||||
* 提供主表及5张子表(教育/工作/实习/项目/竞赛)的查询与保存功能
|
||||
* 提供主表及6张子表(教育/工作/实习/项目/竞赛/社团组织)的查询与保存功能
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@@ -54,7 +54,7 @@ public class UserProfileController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据简历ID同步更新个人资料(主表+5子表全量覆盖,触发一次异步AI分析)
|
||||
* 根据简历ID同步更新个人资料(主表+6子表全量覆盖,触发一次异步AI分析)
|
||||
*/
|
||||
@PostMapping("/syncFromResume")
|
||||
public void syncFromResume(@RequestParam Long resumeId) {
|
||||
@@ -203,4 +203,31 @@ public class UserProfileController {
|
||||
}).collect(Collectors.toList());
|
||||
userProfileService.saveCompetitionList(list);
|
||||
}
|
||||
|
||||
// ==================== 社团组织经历 ====================
|
||||
|
||||
/**
|
||||
* 查询当前用户的社团组织经历列表
|
||||
*/
|
||||
@GetMapping("/organization")
|
||||
public List<UserProfileOrganizationDto> listOrganization() {
|
||||
return userProfileService.listOrganization().stream().map(po -> {
|
||||
UserProfileOrganizationDto dto = new UserProfileOrganizationDto();
|
||||
BeanUtils.copyProperties(po, dto);
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前用户的社团组织经历列表(全量替换)
|
||||
*/
|
||||
@PostMapping("/organization")
|
||||
public void saveOrganization(@Validated @RequestBody List<@Valid UserProfileOrganizationParam> params) {
|
||||
List<UserProfileOrganization> list = params.stream().map(p -> {
|
||||
UserProfileOrganization po = new UserProfileOrganization();
|
||||
BeanUtils.copyProperties(p, po);
|
||||
return po;
|
||||
}).collect(Collectors.toList());
|
||||
userProfileService.saveOrganizationList(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户简历接口
|
||||
* <p>提供简历列表、主表及5张子表(教育/工作/实习/项目/竞赛)的查询与保存、简历删除功能</p>
|
||||
* <p>提供简历列表、主表及6张子表(教育/工作/实习/项目/竞赛/社团组织)的查询与保存、简历删除功能</p>
|
||||
* <p>所有保存接口支持自动创建:前端不传resumeId时自动创建新简历并返回resumeId</p>
|
||||
*
|
||||
* @author zk
|
||||
@@ -306,4 +306,49 @@ public class UserResumeController {
|
||||
}).collect(Collectors.toList());
|
||||
return userResumeService.saveCompetitionList(list, param.getResumeId());
|
||||
}
|
||||
|
||||
// ==================== 社团组织经历 ====================
|
||||
|
||||
/** 查询简历的社团组织经历列表 */
|
||||
@GetMapping("/organization")
|
||||
public List<ResumeOrganizationDto> listOrganization(@RequestParam Long resumeId) {
|
||||
return userResumeService.listOrganization(resumeId).stream().map(po -> {
|
||||
ResumeOrganizationDto dto = new ResumeOrganizationDto();
|
||||
BeanUtils.copyProperties(po, dto);
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** 添加单条社团组织经历,返回新记录id */
|
||||
@PostMapping("/organization/add")
|
||||
public Long addOrganization(@RequestParam Long resumeId, @Validated @RequestBody ResumeOrganizationParam param) {
|
||||
UserResumeOrganization po = new UserResumeOrganization();
|
||||
BeanUtils.copyProperties(param, po);
|
||||
return userResumeService.addOrganization(po, resumeId);
|
||||
}
|
||||
|
||||
/** 根据id编辑单条社团组织经历 */
|
||||
@PostMapping("/organization/update")
|
||||
public void updateOrganization(@Validated @RequestBody ResumeOrganizationUpdateParam param) {
|
||||
UserResumeOrganization po = new UserResumeOrganization();
|
||||
BeanUtils.copyProperties(param, po);
|
||||
userResumeService.updateOrganization(po);
|
||||
}
|
||||
|
||||
/** 根据id删除单条社团组织经历 */
|
||||
@PostMapping("/organization/delete")
|
||||
public void deleteOrganization(@RequestParam Long id) {
|
||||
userResumeService.deleteOrganization(id);
|
||||
}
|
||||
|
||||
/** 保存简历的社团组织经历列表(先删后插),返回resumeId */
|
||||
@PostMapping("/organization")
|
||||
public Long saveOrganization(@Validated @RequestBody ResumeSubTableParam<ResumeOrganizationParam> param) {
|
||||
List<UserResumeOrganization> list = param.getItems().stream().map(p -> {
|
||||
UserResumeOrganization po = new UserResumeOrganization();
|
||||
BeanUtils.copyProperties(p, po);
|
||||
return po;
|
||||
}).collect(Collectors.toList());
|
||||
return userResumeService.saveOrganizationList(list, param.getResumeId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,4 +52,10 @@ public class ResumeDto {
|
||||
|
||||
/** 个人概述 */
|
||||
private String summary;
|
||||
|
||||
/** 兴趣爱好 */
|
||||
private String hobbies;
|
||||
|
||||
/** 语言能力 */
|
||||
private String languageSkills;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.jiayunet.pojo.dto.resume;
|
||||
|
||||
import lombok.Data;
|
||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 简历-社团组织经历返回
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Data
|
||||
public class ResumeOrganizationDto {
|
||||
|
||||
private Long id;
|
||||
|
||||
/** 社团/组织名称 */
|
||||
private String organizationName;
|
||||
|
||||
/** 担任角色 */
|
||||
private String role;
|
||||
|
||||
/** 开始时间 */
|
||||
private String startDate;
|
||||
|
||||
/** 结束时间 */
|
||||
private String endDate;
|
||||
|
||||
/** 描述段落 */
|
||||
private List<DescriptionParagraph> description;
|
||||
}
|
||||
@@ -46,4 +46,10 @@ public class UserProfileDto {
|
||||
|
||||
/** 证书标签列表 */
|
||||
private List<String> certificates;
|
||||
|
||||
/** 兴趣爱好 */
|
||||
private String hobbies;
|
||||
|
||||
/** 语言能力 */
|
||||
private String languageSkills;
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package org.jiayunet.pojo.dto.userProfile;
|
||||
|
||||
import lombok.Data;
|
||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 社团组织经历返回
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Data
|
||||
public class UserProfileOrganizationDto {
|
||||
|
||||
private Long id;
|
||||
|
||||
/** 社团/组织名称 */
|
||||
private String organizationName;
|
||||
|
||||
/** 担任角色 */
|
||||
private String role;
|
||||
|
||||
/** 开始时间 */
|
||||
private String startDate;
|
||||
|
||||
/** 结束时间 */
|
||||
private String endDate;
|
||||
|
||||
/** 描述段落 */
|
||||
private List<DescriptionParagraph> description;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.jiayunet.pojo.param.resume;
|
||||
|
||||
import lombok.Data;
|
||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 简历-社团组织经历保存入参
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Data
|
||||
public class ResumeOrganizationParam {
|
||||
|
||||
/** 社团/组织名称 */
|
||||
@Size(max = 200, message = "社团/组织名称长度不能超过200")
|
||||
private String organizationName;
|
||||
|
||||
/** 担任角色 */
|
||||
@Size(max = 100, message = "担任角色长度不能超过100")
|
||||
private String role;
|
||||
|
||||
/** 开始时间,格式:2023.06 */
|
||||
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||
private String startDate;
|
||||
|
||||
/** 结束时间,格式:2023.09,至今则为空 */
|
||||
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||
private String endDate;
|
||||
|
||||
/** 描述段落 */
|
||||
private List<DescriptionParagraph> description;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package org.jiayunet.pojo.param.resume;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 简历-社团组织经历编辑入参
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ResumeOrganizationUpdateParam extends ResumeOrganizationParam {
|
||||
|
||||
/** 记录ID */
|
||||
@NotNull(message = "id不能为空")
|
||||
private Long id;
|
||||
}
|
||||
@@ -60,4 +60,12 @@ public class ResumeParam {
|
||||
|
||||
/** 个人概述 */
|
||||
private String summary;
|
||||
|
||||
/** 兴趣爱好 */
|
||||
@Size(max = 2000, message = "兴趣爱好长度不能超过2000")
|
||||
private String hobbies;
|
||||
|
||||
/** 语言能力 */
|
||||
@Size(max = 2000, message = "语言能力长度不能超过2000")
|
||||
private String languageSkills;
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package org.jiayunet.pojo.param.userProfile;
|
||||
|
||||
import lombok.Data;
|
||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 社团组织经历保存入参
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Data
|
||||
public class UserProfileOrganizationParam {
|
||||
|
||||
/** 社团/组织名称 */
|
||||
@Size(max = 200, message = "社团/组织名称长度不能超过200")
|
||||
private String organizationName;
|
||||
|
||||
/** 担任角色 */
|
||||
@Size(max = 100, message = "担任角色长度不能超过100")
|
||||
private String role;
|
||||
|
||||
/** 开始时间,格式:2023.06 */
|
||||
@Size(max = 10, message = "开始时间长度不能超过10")
|
||||
private String startDate;
|
||||
|
||||
/** 结束时间,格式:2023.09,至今则为空 */
|
||||
@Size(max = 10, message = "结束时间长度不能超过10")
|
||||
private String endDate;
|
||||
|
||||
/** 描述段落 */
|
||||
private List<DescriptionParagraph> description;
|
||||
}
|
||||
@@ -52,4 +52,12 @@ public class UserProfileParam {
|
||||
|
||||
/** 证书标签列表 */
|
||||
private List<String> certificates;
|
||||
|
||||
/** 兴趣爱好 */
|
||||
@Size(max = 2000, message = "兴趣爱好长度不能超过2000")
|
||||
private String hobbies;
|
||||
|
||||
/** 语言能力 */
|
||||
@Size(max = 2000, message = "语言能力长度不能超过2000")
|
||||
private String languageSkills;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,9 @@ public class JobService {
|
||||
@Autowired
|
||||
private AiChatAbility aiChatAbility;
|
||||
|
||||
/** 匹配度排序的候选池大小,匹配度无法在 SQL 层排序,只能在此范围内做全量算分后排序 */
|
||||
public static final int CANDIDATE_POOL_SIZE = 5000;
|
||||
|
||||
/**
|
||||
* 岗位列表查询
|
||||
* <p>方法逻辑流程:</p>
|
||||
@@ -166,6 +169,37 @@ public class JobService {
|
||||
return new PageResult<>(page.getCurrent(), page.getSize(), page.getTotal(), dtoList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位匹配度排序查询(候选池全量,不分页)
|
||||
* <p>匹配度是内存计算出来的,无法在 SQL 层 ORDER BY,因此一次性取候选池全量算分后排序</p>
|
||||
* <p>结果整体缓存,翻页由调用方对返回列表切片,避免每页重复计算</p>
|
||||
*
|
||||
* <p>方法逻辑流程:</p>
|
||||
* <p>1. 复用 listJobs 查询候选池(含匹配度计算)</p>
|
||||
* <p>2. 按匹配度倒序排序,同分按岗位ID倒序保证顺序稳定</p>
|
||||
*
|
||||
* <p>调用约定:缓存 key 由 param 与 userId 共同决定,且在方法执行前就已生成,
|
||||
* 因此调用方必须先把 pageNum 置为 1、pageSize 置为 CANDIDATE_POOL_SIZE 再传入。
|
||||
* 否则每一页都会生成独立缓存条目,缓存命中率恒为 0(方法内部无法补救)</p>
|
||||
*
|
||||
* @param param 查询条件,pageNum 须为 1、pageSize 须为 CANDIDATE_POOL_SIZE
|
||||
* @param userId 用户ID,匹配度因人而异,参与缓存 key
|
||||
* @return 按匹配度倒序排列的候选池全量列表
|
||||
*/
|
||||
@Cacheable(cacheNames = CacheConfig.JOB_MATCH_SORT)
|
||||
public List<JobDto> listJobsSortedByMatch(JobQueryParam param, Long userId) {
|
||||
|
||||
// 1. 查询候选池(listJobs 内部已完成匹配度计算)
|
||||
PageResult<JobDto> candidates = listJobs(param, userId);
|
||||
List<JobDto> list = new ArrayList<>(candidates.getList());
|
||||
|
||||
// 2. 匹配度倒序,同分按岗位ID倒序(同分数量大,缺少二级排序键会导致翻页重复或遗漏)
|
||||
list.sort(Comparator.comparingInt(JobDto::getMatchScore).reversed().thenComparing(JobDto::getId, Comparator.reverseOrder()));
|
||||
|
||||
log.info("匹配度排序完成 userId:{} 候选池:{}条", userId, list.size());
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位列表总数查询
|
||||
* <p>过滤条件与 listJobs 完全一致(SQL 层共用 jobPageWhere 片段),不分页、不查收藏/投递状态、不算匹配度</p>
|
||||
|
||||
@@ -23,7 +23,8 @@ import java.util.stream.IntStream;
|
||||
* <p>依赖:无</p>
|
||||
* <p>使用表:bg_user_profile(主表CRUD)、bg_user_profile_education(教育经历)、
|
||||
* bg_user_profile_work(工作经历)、bg_user_profile_internship(实习经历)、
|
||||
* bg_user_profile_project(项目经历)、bg_user_profile_competition(竞赛经历)</p>
|
||||
* bg_user_profile_project(项目经历)、bg_user_profile_competition(竞赛经历)、
|
||||
* bg_user_profile_organization(社团组织经历)</p>
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@@ -49,6 +50,9 @@ public class UserProfileService {
|
||||
@Autowired
|
||||
private UserProfileCompetitionMapper competitionMapper;
|
||||
|
||||
@Autowired
|
||||
private UserProfileOrganizationMapper organizationMapper;
|
||||
|
||||
@Autowired
|
||||
private org.jiayunet.service.UserProfileAnalyzeService userProfileAnalyzeService;
|
||||
|
||||
@@ -70,6 +74,9 @@ public class UserProfileService {
|
||||
@Autowired
|
||||
private UserResumeCompetitionMapper resumeCompetitionMapper;
|
||||
|
||||
@Autowired
|
||||
private UserResumeOrganizationMapper resumeOrganizationMapper;
|
||||
|
||||
/** 学历文本→枚举映射 */
|
||||
private static final Map<String, Integer> DEGREE_MAP = Map.of(
|
||||
"大专", 1, "本科", 2, "硕士", 3, "博士", 4
|
||||
@@ -126,6 +133,8 @@ public class UserProfileService {
|
||||
.set(UserProfile::getPortfolioUrl, profile.getPortfolioUrl())
|
||||
.set(UserProfile::getSkills, profile.getSkills(), JSON_TYPE_HANDLER)
|
||||
.set(UserProfile::getCertificates, profile.getCertificates(), JSON_TYPE_HANDLER)
|
||||
.set(UserProfile::getHobbies, profile.getHobbies())
|
||||
.set(UserProfile::getLanguageSkills, profile.getLanguageSkills())
|
||||
.set(UserProfile::getUpdateTime, now));
|
||||
} else {
|
||||
profile.setUserId(userId);
|
||||
@@ -320,11 +329,48 @@ public class UserProfileService {
|
||||
userProfileAnalyzeService.analyzeUserProfile(userId);
|
||||
}
|
||||
|
||||
// ==================== 社团组织经历 ====================
|
||||
|
||||
/** 查询社团组织经历列表 */
|
||||
public List<UserProfileOrganization> listOrganization() {
|
||||
Long userId = UserSecurityTool.getUserId();
|
||||
return organizationMapper.selectList(
|
||||
new LambdaQueryWrapper<UserProfileOrganization>()
|
||||
.eq(UserProfileOrganization::getUserId, userId)
|
||||
.orderByAsc(UserProfileOrganization::getSortOrder));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存社团组织经历列表(先删后插)
|
||||
* <p>1. 删除该用户所有社团组织经历 2. 批量插入新数据</p>
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveOrganizationList(List<UserProfileOrganization> list) {
|
||||
Long userId = UserSecurityTool.getUserId();
|
||||
Long profileId = getOrCreateProfileId(userId);
|
||||
organizationMapper.delete(
|
||||
new LambdaQueryWrapper<UserProfileOrganization>().eq(UserProfileOrganization::getUserId, userId));
|
||||
if (list.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Instant now = Instant.now();
|
||||
IntStream.range(0, list.size()).forEach(i -> {
|
||||
UserProfileOrganization item = list.get(i);
|
||||
item.setUserId(userId);
|
||||
item.setProfileId(profileId);
|
||||
item.setSortOrder(i);
|
||||
item.setCreateTime(now);
|
||||
item.setUpdateTime(now);
|
||||
});
|
||||
organizationMapper.batchInsert(list);
|
||||
userProfileAnalyzeService.analyzeUserProfile(userId);
|
||||
}
|
||||
|
||||
// ==================== 从简历同步到个人资料 ====================
|
||||
|
||||
/**
|
||||
* 根据简历ID同步更新个人资料
|
||||
* <p>1. 校验简历归属 2. 读取简历主表+5子表 3. 事务内覆盖写入Profile主表+5子表 4. 同步执行一次AI分析</p>
|
||||
* <p>1. 校验简历归属 2. 读取简历主表+6子表 3. 事务内覆盖写入Profile主表+6子表 4. 同步执行一次AI分析</p>
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void syncProfileFromResume(Long resumeId) {
|
||||
@@ -336,12 +382,13 @@ public class UserProfileService {
|
||||
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "简历不存在或无权操作");
|
||||
}
|
||||
|
||||
// 2. 读取简历5张子表
|
||||
// 2. 读取简历6张子表
|
||||
List<UserResumeEducation> resumeEducationList = resumeEducationMapper.selectList(new LambdaQueryWrapper<UserResumeEducation>().eq(UserResumeEducation::getResumeId, resumeId).orderByAsc(UserResumeEducation::getSortOrder));
|
||||
List<UserResumeWork> resumeWorkList = resumeWorkMapper.selectList(new LambdaQueryWrapper<UserResumeWork>().eq(UserResumeWork::getResumeId, resumeId).orderByAsc(UserResumeWork::getSortOrder));
|
||||
List<UserResumeInternship> resumeInternshipList = resumeInternshipMapper.selectList(new LambdaQueryWrapper<UserResumeInternship>().eq(UserResumeInternship::getResumeId, resumeId).orderByAsc(UserResumeInternship::getSortOrder));
|
||||
List<UserResumeProject> resumeProjectList = resumeProjectMapper.selectList(new LambdaQueryWrapper<UserResumeProject>().eq(UserResumeProject::getResumeId, resumeId).orderByAsc(UserResumeProject::getSortOrder));
|
||||
List<UserResumeCompetition> resumeCompetitionList = resumeCompetitionMapper.selectList(new LambdaQueryWrapper<UserResumeCompetition>().eq(UserResumeCompetition::getResumeId, resumeId).orderByAsc(UserResumeCompetition::getSortOrder));
|
||||
List<UserResumeOrganization> resumeOrganizationList = resumeOrganizationMapper.selectList(new LambdaQueryWrapper<UserResumeOrganization>().eq(UserResumeOrganization::getResumeId, resumeId).orderByAsc(UserResumeOrganization::getSortOrder));
|
||||
|
||||
Instant now = Instant.now();
|
||||
|
||||
@@ -362,6 +409,8 @@ public class UserProfileService {
|
||||
.set(UserProfile::getPortfolioUrl, resume.getPortfolioUrl())
|
||||
.set(UserProfile::getSkills, resume.getSkills(), JSON_TYPE_HANDLER)
|
||||
.set(UserProfile::getCertificates, resume.getCertificates(), JSON_TYPE_HANDLER)
|
||||
.set(UserProfile::getHobbies, resume.getHobbies())
|
||||
.set(UserProfile::getLanguageSkills, resume.getLanguageSkills())
|
||||
.set(UserProfile::getUpdateTime, now));
|
||||
} else {
|
||||
UserProfile profile = new UserProfile();
|
||||
@@ -373,6 +422,8 @@ public class UserProfileService {
|
||||
profile.setPortfolioUrl(resume.getPortfolioUrl());
|
||||
profile.setSkills(resume.getSkills());
|
||||
profile.setCertificates(resume.getCertificates());
|
||||
profile.setHobbies(resume.getHobbies());
|
||||
profile.setLanguageSkills(resume.getLanguageSkills());
|
||||
profile.setCreateTime(now);
|
||||
profile.setUpdateTime(now);
|
||||
userProfileMapper.insert(profile);
|
||||
@@ -486,7 +537,28 @@ public class UserProfileService {
|
||||
competitionMapper.batchInsert(profileCompetitionList);
|
||||
}
|
||||
|
||||
// 9. 事务内所有数据写入完成,同步执行AI分析并等待完成
|
||||
// 9. 同步社团组织经历
|
||||
organizationMapper.delete(new LambdaQueryWrapper<UserProfileOrganization>().eq(UserProfileOrganization::getUserId, userId));
|
||||
if (!resumeOrganizationList.isEmpty()) {
|
||||
List<UserProfileOrganization> profileOrganizationList = IntStream.range(0, resumeOrganizationList.size()).mapToObj(i -> {
|
||||
UserResumeOrganization src = resumeOrganizationList.get(i);
|
||||
UserProfileOrganization dest = new UserProfileOrganization();
|
||||
dest.setProfileId(profileId);
|
||||
dest.setUserId(userId);
|
||||
dest.setOrganizationName(src.getOrganizationName());
|
||||
dest.setRole(src.getRole());
|
||||
dest.setStartDate(src.getStartDate());
|
||||
dest.setEndDate(src.getEndDate());
|
||||
dest.setDescription(src.getDescription());
|
||||
dest.setSortOrder(i);
|
||||
dest.setCreateTime(now);
|
||||
dest.setUpdateTime(now);
|
||||
return dest;
|
||||
}).collect(Collectors.toList());
|
||||
organizationMapper.batchInsert(profileOrganizationList);
|
||||
}
|
||||
|
||||
// 10. 事务内所有数据写入完成,同步执行AI分析并等待完成
|
||||
userProfileAnalyzeService.analyzeUserProfileSync(userId);
|
||||
}
|
||||
|
||||
@@ -494,9 +566,9 @@ public class UserProfileService {
|
||||
|
||||
/**
|
||||
* 根据当前用户的个人资料创建一份新简历
|
||||
* <p>syncProfileFromResume 的反向操作:读取个人资料主表+5子表,新建一份简历(名"个人资料简历")及对应子表,返回新简历ID</p>
|
||||
* <p>1. 校验个人资料存在 2. 读取个人资料5子表 3. 新建简历主表(复制共有字段,简历独有字段targetPosition/avatarUrl/city/summary留空)
|
||||
* 4. 转换并批量插入5张简历子表(含degree/studyType反向映射) 5. 返回新简历ID</p>
|
||||
* <p>syncProfileFromResume 的反向操作:读取个人资料主表+6子表,新建一份简历(名"个人资料简历")及对应子表,返回新简历ID</p>
|
||||
* <p>1. 校验个人资料存在 2. 读取个人资料6子表 3. 新建简历主表(复制共有字段,简历独有字段targetPosition/avatarUrl/city/summary留空)
|
||||
* 4. 转换并批量插入6张简历子表(含degree/studyType反向映射) 5. 返回新简历ID</p>
|
||||
* <p>注意:这是创建新简历(非覆盖),不删除已有简历;简历侧无AI分析,故不触发分析</p>
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -511,12 +583,13 @@ public class UserProfileService {
|
||||
}
|
||||
Long profileId = profile.getId();
|
||||
|
||||
// 2. 读取个人资料5张子表
|
||||
// 2. 读取个人资料6张子表
|
||||
List<UserProfileEducation> profileEducationList = educationMapper.selectList(new LambdaQueryWrapper<UserProfileEducation>().eq(UserProfileEducation::getProfileId, profileId).orderByAsc(UserProfileEducation::getSortOrder));
|
||||
List<UserProfileWork> profileWorkList = workMapper.selectList(new LambdaQueryWrapper<UserProfileWork>().eq(UserProfileWork::getProfileId, profileId).orderByAsc(UserProfileWork::getSortOrder));
|
||||
List<UserProfileInternship> profileInternshipList = internshipMapper.selectList(new LambdaQueryWrapper<UserProfileInternship>().eq(UserProfileInternship::getProfileId, profileId).orderByAsc(UserProfileInternship::getSortOrder));
|
||||
List<UserProfileProject> profileProjectList = projectMapper.selectList(new LambdaQueryWrapper<UserProfileProject>().eq(UserProfileProject::getProfileId, profileId).orderByAsc(UserProfileProject::getSortOrder));
|
||||
List<UserProfileCompetition> profileCompetitionList = competitionMapper.selectList(new LambdaQueryWrapper<UserProfileCompetition>().eq(UserProfileCompetition::getProfileId, profileId).orderByAsc(UserProfileCompetition::getSortOrder));
|
||||
List<UserProfileOrganization> profileOrganizationList = organizationMapper.selectList(new LambdaQueryWrapper<UserProfileOrganization>().eq(UserProfileOrganization::getProfileId, profileId).orderByAsc(UserProfileOrganization::getSortOrder));
|
||||
|
||||
Instant now = Instant.now();
|
||||
|
||||
@@ -531,6 +604,8 @@ public class UserProfileService {
|
||||
resume.setPortfolioUrl(profile.getPortfolioUrl());
|
||||
resume.setSkills(profile.getSkills());
|
||||
resume.setCertificates(profile.getCertificates());
|
||||
resume.setHobbies(profile.getHobbies());
|
||||
resume.setLanguageSkills(profile.getLanguageSkills());
|
||||
// 无简历时新建的这份自动设为默认,与 UserResumeService.saveResume 逻辑一致
|
||||
boolean hasResume = userResumeMapper.selectCount(new LambdaQueryWrapper<UserResume>().eq(UserResume::getUserId, userId)) > 0;
|
||||
resume.setIsDefault(hasResume ? 0 : 1);
|
||||
@@ -642,7 +717,27 @@ public class UserProfileService {
|
||||
resumeCompetitionMapper.batchInsert(resumeCompetitionList);
|
||||
}
|
||||
|
||||
// 9. 返回新简历ID
|
||||
// 9. 转换并插入简历社团组织经历
|
||||
if (!profileOrganizationList.isEmpty()) {
|
||||
List<UserResumeOrganization> resumeOrganizationList = IntStream.range(0, profileOrganizationList.size()).mapToObj(i -> {
|
||||
UserProfileOrganization src = profileOrganizationList.get(i);
|
||||
UserResumeOrganization dest = new UserResumeOrganization();
|
||||
dest.setResumeId(resumeId);
|
||||
dest.setUserId(userId);
|
||||
dest.setOrganizationName(src.getOrganizationName());
|
||||
dest.setRole(src.getRole());
|
||||
dest.setStartDate(src.getStartDate());
|
||||
dest.setEndDate(src.getEndDate());
|
||||
dest.setDescription(src.getDescription());
|
||||
dest.setSortOrder(i);
|
||||
dest.setCreateTime(now);
|
||||
dest.setUpdateTime(now);
|
||||
return dest;
|
||||
}).collect(Collectors.toList());
|
||||
resumeOrganizationMapper.batchInsert(resumeOrganizationList);
|
||||
}
|
||||
|
||||
// 10. 返回新简历ID
|
||||
return resumeId;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ import java.util.stream.IntStream;
|
||||
* <p>依赖:无</p>
|
||||
* <p>使用表:bg_user_resume(简历主表CRUD)、bg_user_resume_education(教育经历)、
|
||||
* bg_user_resume_work(工作经历)、bg_user_resume_internship(实习经历)、
|
||||
* bg_user_resume_project(项目经历)、bg_user_resume_competition(竞赛经历)</p>
|
||||
* bg_user_resume_project(项目经历)、bg_user_resume_competition(竞赛经历)、
|
||||
* bg_user_resume_organization(社团组织经历)</p>
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@@ -58,6 +59,9 @@ public class UserResumeService {
|
||||
@Autowired
|
||||
private UserResumeCompetitionMapper competitionMapper;
|
||||
|
||||
@Autowired
|
||||
private UserResumeOrganizationMapper organizationMapper;
|
||||
|
||||
// ==================== 简历列表 ====================
|
||||
|
||||
/**
|
||||
@@ -135,6 +139,8 @@ public class UserResumeService {
|
||||
.set(UserResume::getSkills, resume.getSkills(), JSON_TYPE_HANDLER)
|
||||
.set(UserResume::getCertificates, resume.getCertificates(), JSON_TYPE_HANDLER)
|
||||
.set(UserResume::getSummary, resume.getSummary())
|
||||
.set(UserResume::getHobbies, resume.getHobbies())
|
||||
.set(UserResume::getLanguageSkills, resume.getLanguageSkills())
|
||||
.set(UserResume::getUpdateTime, now));
|
||||
return resumeId;
|
||||
}
|
||||
@@ -156,7 +162,7 @@ public class UserResumeService {
|
||||
|
||||
/**
|
||||
* 删除简历(物理删除主表 + 全部子表)
|
||||
* <p>1. 校验归属 2. 删除5张子表 3. 删除主表</p>
|
||||
* <p>1. 校验归属 2. 删除6张子表 3. 删除主表</p>
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteResume(Long resumeId) {
|
||||
@@ -168,6 +174,7 @@ public class UserResumeService {
|
||||
internshipMapper.delete(new LambdaQueryWrapper<UserResumeInternship>().eq(UserResumeInternship::getResumeId, resumeId));
|
||||
projectMapper.delete(new LambdaQueryWrapper<UserResumeProject>().eq(UserResumeProject::getResumeId, resumeId));
|
||||
competitionMapper.delete(new LambdaQueryWrapper<UserResumeCompetition>().eq(UserResumeCompetition::getResumeId, resumeId));
|
||||
organizationMapper.delete(new LambdaQueryWrapper<UserResumeOrganization>().eq(UserResumeOrganization::getResumeId, resumeId));
|
||||
// 删除主表
|
||||
userResumeMapper.deleteById(resumeId);
|
||||
}
|
||||
@@ -342,6 +349,40 @@ public class UserResumeService {
|
||||
return resumeId;
|
||||
}
|
||||
|
||||
// ==================== 社团组织经历 ====================
|
||||
|
||||
/** 查询简历的社团组织经历列表 */
|
||||
public List<UserResumeOrganization> listOrganization(Long resumeId) {
|
||||
checkResumeOwnership(resumeId);
|
||||
return organizationMapper.selectList(new LambdaQueryWrapper<UserResumeOrganization>().eq(UserResumeOrganization::getResumeId, resumeId).orderByAsc(UserResumeOrganization::getSortOrder));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存简历的社团组织经历列表(先删后插)
|
||||
* <p>1. resumeId为空则自动创建新简历 2. 按resumeId删除旧数据 3. 批量插入 4. 更新主表修改时间</p>
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long saveOrganizationList(List<UserResumeOrganization> list, Long resumeId) {
|
||||
Long userId = UserSecurityTool.getUserId();
|
||||
resumeId = getOrCreateResumeId(resumeId, userId);
|
||||
organizationMapper.delete(new LambdaQueryWrapper<UserResumeOrganization>().eq(UserResumeOrganization::getResumeId, resumeId));
|
||||
if (!list.isEmpty()) {
|
||||
Instant now = Instant.now();
|
||||
Long finalResumeId = resumeId;
|
||||
IntStream.range(0, list.size()).forEach(i -> {
|
||||
UserResumeOrganization item = list.get(i);
|
||||
item.setUserId(userId);
|
||||
item.setResumeId(finalResumeId);
|
||||
item.setSortOrder(i);
|
||||
item.setCreateTime(now);
|
||||
item.setUpdateTime(now);
|
||||
});
|
||||
organizationMapper.batchInsert(list);
|
||||
}
|
||||
touchResumeUpdateTime(resumeId);
|
||||
return resumeId;
|
||||
}
|
||||
|
||||
// ==================== 子表单条添加 ====================
|
||||
|
||||
/**
|
||||
@@ -439,6 +480,25 @@ public class UserResumeService {
|
||||
return po.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单条社团组织经历
|
||||
* <p>1. 校验简历归属 2. 设置系统字段 3. insert 4. 刷新主表updateTime</p>
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long addOrganization(UserResumeOrganization po, Long resumeId) {
|
||||
checkResumeOwnership(resumeId);
|
||||
Long userId = UserSecurityTool.getUserId();
|
||||
Instant now = Instant.now();
|
||||
po.setResumeId(resumeId);
|
||||
po.setUserId(userId);
|
||||
po.setSortOrder(0);
|
||||
po.setCreateTime(now);
|
||||
po.setUpdateTime(now);
|
||||
organizationMapper.insert(po);
|
||||
touchResumeUpdateTime(resumeId);
|
||||
return po.getId();
|
||||
}
|
||||
|
||||
// ==================== 子表单条删除 ====================
|
||||
|
||||
/**
|
||||
@@ -506,6 +566,19 @@ public class UserResumeService {
|
||||
touchResumeUpdateTime(existing.getResumeId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id删除社团组织经历
|
||||
* <p>1. 查记录是否存在 2. 校验简历归属 3. 删除 4. 刷新主表updateTime</p>
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteOrganization(Long id) {
|
||||
UserResumeOrganization existing = organizationMapper.selectById(id);
|
||||
Assert.notNull(existing, "社团组织经历不存在");
|
||||
checkResumeOwnership(existing.getResumeId());
|
||||
organizationMapper.deleteById(id);
|
||||
touchResumeUpdateTime(existing.getResumeId());
|
||||
}
|
||||
|
||||
// ==================== 子表单条编辑 ====================
|
||||
|
||||
/**
|
||||
@@ -615,6 +688,27 @@ public class UserResumeService {
|
||||
touchResumeUpdateTime(existing.getResumeId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id编辑社团组织经历
|
||||
* <p>1. 查记录是否存在 2. 校验简历归属 3. 覆盖字段 4. updateById 5. 刷新主表updateTime</p>
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateOrganization(UserResumeOrganization po) {
|
||||
UserResumeOrganization existing = organizationMapper.selectById(po.getId());
|
||||
Assert.notNull(existing, "社团组织经历不存在");
|
||||
checkResumeOwnership(existing.getResumeId());
|
||||
// 显式set用户可编辑字段(允许清空为null);resumeId/userId/sortOrder/createTime保留原值
|
||||
organizationMapper.update(null, new LambdaUpdateWrapper<UserResumeOrganization>()
|
||||
.eq(UserResumeOrganization::getId, po.getId())
|
||||
.set(UserResumeOrganization::getOrganizationName, po.getOrganizationName())
|
||||
.set(UserResumeOrganization::getRole, po.getRole())
|
||||
.set(UserResumeOrganization::getStartDate, po.getStartDate())
|
||||
.set(UserResumeOrganization::getEndDate, po.getEndDate())
|
||||
.set(UserResumeOrganization::getDescription, po.getDescription(), JSON_TYPE_HANDLER)
|
||||
.set(UserResumeOrganization::getUpdateTime, Instant.now()));
|
||||
touchResumeUpdateTime(existing.getResumeId());
|
||||
}
|
||||
|
||||
// ==================== 内部方法 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,9 +30,15 @@ public class CacheConfig {
|
||||
/** 缓存名:热门城市 */
|
||||
public static final String HOT_CITY = "hotCity";
|
||||
|
||||
/** 缓存名:岗位匹配度排序结果(单条目为5000个JobDto,占用大,容量单独控制) */
|
||||
public static final String JOB_MATCH_SORT = "jobMatchSort";
|
||||
|
||||
/** 单个缓存最大条目数 */
|
||||
private static final long MAX_SIZE = 100;
|
||||
|
||||
/** 岗位匹配度排序缓存最大条目数(单条目约5~10MB,不能按 MAX_SIZE 放开) */
|
||||
private static final long JOB_MATCH_SORT_MAX_SIZE = 20;
|
||||
|
||||
/**
|
||||
* 缓存管理器
|
||||
* <p>三个热门榜单各注册一份独立的 Caffeine 实例,过期时间互相独立(单位:分钟)</p>
|
||||
@@ -48,22 +54,26 @@ public class CacheConfig {
|
||||
.maximumSize(MAX_SIZE));
|
||||
|
||||
// 各缓存独立注册,独立过期时间
|
||||
cacheManager.registerCustomCache(HOT_INDUSTRY, buildCache(60));
|
||||
cacheManager.registerCustomCache(HOT_JOB_CATEGORY, buildCache(60));
|
||||
cacheManager.registerCustomCache(HOT_CITY, buildCache(60));
|
||||
cacheManager.registerCustomCache(HOT_INDUSTRY, buildCache(60, MAX_SIZE));
|
||||
cacheManager.registerCustomCache(HOT_JOB_CATEGORY, buildCache(60, MAX_SIZE));
|
||||
cacheManager.registerCustomCache(HOT_CITY, buildCache(60, MAX_SIZE));
|
||||
|
||||
// 匹配度排序结果:单条目占用大,容量收紧;内含收藏/投递状态快照,时长不宜过长
|
||||
cacheManager.registerCustomCache(JOB_MATCH_SORT, buildCache(5, JOB_MATCH_SORT_MAX_SIZE));
|
||||
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一个指定过期时长的 Caffeine 缓存实例
|
||||
* 构建一个 Caffeine 缓存实例
|
||||
*
|
||||
* @param expireMinutes 写入后过期时长(分钟)
|
||||
* @param maximumSize 最大条目数
|
||||
*/
|
||||
private Cache<Object, Object> buildCache(long expireMinutes) {
|
||||
private Cache<Object, Object> buildCache(long expireMinutes, long maximumSize) {
|
||||
return Caffeine.newBuilder()
|
||||
.expireAfterWrite(expireMinutes, TimeUnit.MINUTES)
|
||||
.maximumSize(MAX_SIZE)
|
||||
.maximumSize(maximumSize)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jiayunet.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jiayunet.pojo.po.UserProfileOrganization;
|
||||
|
||||
/**
|
||||
* 用户社团组织经历Mapper
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserProfileOrganizationMapper extends CommonMapper<UserProfileOrganization> {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jiayunet.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jiayunet.pojo.po.UserResumeOrganization;
|
||||
|
||||
/**
|
||||
* 简历-社团组织经历Mapper
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserResumeOrganizationMapper extends CommonMapper<UserResumeOrganization> {
|
||||
}
|
||||
@@ -80,6 +80,12 @@ public class UserProfile {
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> certificates;
|
||||
|
||||
/** 兴趣爱好 */
|
||||
private String hobbies;
|
||||
|
||||
/** 语言能力 */
|
||||
private String languageSkills;
|
||||
|
||||
/** 创建时间 */
|
||||
private Instant createTime;
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.jiayunet.pojo.po;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户社团组织经历表(bg_user_profile_organization,bg_user_profile子表)
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Data
|
||||
@TableName(value = "bg_user_profile_organization", autoResultMap = true)
|
||||
public class UserProfileOrganization {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/** 关联bg_user_profile.id */
|
||||
private Long profileId;
|
||||
|
||||
/** 用户ID(冗余,便于直接按用户查询) */
|
||||
private Long userId;
|
||||
|
||||
/** 社团/组织名称 */
|
||||
private String organizationName;
|
||||
|
||||
/** 担任角色 */
|
||||
private String role;
|
||||
|
||||
/** 开始时间,格式:2023.06 */
|
||||
private String startDate;
|
||||
|
||||
/** 结束时间,格式:2023.09,至今则为空 */
|
||||
private String endDate;
|
||||
|
||||
/** 描述段落,id为前端生成的短标识,用于简历优化时精确定位段落 */
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<DescriptionParagraph> description;
|
||||
|
||||
/** 排序序号,越小越靠前 */
|
||||
private Integer sortOrder;
|
||||
|
||||
/** 创建时间 */
|
||||
private Instant createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private Instant updateTime;
|
||||
}
|
||||
@@ -74,6 +74,12 @@ public class UserResume {
|
||||
/** 个人概述 */
|
||||
private String summary;
|
||||
|
||||
/** 兴趣爱好 */
|
||||
private String hobbies;
|
||||
|
||||
/** 语言能力 */
|
||||
private String languageSkills;
|
||||
|
||||
/** 简历创建时间 */
|
||||
private Instant createTime;
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.jiayunet.pojo.po;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
import org.jiayunet.pojo.vo.DescriptionParagraph;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 简历-社团组织经历表(bg_user_resume_organization)
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Data
|
||||
@TableName(value = "bg_user_resume_organization", autoResultMap = true)
|
||||
public class UserResumeOrganization {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/** 关联bg_user_resume.id */
|
||||
private Long resumeId;
|
||||
|
||||
/** 用户ID(冗余,便于直接按用户查询) */
|
||||
private Long userId;
|
||||
|
||||
/** 社团/组织名称 */
|
||||
private String organizationName;
|
||||
|
||||
/** 担任角色 */
|
||||
private String role;
|
||||
|
||||
/** 开始时间,格式:2023.06 */
|
||||
private String startDate;
|
||||
|
||||
/** 结束时间,格式:2023.09,至今则为空 */
|
||||
private String endDate;
|
||||
|
||||
/** 描述段落,id为前端生成的短标识,用于简历优化时精确定位段落 */
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<DescriptionParagraph> description;
|
||||
|
||||
/** 排序序号,越小越靠前 */
|
||||
private Integer sortOrder;
|
||||
|
||||
/** 创建时间 */
|
||||
private Instant createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private Instant updateTime;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import java.util.stream.StreamSupport;
|
||||
* 用户简历分析服务
|
||||
* <p>主要功能:用户保存简历后异步分析,提取学校等级、经历质量、专业、技能等维度数据</p>
|
||||
* <p>依赖:AiChatAbility(AI调用)、DictCacheService(专业分类缓存)</p>
|
||||
* <p>使用表:bg_user_profile(主表,读取/更新)、bg_user_profile_*(5张子表,读取)、
|
||||
* <p>使用表:bg_user_profile(主表,读取/更新)、bg_user_profile_*(6张子表,读取)、
|
||||
* bg_skill_tag(技能入库)、bg_user_profile_skill_tag_relation(关联表)</p>
|
||||
*
|
||||
* @author zk
|
||||
@@ -56,6 +56,9 @@ public class UserProfileAnalyzeService {
|
||||
@Autowired
|
||||
private UserProfileCompetitionMapper competitionMapper;
|
||||
|
||||
@Autowired
|
||||
private UserProfileOrganizationMapper organizationMapper;
|
||||
|
||||
@Autowired
|
||||
private SkillTagMapper skillTagMapper;
|
||||
|
||||
@@ -94,15 +97,16 @@ public class UserProfileAnalyzeService {
|
||||
List<UserProfileInternship> internshipList = internshipMapper.selectList(new LambdaQueryWrapper<UserProfileInternship>().eq(UserProfileInternship::getUserId, userId));
|
||||
List<UserProfileProject> projectList = projectMapper.selectList(new LambdaQueryWrapper<UserProfileProject>().eq(UserProfileProject::getUserId, userId));
|
||||
List<UserProfileCompetition> competitionList = competitionMapper.selectList(new LambdaQueryWrapper<UserProfileCompetition>().eq(UserProfileCompetition::getUserId, userId));
|
||||
List<UserProfileOrganization> organizationList = organizationMapper.selectList(new LambdaQueryWrapper<UserProfileOrganization>().eq(UserProfileOrganization::getUserId, userId));
|
||||
|
||||
// 2. 数据有效性检查
|
||||
if (educationList.isEmpty() && workList.isEmpty() && internshipList.isEmpty() && projectList.isEmpty() && competitionList.isEmpty()) {
|
||||
if (educationList.isEmpty() && workList.isEmpty() && internshipList.isEmpty() && projectList.isEmpty() && competitionList.isEmpty() && organizationList.isEmpty()) {
|
||||
log.info("用户所有子表为空,清空技能标签, userId={}", userId);
|
||||
clearSkillRelations(userId);
|
||||
return;
|
||||
}
|
||||
|
||||
String profileJson = buildProfileJson(profile, educationList, workList, internshipList, projectList, competitionList);
|
||||
String profileJson = buildProfileJson(profile, educationList, workList, internshipList, projectList, competitionList, organizationList);
|
||||
|
||||
// 3. 第一次AI:简历综合分析(失败不影响后续)
|
||||
try {
|
||||
@@ -445,16 +449,20 @@ public class UserProfileAnalyzeService {
|
||||
/** 构建用户简历JSON */
|
||||
private String buildProfileJson(UserProfile profile, List<UserProfileEducation> educationList,
|
||||
List<UserProfileWork> workList, List<UserProfileInternship> internshipList,
|
||||
List<UserProfileProject> projectList, List<UserProfileCompetition> competitionList) {
|
||||
List<UserProfileProject> projectList, List<UserProfileCompetition> competitionList,
|
||||
List<UserProfileOrganization> organizationList) {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("name", profile.getName());
|
||||
data.put("skills", profile.getSkills());
|
||||
data.put("certificates", profile.getCertificates());
|
||||
data.put("hobbies", profile.getHobbies());
|
||||
data.put("languageSkills", profile.getLanguageSkills());
|
||||
data.put("education", educationList);
|
||||
data.put("work", workList);
|
||||
data.put("internship", internshipList);
|
||||
data.put("project", projectList);
|
||||
data.put("competition", competitionList);
|
||||
data.put("organization", organizationList);
|
||||
try {
|
||||
return HttpTool.objectMapper.writeValueAsString(data);
|
||||
} catch (Exception e) {
|
||||
|
||||
Reference in New Issue
Block a user