| | |
| | | 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; |
| | |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.YearMonth; |
| | | import java.util.List; |
| | | |
| | |
| | | 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) |
| | |
| | | // 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); |
| | | |
| | |
| | | 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())); |
| | | } |
| | | } |
| | | |
| | |
| | | .earlyCount(earlyCount) |
| | | .absentCount(absentCount) |
| | | .overtimeHours(overtimeHours) |
| | | .leaveHours(BigDecimal.ZERO) |
| | | .leaveHours(leaveHours) |
| | | .build(); |
| | | attendanceSummaryMapper.insert(summary); |
| | | } else { |
| | |
| | | summary.setEarlyCount(earlyCount); |
| | | summary.setAbsentCount(absentCount); |
| | | summary.setOvertimeHours(overtimeHours); |
| | | summary.setLeaveHours(leaveHours); // 请假工时也要更新,否则重算时永远停在上一次 |
| | | attendanceSummaryMapper.updateById(summary); |
| | | } |
| | | } |
| | |
| | | /** |
| | | * 计算应出勤天数(工作日数量) |
| | | */ |
| | | /** |
| | | * 计算某一天的请假工时(与考勤记录里的算法保持一致) |
| | | * |
| | | * <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; |