Compare commits

...
12 Commits
Author SHA1 Message Date
xuxin f03c13351c 加免登录的岗位列表接口 2026-08-12 11:19:30 +08:00
xuxin e286997bcc 校招公告列表加发布时间和岗位列表排序修改 2026-08-12 09:41:53 +08:00
zk ea3cb4dead 提取技能维度 补充数据 2026-08-10 18:35:05 +08:00
zk 0a4b97e40e 添加个人资料处理能力 2026-08-10 18:27:48 +08:00
zk 251552ea08 添加 校园经历表 和 兴趣字段 2026-08-10 12:00:28 +08:00
zk 18f6a19a11 切入控制面 2026-08-05 16:21:35 +08:00
zk ff04496117 添加适配度排序 2026-08-05 15:52:55 +08:00
zk d9239a2b68 添加热点缓存 2026-08-05 12:34:25 +08:00
zk 5074297775 添加热门城市 行业 地区 2026-08-05 12:09:48 +08:00
zk 243b1ffc5a 添加岗位列表数量统计接口 2026-08-05 11:23:23 +08:00
zk 9ce99ab3d7 添加发布时间筛选 和 教育筛选 2026-08-05 11:08:08 +08:00
zk 04b6f94935 添加超管注册登录接口 2026-08-04 18:35:28 +08:00
41 changed files with 1353 additions and 55 deletions
@@ -8,14 +8,21 @@ import org.jiayunet.pojo.dto.job.JobDetailDto;
import org.jiayunet.pojo.dto.job.JobDto; import org.jiayunet.pojo.dto.job.JobDto;
import org.jiayunet.pojo.dto.job.JobAgentRecommendDto; import org.jiayunet.pojo.dto.job.JobAgentRecommendDto;
import org.jiayunet.pojo.param.job.*; import org.jiayunet.pojo.param.job.*;
import org.jiayunet.pojo.vo.HotCityVo;
import org.jiayunet.pojo.vo.HotIndustryVo;
import org.jiayunet.pojo.vo.HotJobCategoryVo;
import org.jiayunet.pojo.vo.JobApplyCountVo; import org.jiayunet.pojo.vo.JobApplyCountVo;
import java.util.List; import java.util.List;
import org.jiayunet.pojo.vo.JobFavoriteCountVo; import org.jiayunet.pojo.vo.JobFavoriteCountVo;
import org.jiayunet.service.JobService; import org.jiayunet.service.JobService;
import org.jiayunet.tool.UserSecurityTool; import org.jiayunet.tool.UserSecurityTool;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Collections;
/** /**
* 岗位接口 * 岗位接口
* *
@@ -47,6 +54,102 @@ public class JobController {
return jobService.listJobs(param, userId); 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>
*/
@PostMapping("/list/count")
public Long countJobs(@Validated @RequestBody JobQueryParam param) {
Long userId = 0L;
try {
userId = UserSecurityTool.getUserId();
} catch (Exception e) {
// 接口允许不登录,不处理
}
return jobService.countJobs(param, userId);
}
/**
* 岗位热门行业
* <p>按上架岗位数量倒序返回二级行业,接口允许不登录访问</p>
*
* @param limit 返回条数,默认10
*/
@GetMapping("/industry/hot")
public List<HotIndustryVo> listHotIndustries(@RequestParam(required = false, defaultValue = "10") Integer limit) {
return jobService.listHotIndustries(limit);
}
/**
* 热门岗位类型
* <p>按上架岗位数量倒序返回二级岗位类型,接口允许不登录访问</p>
*
* @param limit 返回条数,默认10
*/
@GetMapping("/category/hot")
public List<HotJobCategoryVo> listHotJobCategories(@RequestParam(required = false, defaultValue = "10") Integer limit) {
return jobService.listHotJobCategories(limit);
}
/**
* 热门城市
* <p>按上架岗位数量倒序返回城市,接口允许不登录访问</p>
*
* @param limit 返回条数,默认10
*/
@GetMapping("/city/hot")
public List<HotCityVo> listHotCities(@RequestParam(required = false, defaultValue = "10") Integer limit) {
return jobService.listHotCities(limit);
}
/** /**
* 岗位详情 * 岗位详情
* <p>返回岗位完整信息、公司信息、匹配度、收藏状态</p> * <p>返回岗位完整信息、公司信息、匹配度、收藏状态</p>
@@ -0,0 +1,55 @@
package org.jiayunet.controller;
import lombok.AllArgsConstructor;
import org.jiayunet.annotation.FuncPermission;
import org.jiayunet.pojo.PageResult;
import org.jiayunet.pojo.dto.job.JobAgentRecommendDto;
import org.jiayunet.pojo.dto.job.JobApplicationDto;
import org.jiayunet.pojo.dto.job.JobDetailDto;
import org.jiayunet.pojo.dto.job.JobDto;
import org.jiayunet.pojo.param.job.*;
import org.jiayunet.pojo.vo.*;
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;
import java.util.List;
/**
* 岗位接口-公开
*
* @author zk
*/
@RestController
@RequestMapping("/public/job")
@AllArgsConstructor
@Validated
public class JobPublicController {
private final JobService jobService;
/**
* 岗位列表查询-公开免登录
* <p>支持按地区/岗位类型/行业/工作类型筛选,自动排除不感兴趣的岗位/公司/地区/行业,返回匹配度和收藏状态</p>
*/
@PostMapping("/list")
@FuncPermission(value = "job_list", key = "#param.pageNum")
public PageResult<JobDto> listJobs(@Validated @RequestBody JobQueryParam param) {
Long userId = 0L;
try {
userId = UserSecurityTool.getUserId();
}catch (Exception e) {
// 接口允许不登录,不处理
}
return jobService.listJobs(param, userId);
}
}
@@ -2,6 +2,7 @@ package org.jiayunet.controller;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.jiayunet.pojo.dto.SmsLoginDto; import org.jiayunet.pojo.dto.SmsLoginDto;
import org.jiayunet.pojo.dto.SuperAdminLoginDto;
import org.jiayunet.pojo.vo.LoginVo; import org.jiayunet.pojo.vo.LoginVo;
import org.jiayunet.service.LoginService; import org.jiayunet.service.LoginService;
import org.jiayunet.tool.UserSecurityTool; import org.jiayunet.tool.UserSecurityTool;
@@ -37,6 +38,17 @@ public class LoginController {
return loginService.smsLogin(dto.getMobileNumber(), dto.getCode(), dto.getInviteCode(), request, response); return loginService.smsLogin(dto.getMobileNumber(), dto.getCode(), dto.getInviteCode(), request, response);
} }
/**
* 超管密码登录
* <p>用手机号+超管密码直接登录,用户不存在则自动注册并赠送正式会员</p>
*/
@PostMapping("/login/superAdminLogin")
public LoginVo superAdminLogin(@Validated @RequestBody SuperAdminLoginDto dto,
HttpServletRequest request,
HttpServletResponse response) {
return loginService.superAdminLogin(dto.getMobileNumber(), dto.getPassword(), request, response);
}
@PostMapping("/logout") @PostMapping("/logout")
public boolean logout(HttpServletRequest request, HttpServletResponse response) { public boolean logout(HttpServletRequest request, HttpServletResponse response) {
loginService.logout(request, response); loginService.logout(request, response);
@@ -15,7 +15,7 @@ import java.util.stream.Collectors;
/** /**
* 用户个人资料接口 * 用户个人资料接口
* 提供主表及5张子表(教育/工作/实习/项目/竞赛)的查询与保存功能 * 提供主表及6张子表(教育/工作/实习/项目/竞赛/社团组织)的查询与保存功能
* *
* @author zk * @author zk
*/ */
@@ -54,7 +54,7 @@ public class UserProfileController {
} }
/** /**
* 根据简历ID同步更新个人资料(主表+5子表全量覆盖,触发一次异步AI分析) * 根据简历ID同步更新个人资料(主表+6子表全量覆盖,触发一次异步AI分析)
*/ */
@PostMapping("/syncFromResume") @PostMapping("/syncFromResume")
public void syncFromResume(@RequestParam Long resumeId) { public void syncFromResume(@RequestParam Long resumeId) {
@@ -203,4 +203,31 @@ public class UserProfileController {
}).collect(Collectors.toList()); }).collect(Collectors.toList());
userProfileService.saveCompetitionList(list); 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> * <p>所有保存接口支持自动创建:前端不传resumeId时自动创建新简历并返回resumeId</p>
* *
* @author zk * @author zk
@@ -306,4 +306,49 @@ public class UserResumeController {
}).collect(Collectors.toList()); }).collect(Collectors.toList());
return userResumeService.saveCompetitionList(list, param.getResumeId()); 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());
}
} }
@@ -0,0 +1,22 @@
package org.jiayunet.pojo.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
/**
* 超管密码登录入参
*
* @author zk
*/
@Data
public class SuperAdminLoginDto {
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String mobileNumber;
@NotBlank(message = "超管密码不能为空")
private String password;
}
@@ -34,6 +34,9 @@ public class RecruitAnnouncementDto {
/** 投递结束时间 */ /** 投递结束时间 */
private Instant applyEndTime; private Instant applyEndTime;
/** 发布时间 */
private Instant publishTime;
/** 投递截止描述 */ /** 投递截止描述 */
private String applyEndDesc; private String applyEndDesc;
@@ -52,4 +52,10 @@ public class ResumeDto {
/** 个人概述 */ /** 个人概述 */
private String summary; 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 List<String> certificates;
/** 兴趣爱好 */
private String hobbies;
/** 语言能力 */
private String languageSkills;
} }
@@ -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;
}
@@ -4,6 +4,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.jiayunet.pojo.PageParam; import org.jiayunet.pojo.PageParam;
import java.time.Instant;
import java.util.List; import java.util.List;
/** /**
@@ -44,4 +45,13 @@ public class JobQueryParam extends PageParam {
/** 公司ID */ /** 公司ID */
private Long companyId; private Long companyId;
/** 学历要求列表 0=不限 1=大专 2=本科 3=硕士 4=博士(可多选,null或空=不限制) */
private List<Integer> educations;
/** 发布开始时间(对应 bg_job.expire_at,闭区间) */
private Instant publishStartTime;
/** 发布截止时间(对应 bg_job.expire_at,闭区间) */
private Instant publishEndTime;
} }
@@ -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;
}
@@ -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; private String summary;
/** 兴趣爱好 */
@Size(max = 2000, message = "兴趣爱好长度不能超过2000")
private String hobbies;
/** 语言能力 */
@Size(max = 2000, message = "语言能力长度不能超过2000")
private String languageSkills;
} }
@@ -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; private List<String> certificates;
/** 兴趣爱好 */
@Size(max = 2000, message = "兴趣爱好长度不能超过2000")
private String hobbies;
/** 语言能力 */
@Size(max = 2000, message = "语言能力长度不能超过2000")
private String languageSkills;
} }
@@ -2,6 +2,8 @@ package org.jiayunet.service;
import org.jiayunet.ai.AiChatAbility; import org.jiayunet.ai.AiChatAbility;
import org.jiayunet.ai.AiResponseCleanTool; import org.jiayunet.ai.AiResponseCleanTool;
import org.jiayunet.config.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.util.StopWatch; import org.springframework.util.StopWatch;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -22,6 +24,9 @@ import org.jiayunet.pojo.po.*;
import org.jiayunet.tool.HttpTool; import org.jiayunet.tool.HttpTool;
import org.jiayunet.exception.BusinessException; import org.jiayunet.exception.BusinessException;
import org.jiayunet.exception.BusinessExpCodeEnum; import org.jiayunet.exception.BusinessExpCodeEnum;
import org.jiayunet.pojo.vo.HotCityVo;
import org.jiayunet.pojo.vo.HotIndustryVo;
import org.jiayunet.pojo.vo.HotJobCategoryVo;
import org.jiayunet.pojo.vo.JobApplyCountVo; import org.jiayunet.pojo.vo.JobApplyCountVo;
import org.jiayunet.pojo.vo.JobFavoriteCountVo; import org.jiayunet.pojo.vo.JobFavoriteCountVo;
import org.jiayunet.pojo.vo.JobListItemVo; import org.jiayunet.pojo.vo.JobListItemVo;
@@ -82,6 +87,9 @@ public class JobService {
@Autowired @Autowired
private AiChatAbility aiChatAbility; private AiChatAbility aiChatAbility;
/** 匹配度排序的候选池大小,匹配度无法在 SQL 层排序,只能在此范围内做全量算分后排序 */
public static final int CANDIDATE_POOL_SIZE = 5000;
/** /**
* 岗位列表查询 * 岗位列表查询
* <p>方法逻辑流程:</p> * <p>方法逻辑流程:</p>
@@ -127,7 +135,7 @@ public class JobService {
// 6. 执行分页查询 // 6. 执行分页查询
Page<JobListItemVo> pageParam = param.toPage(); Page<JobListItemVo> pageParam = param.toPage();
pageParam.setSearchCount(false); pageParam.setSearchCount(false);
Page<JobListItemVo> page = jobMapper.selectJobPage(pageParam, param.getJobIds(),param.getStatusFilter(), param.getKeyword(), expandedRegionCodes, expandedCategoryIds, expandedIndustryIds, param.getEmploymentType(), excludeJobIds, excludeCompanyIds, excludeRegionCodes, excludeIndustryIds, param.getRecruitCategory(), param.getCompanyTypes(), param.getCompanyId()); Page<JobListItemVo> page = jobMapper.selectJobPage(pageParam, param.getJobIds(),param.getStatusFilter(), param.getKeyword(), expandedRegionCodes, expandedCategoryIds, expandedIndustryIds, param.getEmploymentType(), excludeJobIds, excludeCompanyIds, excludeRegionCodes, excludeIndustryIds, param.getRecruitCategory(), param.getCompanyTypes(), param.getCompanyId(), param.getEducations(), param.getPublishStartTime(), param.getPublishEndTime());
// 7. 查询收藏状态 // 7. 查询收藏状态
List<Long> jobIds = page.getRecords().stream().map(JobListItemVo::getId).collect(Collectors.toList()); List<Long> jobIds = page.getRecords().stream().map(JobListItemVo::getId).collect(Collectors.toList());
@@ -161,6 +169,110 @@ public class JobService {
return new PageResult<>(page.getCurrent(), page.getSize(), page.getTotal(), dtoList); 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>
* <p>方法逻辑流程:</p>
* <p>1. 扩展筛选条件的子级(地区/岗位类型/行业)</p>
* <p>2. 查询用户不感兴趣记录</p>
* <p>3. 提取排除列表(岗位/公司/地区/行业)</p>
* <p>4. 执行统计查询</p>
*/
public Long countJobs(JobQueryParam param, Long userId) {
// 默认只查有效岗位
if (param.getStatusFilter() == null) {
param.setStatusFilter(Collections.singletonList(0));
}
// 1. 扩展筛选条件的子级
List<String> expandedRegionCodes = expandRegionCodes(param.getRegionCodes());
List<Long> expandedCategoryIds = expandCategoryIds(param.getCategoryIds());
List<Long> expandedIndustryIds = expandIndustryIds(param.getIndustryIds());
// 2. 查询用户不感兴趣记录
List<UserJobDislike> dislikes = userJobDislikeMapper.selectList(new LambdaQueryWrapper<UserJobDislike>().eq(UserJobDislike::getUserId, userId));
// 3. 提取排除列表
List<Long> excludeJobIds = dislikes.stream().map(UserJobDislike::getJobId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (param.getExcludeJobIds() != null && !param.getExcludeJobIds().isEmpty()) {
excludeJobIds.addAll(param.getExcludeJobIds());
}
List<Long> excludeCompanyIds = dislikes.stream().map(UserJobDislike::getCompanyId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
// 4. 提取不感兴趣的地区(直接使用,不扩展子级)
List<String> excludeRegionCodes = dislikes.stream().map(UserJobDislike::getRegionCode).filter(Objects::nonNull).distinct().collect(Collectors.toList());
// 5. 提取不感兴趣的行业(直接使用,不扩展子级)
List<Long> excludeIndustryIds = dislikes.stream().map(UserJobDislike::getIndustryId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
// 6. 执行统计查询
Long total = jobMapper.countJobPage(param.getJobIds(), param.getStatusFilter(), param.getKeyword(), expandedRegionCodes, expandedCategoryIds, expandedIndustryIds, param.getEmploymentType(), excludeJobIds, excludeCompanyIds, excludeRegionCodes, excludeIndustryIds, param.getRecruitCategory(), param.getCompanyTypes(), param.getCompanyId(), param.getEducations(), param.getPublishStartTime(), param.getPublishEndTime());
return total == null ? 0L : total;
}
/**
* 热门行业查询(二级行业)
* <p>岗位关联公司、公司关联行业(bg_industry level=2),按上架岗位数量倒序取前 limit 个</p>
* <p>只返回行业ID和名称,岗位数量仅用于排序不对外暴露</p>
*/
@Cacheable(cacheNames = CacheConfig.HOT_INDUSTRY)
public List<HotIndustryVo> listHotIndustries(Integer limit) {
return jobMapper.selectHotIndustries(limit);
}
/**
* 热门岗位类型查询(二级岗位类型)
* <p>bg_job.category_id 存三级叶子,SQL 内自关联上溯到二级后按二级分组,按上架岗位数量倒序取前 limit 个</p>
* <p>只返回岗位类型ID和名称,岗位数量仅用于排序不对外暴露</p>
*/
@Cacheable(cacheNames = CacheConfig.HOT_JOB_CATEGORY)
public List<HotJobCategoryVo> listHotJobCategories(Integer limit) {
return jobMapper.selectHotJobCategories(limit);
}
/**
* 热门城市查询
* <p>岗位关联公司、公司关联地区,按上架岗位数量倒序取前 limit 个</p>
* <p>只返回地区编码和名称,岗位数量仅用于排序不对外暴露</p>
*/
@Cacheable(cacheNames = CacheConfig.HOT_CITY)
public List<HotCityVo> listHotCities(Integer limit) {
return jobMapper.selectHotCities(limit);
}
/** /**
* 扩展地区编码(包含自身和所有子级) * 扩展地区编码(包含自身和所有子级)
* <p>一次查询:code本身 + provinceCode匹配 + cityCode匹配</p> * <p>一次查询:code本身 + provinceCode匹配 + cityCode匹配</p>
@@ -22,6 +22,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@@ -33,7 +34,7 @@ import java.util.stream.Collectors;
/** /**
* 登录服务 * 登录服务
* <p>依赖:SmsService(验证码发送与校验)、UserRegisterService(自动注册)</p> * <p>依赖:SmsService(验证码发送与校验)、UserRegisterService(自动注册/超管注册</p>
* <p>使用表:bg_user(查询/创建用户)</p> * <p>使用表:bg_user(查询/创建用户)</p>
* <p>使用Redislogin:token:{userId}(登录令牌与多设备管理)</p> * <p>使用Redislogin:token:{userId}(登录令牌与多设备管理)</p>
* *
@@ -52,6 +53,9 @@ public class LoginService {
@Value("${app.login.device_online_quantity:5}") @Value("${app.login.device_online_quantity:5}")
private int deviceOnlineQuantity; private int deviceOnlineQuantity;
@Value("${app.super_admin.password:}")
private String superAdminPassword;
@Autowired @Autowired
private SmsService smsService; private SmsService smsService;
@@ -106,6 +110,65 @@ public class LoginService {
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号已被禁用"); throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号已被禁用");
} }
// 清除验证码
smsService.clear(mobileNumber, SmsTemplateEnum.UNIVERSAL);
return doLogin(user, request, response);
}
/**
* 超管密码登录
* <p>1. 校验超管密码 2. 查询用户,不存在则走超管注册 3. 检查用户状态 4. 完成登录(签发令牌、多设备管理、写Cookie</p>
*
* @param mobileNumber 手机号
* @param password 超管密码,取 app.super_admin.password,未配置时禁用该登录方式
*/
@Transactional(rollbackFor = Exception.class)
public LoginVo superAdminLogin(String mobileNumber, String password, HttpServletRequest request, HttpServletResponse response) {
Assert.hasText(mobileNumber, "手机号不能为空");
Assert.hasText(password, "超管密码不能为空");
// 未配置超管密码时该登录方式不可用
if (!StringUtils.hasText(superAdminPassword)) {
log.warn("超管登录被拒绝:未配置超管密码 mobileNumber:{}", mobileNumber);
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "超管密码错误");
}
// 校验超管密码
if (!superAdminPassword.equals(password)) {
log.warn("超管登录密码错误 mobileNumber:{} ip:{}", mobileNumber, HttpIpTool.gteRealIP(request));
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "超管密码错误");
}
// 查询用户
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getMobileNumber, mobileNumber));
// 用户不存在则走超管注册
if (user == null) {
user = userRegisterService.supperMasterRegister(mobileNumber);
}
// 检查用户状态
if (user.getStatus() != null && user.getStatus() == 1) {
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "账号已被禁用");
}
log.info("超管登录成功 userId:{} mobileNumber:{} ip:{}", user.getId(), mobileNumber, HttpIpTool.gteRealIP(request));
return doLogin(user, request, response);
}
/**
* 完成登录:签发令牌、维护多设备在线、写入Cookie
* <p>1. 生成uuId并签发JWT 2. 读取或初始化Redis登录信息 3. 过滤过期设备并按上限剔除最早设备 4. 追加当前设备后回写Redis 5. 设置Token Cookie</p>
*
* @param user 已通过身份校验的用户
* @param request 用于获取真实登录IP
* @param response 用于写入Token Cookie
* @return 登录结果
*/
public LoginVo doLogin(User user, HttpServletRequest request, HttpServletResponse response) {
// 生成JWT // 生成JWT
String uuId = UUID.randomUUID().toString().replace("-", ""); String uuId = UUID.randomUUID().toString().replace("-", "");
Algorithm algorithm = Algorithm.HMAC256(secret); Algorithm algorithm = Algorithm.HMAC256(secret);
@@ -150,8 +213,6 @@ public class LoginService {
info.setLoginDevices(devices); info.setLoginDevices(devices);
redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS); redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS);
// 清除验证码
smsService.clear(mobileNumber, SmsTemplateEnum.UNIVERSAL);
// 设置Cookie // 设置Cookie
Cookie cookie = new Cookie("Token", token); Cookie cookie = new Cookie("Token", token);
@@ -23,7 +23,8 @@ import java.util.stream.IntStream;
* <p>依赖:无</p> * <p>依赖:无</p>
* <p>使用表:bg_user_profile(主表CRUD)、bg_user_profile_education(教育经历)、 * <p>使用表:bg_user_profile(主表CRUD)、bg_user_profile_education(教育经历)、
* bg_user_profile_work(工作经历)、bg_user_profile_internship(实习经历)、 * 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 * @author zk
*/ */
@@ -49,6 +50,9 @@ public class UserProfileService {
@Autowired @Autowired
private UserProfileCompetitionMapper competitionMapper; private UserProfileCompetitionMapper competitionMapper;
@Autowired
private UserProfileOrganizationMapper organizationMapper;
@Autowired @Autowired
private org.jiayunet.service.UserProfileAnalyzeService userProfileAnalyzeService; private org.jiayunet.service.UserProfileAnalyzeService userProfileAnalyzeService;
@@ -70,6 +74,9 @@ public class UserProfileService {
@Autowired @Autowired
private UserResumeCompetitionMapper resumeCompetitionMapper; private UserResumeCompetitionMapper resumeCompetitionMapper;
@Autowired
private UserResumeOrganizationMapper resumeOrganizationMapper;
/** 学历文本→枚举映射 */ /** 学历文本→枚举映射 */
private static final Map<String, Integer> DEGREE_MAP = Map.of( private static final Map<String, Integer> DEGREE_MAP = Map.of(
"大专", 1, "本科", 2, "硕士", 3, "博士", 4 "大专", 1, "本科", 2, "硕士", 3, "博士", 4
@@ -126,6 +133,8 @@ public class UserProfileService {
.set(UserProfile::getPortfolioUrl, profile.getPortfolioUrl()) .set(UserProfile::getPortfolioUrl, profile.getPortfolioUrl())
.set(UserProfile::getSkills, profile.getSkills(), JSON_TYPE_HANDLER) .set(UserProfile::getSkills, profile.getSkills(), JSON_TYPE_HANDLER)
.set(UserProfile::getCertificates, profile.getCertificates(), 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)); .set(UserProfile::getUpdateTime, now));
} else { } else {
profile.setUserId(userId); profile.setUserId(userId);
@@ -320,11 +329,48 @@ public class UserProfileService {
userProfileAnalyzeService.analyzeUserProfile(userId); 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同步更新个人资料 * 根据简历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) @Transactional(rollbackFor = Exception.class)
public void syncProfileFromResume(Long resumeId) { public void syncProfileFromResume(Long resumeId) {
@@ -336,12 +382,13 @@ public class UserProfileService {
throw new BusinessException(BusinessExpCodeEnum.PERMISSION_DENIED, "简历不存在或无权操作"); 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<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<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<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<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<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(); Instant now = Instant.now();
@@ -362,6 +409,8 @@ public class UserProfileService {
.set(UserProfile::getPortfolioUrl, resume.getPortfolioUrl()) .set(UserProfile::getPortfolioUrl, resume.getPortfolioUrl())
.set(UserProfile::getSkills, resume.getSkills(), JSON_TYPE_HANDLER) .set(UserProfile::getSkills, resume.getSkills(), JSON_TYPE_HANDLER)
.set(UserProfile::getCertificates, resume.getCertificates(), 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)); .set(UserProfile::getUpdateTime, now));
} else { } else {
UserProfile profile = new UserProfile(); UserProfile profile = new UserProfile();
@@ -373,6 +422,8 @@ public class UserProfileService {
profile.setPortfolioUrl(resume.getPortfolioUrl()); profile.setPortfolioUrl(resume.getPortfolioUrl());
profile.setSkills(resume.getSkills()); profile.setSkills(resume.getSkills());
profile.setCertificates(resume.getCertificates()); profile.setCertificates(resume.getCertificates());
profile.setHobbies(resume.getHobbies());
profile.setLanguageSkills(resume.getLanguageSkills());
profile.setCreateTime(now); profile.setCreateTime(now);
profile.setUpdateTime(now); profile.setUpdateTime(now);
userProfileMapper.insert(profile); userProfileMapper.insert(profile);
@@ -486,7 +537,28 @@ public class UserProfileService {
competitionMapper.batchInsert(profileCompetitionList); 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); userProfileAnalyzeService.analyzeUserProfileSync(userId);
} }
@@ -494,9 +566,9 @@ public class UserProfileService {
/** /**
* 根据当前用户的个人资料创建一份新简历 * 根据当前用户的个人资料创建一份新简历
* <p>syncProfileFromResume 的反向操作:读取个人资料主表+5子表,新建一份简历(名"个人资料简历")及对应子表,返回新简历ID</p> * <p>syncProfileFromResume 的反向操作:读取个人资料主表+6子表,新建一份简历(名"个人资料简历")及对应子表,返回新简历ID</p>
* <p>1. 校验个人资料存在 2. 读取个人资料5子表 3. 新建简历主表(复制共有字段,简历独有字段targetPosition/avatarUrl/city/summary留空) * <p>1. 校验个人资料存在 2. 读取个人资料6子表 3. 新建简历主表(复制共有字段,简历独有字段targetPosition/avatarUrl/city/summary留空)
* 4. 转换并批量插入5张简历子表(含degree/studyType反向映射) 5. 返回新简历ID</p> * 4. 转换并批量插入6张简历子表(含degree/studyType反向映射) 5. 返回新简历ID</p>
* <p>注意:这是创建新简历(非覆盖),不删除已有简历;简历侧无AI分析,故不触发分析</p> * <p>注意:这是创建新简历(非覆盖),不删除已有简历;简历侧无AI分析,故不触发分析</p>
*/ */
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@@ -511,12 +583,13 @@ public class UserProfileService {
} }
Long profileId = profile.getId(); Long profileId = profile.getId();
// 2. 读取个人资料5张子表 // 2. 读取个人资料6张子表
List<UserProfileEducation> profileEducationList = educationMapper.selectList(new LambdaQueryWrapper<UserProfileEducation>().eq(UserProfileEducation::getProfileId, profileId).orderByAsc(UserProfileEducation::getSortOrder)); 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<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<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<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<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(); Instant now = Instant.now();
@@ -531,6 +604,8 @@ public class UserProfileService {
resume.setPortfolioUrl(profile.getPortfolioUrl()); resume.setPortfolioUrl(profile.getPortfolioUrl());
resume.setSkills(profile.getSkills()); resume.setSkills(profile.getSkills());
resume.setCertificates(profile.getCertificates()); resume.setCertificates(profile.getCertificates());
resume.setHobbies(profile.getHobbies());
resume.setLanguageSkills(profile.getLanguageSkills());
// 无简历时新建的这份自动设为默认,与 UserResumeService.saveResume 逻辑一致 // 无简历时新建的这份自动设为默认,与 UserResumeService.saveResume 逻辑一致
boolean hasResume = userResumeMapper.selectCount(new LambdaQueryWrapper<UserResume>().eq(UserResume::getUserId, userId)) > 0; boolean hasResume = userResumeMapper.selectCount(new LambdaQueryWrapper<UserResume>().eq(UserResume::getUserId, userId)) > 0;
resume.setIsDefault(hasResume ? 0 : 1); resume.setIsDefault(hasResume ? 0 : 1);
@@ -642,7 +717,27 @@ public class UserProfileService {
resumeCompetitionMapper.batchInsert(resumeCompetitionList); 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; return resumeId;
} }
@@ -67,6 +67,37 @@ public class UserRegisterService {
@Value("${app.reward.invite-milestone-days:150}") @Value("${app.reward.invite-milestone-days:150}")
private int inviteMilestoneDays; private int inviteMilestoneDays;
@Value("${app.super_admin.register_gift_days:365}")
private int superAdminRegisterGiftDays;
/**
* 超级管理注册新用户
* <p>1. 创建用户并生成邀请码 2. 发放会员,天数取 app.super_admin.register_gift_days</p>
*
* @param mobileNumber 手机号
* @return 新注册的用户
*/
@Transactional(rollbackFor = Exception.class)
public User supperMasterRegister(String mobileNumber) {
// 创建用户
User user = new User();
user.setMobileNumber(mobileNumber);
user.setNick("用户" + mobileNumber.substring(mobileNumber.length() - 4));
user.setInviteCode(generateInviteCode());
user.setStatus(0);
user.setIsDelete(0L);
user.setCreateTime(Instant.now());
userMapper.insert(user);
// 发放新用户注册会员权益
memberGrantService.grant(user.getId(), superAdminRegisterGiftDays, 1); // 1=正式
messageService.sendToUser(1, user.getId(), "欢迎加入OfferPie", "注册成功,已赠送您" + superAdminRegisterGiftDays + "天会员权益,快来体验AI求职助手吧!", null, null);
return user;
}
/** /**
* 注册新用户 * 注册新用户
* <p>1. 创建用户并生成邀请码 2. 若携带邀请码则绑定邀请关系</p> * <p>1. 创建用户并生成邀请码 2. 若携带邀请码则绑定邀请关系</p>
@@ -23,7 +23,8 @@ import java.util.stream.IntStream;
* <p>依赖:无</p> * <p>依赖:无</p>
* <p>使用表:bg_user_resume(简历主表CRUD)、bg_user_resume_education(教育经历)、 * <p>使用表:bg_user_resume(简历主表CRUD)、bg_user_resume_education(教育经历)、
* bg_user_resume_work(工作经历)、bg_user_resume_internship(实习经历)、 * 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 * @author zk
*/ */
@@ -58,6 +59,9 @@ public class UserResumeService {
@Autowired @Autowired
private UserResumeCompetitionMapper competitionMapper; private UserResumeCompetitionMapper competitionMapper;
@Autowired
private UserResumeOrganizationMapper organizationMapper;
// ==================== 简历列表 ==================== // ==================== 简历列表 ====================
/** /**
@@ -135,6 +139,8 @@ public class UserResumeService {
.set(UserResume::getSkills, resume.getSkills(), JSON_TYPE_HANDLER) .set(UserResume::getSkills, resume.getSkills(), JSON_TYPE_HANDLER)
.set(UserResume::getCertificates, resume.getCertificates(), JSON_TYPE_HANDLER) .set(UserResume::getCertificates, resume.getCertificates(), JSON_TYPE_HANDLER)
.set(UserResume::getSummary, resume.getSummary()) .set(UserResume::getSummary, resume.getSummary())
.set(UserResume::getHobbies, resume.getHobbies())
.set(UserResume::getLanguageSkills, resume.getLanguageSkills())
.set(UserResume::getUpdateTime, now)); .set(UserResume::getUpdateTime, now));
return resumeId; 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) @Transactional(rollbackFor = Exception.class)
public void deleteResume(Long resumeId) { public void deleteResume(Long resumeId) {
@@ -168,6 +174,7 @@ public class UserResumeService {
internshipMapper.delete(new LambdaQueryWrapper<UserResumeInternship>().eq(UserResumeInternship::getResumeId, resumeId)); internshipMapper.delete(new LambdaQueryWrapper<UserResumeInternship>().eq(UserResumeInternship::getResumeId, resumeId));
projectMapper.delete(new LambdaQueryWrapper<UserResumeProject>().eq(UserResumeProject::getResumeId, resumeId)); projectMapper.delete(new LambdaQueryWrapper<UserResumeProject>().eq(UserResumeProject::getResumeId, resumeId));
competitionMapper.delete(new LambdaQueryWrapper<UserResumeCompetition>().eq(UserResumeCompetition::getResumeId, resumeId)); competitionMapper.delete(new LambdaQueryWrapper<UserResumeCompetition>().eq(UserResumeCompetition::getResumeId, resumeId));
organizationMapper.delete(new LambdaQueryWrapper<UserResumeOrganization>().eq(UserResumeOrganization::getResumeId, resumeId));
// 删除主表 // 删除主表
userResumeMapper.deleteById(resumeId); userResumeMapper.deleteById(resumeId);
} }
@@ -342,6 +349,40 @@ public class UserResumeService {
return resumeId; 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(); 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()); 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()); 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());
}
// ==================== 内部方法 ==================== // ==================== 内部方法 ====================
/** /**
@@ -57,6 +57,13 @@ app:
#设备在线数量 #设备在线数量
device_online_quantity: 3 device_online_quantity: 3
# 超级管理配置
super_admin:
# 超级管理密码(可绕过验证码直接登录,务必通过环境变量注入)
password: ${SUPER_ADMIN_PASSWORD:H7KQ2XR9LP}
# 超级管理注册赠送会员天数
register_gift_days: ${SUPER_ADMIN_REGISTER_GIFT_DAYS:365}
# 短信 # 短信
sms: sms:
service_provider: aliyun service_provider: aliyun
@@ -57,6 +57,13 @@ app:
#设备在线数量 #设备在线数量
device_online_quantity: 3 device_online_quantity: 3
# 超级管理配置
super_admin:
# 超级管理密码(可绕过验证码直接登录,务必通过环境变量注入)
password: ${SUPER_ADMIN_PASSWORD:H7KQ2XR9LP}
# 超级管理注册赠送会员天数
register_gift_days: ${SUPER_ADMIN_REGISTER_GIFT_DAYS:365}
# 短信 # 短信
sms: sms:
service_provider: aliyun service_provider: aliyun
@@ -58,6 +58,13 @@ app:
#设备在线数量 #设备在线数量
device_online_quantity: 3 device_online_quantity: 3
# 超级管理配置
super_admin:
# 超级管理密码(可绕过验证码直接登录,务必通过环境变量注入)
password: ${SUPER_ADMIN_PASSWORD:H7KQ2XR9LP}
# 超级管理注册赠送会员天数
register_gift_days: ${SUPER_ADMIN_REGISTER_GIFT_DAYS:365}
# 短信 # 短信
sms: sms:
service_provider: aliyun service_provider: aliyun
+9
View File
@@ -45,6 +45,15 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId> <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> </dependency>
<!-- Spring Cache 抽象 + Caffeine 本地缓存,用于热门榜单等全局只读数据 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.redisson</groupId> <groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId> <artifactId>redisson-spring-boot-starter</artifactId>
@@ -0,0 +1,79 @@
package org.jiayunet.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* 本地缓存配置
* <p>基于 Caffeine 的 Spring Cache 实现,用于热门榜单等全局只读、变化慢、允许短时陈旧的数据</p>
* <p>每个缓存独立注册、独立过期时间,调整某个缓存的时长直接改注册时传入的分钟数,互不影响</p>
*
* @author zk
*/
@Configuration
@EnableCaching
public class CacheConfig {
/** 缓存名:热门行业 */
public static final String HOT_INDUSTRY = "hotIndustry";
/** 缓存名:热门岗位类型 */
public static final String HOT_JOB_CATEGORY = "hotJobCategory";
/** 缓存名:热门城市 */
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>
* <p>另设一份兜底策略:业务里如果用了未登记的缓存名,动态创建时同样带过期时间,不会永不过期</p>
*/
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 兜底策略(动态创建未登记缓存时生效)
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.maximumSize(MAX_SIZE));
// 各缓存独立注册,独立过期时间
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 缓存实例
*
* @param expireMinutes 写入后过期时长(分钟)
* @param maximumSize 最大条目数
*/
private Cache<Object, Object> buildCache(long expireMinutes, long maximumSize) {
return Caffeine.newBuilder()
.expireAfterWrite(expireMinutes, TimeUnit.MINUTES)
.maximumSize(maximumSize)
.build();
}
}
@@ -4,8 +4,12 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.jiayunet.pojo.po.Job; import org.jiayunet.pojo.po.Job;
import org.jiayunet.pojo.vo.HotCityVo;
import org.jiayunet.pojo.vo.HotIndustryVo;
import org.jiayunet.pojo.vo.HotJobCategoryVo;
import org.jiayunet.pojo.vo.JobListItemVo; import org.jiayunet.pojo.vo.JobListItemVo;
import java.time.Instant;
import java.util.List; import java.util.List;
/** /**
@@ -19,5 +23,29 @@ public interface JobMapper extends CommonMapper<Job> {
/** /**
* 分页查询岗位列表 * 分页查询岗位列表
*/ */
Page<JobListItemVo> selectJobPage(Page<JobListItemVo> page, @Param("jobIds") List<Long> jobIds, @Param("statusFilter") List<Integer> statusFilter, @Param("keyword") String keyword, @Param("regionCodes") List<String> regionCodes, @Param("categoryIds") List<Long> categoryIds, @Param("industryIds") List<Long> industryIds, @Param("employmentType") Integer employmentType, @Param("excludeJobIds") List<Long> excludeJobIds, @Param("excludeCompanyIds") List<Long> excludeCompanyIds, @Param("excludeRegionCodes") List<String> excludeRegionCodes, @Param("excludeIndustryIds") List<Long> excludeIndustryIds, @Param("recruitCategory") Integer recruitCategory, @Param("companyTypes") List<String> companyTypes, @Param("companyId") Long companyId); Page<JobListItemVo> selectJobPage(Page<JobListItemVo> page, @Param("jobIds") List<Long> jobIds, @Param("statusFilter") List<Integer> statusFilter, @Param("keyword") String keyword, @Param("regionCodes") List<String> regionCodes, @Param("categoryIds") List<Long> categoryIds, @Param("industryIds") List<Long> industryIds, @Param("employmentType") Integer employmentType, @Param("excludeJobIds") List<Long> excludeJobIds, @Param("excludeCompanyIds") List<Long> excludeCompanyIds, @Param("excludeRegionCodes") List<String> excludeRegionCodes, @Param("excludeIndustryIds") List<Long> excludeIndustryIds, @Param("recruitCategory") Integer recruitCategory, @Param("companyTypes") List<String> companyTypes, @Param("companyId") Long companyId, @Param("educations") List<Integer> educations, @Param("publishStartTime") Instant publishStartTime, @Param("publishEndTime") Instant publishEndTime);
/**
* 统计岗位列表总数
* <p>过滤条件与 selectJobPage 共用同一段 SQLjobPageWhere),保证列表与总数口径一致</p>
*/
Long countJobPage(@Param("jobIds") List<Long> jobIds, @Param("statusFilter") List<Integer> statusFilter, @Param("keyword") String keyword, @Param("regionCodes") List<String> regionCodes, @Param("categoryIds") List<Long> categoryIds, @Param("industryIds") List<Long> industryIds, @Param("employmentType") Integer employmentType, @Param("excludeJobIds") List<Long> excludeJobIds, @Param("excludeCompanyIds") List<Long> excludeCompanyIds, @Param("excludeRegionCodes") List<String> excludeRegionCodes, @Param("excludeIndustryIds") List<Long> excludeIndustryIds, @Param("recruitCategory") Integer recruitCategory, @Param("companyTypes") List<String> companyTypes, @Param("companyId") Long companyId, @Param("educations") List<Integer> educations, @Param("publishStartTime") Instant publishStartTime, @Param("publishEndTime") Instant publishEndTime);
/**
* 查询热门行业(二级行业)
* <p>岗位关联公司、公司关联行业,按上架岗位数量倒序取前 limit 个</p>
*/
List<HotIndustryVo> selectHotIndustries(@Param("limit") Integer limit);
/**
* 查询热门岗位类型(二级岗位类型)
* <p>岗位类型表自关联,从三级叶子上溯到二级后分组,按上架岗位数量倒序取前 limit 个</p>
*/
List<HotJobCategoryVo> selectHotJobCategories(@Param("limit") Integer limit);
/**
* 查询热门城市
* <p>岗位关联公司、公司关联地区,按上架岗位数量倒序取前 limit 个</p>
*/
List<HotCityVo> selectHotCities(@Param("limit") Integer limit);
} }
@@ -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) @TableField(typeHandler = JacksonTypeHandler.class)
private List<String> certificates; private List<String> certificates;
/** 兴趣爱好 */
private String hobbies;
/** 语言能力 */
private String languageSkills;
/** 创建时间 */ /** 创建时间 */
private Instant createTime; 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_organizationbg_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 summary;
/** 兴趣爱好 */
private String hobbies;
/** 语言能力 */
private String languageSkills;
/** 简历创建时间 */ /** 简历创建时间 */
private Instant createTime; 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;
}
@@ -0,0 +1,18 @@
package org.jiayunet.pojo.vo;
import lombok.Data;
/**
* 热门城市VO
*
* @author zk
*/
@Data
public class HotCityVo {
/** 地区编码 */
private String code;
/** 地区名称 */
private String name;
}
@@ -0,0 +1,18 @@
package org.jiayunet.pojo.vo;
import lombok.Data;
/**
* 热门行业VO(二级行业)
*
* @author zk
*/
@Data
public class HotIndustryVo {
/** 行业ID */
private Long id;
/** 行业名称 */
private String name;
}
@@ -0,0 +1,18 @@
package org.jiayunet.pojo.vo;
import lombok.Data;
/**
* 热门岗位类型VO(二级岗位类型)
*
* @author zk
*/
@Data
public class HotJobCategoryVo {
/** 岗位类型ID */
private Long id;
/** 岗位类型名称 */
private String name;
}
@@ -33,6 +33,9 @@ public class RecruitAnnouncementListItemVo {
/** 投递结束时间 */ /** 投递结束时间 */
private Instant applyEndTime; private Instant applyEndTime;
/** 发布时间 */
private Instant publishTime;
/** 投递截止描述 */ /** 投递截止描述 */
private String applyEndDesc; private String applyEndDesc;
} }
@@ -23,7 +23,7 @@ import java.util.stream.StreamSupport;
* 用户简历分析服务 * 用户简历分析服务
* <p>主要功能:用户保存简历后异步分析,提取学校等级、经历质量、专业、技能等维度数据</p> * <p>主要功能:用户保存简历后异步分析,提取学校等级、经历质量、专业、技能等维度数据</p>
* <p>依赖:AiChatAbilityAI调用)、DictCacheService(专业分类缓存)</p> * <p>依赖:AiChatAbilityAI调用)、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> * bg_skill_tag(技能入库)、bg_user_profile_skill_tag_relation(关联表)</p>
* *
* @author zk * @author zk
@@ -56,6 +56,9 @@ public class UserProfileAnalyzeService {
@Autowired @Autowired
private UserProfileCompetitionMapper competitionMapper; private UserProfileCompetitionMapper competitionMapper;
@Autowired
private UserProfileOrganizationMapper organizationMapper;
@Autowired @Autowired
private SkillTagMapper skillTagMapper; private SkillTagMapper skillTagMapper;
@@ -94,15 +97,16 @@ public class UserProfileAnalyzeService {
List<UserProfileInternship> internshipList = internshipMapper.selectList(new LambdaQueryWrapper<UserProfileInternship>().eq(UserProfileInternship::getUserId, userId)); 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<UserProfileProject> projectList = projectMapper.selectList(new LambdaQueryWrapper<UserProfileProject>().eq(UserProfileProject::getUserId, userId));
List<UserProfileCompetition> competitionList = competitionMapper.selectList(new LambdaQueryWrapper<UserProfileCompetition>().eq(UserProfileCompetition::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. 数据有效性检查 // 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); log.info("用户所有子表为空,清空技能标签, userId={}", userId);
clearSkillRelations(userId); clearSkillRelations(userId);
return; return;
} }
String profileJson = buildProfileJson(profile, educationList, workList, internshipList, projectList, competitionList); String profileJson = buildProfileJson(profile, educationList, workList, internshipList, projectList, competitionList, organizationList);
// 3. 第一次AI:简历综合分析(失败不影响后续) // 3. 第一次AI:简历综合分析(失败不影响后续)
try { try {
@@ -445,16 +449,20 @@ public class UserProfileAnalyzeService {
/** 构建用户简历JSON */ /** 构建用户简历JSON */
private String buildProfileJson(UserProfile profile, List<UserProfileEducation> educationList, private String buildProfileJson(UserProfile profile, List<UserProfileEducation> educationList,
List<UserProfileWork> workList, List<UserProfileInternship> internshipList, 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<>(); Map<String, Object> data = new HashMap<>();
data.put("name", profile.getName()); data.put("name", profile.getName());
data.put("skills", profile.getSkills()); data.put("skills", profile.getSkills());
data.put("certificates", profile.getCertificates()); data.put("certificates", profile.getCertificates());
data.put("hobbies", profile.getHobbies());
data.put("languageSkills", profile.getLanguageSkills());
data.put("education", educationList); data.put("education", educationList);
data.put("work", workList); data.put("work", workList);
data.put("internship", internshipList); data.put("internship", internshipList);
data.put("project", projectList); data.put("project", projectList);
data.put("competition", competitionList); data.put("competition", competitionList);
data.put("organization", organizationList);
try { try {
return HttpTool.objectMapper.writeValueAsString(data); return HttpTool.objectMapper.writeValueAsString(data);
} catch (Exception e) { } catch (Exception e) {
+125 -32
View File
@@ -30,37 +30,11 @@
<result column="company_industry_id" property="companyIndustryId"/> <result column="company_industry_id" property="companyIndustryId"/>
</resultMap> </resultMap>
<select id="selectJobPage" resultMap="JobListItemVoMap"> <!--
SELECT 岗位查询过滤条件(selectJobPage 与 countJobPage 共用)
j.id, 新增/修改筛选条件只需改这里,保证列表与总数口径一致
j.title, -->
j.salary, <sql id="jobPageWhere">
j.education,
j.tags,
j.source_url,
j.required_industry_id,
j.min_experience,
j.required_major_ids,
j.major_sensitivity,
j.status,
j.expire_at,
j.pyname,
j.recruit_category,
c.id AS company_id,
c.name AS company_name,
c.short_name AS company_short_name,
c.company_type,
c.logo_url AS company_logo_url,
c.tags AS company_tags,
c.industry_id AS company_industry_id,
c.region_code,
r.name AS region_name,
cat.id AS category_id,
cat.name AS category_name
FROM bg_job j force index (`PRIMARY`)
INNER JOIN bg_company c ON j.company_id = c.id
INNER JOIN bg_china_regions_code r ON c.region_code = r.code
INNER JOIN bg_job_category cat ON j.category_id = cat.id
WHERE c.status = 1 WHERE c.status = 1
<!-- 岗位状态过滤 --> <!-- 岗位状态过滤 -->
<if test="statusFilter != null and statusFilter.size() > 0"> <if test="statusFilter != null and statusFilter.size() > 0">
@@ -153,7 +127,126 @@
<if test="companyId != null"> <if test="companyId != null">
AND j.company_id = #{companyId} AND j.company_id = #{companyId}
</if> </if>
ORDER BY j.id DESC <!-- 筛选条件:学历要求(可多选) -->
<if test="educations != null and educations.size() > 0">
AND j.education IN
<foreach collection="educations" item="education" open="(" separator="," close=")">
#{education}
</foreach>
</if>
<!-- 筛选条件:发布开始时间 -->
<if test="publishStartTime != null">
AND j.expire_at &gt;= #{publishStartTime}
</if>
<!-- 筛选条件:发布截止时间 -->
<if test="publishEndTime != null">
AND j.expire_at &lt;= #{publishEndTime}
</if>
</sql>
<select id="selectJobPage" resultMap="JobListItemVoMap">
SELECT
j.id,
j.title,
j.salary,
j.education,
j.tags,
j.source_url,
j.required_industry_id,
j.min_experience,
j.required_major_ids,
j.major_sensitivity,
j.status,
j.expire_at,
j.pyname,
j.recruit_category,
c.id AS company_id,
c.name AS company_name,
c.short_name AS company_short_name,
c.company_type,
c.logo_url AS company_logo_url,
c.tags AS company_tags,
c.industry_id AS company_industry_id,
c.region_code,
r.name AS region_name,
cat.id AS category_id,
cat.name AS category_name
FROM bg_job j force index (`PRIMARY`)
INNER JOIN bg_company c ON j.company_id = c.id
INNER JOIN bg_china_regions_code r ON c.region_code = r.code
INNER JOIN bg_job_category cat ON j.category_id = cat.id
<include refid="jobPageWhere"/>
ORDER BY j.expire_at DESC
</select>
<select id="countJobPage" resultType="java.lang.Long">
SELECT COUNT(*)
FROM bg_job j
INNER JOIN bg_company c ON j.company_id = c.id
INNER JOIN bg_china_regions_code r ON c.region_code = r.code
INNER JOIN bg_job_category cat ON j.category_id = cat.id
<include refid="jobPageWhere"/>
</select>
<!--
热门行业查询(二级行业)
岗位 → 公司 → 公司所属行业,按上架岗位数量倒序取前 limit 个
-->
<select id="selectHotIndustries" resultType="org.jiayunet.pojo.vo.HotIndustryVo">
SELECT
i.id,
i.name
FROM bg_job j
INNER JOIN bg_company c ON j.company_id = c.id
INNER JOIN bg_industry i ON c.industry_id = i.id
WHERE j.status = 0
AND c.status = 1
AND i.level = 2
GROUP BY i.id, i.name
ORDER BY COUNT(j.id) DESC
<if test="limit != null">
LIMIT #{limit}
</if>
</select>
<!--
热门岗位类型查询(二级岗位类型)
bg_job.category_id 存三级叶子,自关联上溯到二级后按二级分组,按上架岗位数量倒序取前 limit 个
-->
<select id="selectHotJobCategories" resultType="org.jiayunet.pojo.vo.HotJobCategoryVo">
SELECT
cat2.id,
cat2.name
FROM bg_job j
INNER JOIN bg_job_category cat3 ON j.category_id = cat3.id
INNER JOIN bg_job_category cat2 ON cat3.parent_id = cat2.id
WHERE j.status = 0
AND cat2.level = 2
GROUP BY cat2.id, cat2.name
ORDER BY COUNT(j.id) DESC
<if test="limit != null">
LIMIT #{limit}
</if>
</select>
<!--
热门城市查询
岗位 → 公司 → 公司所在地区,按上架岗位数量倒序取前 limit 个
-->
<select id="selectHotCities" resultType="org.jiayunet.pojo.vo.HotCityVo">
SELECT
r.code,
r.name
FROM bg_job j
INNER JOIN bg_company c ON j.company_id = c.id
INNER JOIN bg_china_regions_code r ON c.region_code = r.code
WHERE j.status = 0
AND c.status = 1
GROUP BY r.code, r.name
ORDER BY COUNT(j.id) DESC
<if test="limit != null">
LIMIT #{limit}
</if>
</select> </select>
</mapper> </mapper>
@@ -46,6 +46,7 @@
ra.title, ra.title,
ra.major_require, ra.major_require,
ra.apply_end_time, ra.apply_end_time,
ra.publish_time,
ra.apply_end_desc ra.apply_end_desc
FROM bg_recruit_announcement ra FROM bg_recruit_announcement ra
INNER JOIN bg_company c ON ra.company_id = c.id INNER JOIN bg_company c ON ra.company_id = c.id