package cn.iocoder.yudao.module.hrm.service.attendance; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO; 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.HrmAttendanceExceptionDO; 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.enums.HrmAttendanceClockTypeEnum; import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceDataSourceEnum; import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceExceptionTypeEnum; import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*; /** * 考勤记录 Service 实现类 * * @author 芋道源码 */ @Service @Validated public class HrmAttendanceRecordServiceImpl implements HrmAttendanceRecordService { @Resource private HrmAttendanceRecordMapper attendanceRecordMapper; @Resource private HrmEmployeeService employeeService; @Resource private HrmAttendanceRuleService attendanceRuleService; @Resource private HrmAttendanceHolidayService attendanceHolidayService; @Override public PageResult getAttendanceRecordPage(HrmAttendanceRecordPageReqVO pageReqVO) { return attendanceRecordMapper.selectPage(pageReqVO); } @Override public HrmAttendanceRecordDO getAttendanceRecord(Long id) { return attendanceRecordMapper.selectById(id); } @Override public List getAttendanceRecordListByUserIdAndDateBetween(Long userId, String startDate, String endDate) { return attendanceRecordMapper.selectListByUserIdAndDateBetween(userId, LocalDate.parse(startDate), LocalDate.parse(endDate)); } @Override public HrmAttendanceRecordDO getAttendanceRecordByUserIdAndDate(Long userId, LocalDate date) { return attendanceRecordMapper.selectByUserIdAndDate(userId, date); } @Override @Transactional(rollbackFor = Exception.class) public HrmAttendanceRecordDO clockIn(Long userId, LocalDateTime clockTime, String location) { LocalDate clockDate = clockTime.toLocalDate(); // 1. 校验是否工作日 if (!attendanceHolidayService.isWorkDay(clockDate)) { throw exception(ATTENDANCE_CLOCK_FAIL_NOT_WORKDAY); } // 2. 获取员工信息和考勤规则 HrmEmployeeDO employee = employeeService.getEmployee(userId); if (employee == null) { throw exception(EMPLOYEE_NOT_EXISTS); } HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId()); if (rule == null) { throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE); } // 3. 查询当日考勤记录 HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate); if (record != null && record.getClockInTime() != null) { throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED); } // 4. 判断打卡类型 LocalTime workStartTime = rule.getWorkStartTime(); int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0; LocalTime lateThreshold = workStartTime.plusMinutes(lateAllowMinutes); LocalTime clockTimeOnly = clockTime.toLocalTime(); Integer clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold) ? HrmAttendanceClockTypeEnum.NORMAL.getType() : HrmAttendanceClockTypeEnum.LATE.getType(); // 5. 创建或更新考勤记录 if (record == null) { record = HrmAttendanceRecordDO.builder() .userId(userId) .deptId(employee.getDeptId()) .date(clockDate) .clockInTime(clockTime) .clockInType(clockInType) .location(location) .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType()) .build(); attendanceRecordMapper.insert(record); } else { record.setClockInTime(clockTime); record.setClockInType(clockInType); if (location != null) { record.setLocation(location); } attendanceRecordMapper.updateById(record); } return record; } @Override @Transactional(rollbackFor = Exception.class) public HrmAttendanceRecordDO clockOut(Long userId, LocalDateTime clockTime, String location) { LocalDate clockDate = clockTime.toLocalDate(); // 1. 校验是否工作日 if (!attendanceHolidayService.isWorkDay(clockDate)) { throw exception(ATTENDANCE_CLOCK_FAIL_NOT_WORKDAY); } // 2. 获取员工信息和考勤规则 HrmEmployeeDO employee = employeeService.getEmployee(userId); if (employee == null) { throw exception(EMPLOYEE_NOT_EXISTS); } HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId()); if (rule == null) { throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE); } // 3. 查询当日考勤记录 HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate); if (record == null) { // 如果没有上班打卡记录,创建一条缺卡的记录 record = HrmAttendanceRecordDO.builder() .userId(userId) .deptId(employee.getDeptId()) .date(clockDate) .clockInType(HrmAttendanceClockTypeEnum.MISSING.getType()) .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType()) .build(); attendanceRecordMapper.insert(record); } if (record.getClockOutTime() != null) { throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED); } // 4. 判断打卡类型 LocalTime workEndTime = rule.getWorkEndTime(); int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0; LocalTime earlyThreshold = workEndTime.minusMinutes(earlyAllowMinutes); LocalTime clockTimeOnly = clockTime.toLocalTime(); Integer clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold) ? HrmAttendanceClockTypeEnum.NORMAL.getType() : HrmAttendanceClockTypeEnum.EARLY.getType(); // 5. 更新考勤记录 record.setClockOutTime(clockTime); record.setClockOutType(clockOutType); if (location != null) { record.setLocation(location); } // 6. 计算工作时长 if (record.getClockInTime() != null) { Duration duration = Duration.between(record.getClockInTime(), clockTime); BigDecimal workHours = BigDecimal.valueOf(duration.toMinutes()) .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); record.setWorkHours(workHours); } attendanceRecordMapper.updateById(record); return record; } @Override @Transactional(rollbackFor = Exception.class) public void supplementClockIn(HrmAttendanceExceptionDO exception) { // 仅处理补卡类型 if (!HrmAttendanceExceptionTypeEnum.SUPPLEMENT.getType().equals(exception.getType())) { return; } LocalDate exceptionDate = exception.getExceptionDate(); LocalDateTime exceptionTime = exception.getExceptionTime(); Long userId = exception.getUserId(); // 查询考勤记录 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()) .date(exceptionDate) .dataSource(HrmAttendanceDataSourceEnum.SUPPLEMENT.getType()) .build(); attendanceRecordMapper.insert(record); } // 根据异常时间判断是上班还是下班补卡 LocalTime time = exceptionTime.toLocalTime(); if (time.isBefore(LocalTime.of(12, 0))) { // 上班补卡 record.setClockInTime(exceptionTime); record.setClockInType(HrmAttendanceClockTypeEnum.NORMAL.getType()); } else { // 下班补卡 record.setClockOutTime(exceptionTime); record.setClockOutType(HrmAttendanceClockTypeEnum.NORMAL.getType()); } // 更新数据来源为补卡审批 record.setDataSource(HrmAttendanceDataSourceEnum.SUPPLEMENT.getType()); // 计算工作时长 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); } attendanceRecordMapper.updateById(record); } }