From ce5f8a87bb5d411bae67c39b269d879677bb98da Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期日, 08 十一月 2026 19:29:25 +0800
Subject: [PATCH] feat(hrm): 请假导入、加班代申请、批量排班与考勤应出勤重算

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

diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java
index bda7f3a..14b5075 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java
@@ -2,19 +2,27 @@
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 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.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
 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.HrmLeaveApplicationImportExcelVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationImportFailureVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationImportRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationPageReqVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmLeaveApplicationSaveReqVO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
 import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper;
+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.HrmLeaveTypeEnum;
@@ -30,6 +38,12 @@
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -37,6 +51,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -46,15 +61,19 @@
 /**
  * 璇峰亣鐢宠 Service 瀹炵幇绫�
  *
- * @author 鑺嬮亾婧愮爜
+ * @author 瓒呯骇绠$悊鍛�
  */
 @Service
 @Validated
 @Slf4j
 public class HrmLeaveApplicationServiceImpl implements HrmLeaveApplicationService {
 
+    private static final DateTimeFormatter LEAVE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+
     @Resource
     private HrmLeaveApplicationMapper leaveApplicationMapper;
+    @Resource
+    private HrmEmployeeMapper employeeMapper;
     @Resource
     private HrmNoRedisDAO noRedisDAO;
     @Resource
@@ -73,22 +92,33 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createLeaveApplication(HrmLeaveApplicationSaveReqVO createReqVO) {
-        // 1. 鐢熸垚鐢宠鍗曞彿
-        String no = generateUniqueNo();
-        // 2. 璁剧疆鐢宠浜轰俊鎭�
+        // 1. 纭鐢宠浜哄凡鍏宠仈鍛樺伐妗f
         Long userId = createReqVO.getUserId();
         if (userId == null) {
             userId = getCurrentUserId();
         }
         AdminUserRespDTO user = adminUserApi.getUser(userId);
         if (user == null) {
-            userId = getCurrentUserId();
+            throw exception(LEAVE_APPLICATION_APPLICANT_NOT_EXISTS);
         }
+        HrmEmployeeDO employee = employeeMapper.selectByUserId(userId);
+        if (employee == null) {
+            throw exception(Objects.equals(userId, getCurrentUserId())
+                    ? LEAVE_APPLICATION_EMPLOYEE_NOT_EXISTS : LEAVE_APPLICATION_SELECTED_EMPLOYEE_NOT_EXISTS);
+        }
+        if (employee.getDeptId() == null) {
+            throw exception(LEAVE_APPLICATION_EMPLOYEE_DEPT_NOT_FOUND);
+        }
+        if (createReqVO.getDeptId() != null && !Objects.equals(createReqVO.getDeptId(), employee.getDeptId())) {
+            throw exception(LEAVE_APPLICATION_DEPT_MISMATCH);
+        }
+        // 2. 鐢熸垚鐢宠鍗曞彿
+        String no = generateUniqueNo();
         // 3. 鎻掑叆
         HrmLeaveApplicationDO application = BeanUtils.toBean(createReqVO, HrmLeaveApplicationDO.class);
         application.setNo(no);
         application.setUserId(userId);
-        application.setDeptId(createReqVO.getDeptId() != null ? createReqVO.getDeptId() : user.getDeptId());
+        application.setDeptId(employee.getDeptId());
         application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
         leaveApplicationMapper.insert(application);
         // 缁戝畾闄勪欢
@@ -96,6 +126,242 @@
                 application.getId(), createReqVO.getBlobIds());
         return application.getId();
     }
