From d54fbc9d4f8981fa0f7f613b33a28a8cd4f764ea Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期日, 25 十月 2026 16:29:53 +0800
Subject: [PATCH] feat(system): 优化用户搜索功能并完善ERP和MES模块验证逻辑

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java |  154 ++++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 126 insertions(+), 28 deletions(-)

diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
index ea112f1..b91eded 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -6,6 +6,7 @@
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO;
 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.controller.admin.attendance.vo.HrmAttendanceClockReqVO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceHolidayDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO;
@@ -20,6 +21,7 @@
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
 import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -32,6 +34,7 @@
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.List;
+import cn.hutool.core.util.StrUtil;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
@@ -43,6 +46,7 @@
  */
 @Service
 @Validated
+@Slf4j
 public class HrmAttendanceRecordServiceImpl implements HrmAttendanceRecordService {
 
     @Resource
@@ -79,14 +83,21 @@
     @Override
     public HrmAttendanceMyStatusRespVO getMyStatus(Long loginUserId, LocalDate date) {
         LocalDate queryDate = date != null ? date : LocalDate.now();
+        HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO();
+        respVO.setUserId(loginUserId);
+        respVO.setDate(queryDate);
+        respVO.setServerTime(LocalDateTime.now());
         // 1. 鐧诲綍鐢ㄦ埛 -> 鍛樺伐妗f
         HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
         if (employee == null) {
-            throw exception(EMPLOYEE_NOT_EXISTS,
-                    "褰撳墠鐧诲綍璐﹀彿鏈叧鑱斿憳宸ユ。妗堬紝鏃犳硶鎵撳崱銆傝鑱旂郴绠$悊鍛樺湪銆愬憳宸ョ鐞嗐�戜腑灏嗚鐧诲綍璐﹀彿缁戝畾鍒板憳宸ャ��");
+            HrmAttendanceMyStatusRespVO.Clockable clockable = new HrmAttendanceMyStatusRespVO.Clockable();
+            clockable.setClockIn(false);
+            clockable.setClockOut(false);
+            clockable.setNextAction("none");
+            clockable.setMessage("褰撳墠璐﹀彿鏈叧鑱斿憳宸ユ。妗堬紝璇疯仈绯荤鐞嗗憳鍦ㄥ憳宸ョ鐞嗕腑瀹屾垚鍏宠仈");
+            respVO.setClockable(clockable);
+            return respVO;
         }
-        HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO();
-        respVO.setUserId(loginUserId);
         respVO.setEmployeeId(employee.getId());
         respVO.setEmployeeName(employee.getName());
         respVO.setDeptId(employee.getDeptId());
@@ -96,8 +107,6 @@
                 respVO.setDeptName(dept.getName());
             }
         }
-        respVO.setDate(queryDate);
-        respVO.setServerTime(LocalDateTime.now());
 
         // 2. 鏄惁宸ヤ綔鏃� + 浼戞伅鍘熷洜
         boolean workDay = attendanceHolidayService.isWorkDay(queryDate);
@@ -314,7 +323,8 @@
 
     @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)) {
@@ -334,7 +344,9 @@
         if (record != null && record.getClockInTime() != null) {
             throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
         }
-        // 4. 鍒ゆ柇鎵撳崱绫诲瀷
+        // 4. 鍦ㄤ换浣曞啓搴撴搷浣滃墠鏍¢獙浣嶇疆锛涗笉鑳界敤鏃х増 location 瀛楃涓茬粫杩囪寖鍥撮檺鍒躲��
+        ClockLocation clockLocation = validateClockLocation(rule, reqVO);
+        // 5. 鍒ゆ柇鎵撳崱绫诲瀷
         LocalTime workStartTime = rule.getWorkStartTime();
         int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
         LocalTime lateThreshold = workStartTime.plusMinutes(lateAllowMinutes);
@@ -342,7 +354,7 @@
         Integer clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold)
                 ? HrmAttendanceClockTypeEnum.NORMAL.getType()
                 : HrmAttendanceClockTypeEnum.LATE.getType();
-        // 5. 鍒涘缓鎴栨洿鏂拌�冨嫟璁板綍
+        // 6. 鍒涘缓鎴栨洿鏂拌�冨嫟璁板綍
         if (record == null) {
             record = HrmAttendanceRecordDO.builder()
                     .userId(userId)
@@ -350,24 +362,24 @@
                     .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)) {
@@ -384,8 +396,12 @@
         }
         // 3. 鏌ヨ褰撴棩鑰冨嫟璁板綍
         HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate);
+        if (record != null && record.getClockOutTime() != null) {
+            throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
+        }
+        ClockLocation clockLocation = validateClockLocation(rule, reqVO);
+        // 4. 鏃犱笂鐝崱鏃朵篃鍏堝畬鎴愪綅缃牎楠岋紝鍐嶅垱寤虹己鍗¤褰曘��
         if (record == null) {
-            // 濡傛灉娌℃湁涓婄彮鎵撳崱璁板綍锛屽垱寤轰竴鏉$己鍗$殑璁板綍
             record = HrmAttendanceRecordDO.builder()
                     .userId(userId)
                     .deptId(employee.getDeptId())
@@ -393,12 +409,8 @@
                     .clockInType(HrmAttendanceClockTypeEnum.MISSING.getType())
                     .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType())
                     .build();
-            attendanceRecordMapper.insert(record);
         }
-        if (record.getClockOutTime() != null) {
-            throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
-        }
-        // 4. 鍒ゆ柇鎵撳崱绫诲瀷
+        // 5. 鍒ゆ柇鎵撳崱绫诲瀷
         LocalTime workEndTime = rule.getWorkEndTime();
         int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
         LocalTime earlyThreshold = workEndTime.minusMinutes(earlyAllowMinutes);
@@ -406,12 +418,10 @@
         Integer clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold)
                 ? HrmAttendanceClockTypeEnum.NORMAL.getType()
                 : HrmAttendanceClockTypeEnum.EARLY.getType();
-        // 5. 鏇存柊鑰冨嫟璁板綍
+        // 6. 鏇存柊鑰冨嫟璁板綍
         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);
@@ -421,8 +431,91 @@
         }
         // 7. 璁$畻鍔犵彮鏃堕暱锛氳秴杩囪鍒欎笅鐝椂闂寸殑閮ㄥ垎
         record.setOvertimeHours(computeOvertimeHours(rule.getWorkEndTime(), clockTimeOnly));
-        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());
+        }
     }
 
     /**
@@ -483,6 +576,11 @@
         }
         LocalDate exceptionDate = exception.getExceptionDate();
         LocalDateTime exceptionTime = exception.getExceptionTime();
+        // 琛ュ崱鏃堕棿缂哄け鏃舵棤娉曞洖濉�冨嫟璁板綍锛岃褰曟棩蹇楀苟璺宠繃锛岄伩鍏嶇┖鎸囬拡
+        if (exceptionTime == null) {
+            log.warn("[supplementClockIn] 琛ュ崱鐢宠({}) 琛ュ崱鏃堕棿涓虹┖锛岃烦杩囪�冨嫟璁板綍鍥炲~", exception.getId());
+            return;
+        }
         Long userId = exception.getUserId();
         HrmEmployeeDO employee = employeeService.getEmployee(userId);
         HrmAttendanceRuleDO rule = employee == null ? null
@@ -524,4 +622,4 @@
         attendanceRecordMapper.updateById(record);
     }
 
-}
\ No newline at end of file
+}

--
Gitblit v1.9.3