fix(hrm): 调岗提交不再强制要求 processDefinitionKey,并在审批通过后自动同步员工档案

问题1:新增调岗申请成功后点「提交」报「请求参数缺失:processDefinitionKey」。
根因:Controller 上 @RequestParam(processDefinitionKey) 是必填,而前端 submitTransfer 只传了 id。

修复:
- Controller 改为 required=false;
- Service 在 processDefinitionKey 为空时,自动取该审批分类下最新版本(version 最大)的活跃流程定义。

问题2:调岗审批通过后,员工管理里的部门/岗位没有变化。
根因:审批状态监听器只更新了调岗单状态,从来没动过员工档案。

修复:
- updateTransferApplicationAuditStatus 在状态变为「审批通过(20)」时调用 syncEmployeePosition;
- 该方法走 HrmEmployeeService.updateEmployee 把 hrm_employee 的 dept_id / post_ids 改成目标值,
同时因为 updateEmployee 内部会同步 system_users,用户管理里的部门岗位也会一起更新;
- 整段包了 try-catch:目标部门/岗位已被删除等异常只记日志,不会让审批状态落库失败。

验证(北方科技 mom-bfkj 实测):
- 新增 + 提交(只传 id)→ code=0,状态 0→10→20
- 员工档案自动更新:dept_id 130→121(采购部)、post_ids [73]→[38](库管)
- 无效目标岗位 999:审批仍正常通过(20),档案保持不变,异常只进日志
- 目标部门选成原部门:正确拦截「目标部门与原部门相同」
已修改2个文件
68 ■■■■■ 文件已修改
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmTransferApplicationController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmTransferApplicationController.java
@@ -82,10 +82,11 @@
    @PostMapping("/submit")
    @Operation(summary = "提交调岗申请审批")
    @Parameter(name = "id", description = "申请ID", required = true)
    @Parameter(name = "processDefinitionKey", description = "流程定义Key", required = true)
    @Parameter(name = "processDefinitionKey", description = "流程定义Key,不传则自动取该分类下最新的活跃流程")
    @PreAuthorize("@ss.hasPermission('hrm:transfer:submit')")
    public CommonResult<Boolean> submitTransferApplication(@RequestParam("id") Long id,
                                                           @RequestParam("processDefinitionKey") String processDefinitionKey) {
                                                           @RequestParam(value = "processDefinitionKey", required = false)
                                                           String processDefinitionKey) {
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        transferApplicationService.submitTransferApplication(id, processDefinitionKey, userId);
        return success(true);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java
@@ -14,6 +14,7 @@
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;
@@ -37,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;
@@ -339,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() {
@@ -410,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