+
+    @Override
+    public HrmLeaveApplicationImportRespVO importLeaveApplications(List<HrmLeaveApplicationImportExcelVO> list) {
+        if (CollUtil.isEmpty(list)) {
+            throw exception(LEAVE_APPLICATION_IMPORT_FILE_EMPTY);
+        }
+        int totalCount = 0;
+        int successCount = 0;
+        List<HrmLeaveApplicationImportFailureVO> failures = new ArrayList<>();
+        for (int index = 0; index < list.size(); index++) {
+            HrmLeaveApplicationImportExcelVO row = list.get(index);
+            // Excel 绗� 1 琛屾槸琛ㄥご锛岀 index 鏉℃暟鎹搴旂 index + 2 琛�
+            int rowNum = index + 2;
+            if (isBlankRow(row)) {
+                continue;
+            }
+            totalCount++;
+            try {
+                leaveApplicationMapper.insert(buildImportedDraft(row));
+                successCount++;
+            } catch (LeaveImportRowException e) {
+                failures.add(new HrmLeaveApplicationImportFailureVO(rowNum, trimToNull(row.getPhone()),
+                        trimToNull(row.getEmployeeName()), e.getMessage()));
+            } catch (Exception e) {
+                log.error("[importLeaveApplications][绗� {} 琛屽鍏ュけ璐", rowNum, e);
+                failures.add(new HrmLeaveApplicationImportFailureVO(rowNum, trimToNull(row.getPhone()),
+                        trimToNull(row.getEmployeeName()),
+                        "鍐欏叆鏁版嵁搴撳け璐ワ細" + StrUtil.blankToDefault(e.getMessage(), e.getClass().getSimpleName())
+                                + "锛涜妫�鏌ヨ鍋囧師鍥�/澶囨敞闀垮害鍚庨噸鏂板鍏ヨ琛岋紝浠嶅け璐ヨ鑱旂郴绠$悊鍛�"));
+            }
+        }
+        if (totalCount == 0) {
+            throw exception(LEAVE_APPLICATION_IMPORT_FILE_EMPTY);
+        }
+        return HrmLeaveApplicationImportRespVO.builder()
+                .totalCount(totalCount)
+                .successCount(successCount)
+                .failureCount(failures.size())
+                .failures(failures)
+                .build();
+    }
+
+    private boolean isBlankRow(HrmLeaveApplicationImportExcelVO row) {
+        return row == null || (StrUtil.isBlank(row.getPhone()) && StrUtil.isBlank(row.getEmployeeName())
+                && StrUtil.isBlank(row.getLeaveTypeName()) && StrUtil.isBlank(row.getStartTime())
+                && StrUtil.isBlank(row.getEndTime()) && StrUtil.isBlank(row.getDuration())
+                && StrUtil.isBlank(row.getReason()));
+    }
+
+    /**
+     * 灏嗕竴琛� Excel 鏁版嵁杞崲涓鸿鍋囩敵璇疯崏绋匡紱浠讳竴鏍¢獙涓嶉�氳繃鏃舵姏鍑哄甫绮剧‘鍘熷洜鐨� {@link LeaveImportRowException}
+     */
+    private HrmLeaveApplicationDO buildImportedDraft(HrmLeaveApplicationImportExcelVO row) {
+        // 1. 鎵嬫満鍙峰尮閰嶅憳宸ユ。妗�
+        String phone = normalizeCell(row.getPhone());
+        if (StrUtil.isBlank(phone)) {
+            throw new LeaveImportRowException("鎵嬫満鍙蜂负绌猴紝璇峰~鍐欏憳宸ユ。妗堜腑鐧昏鐨� 11 浣嶆墜鏈哄彿");
+        }
+        if (!PHONE_PATTERN.matcher(phone).matches()) {
+            throw new LeaveImportRowException("鎵嬫満鍙穂" + phone + "]涓嶆槸鏈夋晥鐨� 11 浣嶆墜鏈哄彿锛岃妫�鏌ユ槸鍚﹀濉簡绌烘牸銆佺鍙锋垨灏忔暟浣�");
+        }
+        HrmEmployeeDO employee = employeeMapper.selectByPhone(phone);
+        if (employee == null) {
+            throw new LeaveImportRowException("鎵嬫満鍙穂" + phone + "]鏈尮閰嶅埌鍛樺伐妗f锛岃鍏堝湪銆屽憳宸ユ。妗堛�嶄腑缁存姢璇ユ墜鏈哄彿鍚庨噸鏂板鍏�");
+        }
+        String inputName = trimToNull(row.getEmployeeName());
+        if (inputName != null && !Objects.equals(inputName, StrUtil.trim(employee.getName()))) {
+            throw new LeaveImportRowException("鎵嬫満鍙穂" + phone + "]瀵瑰簲鍛樺伐涓篬" + employee.getName()
+                    + "]锛屼笌濉啓鐨勫憳宸ュ鍚峓" + inputName + "]涓嶄竴鑷达紝璇锋牳瀵规墜鏈哄彿鍚庨噸鏂板鍏�");
+        }
+        if (employee.getUserId() == null) {
+            throw new LeaveImportRowException("鍛樺伐妗f[" + employee.getName() + "]锛堟墜鏈哄彿 " + phone
+                    + "锛夋湭鍏宠仈绯荤粺鐧诲綍璐﹀彿锛岃鍏堝湪鍛樺伐妗f涓粦瀹氳处鍙峰悗閲嶆柊瀵煎叆");
+        }
+        if (employee.getDeptId() == null) {
+            throw new LeaveImportRowException("鍛樺伐妗f[" + employee.getName()
+                    + "]鏈缃儴闂紝璇峰厛鍦ㄥ憳宸ユ。妗堜腑琛ュ叏閮ㄩ棬鍚庨噸鏂板鍏�");
+        }
+        // 2. 璇峰亣绫诲瀷
+        Integer leaveType = parseLeaveType(row.getLeaveTypeName());
+        // 3. 璧锋鏃堕棿涓庢椂闀�
+        LocalDateTime startTime = parseDateTime("寮�濮嬫椂闂�", row.getStartTime());
+        LocalDateTime endTime = parseDateTime("缁撴潫鏃堕棿", row.getEndTime());
+        if (!endTime.isAfter(startTime)) {
+            throw new LeaveImportRowException("缁撴潫鏃堕棿[" + formatDateTime(endTime) + "]蹇呴』鏅氫簬寮�濮嬫椂闂碵"
+                    + formatDateTime(startTime) + "]锛岃璋冩暣鍚庨噸鏂板鍏�");
+        }
+        BigDecimal duration = parseDuration(row.getDuration(), startTime, endTime);
+        // 4. 璇峰亣鍘熷洜
+        String reason = trimToNull(row.getReason());
+        if (reason == null) {
+            throw new LeaveImportRowException("璇峰亣鍘熷洜涓虹┖锛岃濉啓璇峰亣浜嬬敱锛堝皢灞曠ず鍦ㄥ鎵瑰崟涓婏級");
+        }
+        if (reason.length() > 500) {
+            throw new LeaveImportRowException("璇峰亣鍘熷洜闀垮害涓� " + reason.length()
+                    + " 涓瓧绗︼紝瓒呰繃 500 瀛椾笂闄愶紝璇风簿绠�鍚庨噸鏂板鍏�");
+        }
+        String remark = trimToNull(row.getRemark());
+        if (remark != null && remark.length() > 500) {
+            throw new LeaveImportRowException("澶囨敞闀垮害涓� " + remark.length()
+                    + " 涓瓧绗︼紝瓒呰繃 500 瀛椾笂闄愶紝璇风簿绠�鍚庨噸鏂板鍏�");
+        }
+        // 5. 缁勮鑽夌锛堜笉鎻愪氦瀹℃壒锛�
+        HrmLeaveApplicationDO application = new HrmLeaveApplicationDO();
+        application.setNo(generateUniqueNo());
+        application.setUserId(employee.getUserId());
+        application.setDeptId(employee.getDeptId());
+        application.setLeaveType(leaveType);
+        application.setStartTime(startTime);
+        application.setEndTime(endTime);
+        application.setDuration(duration);
+        application.setReason(reason);
+        application.setRemark(remark);
+        application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
+        return application;
+    }
+
+    private Integer parseLeaveType(String raw) {
+        String value = normalizeCell(raw);
+        if (StrUtil.isBlank(value)) {
+            throw new LeaveImportRowException("璇峰亣绫诲瀷涓虹┖锛岃濉啓锛氫簨鍋囥�佺梾鍋囥�佸勾鍋囥�佸鍋囥�佷骇鍋囥�佷抚鍋囷紙鎴栨暟瀛� 1-6锛�");
+        }
+        for (HrmLeaveTypeEnum type : HrmLeaveTypeEnum.values()) {
+            if (StrUtil.equals(value, type.getName()) || StrUtil.equals(value, String.valueOf(type.getType()))) {
+                return type.getType();
+            }
+        }
+        throw new LeaveImportRowException("璇峰亣绫诲瀷[" + value
+                + "]涓嶅湪鍙�夎寖鍥村唴锛岃濉啓锛氫簨鍋囥�佺梾鍋囥�佸勾鍋囥�佸鍋囥�佷骇鍋囥�佷抚鍋囷紙鎴栨暟瀛� 1-6锛�");
+    }
+
+    private LocalDateTime parseDateTime(String field, String raw) {
+        String value = normalizeCell(raw);
+        if (StrUtil.isBlank(value)) {
+            throw new LeaveImportRowException(field + "涓虹┖锛岃濉啓鏍煎紡濡� 2026-11-10 09:00 鐨勬椂闂�");
+        }
+        // 鍏煎 Excel 鎶婃棩鏈熷瓨鎴愭暟瀛楀簭鍒楀彿锛堝 46000.375锛�
+        if (EXCEL_SERIAL_PATTERN.matcher(value).matches()) {
+            double serial = Double.parseDouble(value);
+            if (serial > 20000 && serial < 80000) {
+                return LocalDateTime.ofInstant(
+                        org.apache.poi.ss.usermodel.DateUtil.getJavaDate(serial).toInstant(), ZoneId.systemDefault());
+            }
+        }
+        for (DateTimeFormatter formatter : DATE_TIME_FORMATTERS) {
+            try {
+                return LocalDateTime.parse(value, formatter);
+            } catch (DateTimeParseException ignored) {
+                // 灏濊瘯涓嬩竴绉嶆牸寮�
+            }
+        }
+        // 鍙~鏃ユ湡鏃舵寜褰撳ぉ 00:00 澶勭悊
+        for (DateTimeFormatter formatter : DATE_ONLY_FORMATTERS) {
+            try {
+                return java.time.LocalDate.parse(value, formatter).atStartOfDay();
+            } catch (DateTimeParseException ignored) {
+                // 灏濊瘯涓嬩竴绉嶆牸寮�
+            }
+        }
+        throw new LeaveImportRowException(field + "[" + value
+                + "]鏍煎紡涓嶆纭紝璇蜂娇鐢� yyyy-MM-dd HH:mm 鎴� yyyy-MM-dd HH:mm:ss锛堜緥濡� 2026-11-10 09:00锛�");
+    }
+
+    private BigDecimal parseDuration(String raw, LocalDateTime startTime, LocalDateTime endTime) {
+        String value = normalizeCell(raw);
+        if (StrUtil.isBlank(value)) {
+            throw new LeaveImportRowException("璇峰亣鏃堕暱涓虹┖锛岃濉啓澶т簬 0 鐨勫ぉ鏁帮紙鏀寔 0.5 澶╋級");
+        }
+        value = StrUtil.removeSuffix(value, "澶�").trim();
+        BigDecimal duration;
+        try {
+            duration = new BigDecimal(value);
+        } catch (NumberFormatException e) {
+            throw new LeaveImportRowException("璇峰亣鏃堕暱[" + value + "]涓嶆槸鏈夋晥鏁板瓧锛岃濉啓澶т簬 0 鐨勫ぉ鏁帮紙渚嬪 0.5銆�1銆�2.5锛�");
+        }
+        if (duration.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new LeaveImportRowException("璇峰亣鏃堕暱[" + duration.stripTrailingZeros().toPlainString()
+                    + "]蹇呴』澶т簬 0 澶╋紝璇锋寜瀹為檯璇峰亣澶╂暟濉啓");
+        }
+        // 浠呭仛鎻愮ず锛屼笉闃绘柇锛氭椂闀夸笌璧锋鏃堕棿鏄庢樉涓嶇鏃剁粰鍑哄彲鏍稿淇℃伅
+        long minutes = java.time.Duration.between(startTime, endTime).toMinutes();
+        BigDecimal expectedMax = BigDecimal.valueOf(minutes)
+                .divide(BigDecimal.valueOf(60 * 24), 2, RoundingMode.HALF_UP);
+        if (duration.compareTo(expectedMax) > 0) {
+            throw new LeaveImportRowException("璇峰亣鏃堕暱[" + duration.stripTrailingZeros().toPlainString()
+                    + " 澶瓒呰繃璧锋鏃堕棿璺ㄥ害[" + expectedMax.stripTrailingZeros().toPlainString()
+                    + " 澶锛岃鏍稿寮�濮�/缁撴潫鏃堕棿涓庢椂闀垮悗閲嶆柊瀵煎叆");
+        }
+        return duration;
+    }
+
+    private static String normalizeCell(String raw) {
+        if (StrUtil.isBlank(raw)) {
+            return null;
+        }
+        String value = raw.trim();
+        // Excel 鏁板�煎崟鍏冩牸鍙兘琚鎴愮瀛﹁鏁版硶鎴栧甫 .0 鐨勫皬鏁帮紝缁熶竴杩樺師涓烘櫘閫氭暟瀛�
+        if (SCIENTIFIC_PATTERN.matcher(value).matches() || TRAILING_ZERO_PATTERN.matcher(value).matches()) {
+            try {
+                value = new BigDecimal(value).stripTrailingZeros().toPlainString();
+            } catch (NumberFormatException ignored) {
+                // 淇濇寔鍘熷��
+            }
+        }
+        return value;
+    }
+
+    private static String trimToNull(String value) {
+        return StrUtil.isBlank(value) ? null : value.trim();
+    }
+
+    private static String formatDateTime(LocalDateTime value) {
+        return value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
+    }
+
+    /** 鍗曡瀵煎叆鏍¢獙寮傚父锛歮essage 鐩存帴浣滀负璇ヨ鐨勫け璐ュ師鍥犺繑鍥炵粰鐢ㄦ埛 */
+    private static class LeaveImportRowException extends RuntimeException {
+        LeaveImportRowException(String message) {
+            super(message);
+        }
+    }
+
+    private static final Pattern PHONE_PATTERN = Pattern.compile("^1\\d{10}$");
+    private static final Pattern EXCEL_SERIAL_PATTERN = Pattern.compile("^\\d{5}(\\.\\d+)?$");
+    private static final Pattern SCIENTIFIC_PATTERN = Pattern.compile("^-?\\d+(\\.\\d+)?[eE][+-]?\\d+$");
+    private static final Pattern TRAILING_ZERO_PATTERN = Pattern.compile("^-?\\d+\\.0+$");
+    private static final DateTimeFormatter[] DATE_TIME_FORMATTERS = new DateTimeFormatter[]{
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"),
+            DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"),
+            DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm")
+    };
+    private static final DateTimeFormatter[] DATE_ONLY_FORMATTERS = new DateTimeFormatter[]{
+            DateTimeFormatter.ofPattern("yyyy-MM-dd"),
+            DateTimeFormatter.ofPattern("yyyy/MM/dd")
+    };
 
     private Long getCurrentUserId() {
         return SecurityFrameworkUtils.getLoginUserId();
@@ -113,26 +379,29 @@
     public void updateLeaveApplication(HrmLeaveApplicationSaveReqVO updateReqVO) {
         // 鏍¢獙瀛樺湪
         HrmLeaveApplicationDO application = validateLeaveApplicationExists(updateReqVO.getId());
-        // 鏍¢獙鐘舵�佷负鑽夌鎴栧鎵逛腑
+        validateOperator(application, getCurrentUserId());
+        // 椹冲洖鍚庡厑璁镐慨鏀瑰苟閲嶆柊鎻愪氦瀹℃壒銆�
         if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
-                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
-            throw exception(LEAVE_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
+                && !Objects.equals(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
+            throw exception(LEAVE_APPLICATION_UPDATE_FAIL_NOT_EDITABLE);
         }
+        if (updateReqVO.getUserId() != null && !Objects.equals(updateReqVO.getUserId(), application.getUserId())) {
+            throw exception(LEAVE_APPLICATION_APPLICANT_IMMUTABLE);
+        }
+        validateApplicantDept(application.getUserId(),
+                updateReqVO.getDeptId() != null ? updateReqVO.getDeptId() : application.getDeptId());
         // 鏇存柊
         HrmLeaveApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmLeaveApplicationDO.class);
+        updateObj.setUserId(null);
         leaveApplicationMapper.updateById(updateObj);
-        // 閲嶆柊缁戝畾闄勪欢
-        if (updateReqVO.getBlobIds() != null) {
-            storageAttachmentApi.deleteAttachmentsByRecord("hrm_leave_application", updateReqVO.getId());
-            storageAttachmentApi.bindAttachments("file", "hrm_leave_application",
-                    updateReqVO.getId(), updateReqVO.getBlobIds());
-        }
+        storageAttachmentApi.updateAttachments("file", "hrm_leave_application", updateReqVO.getId(), updateReqVO.getBlobIds());
     }
 
     @Override
     public void deleteLeaveApplication(Long id) {
         // 鏍¢獙瀛樺湪
         HrmLeaveApplicationDO application = validateLeaveApplicationExists(id);
+        validateOperator(application, getCurrentUserId());
         // 鏍¢獙鐘舵�佷负鑽夌
         if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
             throw exception(LEAVE_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
@@ -223,29 +492,73 @@
     public void submitLeaveApplication(Long id, String processDefinitionKey, Long userId) {
         // 1. 鏍¢獙璇峰亣鐢宠鏄惁瀛樺湪
         HrmLeaveApplicationDO application = validateLeaveApplicationExists(id);
-        if (ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
+        validateOperator(application, userId);
+        if (ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
+                && ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.REJECT.getStatus())) {
             throw exception(LEAVE_APPLICATION_SUBMIT_FAIL_NOT_DRAFT);
         }
+        validateApplicantDept(application.getUserId(), application.getDeptId());
 
         // 2. 鏍¢獙鍒嗙被鍜屾祦绋嬪畾涔夋槸鍚﹀瓨鍦�
+        // 鍏ㄥ眬鍏嶅鎵癸細涓氬姟鏍¢獙閫氳繃鍚庣洿鎺ユ墽琛屽鎵归�氳繃閫昏緫锛屾棤闇�娴佺▼妯″瀷銆�
+        if (bpmProcessInstanceApi.isGlobalAutoApprove()) {
+            leaveApplicationMapper.updateById(HrmLeaveApplicationDO.builder()
+                    .id(id)
+                    .processInstanceId("")
+                    .status(HrmAuditStatusEnum.PROCESS.getStatus())
+                    .build());
+            updateLeaveApplicationAuditStatus(id, HrmAuditStatusEnum.APPROVE.getStatus());
+            return;
+        }
+
         validateApproveCategoryAndProcessDefinition();
 
         // 3. 鏍¢獙浼犲叆鐨勬祦绋嬪畾涔� Key 鏄惁灞炰簬璇ュ垎绫讳笖澶勪簬婵�娲荤姸鎬�
+        if (processDefinitionKey == null || processDefinitionKey.isBlank()) {
+            throw exception(LEAVE_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
+        }
         ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
-        if (processDefinition == null) {
+        if (processDefinition == null || bpmProcessDefinitionService
+                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE).stream()
+                .noneMatch(info -> Objects.equals(info.getProcessDefinitionId(), processDefinition.getId()))) {
             throw exception(LEAVE_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
         }
 
         // 4. 鍒涘缓瀹℃壒娴佺▼瀹炰緥
-        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
-                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id)));
+        Map<String, Object> variables = new HashMap<>();
+        variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_BUSINESS_NO, application.getNo());
+        AdminUserRespDTO applicant = adminUserApi.getUser(application.getUserId());
+        variables.put("Fl55mt725q5yabc", applicant != null ? applicant.getNickname() : String.valueOf(application.getUserId()));
+        variables.put("F5eqmt725u3naec", LEAVE_TIME_FORMAT.format(application.getStartTime())
+                + " 鑷� " + LEAVE_TIME_FORMAT.format(application.getEndTime()));
+        variables.put("F8wtmt726c3zahc", application.getDuration().toPlainString());
+        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(application.getUserId(), new BpmProcessInstanceCreateReqDTO()
+                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id))
+                .setVariables(variables));
 
         // 5. 鏇存柊璇峰亣鐢宠宸ヤ綔娴佺紪鍙�
