package cn.iocoder.yudao.module.hrm.service.approval; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 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.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmResignationApplicationPageReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmResignationApplicationRespVO; import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmResignationApplicationSaveReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmResignationHandoverReqVO; import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmResignationApplicationDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmResignationApplicationMapper; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper; import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO; import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum; import cn.iocoder.yudao.module.hrm.enums.HrmResignationTypeEnum; import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService; import cn.iocoder.yudao.module.hrm.service.handover.HrmUserHandoverService; 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.storage.StorageAttachmentApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.repository.ProcessDefinition; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*; /** * 离职申请 Service 实现类 * * @author 超级管理员 */ @Service @Validated @Slf4j public class HrmResignationApplicationServiceImpl implements HrmResignationApplicationService { private static final Integer HANDOVER_STATUS_NOT_STARTED = 0; private static final Integer HANDOVER_STATUS_IN_PROGRESS = 10; private static final Integer HANDOVER_STATUS_COMPLETED = 20; /** 员工状态:离职 */ private static final Integer EMPLOYEE_STATUS_RESIGNED = 3; @Resource private HrmResignationApplicationMapper resignationApplicationMapper; @Resource private HrmNoRedisDAO noRedisDAO; @Resource private DeptApi deptApi; @Resource private PostApi postApi; @Resource private AdminUserApi adminUserApi; @Resource private BpmProcessInstanceApi bpmProcessInstanceApi; @Resource private BpmCategoryService bpmCategoryService; @Resource private StorageAttachmentApi storageAttachmentApi; @Resource private BpmProcessDefinitionService bpmProcessDefinitionService; @Resource private BpmProcessInstanceService bpmProcessInstanceService; @Resource private HrmUserHandoverService handoverService; @Resource private HrmEmployeeMapper employeeMapper; @Resource private HrmEmployeeService employeeService; @Override @Transactional(rollbackFor = Exception.class) public Long createResignationApplication(HrmResignationApplicationSaveReqVO createReqVO) { Long userId = createReqVO.getUserId() != null ? createReqVO.getUserId() : getCurrentUserId(); AdminUserRespDTO user = adminUserApi.getUser(userId); if (user == null) { throw new ServiceException(RESIGNATION_APPLICANT_NOT_EXISTS.getCode(), String.format("申请人(用户ID=%s)不存在,请重新选择申请人后重试", userId)); } HrmResignationApplicationDO existing = resignationApplicationMapper.selectByUserIdAndStatusNot(userId, HrmAuditStatusEnum.CANCEL.getStatus()); if (existing != null) { throw new ServiceException(RESIGNATION_APPLICATION_EXISTS.getCode(), String.format("申请人[%s]已存在未撤销的离职申请(单号=%s,状态=%s),请先撤销或删除原申请后再新建", user.getNickname(), existing.getNo(), HrmAuditStatusEnum.getNameByStatus(existing.getStatus()))); } HrmResignationApplicationDO application = BeanUtils.toBean(createReqVO, HrmResignationApplicationDO.class); application.setNo(generateUniqueNo()); application.setUserId(userId); application.setDeptId(createReqVO.getDeptId() != null ? createReqVO.getDeptId() : user.getDeptId()); application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus()); application.setHandoverStatus(HANDOVER_STATUS_NOT_STARTED); application.setHandoverDataStatus(HANDOVER_STATUS_NOT_STARTED); // 统一解析交接员工与系统用户,确保员工ID、用户ID成对落库 resolveHandoverTarget(application, userId); resignationApplicationMapper.insert(application); storageAttachmentApi.bindAttachments("file", "hrm_resignation_application", application.getId(), createReqVO.getBlobIds()); return application.getId(); } private Long getCurrentUserId() { return SecurityFrameworkUtils.getLoginUserId(); } private String generateUniqueNo() { String no = noRedisDAO.generateResignationApplicationNo(); if (resignationApplicationMapper.selectByNo(no) != null) { return generateUniqueNo(); } return no; } @Override public void updateResignationApplication(HrmResignationApplicationSaveReqVO updateReqVO) { HrmResignationApplicationDO application = validateResignationApplicationExists(updateReqVO.getId()); if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) { throw exception(RESIGNATION_APPLICATION_UPDATE_FAIL_NOT_DRAFT); } HrmResignationApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmResignationApplicationDO.class); // 不覆盖申请人;交接人统一解析成“员工ID + 用户ID” updateObj.setUserId(null); boolean handoverProvided = updateReqVO.getHandoverEmployeeId() != null || updateReqVO.getHandoverUserId() != null; if (handoverProvided) { resolveHandoverTarget(updateObj, application.getUserId()); Integer currentHandoverStatus = application.getHandoverStatus() != null ? application.getHandoverStatus() : HANDOVER_STATUS_NOT_STARTED; if (ObjUtil.notEqual(currentHandoverStatus, HANDOVER_STATUS_NOT_STARTED) && !Objects.equals(updateObj.getHandoverEmployeeId(), application.getHandoverEmployeeId())) { throw new ServiceException(RESIGNATION_HANDOVER_CHANGE_AFTER_START.getCode(), String.format("当前交接状态为[%s],交接人[%s]不能再更换;请先将交接状态回退为[未开始];若已完成交接,请重新提交离职申请", getHandoverStatusName(currentHandoverStatus), getEmployeeDisplayName(application.getHandoverEmployeeId()))); } } resignationApplicationMapper.updateById(updateObj); storageAttachmentApi.updateAttachments("file", "hrm_resignation_application", updateReqVO.getId(), updateReqVO.getBlobIds()); } @Override public void deleteResignationApplication(Long id) { HrmResignationApplicationDO application = validateResignationApplicationExists(id); if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) { throw exception(RESIGNATION_APPLICATION_UPDATE_FAIL_NOT_DRAFT); } storageAttachmentApi.deleteAttachmentsByRecord("hrm_resignation_application", id); resignationApplicationMapper.deleteById(id); } private HrmResignationApplicationDO validateResignationApplicationExists(Long id) { HrmResignationApplicationDO application = resignationApplicationMapper.selectById(id); if (application == null) { throw exception(RESIGNATION_APPLICATION_NOT_EXISTS); } return application; } @Override public HrmResignationApplicationDO getResignationApplication(Long id) { return resignationApplicationMapper.selectById(id); } @Override public HrmResignationApplicationRespVO getResignationApplicationDetail(Long id) { HrmResignationApplicationDO application = getResignationApplication(id); if (application == null) { return null; } HrmResignationApplicationRespVO respVO = BeanUtils.toBean(application, HrmResignationApplicationRespVO.class); fillUserNames(respVO, application); fillApplicantEmployeeInfo(respVO, application); if (application.getDeptId() != null) { DeptRespDTO dept = deptApi.getDept(application.getDeptId()); if (dept != null) { respVO.setDeptName(dept.getName()); } } respVO.setResignationTypeName(HrmResignationTypeEnum.getNameByStatus(application.getResignationType())); respVO.setHandoverStatusName(getHandoverStatusName(application.getHandoverStatus())); respVO.setHandoverDataStatusName(getHandoverStatusName(application.getHandoverDataStatus())); respVO.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus())); respVO.setAttachmentList(storageAttachmentApi.listAttachments("hrm_resignation_application", id)); return respVO; } private void fillUserNames(HrmResignationApplicationRespVO respVO, HrmResignationApplicationDO application) { Set userIds = Stream.of(application.getUserId(), application.getHandoverUserId()) .filter(Objects::nonNull).collect(Collectors.toSet()); Map userMap = adminUserApi.getUserMap(userIds); if (application.getUserId() != null && userMap.containsKey(application.getUserId())) { respVO.setUserName(userMap.get(application.getUserId()).getNickname()); } if (application.getHandoverUserId() != null && userMap.containsKey(application.getHandoverUserId())) { respVO.setHandoverUserName(userMap.get(application.getHandoverUserId()).getNickname()); } } /** * 填充申请人员工信息:员工ID 与岗位名称,供前端回显"申请人 + 岗位" */ private void fillApplicantEmployeeInfo(HrmResignationApplicationRespVO respVO, HrmResignationApplicationDO application) { if (application.getUserId() == null) { return; } HrmEmployeeDO employee = employeeService.getEmployeeByUserId(application.getUserId()); if (employee == null) { return; } respVO.setEmployeeId(employee.getId()); respVO.setEmployeeName(employee.getName()); if (StrUtil.isBlank(employee.getPostIds())) { return; } List postIds = JSONUtil.toList(employee.getPostIds(), Long.class); if (CollUtil.isEmpty(postIds)) { return; } Map postMap = postApi.getPostMap(postIds); respVO.setPostName(postIds.stream() .filter(postMap::containsKey) .map(id -> postMap.get(id).getName()) .collect(Collectors.joining(","))); } private String getHandoverStatusName(Integer handoverStatus) { if (handoverStatus == null) { return ""; } if (Objects.equals(handoverStatus, HANDOVER_STATUS_NOT_STARTED)) { return "未开始"; } if (Objects.equals(handoverStatus, HANDOVER_STATUS_IN_PROGRESS)) { return "进行中"; } if (Objects.equals(handoverStatus, HANDOVER_STATUS_COMPLETED)) { return "已完成"; } return ""; } @Override public PageResult getResignationApplicationPage(HrmResignationApplicationPageReqVO pageReqVO) { PageResult pageResult = resignationApplicationMapper.selectPage(pageReqVO); if (pageResult.getList().isEmpty()) { return new PageResult<>(Collections.emptyList(), pageResult.getTotal()); } Set userIds = pageResult.getList().stream() .flatMap(app -> Stream.of(app.getUserId(), app.getHandoverUserId())) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map userMap = adminUserApi.getUserMap(userIds); Set deptIds = pageResult.getList().stream() .map(HrmResignationApplicationDO::getDeptId) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map deptMap = deptApi.getDeptMap(deptIds); List list = pageResult.getList().stream().map(app -> { HrmResignationApplicationRespVO vo = BeanUtils.toBean(app, HrmResignationApplicationRespVO.class); if (app.getUserId() != null && userMap.containsKey(app.getUserId())) { vo.setUserName(userMap.get(app.getUserId()).getNickname()); } // 申请人员工信息:员工未建档时保持为空 HrmEmployeeDO applicantEmployee = app.getUserId() == null ? null : employeeService.getEmployeeByUserId(app.getUserId()); if (applicantEmployee != null) { vo.setEmployeeId(applicantEmployee.getId()); vo.setEmployeeName(applicantEmployee.getName()); } if (app.getHandoverUserId() != null && userMap.containsKey(app.getHandoverUserId())) { vo.setHandoverUserName(userMap.get(app.getHandoverUserId()).getNickname()); } if (app.getDeptId() != null && deptMap.containsKey(app.getDeptId())) { vo.setDeptName(deptMap.get(app.getDeptId()).getName()); } vo.setResignationTypeName(HrmResignationTypeEnum.getNameByStatus(app.getResignationType())); vo.setHandoverStatusName(getHandoverStatusName(app.getHandoverStatus())); vo.setHandoverDataStatusName(getHandoverStatusName(app.getHandoverDataStatus())); vo.setStatusName(HrmAuditStatusEnum.getNameByStatus(app.getStatus())); return vo; }).collect(Collectors.toList()); return new PageResult<>(list, pageResult.getTotal()); } @Override @Transactional(rollbackFor = Exception.class) public void submitResignationApplication(Long id, String processDefinitionKey, Long userId) { HrmResignationApplicationDO application = validateResignationApplicationExists(id); if (ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) { throw exception(RESIGNATION_APPLICATION_SUBMIT_FAIL_NOT_DRAFT); } // 全局免审批:业务校验通过后直接执行审批通过逻辑,无需流程模型。 if (bpmProcessInstanceApi.isGlobalAutoApprove()) { resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(id) .processInstanceId("") .status(HrmAuditStatusEnum.PROCESS.getStatus()) .build()); updateResignationApplicationAuditStatus(id, HrmAuditStatusEnum.APPROVE.getStatus()); return; } ProcessDefinition processDefinition = resolveActiveProcessDefinition(processDefinitionKey); String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO() .setProcessDefinitionKey(processDefinition.getKey()).setBusinessKey(String.valueOf(id))); resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(id) .processInstanceId(processInstanceId) .status(HrmAuditStatusEnum.PROCESS.getStatus()) .build()); } private void validateApproveCategoryAndProcessDefinition() { List codes = Collections.singletonList(APPROVE_CATEGORY_CODE); Map categoryMap = bpmCategoryService.getCategoryMap(codes); if (!categoryMap.containsKey(APPROVE_CATEGORY_CODE)) { throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } List definitionInfoList = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE); if (CollUtil.isEmpty(definitionInfoList)) { throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } Set processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId); List processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds); boolean hasActiveDefinition = processDefinitions.stream().anyMatch(pd -> !pd.isSuspended()); if (!hasActiveDefinition) { throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } } /** * 解析离职审批可用的流程定义 *

* 前端历史版本把分类编码(hrm_resignation_approve)当作流程 Key 传入, * 这里优先使用传入 Key,匹配不到时回退到标准 Key,再回退到该分类下唯一的激活流程。 */ private ProcessDefinition resolveActiveProcessDefinition(String processDefinitionKey) { validateApproveCategoryAndProcessDefinition(); List definitionInfoList = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE); Set processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId); List processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds); Map latestVersionMap = new HashMap<>(); for (ProcessDefinition pd : processDefinitions) { if (pd.isSuspended()) { continue; } ProcessDefinition existing = latestVersionMap.get(pd.getKey()); if (existing == null || pd.getVersion() > existing.getVersion()) { latestVersionMap.put(pd.getKey(), pd); } } if (latestVersionMap.isEmpty()) { throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } if (StrUtil.isNotBlank(processDefinitionKey) && latestVersionMap.containsKey(processDefinitionKey)) { return latestVersionMap.get(processDefinitionKey); } if (latestVersionMap.containsKey(BPM_PROCESS_DEFINITION_KEY)) { return latestVersionMap.get(BPM_PROCESS_DEFINITION_KEY); } if (latestVersionMap.size() == 1) { return latestVersionMap.values().iterator().next(); } throw new ServiceException(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS.getCode(), String.format("离职审批流程不唯一(分类=%s,可用Key=%s),请传入具体的流程定义Key后重试", APPROVE_CATEGORY_CODE, String.join(",", latestVersionMap.keySet()))); } @Override public List> getApproveProcessDefinitionList() { validateApproveCategoryAndProcessDefinition(); List definitionInfoList = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE); Set processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId); List processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds); Map latestVersionMap = new HashMap<>(); for (ProcessDefinition pd : processDefinitions) { if (pd.isSuspended()) continue; ProcessDefinition existing = latestVersionMap.get(pd.getKey()); if (existing == null || pd.getVersion() > existing.getVersion()) { latestVersionMap.put(pd.getKey(), pd); } } List> result = new ArrayList<>(); for (ProcessDefinition pd : latestVersionMap.values()) { Map item = new HashMap<>(); item.put("id", pd.getId()); item.put("key", pd.getKey()); item.put("name", pd.getName()); item.put("version", pd.getVersion()); result.add(item); } return result; } @Override public void updateResignationApplicationAuditStatus(Long id, Integer status) { HrmResignationApplicationDO application = validateResignationApplicationExists(id); if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) { log.error("[updateResignationApplicationAuditStatus][application({}) 不处于审批中,无法更新审批结果({})]", id, status); throw exception(RESIGNATION_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS); } resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(id) .status(status) .approveTime(java.time.LocalDateTime.now()) .build()); } @Override public HrmResignationApplicationDO getResignationApplicationByProcessInstanceId(String processInstanceId) { return resignationApplicationMapper.selectByProcessInstanceId(processInstanceId); } @Override @Transactional(rollbackFor = Exception.class) public void updateHandoverStatus(HrmResignationHandoverReqVO reqVO) { HrmResignationApplicationDO application = validateResignationApplicationExists(reqVO.getId()); Integer targetStatus = reqVO.getHandoverStatus(); if (!Objects.equals(targetStatus, HANDOVER_STATUS_NOT_STARTED) && !Objects.equals(targetStatus, HANDOVER_STATUS_IN_PROGRESS) && !Objects.equals(targetStatus, HANDOVER_STATUS_COMPLETED)) { throw new ServiceException(RESIGNATION_HANDOVER_STATUS_INVALID.getCode(), String.format("交接状态值[%s]不合法,仅支持 0(未开始)、10(进行中)、20(已完成)", targetStatus)); } if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.APPROVE.getStatus())) { throw new ServiceException(RESIGNATION_HANDOVER_NOT_APPROVED.getCode(), String.format("离职申请[%s]当前审批状态为[%s],审批通过后才能交接;请先完成审批再操作", application.getNo(), HrmAuditStatusEnum.getNameByStatus(application.getStatus()))); } Integer currentStatus = application.getHandoverStatus() != null ? application.getHandoverStatus() : HANDOVER_STATUS_NOT_STARTED; if (Objects.equals(currentStatus, HANDOVER_STATUS_COMPLETED) && !Objects.equals(targetStatus, HANDOVER_STATUS_COMPLETED)) { throw new ServiceException(RESIGNATION_HANDOVER_COMPLETED_CANNOT_REVERT.getCode(), String.format("当前交接状态为[已完成],不能修改为[%s];如需重新交接,请删除该离职申请后重新提交", getHandoverStatusName(targetStatus))); } // 审批通过后允许一步完成交接:未开始或进行中均可直接置为已完成,避免多一次无意义的点击 if (Objects.equals(targetStatus, HANDOVER_STATUS_COMPLETED)) { completeHandover(application); return; } if (Objects.equals(targetStatus, HANDOVER_STATUS_NOT_STARTED) && !Objects.equals(currentStatus, HANDOVER_STATUS_NOT_STARTED) && !Objects.equals(currentStatus, HANDOVER_STATUS_IN_PROGRESS)) { throw new ServiceException(RESIGNATION_HANDOVER_COMPLETED_CANNOT_REVERT.getCode(), String.format("当前交接状态为[%s],不能回退为[未开始];如需重新交接,请删除该离职申请后重新提交", getHandoverStatusName(currentStatus))); } // 进行中:必须已指定交接员工;未开始:允许在未指定交接人时回退 if (Objects.equals(targetStatus, HANDOVER_STATUS_IN_PROGRESS)) { resolveStoredHandoverTarget(application); } resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(application.getId()) .handoverStatus(targetStatus) .handoverDataStatus(targetStatus) .build()); } /** * 完成工作交接:写入 员工ID + 用户ID 交接映射,交接人通过 expandUserIds 获得离职人的业务数据 */ private void completeHandover(HrmResignationApplicationDO application) { resolveStoredHandoverTarget(application); // 申请人未建员工档案时(如管理员账号),仍建立用户级交接映射,保证其业务数据能转交;员工侧 ID 为空 HrmEmployeeDO fromEmployee = employeeService.getEmployeeByUserId(application.getUserId()); if (fromEmployee == null) { log.warn("[completeHandover][离职申请({}) 申请人(用户ID={}) 未关联员工档案,仅建立用户级交接映射]", application.getNo(), application.getUserId()); } handoverService.createHandover(fromEmployee != null ? fromEmployee.getId() : null, application.getUserId(), application.getHandoverEmployeeId(), application.getHandoverUserId(), application.getId()); resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(application.getId()) .handoverStatus(HANDOVER_STATUS_COMPLETED) .handoverDataStatus(HANDOVER_STATUS_COMPLETED) .build()); } /** * 校验并回填已保存的交接人(兼容历史数据只存了 handoverUserId 的情况) */ private void resolveStoredHandoverTarget(HrmResignationApplicationDO application) { if (application.getHandoverEmployeeId() == null && application.getHandoverUserId() == null) { throw new ServiceException(RESIGNATION_HANDOVER_TARGET_REQUIRED.getCode(), String.format("离职申请[%s]未指定交接员工,无法开始或完成交接;请先编辑申请并选择交接人", application.getNo())); } resolveHandoverTarget(application, application.getUserId()); resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(application.getId()) .handoverEmployeeId(application.getHandoverEmployeeId()) .handoverUserId(application.getHandoverUserId()) .build()); } @Override @Transactional(rollbackFor = Exception.class) public void cancelResignationApplication(Long id, Long userId) { HrmResignationApplicationDO application = validateResignationApplicationExists(id); Integer status = application.getStatus(); if (Objects.equals(status, HrmAuditStatusEnum.DRAFT.getStatus())) { resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(id) .status(HrmAuditStatusEnum.CANCEL.getStatus()) .build()); return; } if (!Objects.equals(status, HrmAuditStatusEnum.PROCESS.getStatus())) { throw new ServiceException(RESIGNATION_APPLICATION_CANCEL_FAIL_STATUS.getCode(), String.format("离职申请[%s]当前审批状态为[%s],仅草稿或审批中状态可以撤销", application.getNo(), HrmAuditStatusEnum.getNameByStatus(status))); } if (StrUtil.isNotBlank(application.getProcessInstanceId())) { BpmProcessInstanceCancelReqVO cancelReqVO = new BpmProcessInstanceCancelReqVO(); cancelReqVO.setId(application.getProcessInstanceId()); cancelReqVO.setReason("申请人撤销离职申请:" + application.getNo()); Long startUserId = userId != null ? userId : application.getUserId(); bpmProcessInstanceService.cancelProcessInstanceByStartUser(startUserId, cancelReqVO); // 流程监听器会同步更新状态;兜底处理未收到回调的场景 HrmResignationApplicationDO latest = resignationApplicationMapper.selectById(id); if (latest != null && Objects.equals(latest.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) { resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(id) .status(HrmAuditStatusEnum.CANCEL.getStatus()) .build()); } return; } // 无流程实例(历史免审批数据)直接撤销 resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(id) .status(HrmAuditStatusEnum.CANCEL.getStatus()) .build()); } @Override public int autoLeaveResignationApplication() { LocalDate today = LocalDate.now(); List applications = resignationApplicationMapper .selectListByStatusAndExpectedDateLe(HrmAuditStatusEnum.APPROVE.getStatus(), today); int count = 0; for (HrmResignationApplicationDO application : applications) { if (application.getUserId() == null) { log.warn("[autoLeaveResignationApplication][申请({}) 缺少申请人用户ID,跳过]", application.getId()); continue; } // 1. 停用系统登录账号 AdminUserRespDTO user = adminUserApi.getUser(application.getUserId()); if (user == null) { log.warn("[autoLeaveResignationApplication][申请({}) 申请人(用户ID={}) 不存在,跳过账号停用]", application.getId(), application.getUserId()); } else if (!Objects.equals(user.getStatus(), CommonStatusEnum.DISABLE.getStatus())) { adminUserApi.updateUserStatus(application.getUserId(), CommonStatusEnum.DISABLE.getStatus()); log.info("[autoLeaveResignationApplication][用户({}) 到达离职日期({}),系统账号已停用]", application.getUserId(), application.getExpectedDate()); } // 2. 员工档案置为离职 HrmEmployeeDO employee = employeeService.getEmployeeByUserId(application.getUserId()); if (employee == null) { log.warn("[autoLeaveResignationApplication][申请({}) 申请人(用户ID={}) 无员工档案,跳过;请先建立员工档案]", application.getId(), application.getUserId()); continue; } if (!Objects.equals(employee.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED)) { employeeService.updateEmployeeStatusToResignedByUserId(application.getUserId(), application.getExpectedDate()); log.info("[autoLeaveResignationApplication][员工({}) 到达离职日期({}),状态已更新为离职]", employee.getId(), application.getExpectedDate()); } resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder() .id(application.getId()) .actualDate(application.getExpectedDate()) .build()); count++; } return count; } /** * 解析交接员工与系统用户,保证 handoverEmployeeId / handoverUserId 成对且有效 *

