liu
2026-10-30 82155dd6f2b16fbfb718e636a54e2c9dae31246b
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceSummaryServiceImpl.java
@@ -5,11 +5,13 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceSummaryPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceSummaryDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceRecordMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceSummaryMapper;
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceClockTypeEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -19,6 +21,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.List;
@@ -40,6 +43,10 @@
    private HrmEmployeeService employeeService;
    @Resource
    private HrmAttendanceHolidayService attendanceHolidayService;
    @Resource
    private cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper leaveApplicationMapper;
    @Resource
    private HrmAttendanceRuleService attendanceRuleService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -59,6 +66,14 @@
        // 3. 查询该月所有考勤记录
        List<HrmAttendanceRecordDO> records = attendanceRecordMapper.selectListByUserIdAndDateBetween(userId, startDate, endDate);
        // 3.1 查询该月所有审批通过的请假单
        //     考勤记录只在「打卡」时才创建,请假当天没来上班就不会有记录,
        //     所以必须单独查请假单,否则 leaveHours 会漏算。
        List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> monthLeaves =
                leaveApplicationMapper.selectListByUserIdAndOverlapDate(
                        userId, HrmAuditStatusEnum.APPROVE.getStatus(),
                        startDate.atStartOfDay(), endDate.plusDays(1).atStartOfDay());
        // 4. 计算应出勤天数(工作日数量)
        int workDays = calculateWorkDays(startDate, endDate);
@@ -67,31 +82,76 @@
        int earlyCount = 0;
        int absentCount = 0;
        BigDecimal overtimeHours = BigDecimal.ZERO;
        BigDecimal leaveHours = BigDecimal.ZERO;
        int actualDays = 0;
        for (HrmAttendanceRecordDO record : records) {
            // 统计迟到
            if (HrmAttendanceClockTypeEnum.LATE.getType().equals(record.getClockInType())) {
                lateCount++;
            // 请假标记:审批通过的请假单落库时已写入 record.leaveFlag
            boolean onLeave = Boolean.TRUE.equals(record.getLeaveFlag());
            // 累计请假工时
            if (onLeave && record.getLeaveHours() != null) {
                leaveHours = leaveHours.add(record.getLeaveHours());
            }
            // 统计早退
            if (HrmAttendanceClockTypeEnum.EARLY.getType().equals(record.getClockOutType())) {
                earlyCount++;
            }
            // 统计缺卡
            if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) {
                absentCount++;
            }
            if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockOutType())) {
                absentCount++;
            // 请假当天不计迟到、早退、缺卡(请假类型本身是 LEAVE,不会命中下面的判断)
            if (!onLeave) {
                // 统计迟到
                if (HrmAttendanceClockTypeEnum.LATE.getType().equals(record.getClockInType())) {
                    lateCount++;
                }
                // 统计早退
                if (HrmAttendanceClockTypeEnum.EARLY.getType().equals(record.getClockOutType())) {
                    earlyCount++;
                }
                // 统计缺卡:请假当天没打卡属正常,不算缺卡
                if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) {
                    absentCount++;
                }
                if (HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockOutType())) {
                    absentCount++;
                }
            }
            // 累计加班时长
            if (record.getOvertimeHours() != null) {
                overtimeHours = overtimeHours.add(record.getOvertimeHours());
            }
            // 统计实际出勤天数(有正常打卡记录)
            // 统计实际出勤天数(有打卡记录即算;纯请假未打卡不算出勤)
            if (record.getClockInTime() != null || record.getClockOutTime() != null) {
                actualDays++;
            }
        }
        // 5.1 补算「有请假单但没有考勤记录」的日子
        //     这类情况是请假当天完全没来打卡,不会生成 record,
        //     上面循环覆盖不到,需要单独按请假单累计工时。
        if (!monthLeaves.isEmpty()) {
            java.util.Set<LocalDate> recordDates = records.stream()
                    .map(HrmAttendanceRecordDO::getDate).collect(java.util.stream.Collectors.toSet());
            HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
            // 按天归组请假单
            java.util.Map<LocalDate, List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO>> byDate
                    = new java.util.LinkedHashMap<>();
            for (var leave : monthLeaves) {
                if (leave.getStartTime() == null || leave.getEndTime() == null) {
                    continue;
                }
                LocalDate d = leave.getStartTime().toLocalDate();
                LocalDate last = leave.getEndTime().toLocalDate();
                if (last.isAfter(endDate)) {
                    last = endDate;
                }
                for (LocalDate cur = d; !cur.isAfter(last); cur = cur.plusDays(1)) {
                    if (cur.isBefore(startDate)) {
                        continue;
                    }
                    byDate.computeIfAbsent(cur, k -> new java.util.ArrayList<>()).add(leave);
                }
            }
            for (var entry : byDate.entrySet()) {
                // 已有考勤记录的日期,请假工时已在 record.leaveHours 里算过
                if (recordDates.contains(entry.getKey())) {
                    continue;
                }
                leaveHours = leaveHours.add(calcLeaveHoursByDay(entry.getValue(), rule, entry.getKey()));
            }
        }
@@ -108,7 +168,7 @@
                    .earlyCount(earlyCount)
                    .absentCount(absentCount)
                    .overtimeHours(overtimeHours)
                    .leaveHours(BigDecimal.ZERO)
                    .leaveHours(leaveHours)
                    .build();
            attendanceSummaryMapper.insert(summary);
        } else {
@@ -118,6 +178,7 @@
            summary.setEarlyCount(earlyCount);
            summary.setAbsentCount(absentCount);
            summary.setOvertimeHours(overtimeHours);
            summary.setLeaveHours(leaveHours); // 请假工时也要更新,否则重算时永远停在上一次
            attendanceSummaryMapper.updateById(summary);
        }
    }
@@ -155,6 +216,43 @@
    /**
     * 计算应出勤天数(工作日数量)
     */
    /**
     * 计算某一天的请假工时(与考勤记录里的算法保持一致)
     *
     * <p>取每张请假单与考勤规则工作时段的交集累加,精确到 0.01 小时。</p>
     */
    private BigDecimal calcLeaveHoursByDay(
            List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves,
            HrmAttendanceRuleDO rule, LocalDate date) {
        LocalDateTime workStart;
        LocalDateTime workEnd;
        if (rule != null && rule.getWorkStartTime() != null && rule.getWorkEndTime() != null) {
            workStart = LocalDateTime.of(date, rule.getWorkStartTime());
            workEnd = LocalDateTime.of(date, rule.getWorkEndTime());
            if (!workEnd.isAfter(workStart)) {
                workEnd = workEnd.plusDays(1);
            }
        } else {
            workStart = date.atStartOfDay();
            workEnd = date.plusDays(1).atStartOfDay();
        }
        BigDecimal total = BigDecimal.ZERO;
        for (var leave : leaves) {
            if (leave.getStartTime() == null || leave.getEndTime() == null) {
                continue;
            }
            LocalDateTime from = leave.getStartTime().isAfter(workStart) ? leave.getStartTime() : workStart;
            LocalDateTime to = leave.getEndTime().isBefore(workEnd) ? leave.getEndTime() : workEnd;
            if (!to.isAfter(from)) {
                continue;
            }
            long minutes = java.time.Duration.between(from, to).toMinutes();
            total = total.add(BigDecimal.valueOf(minutes)
                    .divide(BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP));
        }
        return total;
    }
    private int calculateWorkDays(LocalDate startDate, LocalDate endDate) {
        int workDays = 0;
        LocalDate current = startDate;