| | |
| | | package cn.iocoder.yudao.module.hrm.service.attendance; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | 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.HrmAttendanceClockReqVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordRespVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleRespVO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceHolidayDO; |
| | | import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceHolidayTypeEnum; |
| | | import cn.iocoder.yudao.module.system.api.dept.DeptApi; |
| | | import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; |
| | | 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 java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.DayOfWeek; |
| | | import java.time.Duration; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | |
| | | private HrmAttendanceRuleService attendanceRuleService; |
| | | @Resource |
| | | private HrmAttendanceHolidayService attendanceHolidayService; |
| | | @Resource |
| | | private DeptApi deptApi; |
| | | |
| | | @Override |
| | | public PageResult<HrmAttendanceRecordDO> getAttendanceRecordPage(HrmAttendanceRecordPageReqVO pageReqVO) { |
| | |
| | | if (record.getDeptId() == null) { |
| | | return; |
| | | } |
| | | // 上班打卡类型:晚于 上班时间+迟到允许 记为迟到(按上班打卡时间匹配班次) |
| | | if (record.getClockInTime() != null) { |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId( |
| | | record.getDeptId(), record.getClockInTime()); |
| | | if (rule != 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()); |
| | | } |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId(), record.getDate()); |
| | | if (rule == null) { |
| | | return; |
| | | } |
| | | // 下班打卡类型:早于 下班时间-早退允许 记为早退(按下班打卡时间匹配班次) |
| | | if (record.getClockOutTime() != null) { |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId( |
| | | record.getDeptId(), record.getClockOutTime()); |
| | | if (rule != 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()); |
| | | } |
| | | // 上班打卡类型:晚于 上班时间+迟到允许 记为迟到 |
| | | 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()); |
| | | } |
| | | } |
| | | |
| | |
| | | .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); |
| | | record.setWorkHours(workHours); |
| | | } |
| | | // 加班时长:按考勤规则的标准下班时间计算超额部分(按下班打卡时间匹配班次) |
| | | // 加班时长:按考勤规则的标准下班时间计算超额部分 |
| | | if (record.getClockOutTime() != null && record.getDeptId() != null) { |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId( |
| | | record.getDeptId(), record.getClockOutTime()); |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId(), record.getDate()); |
| | | if (rule != null && rule.getWorkEndTime() != null) { |
| | | LocalTime workEnd = rule.getWorkEndTime(); |
| | | LocalDate outDate = record.getClockOutTime().toLocalDate(); |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public HrmAttendanceRecordDO clockIn(Long userId, LocalDateTime clockTime, String location) { |
| | | public HrmAttendanceClockResult clockIn(Long userId, LocalDateTime clockTime, |
| | | HrmAttendanceClockReqVO reqVO, String legacyLocation) { |
| | | LocalDate clockDate = clockTime.toLocalDate(); |
| | | // 1. 校验是否工作日 |
| | | if (!attendanceHolidayService.isWorkDay(clockDate)) { |
| | |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS); |
| | | } |
| | | // 按本次打卡时间匹配班次(同一个部门可能配置了多条规则) |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId(), clockTime); |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId(), clockDate); |
| | | if (rule == null) { |
| | | throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE); |
| | | } |
| | |
| | | if (record != null && record.getClockInTime() != null) { |
| | | throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED); |
| | | } |
| | | // 在任何写库操作前完成定位校验,旧版地点字符串不能绕过范围限制。 |
| | | ClockLocation clockLocation = validateClockLocation(rule, reqVO); |
| | | // 4. 判断打卡类型 |
| | | LocalTime workStartTime = rule.getWorkStartTime(); |
| | | int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0; |
| | |
| | | .date(clockDate) |
| | | .clockInTime(clockTime) |
| | | .clockInType(clockInType) |
| | | .location(location) |
| | | .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType()) |
| | | .build(); |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, true); |
| | | attendanceRecordMapper.insert(record); |
| | | } else { |
| | | record.setClockInTime(clockTime); |
| | | record.setClockInType(clockInType); |
| | | if (location != null) { |
| | | record.setLocation(location); |
| | | } |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, true); |
| | | attendanceRecordMapper.updateById(record); |
| | | } |
| | | return record; |
| | | return new HrmAttendanceClockResult(record, rule, |
| | | clockLocation != null ? clockLocation.distanceMeters() : null); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public HrmAttendanceRecordDO clockOut(Long userId, LocalDateTime clockTime, String location) { |
| | | public HrmAttendanceClockResult clockOut(Long userId, LocalDateTime clockTime, |
| | | HrmAttendanceClockReqVO reqVO, String legacyLocation) { |
| | | LocalDate clockDate = clockTime.toLocalDate(); |
| | | // 1. 校验是否工作日 |
| | | if (!attendanceHolidayService.isWorkDay(clockDate)) { |
| | |
| | | if (employee == null) { |
| | | throw exception(EMPLOYEE_NOT_EXISTS); |
| | | } |
| | | // 按本次打卡时间匹配班次(同一个部门可能配置了多条规则) |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId(), clockTime); |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId(), clockDate); |
| | | if (rule == null) { |
| | | throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE); |
| | | } |
| | | // 3. 查询当日考勤记录 |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, 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) |
| | | .deptId(employee.getDeptId()) |
| | |
| | | .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(); |
| | |
| | | // 5. 更新考勤记录 |
| | | record.setClockOutTime(clockTime); |
| | | record.setClockOutType(clockOutType); |
| | | if (location != null) { |
| | | record.setLocation(location); |
| | | } |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, false); |
| | | // 6. 计算工作时长 |
| | | if (record.getClockInTime() != null) { |
| | | Duration duration = Duration.between(record.getClockInTime(), clockTime); |
| | |
| | | .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); |
| | | record.setWorkHours(workHours); |
| | | } |
| | | attendanceRecordMapper.updateById(record); |
| | | return record; |
| | | if (record.getId() == null) { |
| | | attendanceRecordMapper.insert(record); |
| | | } else { |
| | | attendanceRecordMapper.updateById(record); |
| | | } |
| | | return new HrmAttendanceClockResult(record, rule, |
| | | clockLocation != null ? clockLocation.distanceMeters() : null); |
| | | } |
| | | |
| | | record ClockLocation(BigDecimal longitude, BigDecimal latitude, BigDecimal accuracy, |
| | | BigDecimal distanceMeters) { } |
| | | |
| | | ClockLocation validateClockLocation(HrmAttendanceRuleDO rule, HrmAttendanceClockReqVO reqVO) { |
| | | boolean required = Boolean.TRUE.equals(rule.getLocationRequired()); |
| | | if (required && (StrUtil.isBlank(rule.getLocationName()) || rule.getLongitude() == null |
| | | || rule.getLatitude() == null || rule.getRadiusMeters() == null |
| | | || rule.getRadiusMeters() < 10 || rule.getRadiusMeters() > 5000 |
| | | || !"WGS84".equalsIgnoreCase(rule.getCoordinateSystem()))) { |
| | | throw exception(ATTENDANCE_LOCATION_RULE_INVALID); |
| | | } |
| | | if (reqVO == null || reqVO.getLongitude() == null || reqVO.getLatitude() == null |
| | | || reqVO.getAccuracy() == null || StrUtil.isBlank(reqVO.getCoordinateSystem())) { |
| | | if (required) throw exception(ATTENDANCE_LOCATION_REQUIRED); |
| | | return null; |
| | | } |
| | | boolean coordinatesValid = reqVO.getLongitude().compareTo(BigDecimal.valueOf(-180)) >= 0 |
| | | && reqVO.getLongitude().compareTo(BigDecimal.valueOf(180)) <= 0 |
| | | && reqVO.getLatitude().compareTo(BigDecimal.valueOf(-90)) >= 0 |
| | | && reqVO.getLatitude().compareTo(BigDecimal.valueOf(90)) <= 0; |
| | | if (!coordinatesValid) { |
| | | if (required) throw exception(ATTENDANCE_LOCATION_COORDINATE_INVALID); |
| | | return null; |
| | | } |
| | | if (reqVO.getAccuracy().signum() <= 0 |
| | | || reqVO.getAccuracy().compareTo(BigDecimal.valueOf(100)) > 0) { |
| | | if (required) throw exception(ATTENDANCE_LOCATION_ACCURACY_INVALID); |
| | | return null; |
| | | } |
| | | if (!"WGS84".equalsIgnoreCase(reqVO.getCoordinateSystem().trim())) { |
| | | if (required) throw exception(ATTENDANCE_LOCATION_SYSTEM_INVALID); |
| | | return null; |
| | | } |
| | | if (!required) { |
| | | return new ClockLocation(reqVO.getLongitude(), reqVO.getLatitude(), reqVO.getAccuracy(), null); |
| | | } |
| | | double rawDistance = haversineMeters(rule.getLongitude().doubleValue(), rule.getLatitude().doubleValue(), |
| | | reqVO.getLongitude().doubleValue(), reqVO.getLatitude().doubleValue()); |
| | | BigDecimal roundedDistance = BigDecimal.valueOf(rawDistance).setScale(2, RoundingMode.HALF_UP); |
| | | if (rawDistance > rule.getRadiusMeters()) { |
| | | throw exception(ATTENDANCE_LOCATION_OUT_OF_RANGE, rule.getLocationName(), |
| | | roundedDistance.toPlainString(), rule.getRadiusMeters()); |
| | | } |
| | | return new ClockLocation(reqVO.getLongitude(), reqVO.getLatitude(), reqVO.getAccuracy(), roundedDistance); |
| | | } |
| | | |
| | | static double haversineMeters(double lon1, double lat1, double lon2, double lat2) { |
| | | double latDelta = Math.toRadians(lat2 - lat1); |
| | | double lonDelta = Math.toRadians(lon2 - lon1); |
| | | double a = Math.pow(Math.sin(latDelta / 2), 2) |
| | | + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) |
| | | * Math.pow(Math.sin(lonDelta / 2), 2); |
| | | double bounded = Math.max(0D, Math.min(1D, a)); |
| | | return 6_371_000D * 2D * Math.atan2(Math.sqrt(bounded), Math.sqrt(1D - bounded)); |
| | | } |
| | | |
| | | private void applyClockLocation(HrmAttendanceRecordDO record, HrmAttendanceRuleDO rule, |
| | | ClockLocation location, String legacyLocation, boolean clockIn) { |
| | | if (StrUtil.isNotBlank(rule.getLocationName())) { |
| | | record.setLocation(rule.getLocationName()); |
| | | } else if (!Boolean.TRUE.equals(rule.getLocationRequired()) && StrUtil.isNotBlank(legacyLocation)) { |
| | | record.setLocation(legacyLocation); |
| | | } |
| | | if (location == null) return; |
| | | if (clockIn) { |
| | | record.setClockInLongitude(location.longitude()); |
| | | record.setClockInLatitude(location.latitude()); |
| | | record.setClockInAccuracy(location.accuracy()); |
| | | record.setClockInDistance(location.distanceMeters()); |
| | | record.setClockInAddress(rule.getLocationAddress()); |
| | | } else { |
| | | record.setClockOutLongitude(location.longitude()); |
| | | record.setClockOutLatitude(location.latitude()); |
| | | record.setClockOutAccuracy(location.accuracy()); |
| | | record.setClockOutDistance(location.distanceMeters()); |
| | | record.setClockOutAddress(rule.getLocationAddress()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | |
| | | attendanceRecordMapper.updateById(record); |
| | | } |
| | | |
| | | } |
| | | // ==================== 我的考勤状态(App 打卡页) ==================== |
| | | |
| | | @Override |
| | | public HrmAttendanceMyStatusRespVO getMyStatus(Long loginUserId, LocalDate date) { |
| | | LocalDate queryDate = date != null ? date : LocalDate.now(); |
| | | // 1. 登录用户 -> 员工档案 |
| | | HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId); |
| | | if (employee == null) { |
| | | throw new ServiceException(EMPLOYEE_NOT_EXISTS.getCode(), |
| | | "当前登录账号未关联员工档案,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。"); |
| | | } |
| | | HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO(); |
| | | respVO.setUserId(loginUserId); |
| | | respVO.setEmployeeId(employee.getId()); |
| | | respVO.setEmployeeName(employee.getName()); |
| | | respVO.setDeptId(employee.getDeptId()); |
| | | if (employee.getDeptId() != null) { |
| | | DeptRespDTO dept = deptApi.getDept(employee.getDeptId()); |
| | | if (dept != null) { |
| | | respVO.setDeptName(dept.getName()); |
| | | } |
| | | } |
| | | respVO.setDate(queryDate); |
| | | respVO.setServerTime(LocalDateTime.now()); |
| | | |
| | | // 2. 是否工作日 + 休息原因 |
| | | boolean workDay = attendanceHolidayService.isWorkDay(queryDate); |
| | | respVO.setWorkDay(workDay); |
| | | if (!workDay) { |
| | | respVO.setRestDayReason(getRestDayReason(queryDate)); |
| | | } |
| | | |
| | | // 3. 当前生效规则(部门规则优先,其次全局规则;按日期匹配生效区间) |
| | | HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId(), queryDate); |
| | | if (rule != null) { |
| | | HrmAttendanceRuleRespVO ruleRespVO = BeanUtils.toBean(rule, HrmAttendanceRuleRespVO.class); |
| | | if (rule.getDeptId() != null) { |
| | | DeptRespDTO ruleDept = deptApi.getDept(rule.getDeptId()); |
| | | if (ruleDept != null) { |
| | | ruleRespVO.setDeptName(ruleDept.getName()); |
| | | } |
| | | } |
| | | respVO.setRule(ruleRespVO); |
| | | // 兼容旧版客户端:保留平铺的定位字段 |
| | | respVO.setLocationRequired(rule.getLocationRequired()); |
| | | respVO.setLocationName(rule.getLocationName()); |
| | | respVO.setLocationAddress(rule.getLocationAddress()); |
| | | respVO.setLongitude(rule.getLongitude()); |
| | | respVO.setLatitude(rule.getLatitude()); |
| | | respVO.setRadiusMeters(rule.getRadiusMeters()); |
| | | respVO.setCoordinateSystem(rule.getCoordinateSystem()); |
| | | } |
| | | |
| | | // 4. 当日考勤记录 |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(employee.getId(), queryDate); |
| | | if (record != null) { |
| | | respVO.setRecord(buildRecordRespVO(record, employee)); |
| | | respVO.setRecordId(record.getId()); |
| | | } |
| | | |
| | | // 5. 当前可打卡状态 |
| | | respVO.setClockable(buildClockable(workDay, rule, record, queryDate)); |
| | | return respVO; |
| | | } |
| | | |
| | | /** |
| | | * 休息日原因:优先取节假日配置名称,未配置时按星期给出“周六/周日” |
| | | */ |
| | | private String getRestDayReason(LocalDate date) { |
| | | List<HrmAttendanceHolidayDO> holidays = attendanceHolidayService.getAttendanceHolidayListByDateRange(date, date); |
| | | for (HrmAttendanceHolidayDO holiday : holidays) { |
| | | if (HrmAttendanceHolidayTypeEnum.REST_DAY.getType().equals(holiday.getType())) { |
| | | return holiday.getName(); |
| | | } |
| | | } |
| | | DayOfWeek dayOfWeek = date.getDayOfWeek(); |
| | | if (dayOfWeek == DayOfWeek.SATURDAY) { |
| | | return "周六"; |
| | | } |
| | | if (dayOfWeek == DayOfWeek.SUNDAY) { |
| | | return "周日"; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 构建打卡记录返回 VO,补齐打卡类型名称与员工信息 |
| | | */ |
| | | private HrmAttendanceRecordRespVO buildRecordRespVO(HrmAttendanceRecordDO record, HrmEmployeeDO employee) { |
| | | HrmAttendanceRecordRespVO recordRespVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class); |
| | | recordRespVO.setClockInTypeName(HrmAttendanceClockTypeEnum.getNameByType(record.getClockInType())); |
| | | recordRespVO.setClockOutTypeName(HrmAttendanceClockTypeEnum.getNameByType(record.getClockOutType())); |
| | | if (employee != null) { |
| | | recordRespVO.setUserName(employee.getName()); |
| | | } |
| | | return recordRespVO; |
| | | } |
| | | |
| | | /** |
| | | * 计算当前可打卡状态 |
| | | */ |
| | | private HrmAttendanceMyStatusRespVO.Clockable buildClockable(boolean workDay, HrmAttendanceRuleDO rule, |
| | | HrmAttendanceRecordDO record, LocalDate date) { |
| | | HrmAttendanceMyStatusRespVO.Clockable clockable = new HrmAttendanceMyStatusRespVO.Clockable(); |
| | | if (!workDay) { |
| | | clockable.setClockIn(false); |
| | | clockable.setClockOut(false); |
| | | clockable.setNextAction("none"); |
| | | clockable.setMessage("今日休息,无需打卡"); |
| | | return clockable; |
| | | } |
| | | if (rule == null) { |
| | | clockable.setClockIn(false); |
| | | clockable.setClockOut(false); |
| | | clockable.setNextAction("none"); |
| | | clockable.setMessage("未配置考勤规则,请联系管理员"); |
| | | return clockable; |
| | | } |
| | | // 只有下班卡、没有上班卡:当日已结束,不再允许继续打卡 |
| | | if (record != null && record.getClockInTime() == null && record.getClockOutTime() != null) { |
| | | clockable.setClockIn(false); |
| | | clockable.setClockOut(false); |
| | | clockable.setNextAction("none"); |
| | | clockable.setMessage("今日缺上班卡,已完成下班打卡"); |
| | | return clockable; |
| | | } |
| | | // 今天还没打上班卡 |
| | | if (record == null || record.getClockInTime() == null) { |
| | | clockable.setClockIn(true); |
| | | clockable.setClockOut(false); |
| | | clockable.setNextAction("in"); |
| | | clockable.setMessage("可上班打卡"); |
| | | // 仅当天查询时提示已迟到时长 |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | if (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))) { |
| | | long lateMinutes = Duration.between(workStart, now).toMinutes(); |
| | | clockable.setMessage("已超过上班时间 " + lateMinutes + " 分钟"); |
| | | } |
| | | } |
| | | return clockable; |
| | | } |
| | | // 已打上班卡、未打下班卡 |
| | | if (record.getClockOutTime() == null) { |
| | | clockable.setClockIn(false); |
| | | clockable.setClockOut(true); |
| | | clockable.setNextAction("out"); |
| | | clockable.setMessage("已上班打卡,可下班打卡"); |
| | | return clockable; |
| | | } |
| | | // 上下班卡都已打 |
| | | clockable.setClockIn(false); |
| | | clockable.setClockOut(false); |
| | | clockable.setNextAction("none"); |
| | | clockable.setMessage("今日已完成打卡"); |
| | | return clockable; |
| | | } |
| | | |
| | | } |