package cn.iocoder.yudao.module.hrm.service.employee; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEducationSaveReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEmergencyContactSaveReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportExcelVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportRespVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeePageReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeRespVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSaveReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSimpleRespVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeStatisticsRespVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeWorkHistorySaveReqVO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeContractDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeEducationDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeEmergencyContactDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeWorkHistoryDO; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeContractMapper; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeEducationMapper; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeEmergencyContactMapper; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeWorkHistoryMapper; import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.PostApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO; import cn.iocoder.yudao.module.system.api.permission.RoleApi; import cn.iocoder.yudao.module.system.api.permission.dto.RoleRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserCreateReqDTO; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserUpdateReqDTO; import com.mzt.logapi.starter.annotation.LogRecord; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDate; import java.time.YearMonth; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.hrm.enums.LogRecordConstants.*; /** * 员工信息 Service 实现类 * * @author 超级管理员 */ @Service @Validated public class HrmEmployeeServiceImpl implements HrmEmployeeService { /** 员工状态:离职 */ private static final Integer EMPLOYEE_STATUS_RESIGNED = 3; @Resource private HrmEmployeeMapper employeeMapper; @Resource private HrmEmployeeEducationMapper educationMapper; @Resource private HrmEmployeeContractMapper contractMapper; @Resource private HrmEmployeeWorkHistoryMapper workHistoryMapper; @Resource private HrmEmployeeEmergencyContactMapper emergencyContactMapper; @Resource private HrmNoRedisDAO noRedisDAO; @Resource private DeptApi deptApi; @Resource private PostApi postApi; @Resource private AdminUserApi adminUserApi; @Resource private RoleApi roleApi; @Resource @Lazy // 自注入需延迟加载,否则 bean 被代理包装后注入原始版本导致启动失败 private HrmEmployeeService self; @Override @Transactional(rollbackFor = Exception.class) public Long createEmployee(HrmEmployeeSaveReqVO createReqVO) { // 1. 校验部门 deptApi.validateDeptList(Collections.singleton(createReqVO.getDeptId())); // 2. 校验岗位 postApi.validPostList(createReqVO.getPostIds()); // 3. 校验手机号是否已存在 if (employeeMapper.selectByPhone(createReqVO.getPhone()) != null) { throw exception(EMPLOYEE_PHONE_EXISTS); } // 4. 同步创建系统用户 AdminUserCreateReqDTO userCreateReqDTO = new AdminUserCreateReqDTO(); userCreateReqDTO.setUsername(createReqVO.getPhone()); userCreateReqDTO.setNickname(createReqVO.getName()); userCreateReqDTO.setDeptId(createReqVO.getDeptId()); userCreateReqDTO.setPostIds(new LinkedHashSet<>(createReqVO.getPostIds())); userCreateReqDTO.setMobile(createReqVO.getPhone()); userCreateReqDTO.setPassword(createReqVO.getPhone()); userCreateReqDTO.setSex(createReqVO.getGender()); userCreateReqDTO.setEmail(createReqVO.getEmail()); userCreateReqDTO.setRemark(createReqVO.getRemark()); // 设置角色 if (CollUtil.isNotEmpty(createReqVO.getRoleIds())) { userCreateReqDTO.setRoleIds(createReqVO.getRoleIds().stream().collect(Collectors.toSet())); } Long userId = adminUserApi.createUser(userCreateReqDTO); // 5. 生成员工编号 String employeeNo = generateUniqueEmployeeNo(); // 6. 插入员工记录 HrmEmployeeDO employee = BeanUtils.toBean(createReqVO, HrmEmployeeDO.class); employee.setEmployeeNo(employeeNo); employee.setUserId(userId); // 关联系统用户ID // roleIds 存储为 JSON if (CollUtil.isNotEmpty(createReqVO.getRoleIds())) { employee.setRoleIds(JSONUtil.toJsonStr(createReqVO.getRoleIds())); } // postIds 存储为 JSON if (CollUtil.isNotEmpty(createReqVO.getPostIds())) { employee.setPostIds(JSONUtil.toJsonStr(createReqVO.getPostIds())); } employeeMapper.insert(employee); Long employeeId = employee.getId(); // 7. 同步添加教育经历 if (CollUtil.isNotEmpty(createReqVO.getEducations())) { createReqVO.getEducations().forEach(educationVO -> { HrmEmployeeEducationDO education = BeanUtils.toBean(educationVO, HrmEmployeeEducationDO.class); education.setEmployeeId(employeeId); educationMapper.insert(education); }); } // 8. 同步添加工作经历 if (CollUtil.isNotEmpty(createReqVO.getWorkHistories())) { createReqVO.getWorkHistories().forEach(workHistoryVO -> { HrmEmployeeWorkHistoryDO workHistory = BeanUtils.toBean(workHistoryVO, HrmEmployeeWorkHistoryDO.class); workHistory.setEmployeeId(employeeId); workHistoryMapper.insert(workHistory); }); } // 9. 同步添加紧急联系人 if (CollUtil.isNotEmpty(createReqVO.getEmergencyContacts())) { createReqVO.getEmergencyContacts().forEach(emergencyContactVO -> { HrmEmployeeEmergencyContactDO emergencyContact = BeanUtils.toBean(emergencyContactVO, HrmEmployeeEmergencyContactDO.class); emergencyContact.setEmployeeId(employeeId); emergencyContactMapper.insert(emergencyContact); }); } return employeeId; } private String generateUniqueEmployeeNo() { String employeeNo = noRedisDAO.generateEmployeeNo(); if (employeeMapper.selectByEmployeeNo(employeeNo) != null) { return generateUniqueEmployeeNo(); } return employeeNo; } @Override @Transactional(rollbackFor = Exception.class) public void updateEmployee(HrmEmployeeSaveReqVO updateReqVO) { // 校验存在 HrmEmployeeDO employee = validateEmployeeExists(updateReqVO.getId()); // 校验部门 deptApi.validateDeptList(Collections.singleton(updateReqVO.getDeptId())); // 校验岗位 postApi.validPostList(updateReqVO.getPostIds()); // 校验手机号是否已存在(如果手机号有变更) if (!Objects.equals(employee.getPhone(), updateReqVO.getPhone()) && employeeMapper.selectByPhone(updateReqVO.getPhone()) != null) { throw exception(EMPLOYEE_PHONE_EXISTS); } // 更新员工主表 HrmEmployeeDO updateObj = BeanUtils.toBean(updateReqVO, HrmEmployeeDO.class); // roleIds 存储为 JSON if (CollUtil.isNotEmpty(updateReqVO.getRoleIds())) { updateObj.setRoleIds(JSONUtil.toJsonStr(updateReqVO.getRoleIds())); } // postIds 存储为 JSON if (CollUtil.isNotEmpty(updateReqVO.getPostIds())) { updateObj.setPostIds(JSONUtil.toJsonStr(updateReqVO.getPostIds())); } employeeMapper.updateById(updateObj); // 同步更新关联系统用户(岗位/部门/昵称/角色/邮箱/备注),保持与用户管理一致 if (employee.getUserId() != null) { AdminUserUpdateReqDTO userUpdateReqDTO = new AdminUserUpdateReqDTO(); userUpdateReqDTO.setId(employee.getUserId()); userUpdateReqDTO.setNickname(updateReqVO.getName()); userUpdateReqDTO.setDeptId(updateReqVO.getDeptId()); userUpdateReqDTO.setPostIds(new LinkedHashSet<>(updateReqVO.getPostIds())); userUpdateReqDTO.setRoleIds(CollUtil.isEmpty(updateReqVO.getRoleIds()) ? null : new LinkedHashSet<>(updateReqVO.getRoleIds())); userUpdateReqDTO.setEmail(updateReqVO.getEmail()); userUpdateReqDTO.setRemark(updateReqVO.getRemark()); adminUserApi.updateUser(userUpdateReqDTO); } Long employeeId = updateReqVO.getId(); // 同步教育经历 syncEducationList(employeeId, updateReqVO.getEducations()); // 同步工作经历 syncWorkHistoryList(employeeId, updateReqVO.getWorkHistories()); // 同步紧急联系人 syncEmergencyContactList(employeeId, updateReqVO.getEmergencyContacts()); } private void syncEducationList(Long employeeId, List newList) { educationMapper.deleteByEmployeeId(employeeId); if (CollUtil.isNotEmpty(newList)) { newList.forEach(vo -> { HrmEmployeeEducationDO education = BeanUtils.toBean(vo, HrmEmployeeEducationDO.class); education.setId(null); education.setEmployeeId(employeeId); educationMapper.insert(education); }); } } private void syncWorkHistoryList(Long employeeId, List newList) { workHistoryMapper.deleteByEmployeeId(employeeId); if (CollUtil.isNotEmpty(newList)) { newList.forEach(vo -> { HrmEmployeeWorkHistoryDO workHistory = BeanUtils.toBean(vo, HrmEmployeeWorkHistoryDO.class); workHistory.setId(null); workHistory.setEmployeeId(employeeId); workHistoryMapper.insert(workHistory); }); } } private void syncEmergencyContactList(Long employeeId, List newList) { emergencyContactMapper.deleteByEmployeeId(employeeId); if (CollUtil.isNotEmpty(newList)) { newList.forEach(vo -> { HrmEmployeeEmergencyContactDO emergencyContact = BeanUtils.toBean(vo, HrmEmployeeEmergencyContactDO.class); emergencyContact.setId(null); emergencyContact.setEmployeeId(employeeId); emergencyContactMapper.insert(emergencyContact); }); } } @Override @Transactional(rollbackFor = Exception.class) public void deleteEmployee(Long id) { // 校验存在 validateEmployeeExists(id); // 级联删除子表 educationMapper.deleteByEmployeeId(id); workHistoryMapper.deleteByEmployeeId(id); emergencyContactMapper.deleteByEmployeeId(id); // 删除员工 employeeMapper.deleteById(id); } private HrmEmployeeDO validateEmployeeExists(Long id) { HrmEmployeeDO employee = employeeMapper.selectById(id); if (employee == null) { throw exception(EMPLOYEE_NOT_EXISTS); } return employee; } @Override public HrmEmployeeDO getEmployee(Long id) { return employeeMapper.selectById(id); } @Override public HrmEmployeeRespVO getEmployeeDetail(Long id) { HrmEmployeeDO employee = getEmployee(id); if (employee == null) { return null; } HrmEmployeeRespVO respVO = BeanUtils.toBean(employee, HrmEmployeeRespVO.class); // 解析 roleIds JSON if (StrUtil.isNotBlank(employee.getRoleIds())) { respVO.setRoleIds(JSONUtil.toList(employee.getRoleIds(), Long.class)); } // 填充部门名称 if (employee.getDeptId() != null) { DeptRespDTO dept = deptApi.getDept(employee.getDeptId()); if (dept != null) { respVO.setDeptName(dept.getName()); } } // 填充岗位信息 if (StrUtil.isNotBlank(employee.getPostIds())) { List postIds = JSONUtil.toList(employee.getPostIds(), Long.class); respVO.setPostIds(postIds); if (CollUtil.isNotEmpty(postIds)) { Map postMap = postApi.getPostMap(postIds); respVO.setPostNames(postIds.stream() .filter(postMap::containsKey) .map(postId -> postMap.get(postId).getName()) .collect(Collectors.joining(","))); } } // 填充角色名称 if (CollUtil.isNotEmpty(respVO.getRoleIds())) { List roles = roleApi.getRoleList(respVO.getRoleIds()); if (CollUtil.isNotEmpty(roles)) { respVO.setRoleNames(roles.stream().map(RoleRespDTO::getName).collect(Collectors.joining(","))); } } return respVO; } @Override public HrmEmployeeDO validateEmployee(Long id) { HrmEmployeeDO employee = employeeMapper.selectById(id); if (employee == null) { throw exception(EMPLOYEE_NOT_EXISTS); } return employee; } @Override public PageResult getEmployeePage(HrmEmployeePageReqVO pageReqVO) { if (pageReqVO.getDeptId() != null) { // 选中部门时,连带查询其所有子部门的员工(含自身) List childDepts = deptApi.getChildDeptList(pageReqVO.getDeptId()); Set deptIds = new LinkedHashSet<>(); deptIds.add(pageReqVO.getDeptId()); if (CollUtil.isNotEmpty(childDepts)) { childDepts.forEach(dept -> deptIds.add(dept.getId())); } return employeeMapper.selectPageByDeptIds(pageReqVO, deptIds); } return employeeMapper.selectPage(pageReqVO); } @Override public List getEmployeeList(Collection ids) { return employeeMapper.selectByIds(ids); } @Override public List getEmployeeSimpleList(Integer employeeStatus) { List list = employeeMapper.selectListByStatus(employeeStatus); // 填充部门名称 Set deptIds = list.stream() .map(HrmEmployeeDO::getDeptId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map deptMap = deptApi.getDeptMap(deptIds); return list.stream().map(emp -> { HrmEmployeeSimpleRespVO vo = new HrmEmployeeSimpleRespVO(); vo.setId(emp.getId()); vo.setEmployeeNo(emp.getEmployeeNo()); vo.setName(emp.getName()); vo.setEmployeeStatus(emp.getEmployeeStatus()); if (emp.getDeptId() != null && deptMap.containsKey(emp.getDeptId())) { vo.setDeptName(deptMap.get(emp.getDeptId()).getName()); } return vo; }).collect(Collectors.toList()); } @Override public HrmEmployeeDO getEmployeeByUserId(Long userId) { return employeeMapper.selectByUserId(userId); } @Override public boolean updateEmployeeStatusToResignedByUserId(Long userId, LocalDate leaveDate) { if (userId == null) { return false; } return employeeMapper.updateEmployeeStatusByUserId(userId, EMPLOYEE_STATUS_RESIGNED, leaveDate) > 0; } @Override public HrmEmployeeDO getEmployeeByName(String name) { return employeeMapper.selectByName(name); } @Override public List getEmployeeListByName(String name) { return employeeMapper.selectListByName(name); } @Override @LogRecord(type = HRM_EMPLOYEE_TYPE, subType = HRM_EMPLOYEE_IMPORT_SUB_TYPE, bizNo = "0", success = HRM_EMPLOYEE_IMPORT_SUCCESS) public HrmEmployeeImportRespVO importEmployeeList(List importEmployees, HrmEmployeeImportReqVO importReqVO) { // 1. 校验非空 importEmployees = filterList(importEmployees, item -> Objects.nonNull(item.getName())); if (CollUtil.isEmpty(importEmployees)) { throw exception(EMPLOYEE_IMPORT_LIST_IS_EMPTY); } // 2. 预加载部门/岗位/角色 名称 -> 实体 映射 Map deptMap = deptApi.getDeptList().stream() .collect(Collectors.toMap(DeptRespDTO::getName, dept -> dept, (v1, v2) -> v1)); Map postMap = postApi.getPostList().stream() .collect(Collectors.toMap(PostRespDTO::getName, post -> post, (v1, v2) -> v1)); Map roleMap = roleApi.getRoleList().stream() .collect(Collectors.toMap(RoleRespDTO::getName, role -> role, (v1, v2) -> v1)); // 3. 逐条处理 HrmEmployeeImportRespVO respVO = HrmEmployeeImportRespVO.builder() .createNames(new ArrayList<>()) .updateNames(new ArrayList<>()) .failureNames(new LinkedHashMap<>()) .build(); for (HrmEmployeeImportExcelVO importEmployee : importEmployees) { String name = importEmployee.getName(); try { // 3.1 名称转 ID Long deptId = parseDeptId(importEmployee.getDeptName(), deptMap); Set postIds = parsePostIds(importEmployee.getPostName(), postMap); Set roleIds = parseRoleIds(importEmployee.getRoleNames(), roleMap); // 3.2 构建保存请求 HrmEmployeeSaveReqVO saveReqVO = buildImportSaveReqVO(importEmployee, deptId, postIds, roleIds); // 3.3 判断存在性:按手机号 HrmEmployeeDO existEmployee = employeeMapper.selectByPhone(importEmployee.getPhone()); if (existEmployee == null) { self.createEmployee(saveReqVO); respVO.getCreateNames().add(name); } else { if (!Boolean.TRUE.equals(importReqVO.getUpdateSupport())) { respVO.getFailureNames().put(name, EMPLOYEE_PHONE_EXISTS.getMsg()); continue; } saveReqVO.setId(existEmployee.getId()); self.updateEmployee(saveReqVO); respVO.getUpdateNames().add(name); } } catch (ServiceException ex) { respVO.getFailureNames().put(name, ex.getMessage()); } catch (Exception ex) { respVO.getFailureNames().put(name, ex.getMessage()); } } // 记录导入结果汇总,供 @LogRecord 的 success 文案引用 OperateLogUtils.putImportSummary(respVO.getCreateNames(), respVO.getUpdateNames(), respVO.getFailureNames()); return respVO; } private HrmEmployeeSaveReqVO buildImportSaveReqVO(HrmEmployeeImportExcelVO importEmployee, Long deptId, Set postIds, Set roleIds) { if (StrUtil.isBlank(importEmployee.getName())) { throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "员工姓名"); } if (StrUtil.isBlank(importEmployee.getPhone())) { throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "手机号"); } if (importEmployee.getHireDate() == null) { throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "入职日期"); } HrmEmployeeSaveReqVO reqVO = new HrmEmployeeSaveReqVO(); reqVO.setName(importEmployee.getName()); reqVO.setGender(importEmployee.getGender() != null ? importEmployee.getGender() : 1); // 默认男 reqVO.setPhone(importEmployee.getPhone()); reqVO.setDeptId(deptId); reqVO.setPostIds(CollUtil.isEmpty(postIds) ? null : new ArrayList<>(postIds)); reqVO.setRoleIds(CollUtil.isEmpty(roleIds) ? null : new ArrayList<>(roleIds)); reqVO.setHireDate(importEmployee.getHireDate()); reqVO.setEmployeeStatus(importEmployee.getEmployeeStatus() != null ? importEmployee.getEmployeeStatus() : 1); // 默认在职 reqVO.setEmail(importEmployee.getEmail()); reqVO.setBirthday(importEmployee.getBirthday()); reqVO.setIdCard(importEmployee.getIdCard()); reqVO.setNation(importEmployee.getNation()); reqVO.setMarriageStatus(importEmployee.getMarriageStatus()); reqVO.setAge(importEmployee.getAge()); reqVO.setEducation(importEmployee.getEducation()); reqVO.setPoliticalStatus(importEmployee.getPoliticalStatus()); reqVO.setRemark(importEmployee.getRemark()); return reqVO; } private Long parseDeptId(String deptName, Map deptMap) { if (StrUtil.isBlank(deptName)) { throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "部门名称"); } DeptRespDTO dept = deptMap.get(deptName.trim()); if (dept == null) { throw exception(EMPLOYEE_DEPT_NAME_NOT_EXISTS, deptName.trim()); } return dept.getId(); } private Set parsePostIds(String postNames, Map postMap) { if (StrUtil.isBlank(postNames)) { throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "岗位名称"); } Set postIds = new LinkedHashSet<>(); for (String postName : postNames.split("[,,]")) { String trimmed = postName.trim(); if (StrUtil.isBlank(trimmed)) { continue; } PostRespDTO post = postMap.get(trimmed); if (post == null) { throw exception(EMPLOYEE_POST_NAME_NOT_EXISTS, trimmed); } postIds.add(post.getId()); } if (CollUtil.isEmpty(postIds)) { throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "岗位名称"); } return postIds; } private Set parseRoleIds(String roleNames, Map roleMap) { if (StrUtil.isBlank(roleNames)) { return Collections.emptySet(); } Set roleIds = new LinkedHashSet<>(); for (String roleName : roleNames.split("[,,]")) { String trimmed = roleName.trim(); if (StrUtil.isBlank(trimmed)) { continue; } RoleRespDTO role = roleMap.get(trimmed); if (role == null) { throw exception(EMPLOYEE_ROLE_NAME_NOT_EXISTS, trimmed); } roleIds.add(role.getId()); } return roleIds; } // ==================== 人力统计 ==================== /** 员工状态:在职 */ private static final Integer EMPLOYEE_STATUS_ACTIVE = 1; /** 员工状态:试用 */ private static final Integer EMPLOYEE_STATUS_PROBATION = 2; /** 工龄区间名称,与下限(含)一一对应,最后一档无上限 */ private static final String[] TENURE_BUCKET_NAMES = {"1年以内", "1-3年", "3-5年", "5-10年", "10年以上"}; private static final double[] TENURE_BUCKET_MIN = {0, 1, 3, 5, 10}; /** 年龄区间名称,与下限(含)一一对应,最后一档无上限 */ private static final String[] AGE_BUCKET_NAMES = {"25岁以下", "25-30岁", "30-35岁", "35-40岁", "40-50岁", "50岁以上"}; private static final int[] AGE_BUCKET_MIN = {0, 25, 30, 35, 40, 50}; @Override public HrmEmployeeStatisticsRespVO getTenureStatistics() { List allEmployees = employeeMapper.selectList(); LocalDate today = LocalDate.now(); int currentYear = today.getYear(); List activeEmployees = allEmployees.stream() .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_ACTIVE) || Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_PROBATION)) .collect(Collectors.toList()); HrmEmployeeStatisticsRespVO respVO = new HrmEmployeeStatisticsRespVO(); respVO.setTotalCount(activeEmployees.size()); respVO.setActiveCount((int) activeEmployees.stream() .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_ACTIVE)).count()); respVO.setProbationCount((int) activeEmployees.stream() .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_PROBATION)).count()); respVO.setResignedCount((int) allEmployees.stream() .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED)).count()); // 本年入职:在岗员工中入职日期落在今年 respVO.setThisYearHireCount((int) activeEmployees.stream() .filter(e -> e.getHireDate() != null && e.getHireDate().getYear() == currentYear).count()); // 本年离职:离职员工中离职日期落在今年 respVO.setThisYearResignCount((int) allEmployees.stream() .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED)) .filter(e -> e.getLeaveDate() != null && e.getLeaveDate().getYear() == currentYear).count()); // 1. 平均工龄、平均年龄 List tenureList = activeEmployees.stream() .map(e -> calculateTenureYears(e, today)) .filter(Objects::nonNull) .collect(Collectors.toList()); respVO.setAverageTenureYears(averageOf(tenureList)); List ageList = activeEmployees.stream() .map(e -> resolveAge(e, today)) .filter(Objects::nonNull) .collect(Collectors.toList()); respVO.setAverageAge(averageOfInt(ageList)); // 2. 工龄分布 int[] tenureCounts = new int[TENURE_BUCKET_NAMES.length]; for (Double tenure : tenureList) { tenureCounts[resolveTenureBucket(tenure)]++; } List tenureDistribution = new ArrayList<>(); for (int i = 0; i < TENURE_BUCKET_NAMES.length; i++) { tenureDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO(TENURE_BUCKET_NAMES[i], tenureCounts[i])); } respVO.setTenureDistribution(tenureDistribution); // 3. 年龄分布 int[] ageCounts = new int[AGE_BUCKET_NAMES.length]; for (Integer age : ageList) { ageCounts[resolveAgeBucket(age)]++; } List ageDistribution = new ArrayList<>(); for (int i = 0; i < AGE_BUCKET_NAMES.length; i++) { ageDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO(AGE_BUCKET_NAMES[i], ageCounts[i])); } respVO.setAgeDistribution(ageDistribution); // 4. 学历分布(未填写的归入“未填写”) Map educationMap = new LinkedHashMap<>(); activeEmployees.stream() .map(e -> StrUtil.blankToDefault(StrUtil.trim(e.getEducation()), "未填写")) .forEach(name -> educationMap.merge(name, 1, Integer::sum)); respVO.setEducationDistribution(toItemListSorted(educationMap)); // 5. 性别分布(1 男 / 2 女 / 其它 未知) Map genderMap = new LinkedHashMap<>(); activeEmployees.forEach(e -> { String name = Objects.equals(e.getGender(), 1) ? "男" : Objects.equals(e.getGender(), 2) ? "女" : "未知"; genderMap.merge(name, 1, Integer::sum); }); respVO.setGenderDistribution(toItemListSorted(genderMap)); // 6. 部门工龄统计 Set deptIds = activeEmployees.stream() .map(HrmEmployeeDO::getDeptId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map deptMap = CollUtil.isEmpty(deptIds) ? Collections.emptyMap() : deptApi.getDeptMap(deptIds); Map> deptTenureMap = new LinkedHashMap<>(); Map deptNameMap = new LinkedHashMap<>(); for (HrmEmployeeDO employee : activeEmployees) { Double tenure = calculateTenureYears(employee, today); if (employee.getDeptId() == null) { deptTenureMap.computeIfAbsent(0L, k -> new ArrayList<>()).add(tenure == null ? 0D : tenure); deptNameMap.putIfAbsent(0L, "未分配部门"); continue; } deptTenureMap.computeIfAbsent(employee.getDeptId(), k -> new ArrayList<>()) .add(tenure == null ? 0D : tenure); DeptRespDTO dept = deptMap.get(employee.getDeptId()); deptNameMap.put(employee.getDeptId(), dept != null ? dept.getName() : "部门ID=" + employee.getDeptId()); } List deptTenureList = new ArrayList<>(); deptTenureMap.forEach((deptId, tenures) -> { HrmEmployeeStatisticsRespVO.DeptTenureVO deptVO = new HrmEmployeeStatisticsRespVO.DeptTenureVO(); deptVO.setDeptId(deptId == 0L ? null : deptId); deptVO.setDeptName(deptNameMap.get(deptId)); deptVO.setCount(tenures.size()); deptVO.setAverageTenureYears(averageOf(tenures)); deptTenureList.add(deptVO); }); deptTenureList.sort(Comparator.comparing(HrmEmployeeStatisticsRespVO.DeptTenureVO::getAverageTenureYears, Comparator.nullsLast(Comparator.reverseOrder()))); respVO.setDeptTenureList(deptTenureList); // 6.1 部门人数分布(与部门工龄同一份分组,直接复用) Map deptCountMap = new LinkedHashMap<>(); deptTenureMap.forEach((deptId, tenures) -> deptCountMap.put(deptNameMap.get(deptId), tenures.size())); respVO.setDeptDistribution(toItemListSorted(deptCountMap)); // 7. 工龄排行(前 10 名) respVO.setTopTenureEmployees(activeEmployees.stream() .filter(e -> e.getHireDate() != null) .sorted(Comparator.comparing(HrmEmployeeDO::getHireDate)) .limit(10) .map(e -> { HrmEmployeeStatisticsRespVO.TenureEmployeeVO employeeVO = new HrmEmployeeStatisticsRespVO.TenureEmployeeVO(); employeeVO.setId(e.getId()); employeeVO.setName(e.getName()); DeptRespDTO dept = e.getDeptId() == null ? null : deptMap.get(e.getDeptId()); employeeVO.setDeptName(dept != null ? dept.getName() : (e.getDeptId() == null ? "未分配部门" : "部门ID=" + e.getDeptId())); employeeVO.setHireDate(e.getHireDate()); employeeVO.setTenureYears(calculateTenureYears(e, today)); return employeeVO; }) .collect(Collectors.toList())); // 8. 人员状态构成(在职 / 试用 / 离职,其余状态归入“其它”) int activeStatusCount = (int) activeEmployees.stream() .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_ACTIVE)).count(); int probationStatusCount = (int) activeEmployees.stream() .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_PROBATION)).count(); int resignedStatusCount = (int) allEmployees.stream() .filter(e -> Objects.equals(e.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED)).count(); List statusDistribution = new ArrayList<>(); statusDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO("在职", activeStatusCount)); statusDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO("试用", probationStatusCount)); statusDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO("离职", resignedStatusCount)); int otherStatusCount = allEmployees.size() - activeStatusCount - probationStatusCount - resignedStatusCount; if (otherStatusCount > 0) { statusDistribution.add(new HrmEmployeeStatisticsRespVO.ItemVO("其它", otherStatusCount)); } respVO.setStatusDistribution(statusDistribution); // 9. 岗位分布(postIds 为 JSON 数组,可一人多岗;无岗位归入“未分配岗位”) Set postIds = new HashSet<>(); for (HrmEmployeeDO employee : activeEmployees) { if (StrUtil.isNotBlank(employee.getPostIds())) { postIds.addAll(JSONUtil.toList(employee.getPostIds(), Long.class)); } } Map postMap = CollUtil.isEmpty(postIds) ? Collections.emptyMap() : postApi.getPostMap(postIds); Map postCountMap = new LinkedHashMap<>(); for (HrmEmployeeDO employee : activeEmployees) { List employeePostIds = StrUtil.isNotBlank(employee.getPostIds()) ? JSONUtil.toList(employee.getPostIds(), Long.class) : Collections.emptyList(); if (CollUtil.isEmpty(employeePostIds)) { postCountMap.merge("未分配岗位", 1, Integer::sum); continue; } for (Long postId : employeePostIds) { PostRespDTO post = postMap.get(postId); postCountMap.merge(post != null ? post.getName() : "岗位ID=" + postId, 1, Integer::sum); } } respVO.setPostDistribution(toItemListSorted(postCountMap)); // 10. 婚姻状况、政治面貌分布(档案自由文本,空值归入“未填写”) Map marriageMap = new LinkedHashMap<>(); Map politicalMap = new LinkedHashMap<>(); for (HrmEmployeeDO employee : activeEmployees) { marriageMap.merge(StrUtil.blankToDefault(StrUtil.trim(employee.getMarriageStatus()), "未填写"), 1, Integer::sum); politicalMap.merge(StrUtil.blankToDefault(StrUtil.trim(employee.getPoliticalStatus()), "未填写"), 1, Integer::sum); } respVO.setMarriageDistribution(toItemListSorted(marriageMap)); respVO.setPoliticalDistribution(toItemListSorted(politicalMap)); // 11. 近 12 个月入职 / 离职趋势(含本月) Map hireMonthMap = new LinkedHashMap<>(); Map resignMonthMap = new LinkedHashMap<>(); allEmployees.forEach(e -> { if (e.getHireDate() != null) { hireMonthMap.merge(YearMonth.from(e.getHireDate()), 1, Integer::sum); } if (e.getLeaveDate() != null) { resignMonthMap.merge(YearMonth.from(e.getLeaveDate()), 1, Integer::sum); } }); List trendList = new ArrayList<>(); YearMonth startMonth = YearMonth.from(today).minusMonths(11); for (int i = 0; i < 12; i++) { YearMonth month = startMonth.plusMonths(i); HrmEmployeeStatisticsRespVO.TrendVO trendVO = new HrmEmployeeStatisticsRespVO.TrendVO(); trendVO.setMonth(month.toString()); trendVO.setHireCount(hireMonthMap.getOrDefault(month, 0)); trendVO.setResignCount(resignMonthMap.getOrDefault(month, 0)); trendList.add(trendVO); } respVO.setHireResignTrend(trendList); // 12. 合同到期预警(只看在岗员工的当前生效合同) Set activeEmployeeIds = activeEmployees.stream() .map(HrmEmployeeDO::getId) .collect(Collectors.toSet()); Map currentContractMap = contractMapper.selectList().stream() .filter(c -> Boolean.TRUE.equals(c.getIsCurrent())) .filter(c -> c.getTerminateStatus() == null || c.getTerminateStatus() == 0) .filter(c -> activeEmployeeIds.contains(c.getEmployeeId())) .collect(Collectors.toMap(HrmEmployeeContractDO::getEmployeeId, c -> c, (a, b) -> a)); int expiredCount = 0; int expireIn30Count = 0; int expireIn90Count = 0; int expireOver90Count = 0; int unlimitedCount = 0; for (HrmEmployeeContractDO contract : currentContractMap.values()) { if (contract.getEndDate() == null) { unlimitedCount++; continue; } long days = ChronoUnit.DAYS.between(today, contract.getEndDate()); if (days < 0) { expiredCount++; } else if (days <= 30) { expireIn30Count++; } else if (days <= 90) { expireIn90Count++; } else { expireOver90Count++; } } List contractWarning = new ArrayList<>(); contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("已到期", expiredCount)); contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("30天内到期", expireIn30Count)); contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("31-90天到期", expireIn90Count)); contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("90天以上", expireOver90Count)); contractWarning.add(new HrmEmployeeStatisticsRespVO.ItemVO("无固定期限", unlimitedCount)); respVO.setContractWarning(contractWarning); respVO.setNoContractCount(Math.max(0, activeEmployeeIds.size() - currentContractMap.size())); return respVO; } /** * 计算工龄(年,保留 1 位小数);离职员工按离职日期计算,无入职日期返回 null */ private Double calculateTenureYears(HrmEmployeeDO employee, LocalDate today) { if (employee.getHireDate() == null) { return null; } LocalDate endDate = employee.getLeaveDate() != null ? employee.getLeaveDate() : today; if (endDate.isBefore(employee.getHireDate())) { return 0D; } double years = ChronoUnit.DAYS.between(employee.getHireDate(), endDate) / 365.25D; return Math.round(years * 10D) / 10D; } /** * 解析年龄:优先取档案年龄,缺失时用出生日期推算 */ private Integer resolveAge(HrmEmployeeDO employee, LocalDate today) { if (employee.getAge() != null && employee.getAge() > 0) { return employee.getAge(); } if (employee.getBirthday() == null) { return null; } return (int) ChronoUnit.YEARS.between(employee.getBirthday(), today); } private int resolveTenureBucket(double tenure) { for (int i = TENURE_BUCKET_MIN.length - 1; i >= 0; i--) { if (tenure >= TENURE_BUCKET_MIN[i]) { return i; } } return 0; } private int resolveAgeBucket(int age) { for (int i = AGE_BUCKET_MIN.length - 1; i >= 0; i--) { if (age >= AGE_BUCKET_MIN[i]) { return i; } } return 0; } private List toItemListSorted(Map countMap) { return countMap.entrySet().stream() .sorted(Map.Entry.comparingByValue().reversed()) .map(entry -> new HrmEmployeeStatisticsRespVO.ItemVO(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); } private Double averageOf(List values) { if (CollUtil.isEmpty(values)) { return 0D; } double sum = values.stream().mapToDouble(Double::doubleValue).sum(); return Math.round(sum / values.size() * 10D) / 10D; } private Double averageOfInt(List values) { if (CollUtil.isEmpty(values)) { return 0D; } double sum = values.stream().mapToInt(Integer::intValue).sum(); return Math.round(sum / values.size() * 10D) / 10D; } }