From 5611b19e904ec0daa96ea5f648500cf6205c9385 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期一, 26 十月 2026 15:39:37 +0800
Subject: [PATCH] fix(hrm): 合并定位打卡与考勤状态功能

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java  |   41 +++++-
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java              |   19 ++-
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceMyStatusRespVO.java |   74 ++++++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java          |  170 +++++++++++++++++++++++++---
 4 files changed, 271 insertions(+), 33 deletions(-)

diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
index 0528f3e..7908530 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
@@ -8,6 +8,8 @@
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceClockReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceClockRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordImportExcelVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordImportReqVO;
@@ -16,6 +18,7 @@
 import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
 import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceClockTypeEnum;
+import cn.iocoder.yudao.module.hrm.service.attendance.HrmAttendanceClockResult;
 import cn.iocoder.yudao.module.hrm.service.attendance.HrmAttendanceRecordService;
 import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
 import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -64,33 +67,44 @@
 
     @PostMapping("/clock-in")
     @Operation(summary = "涓婄彮鎵撳崱")
-    public CommonResult<HrmAttendanceRecordRespVO> clockIn(@RequestParam("location") String location) {
+    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')")
+    @ApiAccessLog(requestEnable = false)
+    public CommonResult<HrmAttendanceClockRespVO> clockIn(
+            @Valid @RequestBody(required = false) HrmAttendanceClockReqVO reqVO,
+            @RequestParam(value = "location", required = false) String legacyLocation) {
         Long loginUserId = getLoginUserId();
         HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
         if (employee == null) {
             throw exception(EMPLOYEE_NOT_EXISTS,
                     "褰撳墠鐧诲綍璐﹀彿鏈叧鑱斿憳宸ユ。妗堬紝鏃犳硶鎵撳崱銆傝鑱旂郴绠$悊鍛樺湪銆愬憳宸ョ鐞嗐�戜腑灏嗚鐧诲綍璐﹀彿缁戝畾鍒板憳宸ャ��");
         }
-        HrmAttendanceRecordDO record = attendanceRecordService.clockIn(employee.getId(), LocalDateTime.now(), location);
-        return success(buildClockRespVO(record, employee));
+        HrmAttendanceClockResult result = attendanceRecordService.clockIn(
+                employee.getId(), LocalDateTime.now(), reqVO, legacyLocation);
+        return success(buildClockResultVO(result, employee));
     }
 
     @PostMapping("/clock-out")
     @Operation(summary = "涓嬬彮鎵撳崱")
-    public CommonResult<HrmAttendanceRecordRespVO> clockOut(@RequestParam("location") String location) {
+    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')")
+    @ApiAccessLog(requestEnable = false)
+    public CommonResult<HrmAttendanceClockRespVO> clockOut(
+            @Valid @RequestBody(required = false) HrmAttendanceClockReqVO reqVO,
+            @RequestParam(value = "location", required = false) String legacyLocation) {
         Long loginUserId = getLoginUserId();
         HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
         if (employee == null) {
             throw exception(EMPLOYEE_NOT_EXISTS,
                     "褰撳墠鐧诲綍璐﹀彿鏈叧鑱斿憳宸ユ。妗堬紝鏃犳硶鎵撳崱銆傝鑱旂郴绠$悊鍛樺湪銆愬憳宸ョ鐞嗐�戜腑灏嗚鐧诲綍璐﹀彿缁戝畾鍒板憳宸ャ��");
         }
-        HrmAttendanceRecordDO record = attendanceRecordService.clockOut(employee.getId(), LocalDateTime.now(), location);
-        return success(buildClockRespVO(record, employee));
+        HrmAttendanceClockResult result = attendanceRecordService.clockOut(
+                employee.getId(), LocalDateTime.now(), reqVO, legacyLocation);
+        return success(buildClockResultVO(result, employee));
     }
 
     @GetMapping("/my-status")
     @Operation(summary = "鑾峰緱褰撳墠鐧诲綍浜虹殑鑰冨嫟鐘舵��")
     @Parameter(name = "date", description = "鑰冨嫟鏃ユ湡锛屾牸寮� yyyy-MM-dd锛岄粯璁ゆ湇鍔″櫒褰撳ぉ", example = "2026-10-17")
+    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')")
     public CommonResult<HrmAttendanceMyStatusRespVO> getMyStatus(
             @RequestParam(value = "date", required = false)
             @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) LocalDate date) {
@@ -110,6 +124,19 @@
             if (dept != null) {
                 respVO.setDeptName(dept.getName());
             }
+        }
+        return respVO;
+    }
+
+    private HrmAttendanceClockRespVO buildClockResultVO(HrmAttendanceClockResult result,
+                                                         HrmEmployeeDO employee) {
+        HrmAttendanceClockRespVO respVO = new HrmAttendanceClockRespVO();
+        respVO.setRecord(buildClockRespVO(result.record(), employee));
+        respVO.setLocationName(result.rule().getLocationName());
+        if (Boolean.TRUE.equals(result.rule().getLocationRequired())) {
+            respVO.setDistanceMeters(result.distanceMeters());
+            respVO.setRadiusMeters(result.rule().getRadiusMeters());
+            respVO.setWithinRange(true);
         }
         return respVO;
     }