* 兼容前端把“交接人”下拉的员工ID写入 handoverUserId 的历史提交方式: * 先按 handoverEmployeeId 解析;没有时把 handoverUserId 当员工ID尝试;再按用户ID解析。 */ private void resolveHandoverTarget(HrmResignationApplicationDO application, Long applyUserId) { Long employeeId = application.getHandoverEmployeeId(); Long userId = application.getHandoverUserId(); if (employeeId == null && userId == null) { return; } HrmEmployeeDO employee; if (employeeId != null) { employee = employeeMapper.selectById(employeeId); if (employee == null) { throw new ServiceException(RESIGNATION_HANDOVER_EMPLOYEE_NOT_EXISTS.getCode(), String.format("交接员工(ID=%s)不存在,请重新选择交接员工", employeeId)); } } else { // 历史前端:handoverUserId 实际传的是员工ID employee = employeeMapper.selectById(userId); if (employee == null) { AdminUserRespDTO user = adminUserApi.getUser(userId); if (user == null) { throw new ServiceException(RESIGNATION_HANDOVER_EMPLOYEE_NOT_EXISTS.getCode(), String.format("交接人(ID=%s)在员工档案和系统用户中均不存在,请重新选择交接员工", userId)); } employee = employeeMapper.selectByUserId(userId); if (employee == null) { throw new ServiceException(RESIGNATION_HANDOVER_EMPLOYEE_NO_USER.getCode(), String.format("交接人[%s](用户ID=%s)未关联员工档案,无法确定交接员工;请先在员工管理中为该用户建立员工档案", user.getNickname(), userId)); } } employeeId = employee.getId(); } if (employee.getUserId() == null) { throw new ServiceException(RESIGNATION_HANDOVER_EMPLOYEE_NO_USER.getCode(), String.format("交接员工[%s](员工ID=%s)未关联系统用户,无法转移其用户数据;请先在员工管理中关联系统用户", employee.getName(), employee.getId())); } if (applyUserId != null && Objects.equals(employee.getUserId(), applyUserId)) { throw new ServiceException(RESIGNATION_HANDOVER_SELF.getCode(), String.format("交接人[%s](员工ID=%s)与申请人是同一人,请选择其他员工", employee.getName(), employee.getId())); } if (Objects.equals(employee.getEmployeeStatus(), EMPLOYEE_STATUS_RESIGNED)) { throw new ServiceException(RESIGNATION_HANDOVER_EMPLOYEE_RESIGNED.getCode(), String.format("交接员工[%s](员工ID=%s)当前已是离职状态,请重新选择在职员工", employee.getName(), employee.getId())); } application.setHandoverEmployeeId(employeeId); application.setHandoverUserId(employee.getUserId()); } private String getEmployeeDisplayName(Long employeeId) { if (employeeId == null) { return "未选择"; } HrmEmployeeDO employee = employeeMapper.selectById(employeeId); return employee != null ? String.format("%s(ID=%s)", employee.getName(), employeeId) : String.format("ID=%s", employeeId); } }