| | |
| | | private HrmAttendanceHolidayService attendanceHolidayService; |
| | | @Resource |
| | | private DeptApi deptApi; |
| | | @Resource |
| | | private cn.iocoder.yudao.module.hrm.service.approval.HrmLeaveQueryService leaveQueryService; |
| | | |
| | | @Override |
| | | public PageResult<HrmAttendanceRecordDO> getAttendanceRecordPage(HrmAttendanceRecordPageReqVO pageReqVO) { |
| | |
| | | // 1. 登录用户 -> 员工档案 |
| | | HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId); |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS, |
| | | "当前登录账号未关联员工档案,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。"); |
| | | throw exception(ATTENDANCE_EMPLOYEE_NOT_BOUND); |
| | | } |
| | | HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO(); |
| | | respVO.setUserId(loginUserId); |
| | |
| | | } |
| | | |
| | | // 4. 当日考勤记录 |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(employee.getId(), queryDate); |
| | | // 考勤记录表存的是系统用户 ID,这里用 loginUserId 而不是员工档案 ID |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(loginUserId, queryDate); |
| | | if (record != null) { |
| | | respVO.setRecord(buildRecordRespVO(record, employee)); |
| | | fillCompatibleRecordFields(respVO, record); |
| | | } |
| | | |
| | | // 4.5 当天请假情况(请假仍允许打卡,但不算迟到/早退/缺卡) |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves = |
| | | leaveQueryService.getApprovedLeaveList(loginUserId, queryDate); |
| | | boolean onLeave = !leaves.isEmpty(); |
| | | java.math.BigDecimal leaveHours = onLeave ? calcLeaveHours(leaves, rule, queryDate) : java.math.BigDecimal.ZERO; |
| | | respVO.setOnLeave(onLeave); |
| | | respVO.setLeaveHours(leaveHours); |
| | | if (onLeave) { |
| | | List<String> typeNames = leaves.stream() |
| | | .map(l -> cn.iocoder.yudao.module.hrm.enums.HrmLeaveTypeEnum.getNameByStatus(l.getLeaveType())) |
| | | .distinct().toList(); |
| | | respVO.setLeaveTypeNames(String.join("、", typeNames)); |
| | | } |
| | | |
| | | // 5. 当前可打卡状态 |
| | | HrmAttendanceMyStatusRespVO.Clockable clockable = buildClockable(workDay, rule, record, queryDate); |
| | | HrmAttendanceMyStatusRespVO.Clockable clockable = buildClockable(workDay, rule, record, queryDate, onLeave); |
| | | |
| | | // 5.1 请假当天:把「可打卡」的提示换成请假说明,但依然允许打卡 |
| | | if (onLeave) { |
| | | clockable.setMessage("今日已请假(" + respVO.getLeaveTypeNames() + "),如需上班可正常打卡"); |
| | | } |
| | | respVO.setClockable(clockable); |
| | | respVO.setClockInDone(record != null && record.getClockInTime() != null); |
| | | respVO.setClockOutDone(record != null && record.getClockOutTime() != null); |
| | |
| | | respVO.setOvertimeHours(record.getOvertimeHours()); |
| | | respVO.setLocation(record.getLocation()); |
| | | respVO.setDataSource(record.getDataSource()); |
| | | } |
| | | |
| | | /** |
| | | * 把请假信息写入考勤记录(leave_flag / leave_hours / leave_types) |
| | | * |
| | | * @param record 考勤记录 |
| | | * @param leaves 当天审批通过的请假单 |
| | | * @param rule 考勤规则(用于计算工作时段交集) |
| | | * @param date 考勤日期 |
| | | */ |
| | | private void applyLeaveInfo(HrmAttendanceRecordDO record, |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves, |
| | | HrmAttendanceRuleDO rule, LocalDate date) { |
| | | if (leaves == null || leaves.isEmpty()) { |
| | | record.setLeaveFlag(false); |
| | | record.setLeaveHours(BigDecimal.ZERO); |
| | | record.setLeaveTypes(null); |
| | | return; |
| | | } |
| | | record.setLeaveFlag(true); |
| | | record.setLeaveHours(calcLeaveHours(leaves, rule, date)); |
| | | record.setLeaveTypes(leaves.stream() |
| | | .map(l -> String.valueOf(l.getLeaveType())) |
| | | .distinct() |
| | | .collect(java.util.stream.Collectors.joining(","))); |
| | | } |
| | | |
| | | /** |
| | | * 计算某天的请假工时 |
| | | * |
| | | * <p>把每张请假单的 [start_time, end_time] 与考勤规则的工作时段求交集后累加, |
| | | * 精确到 0.01 小时(36 秒)。无规则时按整天的请假时长折算。</p> |
| | | * |
| | | * <p>例:规则 08:30-17:30,请假单 14:00-18:00 → 交集 14:00-17:30 = 3.5 小时。</p> |
| | | */ |
| | | private java.math.BigDecimal calcLeaveHours( |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves, |
| | | HrmAttendanceRuleDO rule, LocalDate date) { |
| | | LocalDateTime dayStart = date.atStartOfDay(); |
| | | LocalDateTime dayEnd = date.plusDays(1).atStartOfDay(); |
| | | // 工作日时段:优先用考勤规则,没配规则就按整天 |
| | | 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()); |
| | | // 跨零点班次(如 20:00-次日 04:00) |
| | | if (!workEnd.isAfter(workStart)) { |
| | | workEnd = workEnd.plusDays(1); |
| | | } |
| | | } else { |
| | | workStart = dayStart; |
| | | workEnd = dayEnd; |
| | | } |
| | | java.math.BigDecimal total = java.math.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(java.math.BigDecimal.valueOf(minutes) |
| | | .divide(java.math.BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP)); |
| | | } |
| | | return total; |
| | | } |
| | | |
| | | /** |
| | |
| | | * 根据工作日、考勤规则与当日记录计算可打卡状态 |
| | | */ |
| | | private HrmAttendanceMyStatusRespVO.Clockable buildClockable(boolean workDay, HrmAttendanceRuleDO rule, |
| | | HrmAttendanceRecordDO record, LocalDate date) { |
| | | HrmAttendanceRecordDO record, LocalDate date, |
| | | boolean onLeave) { |
| | | HrmAttendanceMyStatusRespVO.Clockable clockable = new HrmAttendanceMyStatusRespVO.Clockable(); |
| | | if (!workDay) { |
| | | clockable.setClockIn(false); |
| | |
| | | clockable.setClockOut(false); |
| | | clockable.setNextAction("in"); |
| | | clockable.setMessage("可上班打卡"); |
| | | // 仅当天查询时提示已迟到时长 |
| | | // 仅当天查询时提示已迟到时长;请假当天不提示迟到 |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | if (rule.getWorkStartTime() != null && date.equals(now.toLocalDate())) { |
| | | if (!onLeave && rule.getWorkStartTime() != null && date.equals(now.toLocalDate())) { |
| | | LocalDateTime workStart = LocalDateTime.of(date, rule.getWorkStartTime()); |
| | | int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0; |
| | | if (now.isAfter(workStart.plusMinutes(lateAllowMinutes))) { |
| | |
| | | throw exception(ATTENDANCE_CLOCK_FAIL_NOT_WORKDAY); |
| | | } |
| | | // 2. 获取员工信息和考勤规则 |
| | | // 注意:入参 userId 实际传的是「员工档案 ID」, |
| | | // 而考勤记录表 / 请假表的 user_id 存的是「系统用户 ID」,必须区分。 |
| | | HrmEmployeeDO employee = employeeService.getEmployee(userId); |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS); |
| | | } |
| | | Long systemUserId = employee.getUserId(); |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId()); |
| | | if (rule == null) { |
| | | throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE); |
| | | } |
| | | // 3. 查询当日考勤记录 |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate); |
| | | // 3. 查询当日考勤记录(用系统用户 ID) |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(systemUserId, clockDate); |
| | | if (record != null && record.getClockInTime() != null) { |
| | | throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED); |
| | | } |
| | | // 在任何写库操作前完成定位校验,旧版地点字符串不能绕过范围限制。 |
| | | ClockLocation clockLocation = validateClockLocation(rule, reqVO); |
| | | // 3.1 当天请假情况:请假当天允许打卡,但不判定为迟到 |
| | | // 注意:方法入参 userId 实为「员工档案 ID」,而请假表存的是「系统用户 ID」, |
| | | // 必须用 employee.getUserId() 才能对上。 |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves = |
| | | leaveQueryService.getApprovedLeaveList(employee.getUserId(), clockDate); |
| | | boolean onLeave = !leaves.isEmpty(); |
| | | // 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) |
| | | Integer clockInType; |
| | | if (onLeave) { |
| | | // 请假当天:只要有打卡就算正常,迟到与否不影响考勤 |
| | | clockInType = HrmAttendanceClockTypeEnum.NORMAL.getType(); |
| | | } else { |
| | | clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold) |
| | | ? HrmAttendanceClockTypeEnum.NORMAL.getType() |
| | | : HrmAttendanceClockTypeEnum.LATE.getType(); |
| | | } |
| | | // 5. 创建或更新考勤记录 |
| | | if (record == null) { |
| | | record = HrmAttendanceRecordDO.builder() |
| | | .userId(userId) |
| | | .userId(systemUserId) |
| | | .deptId(employee.getDeptId()) |
| | | .date(clockDate) |
| | | .clockInTime(clockTime) |
| | | .clockInType(clockInType) |
| | | .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType()) |
| | | .build(); |
| | | applyLeaveInfo(record, leaves, rule, clockDate); |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, true); |
| | | attendanceRecordMapper.insert(record); |
| | | } else { |
| | | record.setClockInTime(clockTime); |
| | | record.setClockInType(clockInType); |
| | | applyLeaveInfo(record, leaves, rule, clockDate); |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, true); |
| | | attendanceRecordMapper.updateById(record); |
| | | } |
| | |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS); |
| | | } |
| | | Long systemUserId = employee.getUserId(); |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId()); |
| | | if (rule == null) { |
| | | throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE); |
| | | } |
| | | // 3. 查询当日考勤记录 |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate); |
| | | // 3. 查询当日考勤记录(系统用户 ID) |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(systemUserId, clockDate); |
| | | if (record != null && record.getClockOutTime() != null) { |
| | | throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED); |
| | | } |
| | |
| | | ClockLocation clockLocation = validateClockLocation(rule, reqVO); |
| | | if (record == null) { |
| | | record = HrmAttendanceRecordDO.builder() |
| | | .userId(userId) |
| | | .userId(systemUserId) |
| | | .deptId(employee.getDeptId()) |
| | | .date(clockDate) |
| | | .clockInType(HrmAttendanceClockTypeEnum.MISSING.getType()) |
| | | .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType()) |
| | | .build(); |
| | | } |
| | | // 3.1 当天请假情况:请假当天允许打卡,不判定为早退,也不算缺上班卡 |
| | | // 同上:用系统用户 ID 查请假表 |
| | | List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves = |
| | | leaveQueryService.getApprovedLeaveList(employee.getUserId(), clockDate); |
| | | boolean onLeave = !leaves.isEmpty(); |
| | | if (onLeave && HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) { |
| | | // 请假当天没打上班卡属于正常(人没来),不应记缺卡 |
| | | record.setClockInType(HrmAttendanceClockTypeEnum.LEAVE.getType()); |
| | | } |
| | | // 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) |
| | | Integer clockOutType; |
| | | if (onLeave) { |
| | | // 请假当天:有打卡就算正常 |
| | | clockOutType = HrmAttendanceClockTypeEnum.NORMAL.getType(); |
| | | } else { |
| | | clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold) |
| | | ? HrmAttendanceClockTypeEnum.NORMAL.getType() |
| | | : HrmAttendanceClockTypeEnum.EARLY.getType(); |
| | | } |
| | | // 5. 更新考勤记录 |
| | | record.setClockOutTime(clockTime); |
| | | record.setClockOutType(clockOutType); |
| | | applyLeaveInfo(record, leaves, rule, clockDate); |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, false); |
| | | // 6. 计算工作时长 |
| | | if (record.getClockInTime() != null) { |