gaoluyang
3 天以前 d39e4f7d839167c16425086dfa2f8747bccfdff9
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
@@ -2,25 +2,37 @@
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;
@@ -31,6 +43,8 @@
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;
@@ -59,12 +73,17 @@
    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
@@ -76,31 +95,40 @@
    @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();
        if (userId == null) {
            userId = getCurrentUserId();
        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 exception(RESIGNATION_APPLICATION_EXISTS);
        }
        String no = generateUniqueNo();
        AdminUserRespDTO user = adminUserApi.getUser(userId);
        if (user == null) {
            userId = getCurrentUserId();
            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(no);
        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());
@@ -108,7 +136,7 @@
    }
    private Long getCurrentUserId() {
        return 1L;
        return SecurityFrameworkUtils.getLoginUserId();
    }
    private String generateUniqueNo() {
@@ -126,6 +154,22 @@
            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());
    }
@@ -161,6 +205,7 @@
        }
        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) {
@@ -169,14 +214,15 @@
        }
        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<Long> userIds = Set.of(application.getUserId(), application.getHandoverUserId())
                .stream().filter(Objects::nonNull).collect(Collectors.toSet());
        Set<Long> userIds = Stream.of(application.getUserId(), application.getHandoverUserId())
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        if (application.getUserId() != null && userMap.containsKey(application.getUserId())) {
            respVO.setUserName(userMap.get(application.getUserId()).getNickname());
@@ -184,6 +230,34 @@
        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<Long> postIds = JSONUtil.toList(employee.getPostIds(), Long.class);
        if (CollUtil.isEmpty(postIds)) {
            return;
        }
        Map<Long, PostRespDTO> 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) {
@@ -223,6 +297,13 @@
            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());
            }
@@ -231,6 +312,7 @@
            }
            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());
@@ -255,13 +337,9 @@
            return;
        }
        validateApproveCategoryAndProcessDefinition();
        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        if (processDefinition == null) {
            throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        ProcessDefinition processDefinition = resolveActiveProcessDefinition(processDefinitionKey);
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id)));
                .setProcessDefinitionKey(processDefinition.getKey()).setBusinessKey(String.valueOf(id)));
        resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder()
                .id(id)
                .processInstanceId(processInstanceId)
@@ -286,6 +364,45 @@
        if (!hasActiveDefinition) {
            throw exception(RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
    }
    /**
     * 解析离职审批可用的流程定义
     * <p>
     * 前端历史版本把分类编码(hrm_resignation_approve)当作流程 Key 传入,
     * 这里优先使用传入 Key,匹配不到时回退到标准 Key,再回退到该分类下唯一的激活流程。
     */
    private ProcessDefinition resolveActiveProcessDefinition(String processDefinitionKey) {
        validateApproveCategoryAndProcessDefinition();
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
        Map<String, ProcessDefinition> 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
@@ -338,18 +455,233 @@
    @Transactional(rollbackFor = Exception.class)
    public void updateHandoverStatus(HrmResignationHandoverReqVO reqVO) {
        HrmResignationApplicationDO application = validateResignationApplicationExists(reqVO.getId());
        resignationApplicationMapper.updateById(HrmResignationApplicationDO.builder()
                .id(reqVO.getId())
                .handoverStatus(reqVO.getHandoverStatus())
                .build());
        // 交接完成时,创建用户数据交接映射
        if (Objects.equals(reqVO.getHandoverStatus(), HANDOVER_STATUS_COMPLETED)
                && application.getHandoverUserId() != null) {
            handoverService.createHandover(
                    application.getUserId(),
                    application.getHandoverUserId(),
                    application.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<HrmResignationApplicationDO> 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 成对且有效
     * <p>
     * 兼容前端把“交接人”下拉的员工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);
    }
}