| | |
| | | |
| | | 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.HrmAttendanceMyStatusRespVO; |
| | | 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; |
| | |
| | | /** |
| | | * 考勤记录 Service 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | |
| | | @Override |
| | | public HrmAttendanceRecordDO getAttendanceRecordByUserIdAndDate(Long userId, LocalDate date) { |
| | | return attendanceRecordMapper.selectByUserIdAndDate(userId, date); |
| | | } |
| | | |
| | | @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 |
| | |
| | | } |
| | | |
| | | @Override |
| | | public HrmAttendanceMyStatusRespVO getMyStatus(Long userId) { |
| | | // 1. 员工不存在时返回 null,由前端提示未关联员工,避免打卡页加载直接报错 |
| | | HrmEmployeeDO employee = employeeService.getEmployee(userId); |
| | | if (employee == null) { |
| | | return null; |
| | | } |
| | | LocalDate today = LocalDate.now(); |
| | | // 2. 查询今日考勤记录与考勤规则 |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, today); |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId()); |
| | | boolean workDay = attendanceHolidayService.isWorkDay(today); |
| | | boolean clockInDone = record != null && record.getClockInTime() != null; |
| | | boolean clockOutDone = record != null && record.getClockOutTime() != null; |
| | | |
| | | // 3. 组装今日打卡状态 |
| | | HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO(); |
| | | respVO.setDate(today); |
| | | respVO.setWorkDay(workDay); |
| | | respVO.setEmployeeId(employee.getId()); |
| | | respVO.setUserName(employee.getName()); |
| | | respVO.setDeptId(employee.getDeptId()); |
| | | if (rule != null) { |
| | | respVO.setRuleName(rule.getName()); |
| | | respVO.setWorkStartTime(rule.getWorkStartTime()); |
| | | respVO.setWorkEndTime(rule.getWorkEndTime()); |
| | | } |
| | | if (record != null) { |
| | | respVO.setRecordId(record.getId()); |
| | | respVO.setClockInTime(record.getClockInTime()); |
| | | respVO.setClockOutTime(record.getClockOutTime()); |
| | | respVO.setClockInType(record.getClockInType()); |
| | | respVO.setClockOutType(record.getClockOutType()); |
| | | respVO.setWorkHours(record.getWorkHours()); |
| | | respVO.setOvertimeHours(record.getOvertimeHours()); |
| | | respVO.setLocation(record.getLocation()); |
| | | respVO.setDataSource(record.getDataSource()); |
| | | } |
| | | respVO.setClockInDone(clockInDone); |
| | | respVO.setClockOutDone(clockOutDone); |
| | | // 非工作日不可打卡,与打卡接口的校验保持一致 |
| | | respVO.setCanClockIn(workDay && !clockInDone); |
| | | respVO.setCanClockOut(workDay && !clockOutDone); |
| | | return respVO; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void supplementClockIn(HrmAttendanceExceptionDO exception) { |
| | | // 仅处理补卡类型 |