-        leaveApplicationMapper.updateById(HrmLeaveApplicationDO.builder()
-                .id(id)
-                .processInstanceId(processInstanceId)
-                .status(HrmAuditStatusEnum.PROCESS.getStatus())
-                .build());
+        leaveApplicationMapper.update(null, new LambdaUpdateWrapper<HrmLeaveApplicationDO>()
+                .eq(HrmLeaveApplicationDO::getId, id)
+                .set(HrmLeaveApplicationDO::getProcessInstanceId, processInstanceId)
+                .set(HrmLeaveApplicationDO::getStatus, HrmAuditStatusEnum.PROCESS.getStatus())
+                .set(HrmLeaveApplicationDO::getApproveTime, null));
+    }
+
+    private void validateOperator(HrmLeaveApplicationDO application, Long operatorId) {
+        if (!Objects.equals(application.getUserId(), operatorId)
+                && !Objects.equals(application.getCreator(), String.valueOf(operatorId))) {
+            throw exception(LEAVE_APPLICATION_OPERATOR_NOT_ALLOWED);
+        }
+    }
+
+    private void validateApplicantDept(Long userId, Long deptId) {
+        HrmEmployeeDO employee = employeeMapper.selectByUserId(userId);
+        if (employee == null || employee.getDeptId() == null) {
+            throw exception(LEAVE_APPLICATION_EMPLOYEE_DEPT_NOT_FOUND);
+        }
+        if (!Objects.equals(employee.getDeptId(), deptId)) {
+            throw exception(LEAVE_APPLICATION_DEPT_MISMATCH);
+        }
     }
 
     /**
@@ -325,11 +638,12 @@
             throw exception(LEAVE_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
         }
         // 鏇存柊鐘舵��
-        leaveApplicationMapper.updateById(HrmLeaveApplicationDO.builder()
-                .id(id)
-                .status(status)
-                .approveTime(java.time.LocalDateTime.now())
-                .build());
+        leaveApplicationMapper.update(null, new LambdaUpdateWrapper<HrmLeaveApplicationDO>()
+                .eq(HrmLeaveApplicationDO::getId, id)
+                .set(HrmLeaveApplicationDO::getStatus, status)
+                .set(HrmLeaveApplicationDO::getApproveTime,
+                        Objects.equals(status, HrmAuditStatusEnum.APPROVE.getStatus())
+                                ? java.time.LocalDateTime.now() : null));
     }
 
     @Override
@@ -337,4 +651,4 @@
         return leaveApplicationMapper.selectByProcessInstanceId(processInstanceId);
     }
 
-}
\ No newline at end of file
+}

--
Gitblit v1.9.3