yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java
@@ -2,6 +2,8 @@
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;
@@ -12,11 +14,14 @@
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;
@@ -33,6 +38,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -48,7 +54,7 @@
/**
 * 调岗申请 Service 实现类
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@@ -66,6 +72,8 @@
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private HrmEmployeeService employeeService;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private BpmCategoryService bpmCategoryService;
@@ -77,25 +85,60 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createTransferApplication(HrmTransferApplicationSaveReqVO createReqVO) {
        // 1. 校验目标部门与原部门不能相同
        // 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);
        }
        // 2. 校验目标岗位与原岗位不能相同
        // 3. 校验目标岗位与原岗位不能相同
        if (Objects.equals(createReqVO.getTargetPostId(), createReqVO.getCurrentPostId())) {
            throw exception(TRANSFER_APPLICATION_SAME_POST);
        }
        // 3. 生成申请单号
        // 4. 生成申请单号
        String no = generateUniqueNo();
        // 4. 设置申请人信息
        Long userId = createReqVO.getUserId();
        if (userId == null) {
            userId = getCurrentUserId();
        }
        AdminUserRespDTO user = adminUserApi.getUser(userId);
        if (user == null) {
            userId = getCurrentUserId();
        }
        // 5. 插入
        HrmTransferApplicationDO application = BeanUtils.toBean(createReqVO, HrmTransferApplicationDO.class);
        application.setNo(no);
@@ -105,6 +148,27 @@
        storageAttachmentApi.bindAttachments("file", "hrm_transfer_application",
                application.getId(), createReqVO.getBlobIds());
        return application.getId();
    }
    /**
     * 取员工岗位列表中的第一个作为「原岗位」
     *
     * <p>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<Long> 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() {
@@ -168,9 +232,14 @@
        }
        HrmTransferApplicationRespVO respVO = BeanUtils.toBean(application, HrmTransferApplicationRespVO.class);
        if (application.getUserId() != null) {
            AdminUserRespDTO user = adminUserApi.getUser(application.getUserId());
            if (user != null) {
                respVO.setUserName(user.getNickname());
            // 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);
@@ -215,7 +284,17 @@
                .map(HrmTransferApplicationDO::getUserId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        // user_id 正常存 system_users.id;历史数据也有存「员工主键」的,两种都兼容
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        Map<Long, String> 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<Long> deptIds = pageResult.getList().stream()
                .flatMap(app -> Stream.of(app.getCurrentDeptId(), app.getTargetDeptId()))
                .filter(Objects::nonNull)
@@ -228,8 +307,12 @@
        Map<Long, PostRespDTO> postMap = postApi.getPostMap(postIds);
        List<HrmTransferApplicationRespVO> list = pageResult.getList().stream().map(app -> {
            HrmTransferApplicationRespVO vo = BeanUtils.toBean(app, HrmTransferApplicationRespVO.class);
            if (app.getUserId() != null && userMap.containsKey(app.getUserId())) {
                vo.setUserName(userMap.get(app.getUserId()).getNickname());
            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());
@@ -258,17 +341,40 @@
            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(processDefinitionKey).setBusinessKey(String.valueOf(id)));
                .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<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
        if (CollUtil.isEmpty(definitionInfoList)) {
            throw exception(TRANSFER_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
        List<ProcessDefinition> 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() {
@@ -329,6 +435,42 @@
                .status(status)
                .approveTime(java.time.LocalDateTime.now())
                .build());
        // 审批通过:把员工档案里的部门/岗位同步成调岗后的目标值
        if (HrmAuditStatusEnum.APPROVE.getStatus().equals(status)) {
            syncEmployeePosition(application);
        }
    }
    /**
     * 调岗审批通过后,把员工档案的部门/岗位更新为目标部门/岗位
     *
     * <p>走 {@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