gaoluyang
5 天以前 ce5f8a87bb5d411bae67c39b269d879677bb98da
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 {
                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. 查询已审批通过的请假(按系统用户ID)
        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) {
                    employeeScheduleMapper.insert(BeanUtils.toBean(reqVO, HrmEmployeeScheduleDO.class));
                } else {
                    existing.setShiftId(reqVO.getShiftId());
                    existing.setRemark(reqVO.getRemark());
            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);
    }
}