From d39e4f7d839167c16425086dfa2f8747bccfdff9 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 05 十一月 2026 17:39:01 +0800
Subject: [PATCH] feat: 打通销售采购财务闭环并完成累计业务优化

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java |  392 +++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 362 insertions(+), 30 deletions(-)

diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
index df408d2..8b8fe32 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
+++ b/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);
+        // 缁熶竴瑙f瀽浜ゆ帴鍛樺伐涓庣郴缁熺敤鎴凤紝纭繚鍛樺伐ID銆佺敤鎴稩D鎴愬钀藉簱
+        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);
+        // 涓嶈鐩栫敵璇蜂汉锛涗氦鎺ヤ汉缁熶竴瑙f瀽鎴愨�滃憳宸D + 鐢ㄦ埛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());
             }
+            // 鐢宠浜哄憳宸ヤ俊鎭細鍛樺伐鏈缓妗f椂淇濇寔涓虹┖
+            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);
         }
+    }
+
+    /**
+     * 瑙f瀽绂昏亴瀹℃壒鍙敤鐨勬祦绋嬪畾涔�
+     * <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锛屽彲鐢↘ey=%s)锛岃浼犲叆鍏蜂綋鐨勬祦绋嬪畾涔塊ey鍚庨噸璇�",
+                        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][鐢宠({}) 缂哄皯鐢宠浜虹敤鎴稩D锛岃烦杩嘳", 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. 鍛樺伐妗f缃负绂昏亴
+            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;
+    }
+
+    /**
+     * 瑙f瀽浜ゆ帴鍛樺伐涓庣郴缁熺敤鎴凤紝淇濊瘉 handoverEmployeeId / handoverUserId 鎴愬涓旀湁鏁�
+     * <p>
+     * 鍏煎鍓嶇鎶娾�滀氦鎺ヤ汉鈥濅笅鎷夌殑鍛樺伐ID鍐欏叆 handoverUserId 鐨勫巻鍙叉彁浜ゆ柟寮忥細
+     * 鍏堟寜 handoverEmployeeId 瑙f瀽锛涙病鏈夋椂鎶� handoverUserId 褰撳憳宸D灏濊瘯锛涘啀鎸夌敤鎴稩D瑙f瀽銆�
+     */
+    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 {
+            // 鍘嗗彶鍓嶇锛歨andoverUserId 瀹為檯浼犵殑鏄憳宸D
+            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);
     }
 
 }
\ No newline at end of file

--
Gitblit v1.9.3