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<HrmEmployeeEducationSaveReqVO> 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<HrmEmployeeWorkHistorySaveReqVO> 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<HrmEmployeeEmergencyContactSaveReqVO> 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<Long> postIds = JSONUtil.toList(employee.getPostIds(), Long.class);
|
respVO.setPostIds(postIds);
|
if (CollUtil.isNotEmpty(postIds)) {
|
Map<Long, PostRespDTO> 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<RoleRespDTO> 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<HrmEmployeeDO> getEmployeePage(HrmEmployeePageReqVO pageReqVO) {
|
if (pageReqVO.getDeptId() != null) {
|
// 选中部门时,连带查询其所有子部门的员工(含自身)
|
List<DeptRespDTO> childDepts = deptApi.getChildDeptList(pageReqVO.getDeptId());
|
Set<Long> 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<HrmEmployeeDO> getEmployeeList(Collection<Long> ids) {
|
return employeeMapper.selectByIds(ids);
|
}
|
|
@Override
|
public List<HrmEmployeeSimpleRespVO> getEmployeeSimpleList(Integer employeeStatus) {
|
List<HrmEmployeeDO> list = employeeMapper.selectListByStatus(employeeStatus);
|
// 填充部门名称
|
Set<Long> deptIds = list.stream()
|
.map(HrmEmployeeDO::getDeptId)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toSet());
|
Map<Long, DeptRespDTO> 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<HrmEmployeeDO> 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<HrmEmployeeImportExcelVO> 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<String, DeptRespDTO> deptMap = deptApi.getDeptList().stream()
|
.collect(Collectors.toMap(DeptRespDTO::getName, dept -> dept, (v1, v2) -> v1));
|
Map<String, PostRespDTO> postMap = postApi.getPostList().stream()
|
.collect(Collectors.toMap(PostRespDTO::getName, post -> post, (v1, v2) -> v1));
|
Map<String, RoleRespDTO> 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<Long> postIds = parsePostIds(importEmployee.getPostName(), postMap);
|
Set<Long> 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<Long> postIds, Set<Long> 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<String, DeptRespDTO> 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<Long> parsePostIds(String postNames, Map<String, PostRespDTO> postMap) {
|
if (StrUtil.isBlank(postNames)) {
|
throw exception(EMPLOYEE_IMPORT_FIELD_REQUIRED, "岗位名称");
|
}
|
Set<Long> 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<Long> parseRoleIds(String roleNames, Map<String, RoleRespDTO> roleMap) {
|
if (StrUtil.isBlank(roleNames)) {
|
return Collections.emptySet();
|
}
|
Set<Long> 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<HrmEmployeeDO> allEmployees = employeeMapper.selectList();
|
LocalDate today = LocalDate.now();
|
int currentYear = today.getYear();
|
|
List<HrmEmployeeDO> 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<Double> tenureList = activeEmployees.stream()
|
.map(e -> calculateTenureYears(e, today))
|
.filter(Objects::nonNull)
|
.collect(Collectors.toList());
|
respVO.setAverageTenureYears(averageOf(tenureList));
|
List<Integer> 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<HrmEmployeeStatisticsRespVO.ItemVO> 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<HrmEmployeeStatisticsRespVO.ItemVO> 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<String, Integer> 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<String, Integer> 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<Long> deptIds = activeEmployees.stream()
|
.map(HrmEmployeeDO::getDeptId)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toSet());
|
Map<Long, DeptRespDTO> deptMap = CollUtil.isEmpty(deptIds) ? Collections.emptyMap() : deptApi.getDeptMap(deptIds);
|
Map<Long, List<Double>> deptTenureMap = new LinkedHashMap<>();
|
Map<Long, String> 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<HrmEmployeeStatisticsRespVO.DeptTenureVO> 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<String, Integer> 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<HrmEmployeeStatisticsRespVO.ItemVO> 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<Long> postIds = new HashSet<>();
|
for (HrmEmployeeDO employee : activeEmployees) {
|
if (StrUtil.isNotBlank(employee.getPostIds())) {
|
postIds.addAll(JSONUtil.toList(employee.getPostIds(), Long.class));
|
}
|
}
|
Map<Long, PostRespDTO> postMap = CollUtil.isEmpty(postIds) ? Collections.emptyMap() : postApi.getPostMap(postIds);
|
Map<String, Integer> postCountMap = new LinkedHashMap<>();
|
for (HrmEmployeeDO employee : activeEmployees) {
|
List<Long> 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<String, Integer> marriageMap = new LinkedHashMap<>();
|
Map<String, Integer> 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<YearMonth, Integer> hireMonthMap = new LinkedHashMap<>();
|
Map<YearMonth, Integer> 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<HrmEmployeeStatisticsRespVO.TrendVO> 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<Long> activeEmployeeIds = activeEmployees.stream()
|
.map(HrmEmployeeDO::getId)
|
.collect(Collectors.toSet());
|
Map<Long, HrmEmployeeContractDO> 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<HrmEmployeeStatisticsRespVO.ItemVO> 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<HrmEmployeeStatisticsRespVO.ItemVO> toItemListSorted(Map<String, Integer> countMap) {
|
return countMap.entrySet().stream()
|
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
|
.map(entry -> new HrmEmployeeStatisticsRespVO.ItemVO(entry.getKey(), entry.getValue()))
|
.collect(Collectors.toList());
|
}
|
|
private Double averageOf(List<Double> 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<Integer> values) {
|
if (CollUtil.isEmpty(values)) {
|
return 0D;
|
}
|
double sum = values.stream().mapToInt(Integer::intValue).sum();
|
return Math.round(sum / values.size() * 10D) / 10D;
|
}
|
|
}
|