| | |
| | | package cn.iocoder.yudao.module.hrm.service.attendance; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | 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.HrmAttendanceMyStatusRespVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO; |
| | |
| | | |
| | | @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 (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)) { |
| | |
| | | } |
| | | // 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); |
| | | } |
| | | if (record.getId() == null) { |
| | | attendanceRecordMapper.insert(record); |
| | | } else { |
| | | attendanceRecordMapper.updateById(record); |
| | | return 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 |
| | |
| | | respVO.setRuleName(rule.getName()); |
| | | respVO.setWorkStartTime(rule.getWorkStartTime()); |
| | | respVO.setWorkEndTime(rule.getWorkEndTime()); |
| | | 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()); |
| | | } |
| | | if (record != null) { |
| | | respVO.setRecordId(record.getId()); |