@@ -326,4 +353,4 @@
         ExcelUtils.write(response, "鑰冨嫟璁板綍.xls", "鏁版嵁", HrmAttendanceRecordRespVO.class, list);
     }
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceMyStatusRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceMyStatusRespVO.java
index f5d1198..ad05c5b 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceMyStatusRespVO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceMyStatusRespVO.java
@@ -3,8 +3,10 @@
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
+import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 
 @Schema(description = "绠$悊鍚庡彴 - 鎴戠殑鑰冨嫟鐘舵�� Response VO")
 @Data
@@ -18,6 +20,9 @@
 
     @Schema(description = "鍛樺伐濮撳悕", example = "闃垮崪鏉滃厠鐑湪路鍚炬媺渚濇湪")
     private String employeeName;
+
+    @Schema(description = "鍛樺伐濮撳悕锛堝吋瀹规棫鐗堝鎴风锛�")
+    private String userName;
 
     @Schema(description = "閮ㄩ棬ID", example = "131")
     private Long deptId;
@@ -40,9 +45,78 @@
     @Schema(description = "褰撳墠鐢熸晥鐨勮�冨嫟瑙勫垯锛屾湭閰嶇疆鏃朵负 null")
     private HrmAttendanceRuleRespVO rule;
 
+    @Schema(description = "鑰冨嫟瑙勫垯鍚嶇О锛堝吋瀹规棫鐗堝鎴风锛�")
+    private String ruleName;
+
+    @Schema(description = "涓婄彮鏃堕棿锛堝吋瀹规棫鐗堝鎴风锛�")
+    private LocalTime workStartTime;
+
+    @Schema(description = "涓嬬彮鏃堕棿锛堝吋瀹规棫鐗堝鎴风锛�")
+    private LocalTime workEndTime;
+
+    @Schema(description = "鏄惁鍚敤瀹氫綅鑼冨洿鏍¢獙")
+    private Boolean locationRequired;
+
+    @Schema(description = "鎵撳崱鍦扮偣鍚嶇О")
+    private String locationName;
+
+    @Schema(description = "鎵撳崱鍦扮偣鍦板潃")
+    private String locationAddress;
+
+    @Schema(description = "鎵撳崱涓績缁忓害锛圵GS84锛�")
+    private BigDecimal longitude;
+
+    @Schema(description = "鎵撳崱涓績绾害锛圵GS84锛�")
+    private BigDecimal latitude;
+
+    @Schema(description = "鍏佽鎵撳崱鍗婂緞锛屽崟浣嶇背")
+    private Integer radiusMeters;
+
+    @Schema(description = "鍧愭爣绯�")
+    private String coordinateSystem;
+
     @Schema(description = "褰撴棩鑰冨嫟璁板綍锛屾湭鎵撳崱鏃朵负 null")
     private HrmAttendanceRecordRespVO record;
 
