package cn.iocoder.yudao.module.hrm.service.employee; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; 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.module.hrm.controller.admin.employee.vo.HrmEmployeeContractPageReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractRespVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractSaveReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractImportExcelVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractImportReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractImportRespVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractTerminateReqVO; 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.mysql.employee.HrmEmployeeContractMapper; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper; 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.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; 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.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; 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.module.hrm.enums.ErrorCodeConstants.*; /** * 员工合同 Service 实现类 * * @author 超级管理员 */ @Service @Validated public class HrmEmployeeContractServiceImpl implements HrmEmployeeContractService { /** * 附件业务记录类型 */ private static final String RECORD_TYPE = "hrm_employee_contract"; @Resource private HrmEmployeeContractMapper contractMapper; @Resource private HrmEmployeeService employeeService; @Resource private HrmEmployeeMapper employeeMapper; @Resource private DeptApi deptApi; @Resource private StorageAttachmentApi storageAttachmentApi; @Resource private HrmNoRedisDAO noRedisDAO; @Resource @Lazy // 自注入需延迟加载,导入时逐条走事务边界 private HrmEmployeeContractService self; @Override @Transactional(rollbackFor = Exception.class) public Long createContract(HrmEmployeeContractSaveReqVO createReqVO) { // 1. 校验员工存在 employeeService.validateEmployee(createReqVO.getEmployeeId()); // 2. 校验合同日期 validateContractDate(createReqVO); // 3. 校验同一员工时间重叠的合同 validateNoOverlap(createReqVO.getEmployeeId(), createReqVO.getStartDate(), createReqVO.getEndDate(), null); // 4. 生成合同编号 String contractNo = noRedisDAO.generateEmployeeContractNo(); // 5. 查询该员工已有合同,确定续签关系 List existing = contractMapper.selectListByEmployee(createReqVO.getEmployeeId()); // 6. 插入合同记录(新签合同即员工当前合同) HrmEmployeeContractDO contract = BeanUtils.toBean(createReqVO, HrmEmployeeContractDO.class); contract.setContractNo(contractNo); contract.setIsCurrent(true); // 续签:优先使用前端指定的 parentId(用户点击的那份合同),否则自动关联员工最近一份合同 if (createReqVO.getParentId() == null) { contract.setParentId(CollUtil.isEmpty(existing) ? null : existing.get(0).getId()); } if (contract.getTerminateStatus() == null) { contract.setTerminateStatus(HrmEmployeeContractMapper.TERMINATE_NORMAL); } // 7. 先清除旧合同的"当前合同"标记,再插入新合同 contractMapper.clearCurrentByEmployee(createReqVO.getEmployeeId()); contractMapper.insert(contract); // 8. 绑定附件 if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) { storageAttachmentApi.bindAttachments("file", RECORD_TYPE, contract.getId(), createReqVO.getBlobIds()); } return contract.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateContract(HrmEmployeeContractSaveReqVO updateReqVO) { // 1. 校验存在 validateContractExists(updateReqVO.getId()); // 2. 校验员工存在 employeeService.validateEmployee(updateReqVO.getEmployeeId()); // 3. 校验合同日期 validateContractDate(updateReqVO); // 4. 校验同一员工时间重叠的合同(排除自身) validateNoOverlap(updateReqVO.getEmployeeId(), updateReqVO.getStartDate(), updateReqVO.getEndDate(), updateReqVO.getId()); // 5. 更新合同记录 HrmEmployeeContractDO updateObj = BeanUtils.toBean(updateReqVO, HrmEmployeeContractDO.class); contractMapper.updateById(updateObj); // 6. 更新附件 storageAttachmentApi.updateAttachments("file", RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds()); } @Override @Transactional(rollbackFor = Exception.class) public void deleteContract(Long id) { // 1. 校验存在 validateContractExists(id); // 2. 删除合同 contractMapper.deleteById(id); // 3. 删除附件 storageAttachmentApi.deleteAttachmentsByRecord(RECORD_TYPE, id); } @Override @Transactional(rollbackFor = Exception.class) public void terminateContract(HrmEmployeeContractTerminateReqVO reqVO) { // 1. 校验存在 HrmEmployeeContractDO contract = validateContractExists(reqVO.getId()); // 2. 校验解除/终止参数 Integer terminateStatus = reqVO.getTerminateStatus(); if (terminateStatus == null || (terminateStatus != HrmEmployeeContractMapper.TERMINATE_CANCELED && terminateStatus != HrmEmployeeContractMapper.TERMINATE_TERMINATED) || reqVO.getTerminateDate() == null || StrUtil.isBlank(reqVO.getTerminateReason())) { throw exception(EMPLOYEE_CONTRACT_TERMINATE_REQUIRE_DATE_REASON); } // 3. 已解除/已终止的合同不允许重复操作 if (contract.getTerminateStatus() != null && contract.getTerminateStatus() != HrmEmployeeContractMapper.TERMINATE_NORMAL) { throw exception(EMPLOYEE_CONTRACT_TERMINATE_REQUIRE_DATE_REASON); } // 4. 更新解除/终止信息 HrmEmployeeContractDO updateObj = new HrmEmployeeContractDO(); updateObj.setId(contract.getId()); updateObj.setTerminateStatus(terminateStatus); updateObj.setTerminateDate(reqVO.getTerminateDate()); updateObj.setTerminateReason(reqVO.getTerminateReason()); // 5. 若解除/终止的是员工当前合同,清除当前标记 if (Boolean.TRUE.equals(contract.getIsCurrent())) { updateObj.setIsCurrent(false); } contractMapper.updateById(updateObj); } @Override public HrmEmployeeContractRespVO getContract(Long id) { HrmEmployeeContractDO contract = validateContractExists(id); HrmEmployeeContractRespVO respVO = BeanUtils.toBean(contract, HrmEmployeeContractRespVO.class); // 填充员工信息 HrmEmployeeDO employee = employeeService.getEmployee(contract.getEmployeeId()); if (employee != null) { Map deptMap = deptApi.getDeptMap(Collections.singleton(employee.getDeptId())); fillEmployeeInfo(respVO, employee, deptMap); } // 填充续签上一份合同编号 fillParentNo(respVO, contract.getParentId()); // 填充附件 respVO.setAttachmentList(storageAttachmentApi.listAttachments(RECORD_TYPE, id)); // 动态计算状态 respVO.setStatus(calculateStatus(contract, LocalDate.now())); return respVO; } @Override public PageResult getContractPage(HrmEmployeeContractPageReqVO pageReqVO) { LocalDate today = LocalDate.now(); PageResult pageResult = contractMapper.selectPage(pageReqVO, today); if (CollUtil.isEmpty(pageResult.getList())) { return PageResult.empty(); } // 批量获取员工信息 Set employeeIds = pageResult.getList().stream() .map(HrmEmployeeContractDO::getEmployeeId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map employeeMap = CollUtil.isEmpty(employeeIds) ? Collections.emptyMap() : employeeService.getEmployeeList(employeeIds).stream() .collect(Collectors.toMap(HrmEmployeeDO::getId, e -> e)); // 批量获取部门信息 Set deptIds = employeeMap.values().stream() .map(HrmEmployeeDO::getDeptId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map deptMap = CollUtil.isEmpty(deptIds) ? Collections.emptyMap() : deptApi.getDeptMap(deptIds); // 批量获取续签上一份合同信息 Set parentIds = pageResult.getList().stream() .map(HrmEmployeeContractDO::getParentId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map parentMap = CollUtil.isEmpty(parentIds) ? Collections.emptyMap() : contractMapper.selectList(HrmEmployeeContractDO::getId, parentIds).stream() .collect(Collectors.toMap(HrmEmployeeContractDO::getId, c -> c)); // 组装 VO List list = pageResult.getList().stream().map(contract -> { HrmEmployeeContractRespVO respVO = BeanUtils.toBean(contract, HrmEmployeeContractRespVO.class); fillEmployeeInfo(respVO, employeeMap.get(contract.getEmployeeId()), deptMap); fillParentNo(respVO, contract.getParentId(), parentMap); respVO.setStatus(calculateStatus(contract, today)); return respVO; }).collect(Collectors.toList()); return new PageResult<>(list, pageResult.getTotal()); } @Override public Long getExpiringCount() { return contractMapper.selectCountByExpiring(LocalDate.now()); } private HrmEmployeeContractDO validateContractExists(Long id) { HrmEmployeeContractDO contract = contractMapper.selectById(id); if (contract == null) { throw exception(EMPLOYEE_CONTRACT_NOT_EXISTS); } return contract; } /** * 校验同一员工时间重叠的合同,避免一个员工同时存在多份未解除/未终止的生效合同 */ private void validateNoOverlap(Long employeeId, LocalDate startDate, LocalDate endDate, Long excludeId) { List overlaps = contractMapper.selectOverlapping(employeeId, startDate, endDate, excludeId); if (CollUtil.isNotEmpty(overlaps)) { throw exception(EMPLOYEE_CONTRACT_DATE_OVERLAP); } } /** * 填充续签上一份合同编号 */ private void fillParentNo(HrmEmployeeContractRespVO respVO, Long parentId) { if (parentId != null) { HrmEmployeeContractDO parent = contractMapper.selectById(parentId); if (parent != null) { respVO.setParentNo(parent.getContractNo()); } } } /** * 批量填充续签上一份合同编号 */ private void fillParentNo(HrmEmployeeContractRespVO respVO, Long parentId, Map parentMap) { if (parentId != null) { HrmEmployeeContractDO parent = parentMap.get(parentId); if (parent != null) { respVO.setParentNo(parent.getContractNo()); } } } private void validateContractDate(HrmEmployeeContractSaveReqVO reqVO) { // 非无固定期限合同(期限类型 != 2)必须填写结束日期 if (!Integer.valueOf(2).equals(reqVO.getContractTermType()) && reqVO.getEndDate() == null) { throw exception(EMPLOYEE_CONTRACT_END_DATE_REQUIRE_FOR_FIXED); } // 开始日期不能晚于结束日期 if (reqVO.getStartDate() != null && reqVO.getEndDate() != null && reqVO.getStartDate().isAfter(reqVO.getEndDate())) { throw exception(EMPLOYEE_CONTRACT_DATE_INVALID); } // 试用期开始日期不能晚于试用期结束日期 if (reqVO.getProbationStartDate() != null && reqVO.getProbationEndDate() != null && reqVO.getProbationStartDate().isAfter(reqVO.getProbationEndDate())) { throw exception(EMPLOYEE_CONTRACT_DATE_INVALID); } } /** * 动态计算合同状态 * * @param contract 合同记录 * @param today 当前日期 * @return 状态:1-待生效 2-生效中 3-即将到期 4-已到期 5-已解除 6-已终止 */ private Integer calculateStatus(HrmEmployeeContractDO contract, LocalDate today) { Integer terminateStatus = contract.getTerminateStatus(); if (terminateStatus != null && terminateStatus == HrmEmployeeContractMapper.TERMINATE_CANCELED) { return HrmEmployeeContractPageReqVO.STATUS_CANCELED; } if (terminateStatus != null && terminateStatus == HrmEmployeeContractMapper.TERMINATE_TERMINATED) { return HrmEmployeeContractPageReqVO.STATUS_TERMINATED; } LocalDate endDate = contract.getEndDate(); if (endDate != null && endDate.isBefore(today)) { return HrmEmployeeContractPageReqVO.STATUS_EXPIRED; } LocalDate startDate = contract.getStartDate(); if (startDate != null && startDate.isAfter(today)) { return HrmEmployeeContractPageReqVO.STATUS_PENDING; } if (endDate != null && !endDate.isAfter(today.plusDays(HrmEmployeeContractMapper.REMIND_DAYS))) { return HrmEmployeeContractPageReqVO.STATUS_EXPIRING_SOON; } return HrmEmployeeContractPageReqVO.STATUS_ACTIVE; } private void fillEmployeeInfo(HrmEmployeeContractRespVO respVO, HrmEmployeeDO employee, Map deptMap) { if (employee != null) { respVO.setEmployeeName(employee.getName()); respVO.setEmployeeNo(employee.getEmployeeNo()); if (employee.getDeptId() != null && deptMap != null) { DeptRespDTO dept = deptMap.get(employee.getDeptId()); if (dept != null) { respVO.setDeptName(dept.getName()); } } } } @Override public HrmEmployeeContractImportRespVO importContractList(List importContracts, HrmEmployeeContractImportReqVO importReqVO) { // 1. 过滤员工姓名为空的行 importContracts = importContracts.stream() .filter(item -> item.getName() != null && StrUtil.isNotBlank(item.getName().trim())) .collect(Collectors.toList()); if (CollUtil.isEmpty(importContracts)) { throw exception(EMPLOYEE_CONTRACT_IMPORT_LIST_IS_EMPTY); } // 2. 预加载员工姓名 -> 员工档案 映射(含已离职,便于补录历史合同) Map> employeeNameMap = employeeMapper.selectList().stream() .filter(emp -> StrUtil.isNotBlank(emp.getName())) .collect(Collectors.groupingBy(emp -> emp.getName().trim(), LinkedHashMap::new, Collectors.toList())); // 3. 逐条处理:匹配到则覆盖更新,否则新建 HrmEmployeeContractImportRespVO respVO = HrmEmployeeContractImportRespVO.builder() .createNames(new ArrayList<>()) .updateNames(new ArrayList<>()) .failureNames(new LinkedHashMap<>()) .build(); for (HrmEmployeeContractImportExcelVO importContract : importContracts) { String name = importContract.getName().trim(); try { // 3.1 员工姓名转员工 ID Long employeeId = parseEmployeeId(name, employeeNameMap); // 3.2 组装保存请求(必填字段校验) HrmEmployeeContractSaveReqVO saveReqVO = buildImportSaveReqVO(importContract, employeeId); // 3.3 按「员工 + 合同类型 + 合同开始日期」判定已存在 HrmEmployeeContractDO exist = contractMapper.selectByEmployeeTypeStartDate(employeeId, saveReqVO.getContractType(), saveReqVO.getStartDate()); if (exist == null) { self.createContract(saveReqVO); respVO.getCreateNames().add(name); } else { if (!Boolean.TRUE.equals(importReqVO.getUpdateSupport())) { respVO.getFailureNames().put(name, StrUtil.format(EMPLOYEE_CONTRACT_IMPORT_CONTRACT_EXISTS.getMsg(), saveReqVO.getStartDate().toString())); continue; } saveReqVO.setId(exist.getId()); self.updateContract(saveReqVO); respVO.getUpdateNames().add(name); } } catch (ServiceException ex) { respVO.getFailureNames().put(name, ex.getMessage()); } catch (Exception ex) { respVO.getFailureNames().put(name, ex.getMessage()); } } return respVO; } /** * 员工姓名解析为员工 ID,重名时提示需手工处理 */ private Long parseEmployeeId(String name, Map> employeeNameMap) { List employees = employeeNameMap.get(name); if (CollUtil.isEmpty(employees)) { throw exception(EMPLOYEE_CONTRACT_IMPORT_EMPLOYEE_NAME_NOT_EXISTS, name); } if (employees.size() > 1) { throw exception(EMPLOYEE_CONTRACT_IMPORT_EMPLOYEE_NAME_MULTIPLE, name); } return employees.get(0).getId(); } /** * 组装导入合同保存请求并做必填字段校验(日期逻辑校验交由 create/update 处理) */ private HrmEmployeeContractSaveReqVO buildImportSaveReqVO(HrmEmployeeContractImportExcelVO importContract, Long employeeId) { if (importContract.getContractType() == null) { throw exception(EMPLOYEE_CONTRACT_IMPORT_FIELD_REQUIRED, "合同类型"); } if (importContract.getContractTermType() == null) { throw exception(EMPLOYEE_CONTRACT_IMPORT_FIELD_REQUIRED, "合同期限类型"); } if (importContract.getStartDate() == null) { throw exception(EMPLOYEE_CONTRACT_IMPORT_FIELD_REQUIRED, "合同开始日期"); } HrmEmployeeContractSaveReqVO reqVO = new HrmEmployeeContractSaveReqVO(); reqVO.setEmployeeId(employeeId); reqVO.setContractType(importContract.getContractType()); reqVO.setContractTermType(importContract.getContractTermType()); reqVO.setSignCompany(importContract.getSignCompany()); reqVO.setSignDate(importContract.getSignDate()); reqVO.setStartDate(importContract.getStartDate()); reqVO.setEndDate(importContract.getEndDate()); reqVO.setProbationStartDate(importContract.getProbationStartDate()); reqVO.setProbationEndDate(importContract.getProbationEndDate()); reqVO.setProbationSalary(importContract.getProbationSalary()); reqVO.setRegularSalary(importContract.getRegularSalary()); reqVO.setRemark(importContract.getRemark()); return reqVO; } }