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/attendance/HrmEmployeeScheduleServiceImpl.java |  294 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 281 insertions(+), 13 deletions(-)

diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
index 36eb169..3164235 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
@@ -1,24 +1,48 @@
 package cn.iocoder.yudao.module.hrm.service.attendance;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchAssignReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchAssignRespVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleLeaveRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleListReqVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleSaveReqVO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmEmployeeScheduleDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
 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.attendance.HrmEmployeeScheduleMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmShiftMapper;
 import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
+import cn.iocoder.yudao.module.hrm.enums.HrmEmployeeStatusEnum;
+import cn.iocoder.yudao.module.hrm.enums.HrmLeaveTypeEnum;
+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 jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import java.time.LocalDate;
 import java.time.YearMonth;
-import java.util.*;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -32,7 +56,11 @@
  */
 @Service
 @Validated
+@Slf4j
 public class HrmEmployeeScheduleServiceImpl implements HrmEmployeeScheduleService {
+
+    /** 鎵归噺鎺掔彮鍗曟鏈�澶氳鐩栫殑澶╂暟锛岄伩鍏嶄竴娆$敓鎴愯繃澶氳鎷栧灝璇锋眰 */
+    private static final int BATCH_MAX_DAYS = 62;
 
     @Resource
     private HrmEmployeeScheduleMapper employeeScheduleMapper;
@@ -40,6 +68,14 @@
     private HrmEmployeeMapper employeeMapper;
     @Resource
     private HrmShiftMapper shiftMapper;
+    @Resource
+    private HrmLeaveApplicationMapper leaveApplicationMapper;
+    @Resource
+    private HrmAttendanceHolidayService attendanceHolidayService;
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    private PostApi postApi;
 
     @Override
     public List<HrmEmployeeScheduleRespVO> getScheduleList(HrmEmployeeScheduleListReqVO reqVO) {
@@ -120,24 +156,256 @@
                 throw exception(SCHEDULE_SHIFT_NOT_EXISTS);
             }
         }