+    @Schema(description = "浠婃棩璁板綍ID锛堝吋瀹规棫鐗堝鎴风锛�")
+    private Long recordId;
+
+    @Schema(description = "涓婄彮鎵撳崱鏃堕棿锛堝吋瀹规棫鐗堝鎴风锛�")
+    private LocalDateTime clockInTime;
+
+    @Schema(description = "涓嬬彮鎵撳崱鏃堕棿锛堝吋瀹规棫鐗堝鎴风锛�")
+    private LocalDateTime clockOutTime;
+
+    @Schema(description = "涓婄彮鎵撳崱绫诲瀷锛堝吋瀹规棫鐗堝鎴风锛�")
+    private Integer clockInType;
+
+    @Schema(description = "涓嬬彮鎵撳崱绫诲瀷锛堝吋瀹规棫鐗堝鎴风锛�")
+    private Integer clockOutType;
+
+    @Schema(description = "鏄惁宸蹭笂鐝墦鍗★紙鍏煎鏃х増瀹㈡埛绔級")
+    private Boolean clockInDone;
+
+    @Schema(description = "鏄惁宸蹭笅鐝墦鍗★紙鍏煎鏃х増瀹㈡埛绔級")
+    private Boolean clockOutDone;
+
+    @Schema(description = "鏄惁鍙笂鐝墦鍗★紙鍏煎鏃х増瀹㈡埛绔級")
+    private Boolean canClockIn;
+
+    @Schema(description = "鏄惁鍙笅鐝墦鍗★紙鍏煎鏃х増瀹㈡埛绔級")
+    private Boolean canClockOut;
+
+    @Schema(description = "宸ヤ綔鏃堕暱锛屽皬鏃讹紙鍏煎鏃х増瀹㈡埛绔級")
+    private BigDecimal workHours;
+
+    @Schema(description = "鍔犵彮鏃堕暱锛屽皬鏃讹紙鍏煎鏃х増瀹㈡埛绔級")
+    private BigDecimal overtimeHours;
+
+    @Schema(description = "鎵撳崱鍦扮偣锛堝吋瀹规棫鐗堝鎴风锛�")
+    private String location;
+
+    @Schema(description = "鏁版嵁鏉ユ簮锛堝吋瀹规棫鐗堝鎴风锛�")
+    private Integer dataSource;
+
     @Schema(description = "褰撳墠鍙墦鍗$姸鎬�")
     private Clockable clockable;
 
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java
index 184295a..46fdf36 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java
@@ -2,6 +2,7 @@
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO;
+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.dal.dataobject.attendance.HrmAttendanceRecordDO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO;
@@ -58,20 +59,24 @@
      *
      * @param userId 鍛樺伐ID
      * @param clockTime 鎵撳崱鏃堕棿
-     * @param location 鎵撳崱鍦扮偣
-     * @return 鑰冨嫟璁板綍
+     * @param reqVO 瀹氫綅鍘熷鏁版嵁
+     * @param legacyLocation 鏃у鎴风浼犲叆鐨勫湴鐐规枃鏈紝浠呭湪鏈惎鐢ㄥ畾浣嶈寖鍥存椂鍏煎浣跨敤
+     * @return 鎵撳崱缁撴灉
      */
-    HrmAttendanceRecordDO clockIn(Long userId, LocalDateTime clockTime, String location);
+    HrmAttendanceClockResult clockIn(Long userId, LocalDateTime clockTime,
+                                     HrmAttendanceClockReqVO reqVO, String legacyLocation);
 
     /**
      * 鎵撳崱锛堜笅鐝墦鍗★級
      *
      * @param userId 鍛樺伐ID
      * @param clockTime 鎵撳崱鏃堕棿
-     * @param location 鎵撳崱鍦扮偣
-     * @return 鑰冨嫟璁板綍
+     * @param reqVO 瀹氫綅鍘熷鏁版嵁
+     * @param legacyLocation 鏃у鎴风浼犲叆鐨勫湴鐐规枃鏈紝浠呭湪鏈惎鐢ㄥ畾浣嶈寖鍥存椂鍏煎浣跨敤
+     * @return 鎵撳崱缁撴灉
      */
