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.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 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.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.hrm.controller.admin.approval.vo.HrmTransferApplicationPageReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmTransferApplicationRespVO; import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmTransferApplicationSaveReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSaveReqVO; import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmTransferApplicationDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmTransferApplicationMapper; import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO; import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum; import cn.iocoder.yudao.module.hrm.enums.HrmTransferTypeEnum; import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService; 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.storage.StorageAttachmentApi; 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.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.util.ArrayList; import java.util.Collections; import java.util.Comparator; 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 HrmTransferApplicationServiceImpl implements HrmTransferApplicationService { @Resource private HrmTransferApplicationMapper transferApplicationMapper; @Resource private HrmNoRedisDAO noRedisDAO; @Resource private DeptApi deptApi; @Resource private PostApi postApi; @Resource private AdminUserApi adminUserApi; @Resource private HrmEmployeeService employeeService; @Resource private BpmProcessInstanceApi bpmProcessInstanceApi; @Resource private BpmCategoryService bpmCategoryService; @Resource private StorageAttachmentApi storageAttachmentApi; @Resource private BpmProcessDefinitionService bpmProcessDefinitionService; @Override @Transactional(rollbackFor = Exception.class) public Long createTransferApplication(HrmTransferApplicationSaveReqVO createReqVO) { // 1. 解析申请人 & 补全原部门/原岗位 // 前端表单只让用户选择「被调岗员工」,不直接提交 currentDeptId/currentPostId, // 而这两个字段在库里是 NOT NULL,缺省会直接抛 SQL 异常(Field 'current_dept_id' doesn't have a default value) Long userId = createReqVO.getUserId(); Long employeeId = createReqVO.getEmployeeId(); HrmEmployeeDO employee = null; if (employeeId != null) { employee = employeeService.getEmployee(employeeId); } if (employee == null && userId != null) { // 约定 user_id 存 system_users.id;但历史上前端也有把「员工主键」塞进 userId 的情况,两种都兼容 employee = employeeService.getEmployeeByUserId(userId); if (employee == null) { employee = employeeService.getEmployee(userId); } } // 以员工档案上的 user_id 为准(前端可能把员工主键塞在 userId 字段里,直接用会写错申请人) if (employee != null && employee.getUserId() != null) { userId = employee.getUserId(); } if (userId == null && employee != null) { userId = employee.getUserId(); } if (userId == null) { userId = getCurrentUserId(); if (employee == null) { employee = employeeService.getEmployeeByUserId(userId); } } if (adminUserApi.getUser(userId) == null) { userId = getCurrentUserId(); } if (createReqVO.getCurrentDeptId() == null && employee != null) { createReqVO.setCurrentDeptId(employee.getDeptId()); } if (createReqVO.getCurrentPostId() == null && employee != null) { createReqVO.setCurrentPostId(parseFirstPostId(employee.getPostIds())); } if (createReqVO.getCurrentDeptId() == null) { throw exception(TRANSFER_APPLICATION_CURRENT_DEPT_REQUIRED); } if (createReqVO.getCurrentPostId() == null) { throw exception(TRANSFER_APPLICATION_CURRENT_POST_REQUIRED); } // 2. 校验目标部门与原部门不能相同(放在补全之后,否则补全出来的原部门绕过了校验) if (Objects.equals(createReqVO.getTargetDeptId(), createReqVO.getCurrentDeptId())) { throw exception(TRANSFER_APPLICATION_SAME_DEPT); } // 3. 校验目标岗位与原岗位不能相同 if (Objects.equals(createReqVO.getTargetPostId(), createReqVO.getCurrentPostId())) { throw exception(TRANSFER_APPLICATION_SAME_POST); } // 4. 生成申请单号 String no = generateUniqueNo(); // 5. 插入 HrmTransferApplicationDO application = BeanUtils.toBean(createReqVO, HrmTransferApplicationDO.class); application.setNo(no); application.setUserId(userId); application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus()); transferApplicationMapper.insert(application); storageAttachmentApi.bindAttachments("file", "hrm_transfer_application", application.getId(), createReqVO.getBlobIds()); return application.getId(); } /** * 取员工岗位列表中的第一个作为「原岗位」 * *

hrm_employee.post_ids 以 JSON 数组字符串存储,例如 {@code [20,21]} * * @param postIds 岗位编号 JSON 字符串 * @return 第一个岗位编号;解析不出来时返回 {@code null} */ private Long parseFirstPostId(String postIds) { if (StrUtil.isBlank(postIds)) { return null; } try { List ids = JSONUtil.toList(postIds, Long.class); return CollUtil.isEmpty(ids) ? null : ids.get(0); } catch (Exception ex) { log.warn("[parseFirstPostId][postIds({}) 解析失败]", postIds, ex); return null; } } private Long getCurrentUserId() { return 1L; } private String generateUniqueNo() { String no = noRedisDAO.generateTransferApplicationNo(); if (transferApplicationMapper.selectByNo(no) != null) { return generateUniqueNo(); } return no; } @Override public void updateTransferApplication(HrmTransferApplicationSaveReqVO updateReqVO) { HrmTransferApplicationDO application = validateTransferApplicationExists(updateReqVO.getId()); if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus()) && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(TRANSFER_APPLICATION_UPDATE_FAIL_NOT_DRAFT); } if (Objects.equals(updateReqVO.getTargetDeptId(), updateReqVO.getCurrentDeptId())) { throw exception(TRANSFER_APPLICATION_SAME_DEPT); } if (Objects.equals(updateReqVO.getTargetPostId(), updateReqVO.getCurrentPostId())) { throw exception(TRANSFER_APPLICATION_SAME_POST); } HrmTransferApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmTransferApplicationDO.class); transferApplicationMapper.updateById(updateObj); storageAttachmentApi.updateAttachments("file", "hrm_transfer_application", updateReqVO.getId(), updateReqVO.getBlobIds()); } @Override public void deleteTransferApplication(Long id) { HrmTransferApplicationDO application = validateTransferApplicationExists(id); if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) { throw exception(TRANSFER_APPLICATION_UPDATE_FAIL_NOT_DRAFT); } storageAttachmentApi.deleteAttachmentsByRecord("hrm_transfer_application", id); transferApplicationMapper.deleteById(id); } private HrmTransferApplicationDO validateTransferApplicationExists(Long id) { HrmTransferApplicationDO application = transferApplicationMapper.selectById(id); if (application == null) { throw exception(TRANSFER_APPLICATION_NOT_EXISTS); } return application; } @Override public HrmTransferApplicationDO getTransferApplication(Long id) { return transferApplicationMapper.selectById(id); } @Override public HrmTransferApplicationRespVO getTransferApplicationDetail(Long id) { HrmTransferApplicationDO application = getTransferApplication(id); if (application == null) { return null; } HrmTransferApplicationRespVO respVO = BeanUtils.toBean(application, HrmTransferApplicationRespVO.class); if (application.getUserId() != null) { // user_id 正常存 system_users.id;历史数据也有存「员工主键」的,两种都兼容 HrmEmployeeDO employee = employeeService.getEmployeeByUserId(application.getUserId()); if (employee == null) { employee = employeeService.getEmployee(application.getUserId()); } if (employee != null) { respVO.setUserName(employee.getName()); respVO.setEmployeeId(employee.getId()); } } fillDeptNames(respVO, application); fillPostNames(respVO, application); respVO.setTransferTypeName(HrmTransferTypeEnum.getNameByStatus(application.getTransferType())); respVO.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus())); respVO.setAttachmentList(storageAttachmentApi.listAttachments("hrm_transfer_application", id)); return respVO; } private void fillDeptNames(HrmTransferApplicationRespVO respVO, HrmTransferApplicationDO application) { Set deptIds = Set.of(application.getCurrentDeptId(), application.getTargetDeptId()) .stream().filter(Objects::nonNull).collect(Collectors.toSet()); Map deptMap = deptApi.getDeptMap(deptIds); if (application.getCurrentDeptId() != null && deptMap.containsKey(application.getCurrentDeptId())) { respVO.setCurrentDeptName(deptMap.get(application.getCurrentDeptId()).getName()); } if (application.getTargetDeptId() != null && deptMap.containsKey(application.getTargetDeptId())) { respVO.setTargetDeptName(deptMap.get(application.getTargetDeptId()).getName()); } } private void fillPostNames(HrmTransferApplicationRespVO respVO, HrmTransferApplicationDO application) { Set postIds = Set.of(application.getCurrentPostId(), application.getTargetPostId()) .stream().filter(Objects::nonNull).collect(Collectors.toSet()); Map postMap = postApi.getPostMap(postIds); if (application.getCurrentPostId() != null && postMap.containsKey(application.getCurrentPostId())) { respVO.setCurrentPostName(postMap.get(application.getCurrentPostId()).getName()); } if (application.getTargetPostId() != null && postMap.containsKey(application.getTargetPostId())) { respVO.setTargetPostName(postMap.get(application.getTargetPostId()).getName()); } } @Override public PageResult getTransferApplicationPage(HrmTransferApplicationPageReqVO pageReqVO) { PageResult pageResult = transferApplicationMapper.selectPage(pageReqVO); if (pageResult.getList().isEmpty()) { return new PageResult<>(Collections.emptyList(), pageResult.getTotal()); } Set userIds = pageResult.getList().stream() .map(HrmTransferApplicationDO::getUserId) .filter(Objects::nonNull) .collect(Collectors.toSet()); // user_id 正常存 system_users.id;历史数据也有存「员工主键」的,两种都兼容 Map userMap = adminUserApi.getUserMap(userIds); Map employeeNameMap = new HashMap<>(); for (Long userId : userIds) { if (!userMap.containsKey(userId)) { HrmEmployeeDO employee = employeeService.getEmployee(userId); if (employee != null) { employeeNameMap.put(userId, employee.getName()); } } } Set deptIds = pageResult.getList().stream() .flatMap(app -> Stream.of(app.getCurrentDeptId(), app.getTargetDeptId())) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map deptMap = deptApi.getDeptMap(deptIds); Set postIds = pageResult.getList().stream() .flatMap(app -> Stream.of(app.getCurrentPostId(), app.getTargetPostId())) .filter(Objects::nonNull) .collect(Collectors.toSet()); Map postMap = postApi.getPostMap(postIds); List list = pageResult.getList().stream().map(app -> { HrmTransferApplicationRespVO vo = BeanUtils.toBean(app, HrmTransferApplicationRespVO.class); if (app.getUserId() != null) { if (userMap.containsKey(app.getUserId())) { vo.setUserName(userMap.get(app.getUserId()).getNickname()); } else if (employeeNameMap.containsKey(app.getUserId())) { vo.setUserName(employeeNameMap.get(app.getUserId())); } } if (app.getCurrentDeptId() != null && deptMap.containsKey(app.getCurrentDeptId())) { vo.setCurrentDeptName(deptMap.get(app.getCurrentDeptId()).getName()); } if (app.getTargetDeptId() != null && deptMap.containsKey(app.getTargetDeptId())) { vo.setTargetDeptName(deptMap.get(app.getTargetDeptId()).getName()); } if (app.getCurrentPostId() != null && postMap.containsKey(app.getCurrentPostId())) { vo.setCurrentPostName(postMap.get(app.getCurrentPostId()).getName()); } if (app.getTargetPostId() != null && postMap.containsKey(app.getTargetPostId())) { vo.setTargetPostName(postMap.get(app.getTargetPostId()).getName()); } vo.setTransferTypeName(HrmTransferTypeEnum.getNameByStatus(app.getTransferType())); vo.setStatusName(HrmAuditStatusEnum.getNameByStatus(app.getStatus())); return vo; }).collect(Collectors.toList()); return new PageResult<>(list, pageResult.getTotal()); } @Override @Transactional(rollbackFor = Exception.class) public void submitTransferApplication(Long id, String processDefinitionKey, Long userId) { HrmTransferApplicationDO application = validateTransferApplicationExists(id); if (ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) { throw exception(TRANSFER_APPLICATION_SUBMIT_FAIL_NOT_DRAFT); } validateApproveCategoryAndProcessDefinition(); // 前端不传流程定义 Key 时,自动取该审批分类下最新版本的活跃流程, // 避免直接把 MissingServletRequestParameterException 抛给用户 if (StrUtil.isBlank(processDefinitionKey)) { processDefinitionKey = resolveActiveProcessDefinitionKey(); } ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey); if (processDefinition == null) { throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO() .setProcessDefinitionKey(processDefinition.getKey()).setBusinessKey(String.valueOf(id))); transferApplicationMapper.updateById(HrmTransferApplicationDO.builder() .id(id) .processInstanceId(processInstanceId) .status(HrmAuditStatusEnum.PROCESS.getStatus()) .build()); } /** * 取调岗审批分类下最新版本的活跃流程定义 Key */ private String resolveActiveProcessDefinitionKey() { List definitionInfoList = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE); if (CollUtil.isEmpty(definitionInfoList)) { throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } Set processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId); List processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds); return processDefinitions.stream() .filter(pd -> !pd.isSuspended()) .max(Comparator.comparingInt(ProcessDefinition::getVersion)) .map(ProcessDefinition::getKey) .orElseThrow(() -> exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS)); } private void validateApproveCategoryAndProcessDefinition() { List codes = Collections.singletonList(APPROVE_CATEGORY_CODE); Map categoryMap = bpmCategoryService.getCategoryMap(codes); if (!categoryMap.containsKey(APPROVE_CATEGORY_CODE)) { throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } List definitionInfoList = bpmProcessDefinitionService .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE); if (CollUtil.isEmpty(definitionInfoList)) { throw exception(TRANSFER_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(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS); } } @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 updateTransferApplicationAuditStatus(Long id, Integer status) { HrmTransferApplicationDO application = validateTransferApplicationExists(id); if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) { log.error("[updateTransferApplicationAuditStatus][application({}) 不处于审批中,无法更新审批结果({})]", id, status); throw exception(TRANSFER_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS); } transferApplicationMapper.updateById(HrmTransferApplicationDO.builder() .id(id) .status(status) .approveTime(java.time.LocalDateTime.now()) .build()); // 审批通过:把员工档案里的部门/岗位同步成调岗后的目标值 if (HrmAuditStatusEnum.APPROVE.getStatus().equals(status)) { syncEmployeePosition(application); } } /** * 调岗审批通过后,把员工档案的部门/岗位更新为目标部门/岗位 * *

走 {@link HrmEmployeeService#updateEmployee} 而不是直接改表, * 这样 system_users 上的部门/岗位关联也会一并同步,员工管理列表能立刻看到新岗位。 */ private void syncEmployeePosition(HrmTransferApplicationDO application) { if (application.getTargetDeptId() == null || application.getTargetPostId() == null) { return; } try { HrmEmployeeDO employee = employeeService.getEmployeeByUserId(application.getUserId()); if (employee == null) { employee = employeeService.getEmployee(application.getUserId()); } if (employee == null) { log.warn("[syncEmployeePosition][调岗申请({}) 找不到员工档案,跳过档案同步]", application.getId()); return; } HrmEmployeeSaveReqVO updateReqVO = BeanUtils.toBean(employee, HrmEmployeeSaveReqVO.class); updateReqVO.setDeptId(application.getTargetDeptId()); // 调岗按单岗位处理,直接落目标岗位 updateReqVO.setPostIds(Collections.singletonList(application.getTargetPostId())); employeeService.updateEmployee(updateReqVO); log.info("[syncEmployeePosition][调岗申请({}) 已将员工({}) 部门改为 {}、岗位改为 {}]", application.getId(), employee.getName(), application.getTargetDeptId(), application.getTargetPostId()); } catch (Exception ex) { // 档案同步失败(例如目标部门/岗位已被删除)不应该阻断审批状态落库,只记录日志 log.error("[syncEmployeePosition][调岗申请({}) 同步员工档案失败]", application.getId(), ex); } } @Override public HrmTransferApplicationDO getTransferApplicationByProcessInstanceId(String processInstanceId) { return transferApplicationMapper.selectByProcessInstanceId(processInstanceId); } }