-        // 3. 閫愭潯 upsert锛氭湁鐝鍒欎繚瀛橈紝鏃犵彮娆″垯鍒犻櫎
+        // 3. 閫愭潯 upsert锛氭湁鐝鍒欎繚瀛橈紝鏃犵彮娆″垯娓呯┖
         for (HrmEmployeeScheduleSaveReqVO reqVO : saveReqVOList) {
-            HrmEmployeeScheduleDO existing = employeeScheduleMapper.selectByEmployeeIdAndDate(
-                    reqVO.getEmployeeId(), reqVO.getDate());
             if (reqVO.getShiftId() == null) {
-                if (existing != null) {
-                    employeeScheduleMapper.deleteById(existing.getId());
-                }
+                clearShift(reqVO.getEmployeeId(), reqVO.getDate());
             } else {
-                if (existing == null) {
-                    employeeScheduleMapper.insert(BeanUtils.toBean(reqVO, HrmEmployeeScheduleDO.class));
-                } else {
-                    existing.setShiftId(reqVO.getShiftId());
-                    existing.setRemark(reqVO.getRemark());
-                    employeeScheduleMapper.updateById(existing);
+                upsertShift(reqVO.getEmployeeId(), reqVO.getDate(), reqVO.getShiftId(), reqVO.getRemark());
+            }
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public HrmEmployeeScheduleBatchAssignRespVO batchAssign(HrmEmployeeScheduleBatchAssignReqVO reqVO) {
+        // 1. 鏍¢獙鏃ユ湡鍖洪棿
+        LocalDate startDate = reqVO.getStartDate();
+        LocalDate endDate = reqVO.getEndDate();
+        if (endDate.isBefore(startDate)) {
+            throw exception(SCHEDULE_BATCH_DATE_RANGE_INVALID, endDate, startDate);
+        }
+        long days = ChronoUnit.DAYS.between(startDate, endDate) + 1;
+        if (days > BATCH_MAX_DAYS) {
+            throw exception(SCHEDULE_BATCH_RANGE_TOO_LONG, startDate, endDate, days);
+        }
+        // 2. 鏍¢獙鐝瀛樺湪
+        HrmShiftDO shift = shiftMapper.selectById(reqVO.getShiftId());
+        if (shift == null) {
+            throw exception(SCHEDULE_SHIFT_NOT_EXISTS);
+        }
+        // 3. 绛涢�夊憳宸ワ細閮ㄩ棬锛堝惈涓嬬骇锛�+ 宀椾綅 + 鍛樺伐鐘舵��
+        Set<Long> deptIds = new HashSet<>();
+        deptIds.add(reqVO.getDeptId());
+        List<DeptRespDTO> childDepts = deptApi.getChildDeptList(reqVO.getDeptId());
+        if (CollUtil.isNotEmpty(childDepts)) {
+            childDepts.forEach(dept -> deptIds.add(dept.getId()));
+        }
+        List<HrmEmployeeDO> employees = employeeMapper.selectListByDeptIds(deptIds).stream()
+                .filter(employee -> reqVO.getEmployeeStatus() == null
+                        || Objects.equals(reqVO.getEmployeeStatus(), employee.getEmployeeStatus()))
+                .filter(employee -> matchesPost(employee, reqVO.getPostId()))
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(employees)) {
+            throw exception(SCHEDULE_BATCH_NO_EMPLOYEE,
+                    resolveDeptName(reqVO.getDeptId()),
+                    resolvePostName(reqVO.getPostId()),
+                    resolveStatusName(reqVO.getEmployeeStatus()));
+        }
+        // 4. 棰勫姞杞藉凡瀹℃壒璇峰亣锛屾寜鈥滃憳宸� + 鏃ユ湡鈥濇爣璁伴渶瑕佽烦杩囩殑鍗曞厓鏍�
+        Set<String> leaveKeys = buildApprovedLeaveKeys(employees, startDate, endDate);
+        // 5. 鎸夋棩鏈熼�愬ぉ鎺掔彮锛氬伐浣滄棩鍐欑彮娆°�佷紤鎭棩娓呯┖銆佸凡瀹℃壒璇峰亣璺宠繃
+        int scheduledCount = 0;
+        int skippedLeaveCount = 0;
+        int clearedRestDayCount = 0;
+        for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
+            boolean workDay = attendanceHolidayService.isWorkDay(date);
+            for (HrmEmployeeDO employee : employees) {
+                if (leaveKeys.contains(buildKey(employee.getId(), date))) {
+                    skippedLeaveCount++;
+                    continue;
+                }
+                if (workDay) {
+                    upsertShift(employee.getId(), date, reqVO.getShiftId(), reqVO.getRemark());
+                    scheduledCount++;
+                } else if (clearShift(employee.getId(), date)) {
+                    clearedRestDayCount++;
                 }
             }
         }
+        log.info("[batchAssign][閮ㄩ棬({}) 宀椾綅({}) 鐘舵��({}) {}~{} 鐝({}) 鍛戒腑鍛樺伐 {} 浜猴紝鍐欏叆 {} 鏉★紝璺宠繃璇峰亣 {} 鏉★紝娓呯┖浼戞伅鏃� {} 鏉",
+                reqVO.getDeptId(), reqVO.getPostId(), reqVO.getEmployeeStatus(), startDate, endDate,
+                reqVO.getShiftId(), employees.size(), scheduledCount, skippedLeaveCount, clearedRestDayCount);
+        return HrmEmployeeScheduleBatchAssignRespVO.builder()
+                .matchedEmployeeCount(employees.size())
+                .scheduledCount(scheduledCount)
+                .skippedLeaveCount(skippedLeaveCount)
+                .clearedRestDayCount(clearedRestDayCount)
+                .build();
+    }
+
+    @Override
+    public List<HrmEmployeeScheduleLeaveRespVO> getScheduleLeaveList(HrmEmployeeScheduleListReqVO reqVO) {
+        YearMonth yearMonth = YearMonth.parse(reqVO.getYearMonth());
+        LocalDate startDate = yearMonth.atDay(1);
+        LocalDate endDate = yearMonth.atEndOfMonth();
+        // 1. 纭畾闇�瑕佹爣璁扮殑鍛樺伐
+        List<HrmEmployeeDO> employees;
+        if (reqVO.getEmployeeId() != null) {
+            HrmEmployeeDO employee = employeeMapper.selectById(reqVO.getEmployeeId());
+            employees = employee == null ? Collections.emptyList() : List.of(employee);
+        } else if (reqVO.getDeptId() != null) {
+            employees = employeeMapper.selectByDeptId(reqVO.getDeptId());
+        } else {
+            employees = employeeMapper.selectList();
+        }
+        if (CollUtil.isEmpty(employees)) {
+            return Collections.emptyList();
+        }
+        // 2. 鏌ヨ宸插鎵归�氳繃鐨勮鍋囷紙鎸夌郴缁熺敤鎴稩D锛�
+        Map<Long, List<HrmEmployeeDO>> employeeByUserId = employees.stream()
+                .filter(employee -> employee.getUserId() != null)
+                .collect(Collectors.groupingBy(HrmEmployeeDO::getUserId));
+        if (employeeByUserId.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<HrmLeaveApplicationDO> leaves = leaveApplicationMapper.selectApprovedListByUserIdsAndTimeRange(
+                employeeByUserId.keySet(), startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
+        if (CollUtil.isEmpty(leaves)) {
+            return Collections.emptyList();
+        }
+        // 3. 鎸夊ぉ灞曞紑涓烘帓鐝〉鏍囪
+        List<HrmEmployeeScheduleLeaveRespVO> result = new ArrayList<>();
+        for (HrmLeaveApplicationDO leave : leaves) {
+            List<HrmEmployeeDO> matchedEmployees = employeeByUserId.get(leave.getUserId());
+            if (CollUtil.isEmpty(matchedEmployees)) {
+                continue;
+            }
+            LocalDate from = leave.getStartTime().toLocalDate().isBefore(startDate)
+                    ? startDate : leave.getStartTime().toLocalDate();
+            LocalDate to = leave.getEndTime().toLocalDate().isAfter(endDate)
+                    ? endDate : leave.getEndTime().toLocalDate();
+            String leaveTypeName = HrmLeaveTypeEnum.getNameByStatus(leave.getLeaveType());
+            for (LocalDate date = from; !date.isAfter(to); date = date.plusDays(1)) {
+                for (HrmEmployeeDO employee : matchedEmployees) {
+                    result.add(new HrmEmployeeScheduleLeaveRespVO(employee.getId(), employee.getName(), date,
+                            leave.getLeaveType(), leaveTypeName));
+                }
+            }
+        }
+        result.sort((a, b) -> {
+            int compare = a.getDate().compareTo(b.getDate());
+            return compare != 0 ? compare : a.getEmployeeId().compareTo(b.getEmployeeId());
+        });
+        return result;
+    }
+
+    /**
+     * 鍐欏叆/瑕嗙洊鐝锛涜嫢璇ュ憳宸ヨ鏃ユ湡瀛樺湪閫昏緫鍒犻櫎鐨勬棫琛岋紝鍒欏厛澶嶆椿锛岃閬垮敮涓�閿啿绐併��
+     */
+    private void upsertShift(Long employeeId, LocalDate date, Long shiftId, String remark) {
+        HrmEmployeeScheduleDO existing = employeeScheduleMapper.selectByEmployeeIdAndDateIncludeDeleted(employeeId, date);
+        if (existing == null) {
+            HrmEmployeeScheduleDO insert = new HrmEmployeeScheduleDO();
+            insert.setEmployeeId(employeeId);
+            insert.setDate(date);
+            insert.setShiftId(shiftId);
+            insert.setRemark(remark);
+            employeeScheduleMapper.insert(insert);
+            return;
+        }
+        if (Boolean.TRUE.equals(existing.getDeleted())) {
+            employeeScheduleMapper.reviveById(existing.getId(), shiftId, remark, currentUpdater());
+            return;
+        }
+        existing.setShiftId(shiftId);
+        existing.setRemark(remark);
+        employeeScheduleMapper.updateById(existing);
+    }
+
+    /**
+     * 娓呯┖鐝锛堥�昏緫鍒犻櫎锛夛紱杩斿洖鏈鏄惁鐪熺殑娓呮帀浜嗕竴鏉¢潪绌虹彮娆°��
+     */
+    private boolean clearShift(Long employeeId, LocalDate date) {
+        HrmEmployeeScheduleDO existing = employeeScheduleMapper.selectByEmployeeIdAndDateIncludeDeleted(employeeId, date);
+        if (existing == null || Boolean.TRUE.equals(existing.getDeleted()) || existing.getShiftId() == null) {
+            return false;
+        }
+        employeeScheduleMapper.deleteById(existing.getId());
+        return true;
+    }
+
+    /**
+     * 鏋勯�犫�滃憳宸� + 鏃ユ湡鈥濈殑宸插鎵硅鍋囬泦鍚�
+     */
+    private Set<String> buildApprovedLeaveKeys(List<HrmEmployeeDO> employees, LocalDate startDate, LocalDate endDate) {
+        Map<Long, List<HrmEmployeeDO>> employeeByUserId = employees.stream()
+                .filter(employee -> employee.getUserId() != null)
+                .collect(Collectors.groupingBy(HrmEmployeeDO::getUserId));
+        if (employeeByUserId.isEmpty()) {
+            return Collections.emptySet();
+        }
+        List<HrmLeaveApplicationDO> leaves = leaveApplicationMapper.selectApprovedListByUserIdsAndTimeRange(
+                employeeByUserId.keySet(), startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
+        if (CollUtil.isEmpty(leaves)) {
+            return Collections.emptySet();
+        }
+        Set<String> keys = new HashSet<>();
+        for (HrmLeaveApplicationDO leave : leaves) {
+            List<HrmEmployeeDO> matchedEmployees = employeeByUserId.get(leave.getUserId());
+            if (CollUtil.isEmpty(matchedEmployees)) {
+                continue;
+            }
+            LocalDate from = leave.getStartTime().toLocalDate().isBefore(startDate)
+                    ? startDate : leave.getStartTime().toLocalDate();
+            LocalDate to = leave.getEndTime().toLocalDate().isAfter(endDate)
+                    ? endDate : leave.getEndTime().toLocalDate();
+            for (LocalDate date = from; !date.isAfter(to); date = date.plusDays(1)) {
+                for (HrmEmployeeDO employee : matchedEmployees) {
+                    keys.add(buildKey(employee.getId(), date));
+                }
+            }
+        }
+        return keys;
+    }
+
+    private static String buildKey(Long employeeId, LocalDate date) {
+        return employeeId + "_" + date;
+    }
+
+    private boolean matchesPost(HrmEmployeeDO employee, Long postId) {
+        if (postId == null) {
+            return true;
+        }
+        if (StrUtil.isBlank(employee.getPostIds())) {
+            return false;
+        }
+        try {
+            return JSONUtil.toList(employee.getPostIds(), Long.class).contains(postId);
+        } catch (Exception e) {
+            log.warn("[matchesPost][鍛樺伐({}) 鐨� postIds({}) 涓嶆槸鍚堟硶 JSON锛岃烦杩囧矖浣嶅尮閰峕", employee.getId(), employee.getPostIds());
+            return false;
+        }
+    }
+
+    private String resolveDeptName(Long deptId) {
+        DeptRespDTO dept = deptId == null ? null : deptApi.getDept(deptId);
+        return dept == null ? "ID=" + deptId : dept.getName();
+    }
+
+    private String resolvePostName(Long postId) {
+        if (postId == null) {
+            return "涓嶉檺";
+        }
+        List<PostRespDTO> posts = postApi.getPostList(Collections.singleton(postId));
+        return CollUtil.isEmpty(posts) ? "ID=" + postId : posts.get(0).getName();
+    }
+
+    private String resolveStatusName(Integer employeeStatus) {
+        if (employeeStatus == null) {
+            return "涓嶉檺";
+        }
+        for (HrmEmployeeStatusEnum status : HrmEmployeeStatusEnum.values()) {
+            if (Objects.equals(status.getStatus(), employeeStatus)) {
+                return status.getName();
+            }
+        }
+        return "ID=" + employeeStatus;
+    }
+
+    private static String currentUpdater() {
+        Long userId = SecurityFrameworkUtils.getLoginUserId();
+        return userId == null ? "" : String.valueOf(userId);
     }
 
 }

--
Gitblit v1.9.3