-    HrmAttendanceRecordDO clockOut(Long userId, LocalDateTime clockTime, String location);
+    HrmAttendanceClockResult clockOut(Long userId, LocalDateTime clockTime,
+                                      HrmAttendanceClockReqVO reqVO, String legacyLocation);
 
     /**
      * 琛ュ崱瀹℃壒閫氳繃鍚庢洿鏂拌�冨嫟璁板綍
@@ -80,4 +85,4 @@
      */
     void supplementClockIn(HrmAttendanceExceptionDO exception);
 
-}
\ No newline at end of file
+}
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 92f745d..dc658dd 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
@@ -1,7 +1,9 @@
 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.controller.admin.attendance.vo.HrmAttendanceRecordRespVO;
@@ -89,6 +91,7 @@
         respVO.setUserId(loginUserId);
         respVO.setEmployeeId(employee.getId());
         respVO.setEmployeeName(employee.getName());
+        respVO.setUserName(employee.getName());
         respVO.setDeptId(employee.getDeptId());
         if (employee.getDeptId() != null) {
             DeptRespDTO dept = deptApi.getDept(employee.getDeptId());
@@ -117,17 +120,51 @@
                 }
             }
             respVO.setRule(ruleRespVO);
+            fillCompatibleRuleFields(respVO, rule);
         }
 
         // 4. 褰撴棩鑰冨嫟璁板綍
         HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(employee.getId(), queryDate);
         if (record != null) {
             respVO.setRecord(buildRecordRespVO(record, employee));
+            fillCompatibleRecordFields(respVO, record);
         }
 
         // 5. 褰撳墠鍙墦鍗$姸鎬�
-        respVO.setClockable(buildClockable(workDay, rule, record, queryDate));
+        HrmAttendanceMyStatusRespVO.Clockable clockable = buildClockable(workDay, rule, record, queryDate);
+        respVO.setClockable(clockable);
+        respVO.setClockInDone(record != null && record.getClockInTime() != null);
+        respVO.setClockOutDone(record != null && record.getClockOutTime() != null);
+        respVO.setCanClockIn(clockable.getClockIn());
+        respVO.setCanClockOut(clockable.getClockOut());
         return respVO;
+    }
+
+    /** 鍚屾椂淇濈暀鏃х増 App/Web 浣跨敤鐨勫钩閾鸿鍒欏瓧娈点�� */
+    private void fillCompatibleRuleFields(HrmAttendanceMyStatusRespVO respVO, HrmAttendanceRuleDO rule) {
+        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());
+    }
+
+    /** 鍚屾椂淇濈暀鏃х増 App/Web 浣跨敤鐨勫钩閾鸿�冨嫟璁板綍瀛楁銆� */
+    private void fillCompatibleRecordFields(HrmAttendanceMyStatusRespVO respVO, HrmAttendanceRecordDO record) {
+        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());
     }
 
     /**
@@ -314,7 +351,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,6 +372,8 @@
         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;
@@ -350,24 +390,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 +424,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);
         if (record == null) {
-            // 濡傛灉娌℃湁涓婄彮鎵撳崱璁板綍锛屽垱寤轰竴鏉$己鍗$殑璁板綍
             record = HrmAttendanceRecordDO.builder()
                     .userId(userId)
                     .deptId(employee.getDeptId())
@@ -393,10 +437,6 @@
                     .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();
@@ -409,9 +449,7 @@
         // 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);
@@ -419,8 +457,102 @@
                     .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
@@ -469,4 +601,4 @@
         attendanceRecordMapper.updateById(record);
     }
 
-}
\ No newline at end of file
+}

--
Gitblit v1.9.3