gaoluyang
97 分钟以前 578f84ece732a710278e7914396e127d8a27ed8a
feat(hrm): 排班管理联动已审批请假

- 新增 GET /hrm/employee-schedule/leave-list 查询区间内审批通过的请假
- 批量排班遇到员工请假日跳过:不新建班次,也不清除已有排班
- 请假申请人按 user_id 映射到员工档案

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改4个文件
176 ■■■■■ 文件已修改
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmEmployeeScheduleController.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleLeaveRespVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmLeaveApplicationMapper.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmEmployeeScheduleController.java
@@ -3,18 +3,22 @@
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchSaveRespVO;
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.service.attendance.HrmEmployeeScheduleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -53,4 +57,15 @@
        return success(employeeScheduleService.batchSaveSchedule(reqVO));
    }
    @GetMapping("/leave-list")
    @Operation(summary = "获得日期范围内审批通过的请假记录(排班日历展示用)")
    @Parameter(name = "startDate", description = "开始日期", required = true, example = "2026-11-01")
    @Parameter(name = "endDate", description = "结束日期(含)", required = true, example = "2026-11-30")
    @PreAuthorize("@ss.hasPermission('hrm:employee-schedule:query')")
    public CommonResult<List<HrmEmployeeScheduleLeaveRespVO>> getApprovedLeaveList(
            @RequestParam("startDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
            @RequestParam("endDate") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {
        return success(employeeScheduleService.getApprovedLeaveList(startDate, endDate));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmEmployeeScheduleLeaveRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * ç®¡ç†åŽå° - æŽ’班日历展示用的请假记录 Response VO
 *
 * åªè¿”回审批通过的请假,用于在排班网格中标出员工请假日期。
 */
@Schema(description = "管理后台 - æŽ’班请假记录 Response VO")
@Data
public class HrmEmployeeScheduleLeaveRespVO {
    @Schema(description = "请假申请ID", example = "1")
    private Long id;
    @Schema(description = "员工ID", example = "1")
    private Long employeeId;
    @Schema(description = "员工姓名", example = "张三")
    private String employeeName;
    @Schema(description = "请假类型:1-事假,2-病假,3-年假,4-婚假,5-产假,6-丧假", example = "3")
    private Integer leaveType;
    @Schema(description = "请假类型名称", example = "年假")
    private String leaveTypeName;
    @Schema(description = "开始时间")
    private LocalDateTime startTime;
    @Schema(description = "结束时间")
    private LocalDateTime endTime;
    @Schema(description = "请假时长(天)", example = "2")
    private BigDecimal duration;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmLeaveApplicationMapper.java
@@ -54,4 +54,25 @@
                .le(HrmLeaveApplicationDO::getEndTime, endTime));
    }
    /**
     * æŸ¥è¯¢ä¸ŽæŒ‡å®šæ—¶é—´èŒƒå›´æœ‰äº¤é›†çš„审批通过请假记录(可限定申请人)
     *
     * è¯·å‡å¼€å§‹æ—©äºŽèŒƒå›´èµ·ç‚¹ã€ç»“束晚于范围终点也算交集,因此用 start &lt;= èŒƒå›´ç»ˆç‚¹ ä¸” end &gt;= èŒƒå›´èµ·ç‚¹ã€‚
     *
     * @param startTime èŒƒå›´å¼€å§‹æ—¶é—´
     * @param endTime   èŒƒå›´ç»“束时间
     * @param userIds   ç”³è¯·äººID集合,为空表示不限
     * @return è¯·å‡è®°å½•列表
     */
    default List<HrmLeaveApplicationDO> selectApprovedListByTimeRange(LocalDateTime startTime, LocalDateTime endTime,
                                                                      java.util.Collection<Long> userIds) {
        LambdaQueryWrapperX<HrmLeaveApplicationDO> query = new LambdaQueryWrapperX<HrmLeaveApplicationDO>()
                .inIfPresent(HrmLeaveApplicationDO::getUserId, userIds);
        query.eq(HrmLeaveApplicationDO::getStatus, HrmAuditStatusEnum.APPROVE.getStatus())
                .le(HrmLeaveApplicationDO::getStartTime, endTime)
                .ge(HrmLeaveApplicationDO::getEndTime, startTime)
                .orderByAsc(HrmLeaveApplicationDO::getStartTime);
        return selectList(query);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleService.java
@@ -2,10 +2,12 @@
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchSaveRespVO;
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 java.time.LocalDate;
import java.util.List;
/**
@@ -30,4 +32,13 @@
     */
    HrmEmployeeScheduleBatchSaveRespVO batchSaveSchedule(HrmEmployeeScheduleBatchSaveReqVO reqVO);
    /**
     * æŸ¥è¯¢æ—¥æœŸèŒƒå›´å†…审批通过的请假记录(排班日历展示用)
     *
     * @param startDate å¼€å§‹æ—¥æœŸ
     * @param endDate   ç»“束日期
     * @return è¯·å‡è®°å½•列表(含员工ID)
     */
    List<HrmEmployeeScheduleLeaveRespVO> getApprovedLeaveList(LocalDate startDate, LocalDate endDate);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmEmployeeScheduleServiceImpl.java
@@ -6,14 +6,19 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmEmployeeScheduleBatchSaveRespVO;
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.HrmAttendanceHolidayDO;
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.enums.HrmAttendanceHolidayTypeEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmLeaveTypeEnum;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
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;
@@ -26,6 +31,7 @@
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.YearMonth;
import java.util.*;
import java.util.function.Function;
@@ -53,6 +59,8 @@
    private DeptApi deptApi;
    @Resource
    private HrmAttendanceHolidayService attendanceHolidayService;
    @Resource
    private HrmLeaveApplicationMapper leaveApplicationMapper;
    @Override
    public List<HrmEmployeeScheduleRespVO> getScheduleList(HrmEmployeeScheduleListReqVO reqVO) {
@@ -191,12 +199,20 @@
                .collect(Collectors.toMap(HrmAttendanceHolidayDO::getDate, HrmAttendanceHolidayDO::getType,
                        (a, b) -> a));
        // 5. æŒ‰æ—¥æœŸé€å¤©ç”ŸæˆæŽ’班:休息日删除原班次,工作日 upsert
        // 5. é¢„加载目标员工的已审批请假:请假当天跳过,不排班也不清除已有排班
        Map<Long, Set<LocalDate>> leaveDateMap = buildLeaveDateMap(employees,
                reqVO.getStartDate(), reqVO.getEndDate());
        // 6. æŒ‰æ—¥æœŸé€å¤©ç”ŸæˆæŽ’班:休息日删除原班次,工作日 upsert
        int itemCount = 0;
        for (LocalDate date = reqVO.getStartDate(); !date.isAfter(reqVO.getEndDate()); date = date.plusDays(1)) {
            boolean rest = isRestDay(date, reqVO.getRestMode(), holidayTypeMap.get(date))
                    || reqVO.getShiftId() == null;
            for (HrmEmployeeDO employee : employees) {
                // å‘˜å·¥å½“天在请假期内,保持原排班不动
                if (leaveDateMap.getOrDefault(employee.getId(), Collections.emptySet()).contains(date)) {
                    continue;
                }
                HrmEmployeeScheduleDO existing = existingMap.get(scheduleKey(employee.getId(), date));
                if (rest) {
                    if (existing != null) {
@@ -226,6 +242,75 @@
        return respVO;
    }
    @Override
    public List<HrmEmployeeScheduleLeaveRespVO> getApprovedLeaveList(LocalDate startDate, LocalDate endDate) {
        if (startDate == null || endDate == null || endDate.isBefore(startDate)) {
            return Collections.emptyList();
        }
        List<HrmLeaveApplicationDO> leaves = leaveApplicationMapper.selectApprovedListByTimeRange(
                startDate.atStartOfDay(), endDate.atTime(LocalTime.MAX), null);
        if (CollUtil.isEmpty(leaves)) {
            return Collections.emptyList();
        }
        Set<Long> userIds = leaves.stream().map(HrmLeaveApplicationDO::getUserId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, HrmEmployeeDO> userEmployeeMap = CollUtil.isEmpty(userIds) ? Collections.emptyMap()
                : employeeMapper.selectList(new LambdaQueryWrapperX<HrmEmployeeDO>()
                        .in(HrmEmployeeDO::getUserId, userIds)).stream()
                .filter(employee -> employee.getUserId() != null)
                .collect(Collectors.toMap(HrmEmployeeDO::getUserId, Function.identity(), (a, b) -> a));
        return leaves.stream().map(leave -> {
            HrmEmployeeDO employee = userEmployeeMap.get(leave.getUserId());
            if (employee == null) {
                return null;
            }
            HrmEmployeeScheduleLeaveRespVO respVO = new HrmEmployeeScheduleLeaveRespVO();
            respVO.setId(leave.getId());
            respVO.setEmployeeId(employee.getId());
            respVO.setEmployeeName(employee.getName());
            respVO.setLeaveType(leave.getLeaveType());
            respVO.setLeaveTypeName(HrmLeaveTypeEnum.getNameByStatus(leave.getLeaveType()));
            respVO.setStartTime(leave.getStartTime());
            respVO.setEndTime(leave.getEndTime());
            respVO.setDuration(leave.getDuration());
            return respVO;
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }
    /**
     * æž„建「员工ID -> è¯·å‡æ—¥é›†åˆã€æ˜ å°„(只取审批通过的请假,且裁剪到排班日期范围内)
     */
    private Map<Long, Set<LocalDate>> buildLeaveDateMap(List<HrmEmployeeDO> employees,
                                                        LocalDate startDate, LocalDate endDate) {
        Map<Long, HrmEmployeeDO> userEmployeeMap = employees.stream()
                .filter(employee -> employee.getUserId() != null)
                .collect(Collectors.toMap(HrmEmployeeDO::getUserId, Function.identity(), (a, b) -> a));
        if (userEmployeeMap.isEmpty()) {
            return Collections.emptyMap();
        }
        List<HrmLeaveApplicationDO> leaves = leaveApplicationMapper.selectApprovedListByTimeRange(
                startDate.atStartOfDay(), endDate.atTime(LocalTime.MAX), userEmployeeMap.keySet());
        if (CollUtil.isEmpty(leaves)) {
            return Collections.emptyMap();
        }
        Map<Long, Set<LocalDate>> result = new HashMap<>();
        for (HrmLeaveApplicationDO leave : leaves) {
            HrmEmployeeDO employee = userEmployeeMap.get(leave.getUserId());
            if (employee == null || leave.getStartTime() == null || leave.getEndTime() == null) {
                continue;
            }
            LocalDate from = leave.getStartTime().toLocalDate().isBefore(startDate)
                    ? startDate : leave.getStartTime().toLocalDate();
            LocalDate to = leave.getEndTime().toLocalDate().isAfter(endDate)
                    ? endDate : leave.getEndTime().toLocalDate();
            Set<LocalDate> dates = result.computeIfAbsent(employee.getId(), key -> new HashSet<>());
            for (LocalDate date = from; !date.isAfter(to); date = date.plusDays(1)) {
                dates.add(date);
            }
        }
        return result;
    }
    private Set<Long> getSelfAndChildDeptIds(Long deptId) {
        Set<Long> deptIds = new LinkedHashSet<>();
        deptIds.add(deptId);