liu
2026-10-16 a8d5d6eadc95bcce27cce71b59c1626200d162fd
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -67,6 +67,86 @@
    }
    @Override
    public void createImportedRecord(HrmAttendanceRecordDO record) {
        // 按考勤规则重算打卡类型与时长,不信任 Excel 填写的值
        fillClockTypes(record);
        fillCalculatedHours(record);
        attendanceRecordMapper.insert(record);
    }
    @Override
    public void updateImportedRecord(HrmAttendanceRecordDO record) {
        fillClockTypes(record);
        fillCalculatedHours(record);
        attendanceRecordMapper.updateById(record);
    }
    /**
     * 按考勤规则重算上下班打卡类型(正常/迟到/早退),与打卡逻辑保持一致
     */
    private void fillClockTypes(HrmAttendanceRecordDO record) {
        if (record.getDeptId() == null) {
            return;
        }
        HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
        if (rule == null) {
            return;
        }
        // 上班打卡类型:晚于 上班时间+迟到允许 记为迟到
        if (record.getClockInTime() != null && rule.getWorkStartTime() != null) {
            int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
            LocalTime lateThreshold = rule.getWorkStartTime().plusMinutes(lateAllowMinutes);
            LocalTime clockInOnly = record.getClockInTime().toLocalTime();
            record.setClockInType(clockInOnly.isAfter(lateThreshold)
                    ? HrmAttendanceClockTypeEnum.LATE.getType()
                    : HrmAttendanceClockTypeEnum.NORMAL.getType());
        }
        // 下班打卡类型:早于 下班时间-早退允许 记为早退
        if (record.getClockOutTime() != null && rule.getWorkEndTime() != null) {
            int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
            LocalTime earlyThreshold = rule.getWorkEndTime().minusMinutes(earlyAllowMinutes);
            LocalTime clockOutOnly = record.getClockOutTime().toLocalTime();
            record.setClockOutType(clockOutOnly.isBefore(earlyThreshold)
                    ? HrmAttendanceClockTypeEnum.EARLY.getType()
                    : HrmAttendanceClockTypeEnum.NORMAL.getType());
        }
    }
    /**
     * 根据上下班打卡时间和考勤规则,计算工作时长与加班时长
     */
    private void fillCalculatedHours(HrmAttendanceRecordDO record) {
        // 工作时长 = 下班打卡时间 - 上班打卡时间(与打卡逻辑一致)
        if (record.getClockInTime() != null && record.getClockOutTime() != null) {
            Duration duration = Duration.between(record.getClockInTime(), record.getClockOutTime());
            BigDecimal workHours = BigDecimal.valueOf(duration.toMinutes())
                    .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
            record.setWorkHours(workHours);
        }
        // 加班时长:按考勤规则的标准下班时间计算超额部分
        if (record.getClockOutTime() != null && record.getDeptId() != null) {
            HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
            if (rule != null && rule.getWorkEndTime() != null) {
                LocalTime workEnd = rule.getWorkEndTime();
                LocalDate outDate = record.getClockOutTime().toLocalDate();
                LocalDateTime workEndDateTime = LocalDateTime.of(outDate, workEnd);
                if (record.getClockOutTime().isAfter(workEndDateTime)) {
                    Duration overtimeDuration = Duration.between(workEndDateTime, record.getClockOutTime());
                    BigDecimal overtimeThreshold = rule.getOvertimeThreshold() != null
                            ? rule.getOvertimeThreshold() : BigDecimal.ZERO;
                    BigDecimal overtimeHours = BigDecimal.valueOf(overtimeDuration.toMinutes())
                            .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
                    record.setOvertimeHours(overtimeHours.compareTo(overtimeThreshold) > 0 ? overtimeHours : BigDecimal.ZERO);
                } else {
                    record.setOvertimeHours(BigDecimal.ZERO);
                }
            } else {
                record.setOvertimeHours(BigDecimal.ZERO);
            }
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public HrmAttendanceRecordDO clockIn(Long userId, LocalDateTime clockTime, String location) {
        LocalDate clockDate = clockTime.toLocalDate();
@@ -173,8 +253,59 @@
                    .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
            record.setWorkHours(workHours);
        }
        // 7. 计算加班时长:超过规则下班时间的部分
        record.setOvertimeHours(computeOvertimeHours(rule.getWorkEndTime(), clockTimeOnly));
        attendanceRecordMapper.updateById(record);
        return record;
    }
    /**
     * 计算加班时长(小时):clockTimeOnly 晚于 workEndTime 的部分,否则为 0
     */
    private BigDecimal computeOvertimeHours(LocalTime workEndTime, LocalTime clockTimeOnly) {
        if (workEndTime == null || clockTimeOnly == null || !clockTimeOnly.isAfter(workEndTime)) {
            return BigDecimal.ZERO;
        }
        return BigDecimal.valueOf(Duration.between(workEndTime, clockTimeOnly).toMinutes())
                .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void recordOvertime(HrmAttendanceExceptionDO exception) {
        // 仅处理加班类型
        if (!HrmAttendanceExceptionTypeEnum.OVERTIME.getType().equals(exception.getType())
                || exception.getExceptionTime() == null) {
            return;
        }
        // 1. 取员工考勤规则,用规则下班时间到异常时间(实际下班点)折算加班时长;无规则则无法折算,跳过
        BigDecimal overtimeHours = BigDecimal.ZERO;
        HrmEmployeeDO employee = employeeService.getEmployee(exception.getUserId());
        HrmAttendanceRuleDO rule = employee == null ? null
                : attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
        if (rule != null) {
            overtimeHours = computeOvertimeHours(rule.getWorkEndTime(), exception.getExceptionTime().toLocalTime());
        }
        if (overtimeHours.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        // 2. 查询当日考勤记录,不存在则补建
        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(exception.getUserId(), exception.getExceptionDate());
        if (record == null) {
            record = HrmAttendanceRecordDO.builder()
                    .userId(exception.getUserId())
                    .deptId(employee != null ? employee.getDeptId() : exception.getDeptId())
                    .date(exception.getExceptionDate())
                    .dataSource(HrmAttendanceDataSourceEnum.SUPPLEMENT.getType())
                    .build();
            record.setOvertimeHours(overtimeHours);
            attendanceRecordMapper.insert(record);
            return;
        }
        // 3. 已有记录则累加加班时长
        BigDecimal existed = record.getOvertimeHours() != null ? record.getOvertimeHours() : BigDecimal.ZERO;
        record.setOvertimeHours(existed.add(overtimeHours));
        attendanceRecordMapper.updateById(record);
    }
    @Override
@@ -187,11 +318,13 @@
        LocalDate exceptionDate = exception.getExceptionDate();
        LocalDateTime exceptionTime = exception.getExceptionTime();
        Long userId = exception.getUserId();
        HrmEmployeeDO employee = employeeService.getEmployee(userId);
        HrmAttendanceRuleDO rule = employee == null ? null
                : attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
        // 查询考勤记录
        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, exceptionDate);
        if (record == null) {
            // 如果没有记录,创建一条
            HrmEmployeeDO employee = employeeService.getEmployee(userId);
            record = HrmAttendanceRecordDO.builder()
                    .userId(userId)
                    .deptId(employee != null ? employee.getDeptId() : exception.getDeptId())
@@ -210,6 +343,8 @@
            // 下班补卡
            record.setClockOutTime(exceptionTime);
            record.setClockOutType(HrmAttendanceClockTypeEnum.NORMAL.getType());
            // 下班补卡同时折算加班时长(超过规则下班时间部分)
            record.setOvertimeHours(computeOvertimeHours(rule == null ? null : rule.getWorkEndTime(), time));
        }
        // 更新数据来源为补卡审批
        record.setDataSource(HrmAttendanceDataSourceEnum.SUPPLEMENT.getType());