From 997d864a76bc718d49aee6cb41d9412ac2a88d73 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期一, 26 十月 2026 14:44:55 +0800
Subject: [PATCH] feat(hrm): 支持定位范围打卡

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleRespVO.java     |   17 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java  |   50 +++-
 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/HrmAttendanceClockReqVO.java     |   19 +
 yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java                               |    6 
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceClockRespVO.java    |   20 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceMyStatusRespVO.java |   21 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockResult.java                |   11 +
 sql/mysql/hrm_attendance_location_20261024.sql                                                                             |   84 ++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRecordDO.java            |   13 +
 yudao-module-hrm/src/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockLocationTest.java          |   84 ++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleSaveReqVO.java  |   29 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRuleDO.java              |   11 +
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java          |  129 +++++++++++-
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java            |   53 +++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java   |   23 ++
 16 files changed, 542 insertions(+), 47 deletions(-)

diff --git a/sql/mysql/hrm_attendance_location_20261024.sql b/sql/mysql/hrm_attendance_location_20261024.sql
new file mode 100644
index 0000000..5094a81
--- /dev/null
+++ b/sql/mysql/hrm_attendance_location_20261024.sql
@@ -0,0 +1,84 @@
+-- 鑰冨嫟瀹氫綅瀛楁杩佺Щ銆傛棫瑙勫垯淇濇寔 location_required=0锛屽厛閰嶇疆鍦扮偣鍐嶅惎鐢ㄨ寖鍥撮檺鍒躲��
+DELIMITER $$
+DROP PROCEDURE IF EXISTS xgdl_hrm_attendance_add_column_20261024$$
+CREATE PROCEDURE xgdl_hrm_attendance_add_column_20261024(
+    IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64), IN p_ddl_sql VARCHAR(1000))
+BEGIN
+    IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
+                   WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = p_table_name AND COLUMN_NAME = p_column_name) THEN
+        SET @hrm_location_ddl = p_ddl_sql;
+        PREPARE hrm_location_stmt FROM @hrm_location_ddl;
+        EXECUTE hrm_location_stmt;
+        DEALLOCATE PREPARE hrm_location_stmt;
+    END IF;
+END$$
+DELIMITER ;
+
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','location_name',
+    'ALTER TABLE hrm_attendance_rule ADD COLUMN location_name varchar(100) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','location_address',
+    'ALTER TABLE hrm_attendance_rule ADD COLUMN location_address varchar(255) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','longitude',
+    'ALTER TABLE hrm_attendance_rule ADD COLUMN longitude decimal(10,7) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','latitude',
+    'ALTER TABLE hrm_attendance_rule ADD COLUMN latitude decimal(10,7) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','radius_meters',
+    'ALTER TABLE hrm_attendance_rule ADD COLUMN radius_meters int DEFAULT 10');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','location_required',
+    'ALTER TABLE hrm_attendance_rule ADD COLUMN location_required bit(1) NOT NULL DEFAULT b''0''');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','coordinate_system',
+    'ALTER TABLE hrm_attendance_rule ADD COLUMN coordinate_system varchar(20) DEFAULT ''WGS84''');
+
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_longitude',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_longitude decimal(10,7) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_latitude',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_latitude decimal(10,7) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_accuracy',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_accuracy decimal(8,2) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_distance',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_distance decimal(10,2) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_address',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_address varchar(255) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_longitude',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_longitude decimal(10,7) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_latitude',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_latitude decimal(10,7) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_accuracy',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_accuracy decimal(8,2) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_distance',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_distance decimal(10,2) NULL');
+CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_address',
+    'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_address varchar(255) NULL');
+
+DROP PROCEDURE xgdl_hrm_attendance_add_column_20261024;
+
+-- 瀵归綈鑰冨嫟瑙勫垯涓庢墦鍗℃寜閽潈闄愮爜锛岀‘淇� Web 鍜� APP 鏅�氳鑹插彲浠ユ巿鏉冧娇鐢ㄣ��
+UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:query' WHERE `id` = 60131;
+UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:create' WHERE `id` = 60132;
+UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:update' WHERE `id` = 60133;
+UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:delete' WHERE `id` = 60134;
+UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:export' WHERE `id` = 60135;
+
+INSERT INTO `system_menu`
+(`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`,
+ `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`,
+ `updater`, `update_time`, `deleted`)
+VALUES
+(60114, '瀹氫綅鎵撳崱', 'hrm:attendance-record:clock', 3, 4, 6011, '', '', '', '',
+ 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0')
+ON DUPLICATE KEY UPDATE
+`name` = VALUES(`name`), `permission` = VALUES(`permission`), `status` = 0, `deleted` = b'0';
+
+-- 宸叉嫢鏈夆�滆�冨嫟璁板綍鈥濊彍鍗曠殑瑙掕壊鑷姩鑾峰緱瀹氫綅鎵撳崱鎸夐挳鏉冮檺銆�
+INSERT INTO `system_role_menu`
+(`role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`)
+SELECT DISTINCT parent_role.`role_id`, 60114, '1', NOW(), '1', NOW(), b'0', parent_role.`tenant_id`
+FROM `system_role_menu` parent_role
+WHERE parent_role.`menu_id` = 6011 AND parent_role.`deleted` = b'0'
+  AND NOT EXISTS (
+      SELECT 1 FROM `system_role_menu` current_grant
+      WHERE current_grant.`role_id` = parent_role.`role_id`
+        AND current_grant.`menu_id` = 60114
+        AND current_grant.`tenant_id` = parent_role.`tenant_id`
+        AND current_grant.`deleted` = b'0'
+  );
diff --git a/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java b/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
index ec3dae4..dbc337c 100644
--- a/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
+++ b/yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -94,6 +94,12 @@
     ErrorCode ATTENDANCE_EXCEPTION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_004_015, "鑰冨嫟寮傚父瀹℃壒娴佺▼涓嶅瓨鍦�");
     ErrorCode ATTENDANCE_CLOCK_ALREADY_CLOCKED = new ErrorCode(1_030_004_016, "宸叉墦杩囧崱锛岃鍕块噸澶嶆墦鍗�");
     ErrorCode ATTENDANCE_HOLIDAY_DATE_RANGE_INVALID = new ErrorCode(1_030_004_017, "缁撴潫鏃ユ湡銆恵}銆戜笉鑳芥棭浜庡紑濮嬫棩鏈熴�恵}銆戯紝鏃犳硶淇濆瓨銆傝閲嶆柊閫夋嫨姝g‘鐨勬棩鏈熷尯闂�");
+    ErrorCode ATTENDANCE_LOCATION_RULE_INVALID = new ErrorCode(1_030_004_050, "鑰冨嫟瑙勫垯鍦扮偣閰嶇疆涓嶅畬鏁达紝璇疯仈绯荤鐞嗗憳");
+    ErrorCode ATTENDANCE_LOCATION_REQUIRED = new ErrorCode(1_030_004_051, "璇峰紑鍚畾浣嶅苟鎻愪緵缁忕含搴︺�佺簿搴﹀拰鍧愭爣绯诲悗鎵撳崱");
+    ErrorCode ATTENDANCE_LOCATION_COORDINATE_INVALID = new ErrorCode(1_030_004_052, "鎵撳崱缁忕含搴﹁秴鍑烘湁鏁堣寖鍥�");
+    ErrorCode ATTENDANCE_LOCATION_ACCURACY_INVALID = new ErrorCode(1_030_004_053, "瀹氫綅绮惧害椤诲ぇ浜� 0 涓斾笉瓒呰繃 100 绫筹紝璇峰埌寮�闃斾綅缃噸璇�");
+    ErrorCode ATTENDANCE_LOCATION_SYSTEM_INVALID = new ErrorCode(1_030_004_054, "鎵撳崱鍧愭爣绯诲繀椤讳负 WGS84");
+    ErrorCode ATTENDANCE_LOCATION_OUT_OF_RANGE = new ErrorCode(1_030_004_055, "褰撳墠浣嶇疆璺濃�渰}鈥漿}绫筹紝瓒呰繃鍏佽鑼冨洿{}绫�");
 
     // ========== 钖叕绠$悊 1-030-005-000 ==========
     ErrorCode SALARY_STRUCTURE_NOT_EXISTS = new ErrorCode(1_030_005_000, "钖叕缁撴瀯涓嶅瓨鍦�");
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 6cbb9fc..bf2016d 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
@@ -7,6 +7,8 @@
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+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;
@@ -14,6 +16,7 @@
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordRespVO;
 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.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;
@@ -70,34 +73,38 @@
     @PostMapping("/clock-in")
     @Operation(summary = "涓婄彮鎵撳崱")
     @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')")
-    public CommonResult<HrmAttendanceRecordRespVO> clockIn(@RequestParam("location") String location) {
+    @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);
-        HrmAttendanceRecordRespVO respVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class);
-        respVO.setUserName(employee.getName());
-        if (record.getDeptId() != null) {
-            DeptRespDTO dept = deptApi.getDept(record.getDeptId());
-            if (dept != null) {
-                respVO.setDeptName(dept.getName());
-            }
-        }
-        return success(respVO);
+        HrmAttendanceClockResult result = attendanceRecordService.clockIn(
+                employee.getId(), LocalDateTime.now(), reqVO, legacyLocation);
+        return success(buildClockResultVO(result, employee));
     }
 
     @PostMapping("/clock-out")
     @Operation(summary = "涓嬬彮鎵撳崱")
     @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')")
-    public CommonResult<HrmAttendanceRecordRespVO> clockOut(@RequestParam("location") String location) {
+    @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);
+        HrmAttendanceClockResult result = attendanceRecordService.clockOut(
+                employee.getId(), LocalDateTime.now(), reqVO, legacyLocation);
+        return success(buildClockResultVO(result, employee));
+    }
+
+    private HrmAttendanceRecordRespVO buildClockRespVO(HrmAttendanceRecordDO record, HrmEmployeeDO employee) {
         HrmAttendanceRecordRespVO respVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class);
         respVO.setUserName(employee.getName());
         if (record.getDeptId() != null) {
@@ -106,7 +113,20 @@
                 respVO.setDeptName(dept.getName());
             }
         }
-        return success(respVO);
+        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;
     }
 
     private Long getLoginUserId() {
@@ -310,4 +330,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/HrmAttendanceClockReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceClockReqVO.java
new file mode 100644
index 0000000..594bc07
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceClockReqVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/** App/Web 鎵撳崱瀹氫綅鍘熷鏁版嵁锛涙槸鍚﹀繀濉敱褰撳墠鐢熸晥瑙勫垯鍐冲畾銆� */
+@Data
+public class HrmAttendanceClockReqVO {
+    @Schema(description = "璁惧缁忓害锛圵GS84锛�")
+    private BigDecimal longitude;
+    @Schema(description = "璁惧绾害锛圵GS84锛�")
+    private BigDecimal latitude;
+    @Schema(description = "瀹氫綅绮惧害锛岀背")
+    private BigDecimal accuracy;
+    @Schema(description = "鍧愭爣绯伙紝浠呮敮鎸� WGS84")
+    private String coordinateSystem;
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceClockRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceClockRespVO.java
new file mode 100644
index 0000000..8f45c4f
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceClockRespVO.java
@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class HrmAttendanceClockRespVO {
+    @Schema(description = "鑰冨嫟璁板綍")
+    private HrmAttendanceRecordRespVO record;
+    @Schema(description = "瑙勫垯鍦扮偣鍚嶇О")
+    private String locationName;
+    @Schema(description = "璺濊鍒欏湴鐐硅窛绂伙紝绫筹紱鏈惎鐢ㄤ綅缃檺鍒舵椂涓虹┖")
+    private BigDecimal distanceMeters;
+    @Schema(description = "鍏佽鍗婂緞锛岀背锛涙湭鍚敤浣嶇疆闄愬埗鏃朵负绌�")
+    private Integer radiusMeters;
+    @Schema(description = "鏄惁鍦ㄥ厑璁歌寖鍥达紱鏈惎鐢ㄤ綅缃檺鍒舵椂涓虹┖")
+    private Boolean withinRange;
+}
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 5dd16a5..cdacc8c 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
@@ -39,6 +39,27 @@
     @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 = "浠婃棩璁板綍ID锛屾湭鎵撳崱鏃朵负绌�")
     private Long recordId;
 
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java
index 7b91afa..ef80ec4 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java
@@ -72,6 +72,27 @@
     @ExcelProperty("鎵撳崱鍦扮偣")
     private String location;
 
+    @Schema(description = "涓婄彮鎵撳崱缁忓害锛圵GS84锛�")
+    private BigDecimal clockInLongitude;
+    @Schema(description = "涓婄彮鎵撳崱绾害锛圵GS84锛�")
+    private BigDecimal clockInLatitude;
+    @Schema(description = "涓婄彮瀹氫綅绮惧害锛岀背")
+    private BigDecimal clockInAccuracy;
+    @Schema(description = "涓婄彮璺濊鍒欏湴鐐硅窛绂伙紝绫�")
+    private BigDecimal clockInDistance;
+    @Schema(description = "涓婄彮瑙勫垯鍦扮偣鍦板潃蹇収")
+    private String clockInAddress;
+    @Schema(description = "涓嬬彮鎵撳崱缁忓害锛圵GS84锛�")
+    private BigDecimal clockOutLongitude;
+    @Schema(description = "涓嬬彮鎵撳崱绾害锛圵GS84锛�")
+    private BigDecimal clockOutLatitude;
+    @Schema(description = "涓嬬彮瀹氫綅绮惧害锛岀背")
+    private BigDecimal clockOutAccuracy;
+    @Schema(description = "涓嬬彮璺濊鍒欏湴鐐硅窛绂伙紝绫�")
+    private BigDecimal clockOutDistance;
+    @Schema(description = "涓嬬彮瑙勫垯鍦扮偣鍦板潃蹇収")
+    private String clockOutAddress;
+
     @Schema(description = "澶囨敞")
     @ExcelProperty("澶囨敞")
     private String remark;
@@ -85,4 +106,4 @@
     @ExcelProperty("鍒涘缓鏃堕棿")
     private LocalDateTime createTime;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleRespVO.java
index 360c315..6c85005 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleRespVO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleRespVO.java
@@ -51,6 +51,21 @@
     @ExcelProperty("鍔犵彮璧风畻鏃堕暱")
     private BigDecimal overtimeThreshold;
 
+    @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 Boolean locationRequired;
+    @Schema(description = "鍧愭爣绯伙紝浠呮敮鎸� WGS84")
+    private String coordinateSystem;
+
     @Schema(description = "鐘舵�侊細0-鍚敤锛�1-绂佺敤")
     @ExcelProperty(value = "鐘舵��", converter = DictConvert.class)
     @DictFormat("hrm_rule_status")
@@ -64,4 +79,4 @@
     @ExcelProperty("鍒涘缓鏃堕棿")
     private LocalDateTime createTime;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleSaveReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleSaveReqVO.java
index 8c244a8..e775b00 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleSaveReqVO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleSaveReqVO.java
@@ -5,6 +5,7 @@
 
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.AssertTrue;
 import java.math.BigDecimal;
 import java.time.LocalTime;
 
@@ -39,10 +40,36 @@
     @Schema(description = "鍔犵彮璧风畻鏃堕暱锛堝皬鏃讹級", example = "1")
     private BigDecimal overtimeThreshold;
 
+    @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 Boolean locationRequired;
+    @Schema(description = "鍧愭爣绯伙紝浠呮敮鎸� WGS84")
+    private String coordinateSystem;
+
+    @AssertTrue(message = "鍚敤瀹氫綅鎵撳崱鏃讹紝鍦扮偣鍚嶇О銆佺粡绾害鍜�10~5000绫虫墦鍗″崐寰勫繀椤诲畬鏁撮厤缃�")
+    public boolean isLocationConfigValid() {
+        if (!Boolean.TRUE.equals(locationRequired)) {
+            return true;
+        }
+        return locationName != null && !locationName.isBlank()
+                && longitude != null && latitude != null && radiusMeters != null
+                && radiusMeters >= 10 && radiusMeters <= 5000
+                && "WGS84".equalsIgnoreCase(coordinateSystem);
+    }
+
     @Schema(description = "鐘舵�侊細0-鍚敤锛�1-绂佺敤", example = "0")
     private Integer status;
 
     @Schema(description = "澶囨敞", example = "閫傜敤浜庣爺鍙戦儴闂�")
     private String remark;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRecordDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRecordDO.java
index 47c8845..3e01e8e 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRecordDO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRecordDO.java
@@ -83,6 +83,17 @@
      */
     private String location;
 
+    private BigDecimal clockInLongitude;
+    private BigDecimal clockInLatitude;
+    private BigDecimal clockInAccuracy;
+    private BigDecimal clockInDistance;
+    private String clockInAddress;
+    private BigDecimal clockOutLongitude;
+    private BigDecimal clockOutLatitude;
+    private BigDecimal clockOutAccuracy;
+    private BigDecimal clockOutDistance;
+    private String clockOutAddress;
+
     /**
      * 澶囨敞
      */
@@ -93,4 +104,4 @@
      */
     private Integer dataSource;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRuleDO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRuleDO.java
index 71bdfdc..deac5a0 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRuleDO.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceRuleDO.java
@@ -67,6 +67,15 @@
      */
     private BigDecimal overtimeThreshold;
 
+    /** 鑰冨嫟鍦扮偣涓庡彲鎵撳崱鑼冨洿锛涙棫瑙勫垯榛樿涓嶅惎鐢ㄤ綅缃檺鍒躲�� */
+    private String locationName;
+    private String locationAddress;
+    private BigDecimal longitude;
+    private BigDecimal latitude;
+    private Integer radiusMeters;
+    private Boolean locationRequired;
+    private String coordinateSystem;
+
     /**
      * 鐘舵�侊細0-鍚敤锛�1-绂佺敤
      */
@@ -77,4 +86,4 @@
      */
     private String remark;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockResult.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockResult.java
new file mode 100644
index 0000000..26b6ed9
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockResult.java
@@ -0,0 +1,11 @@
+package cn.iocoder.yudao.module.hrm.service.attendance;
+
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO;
+
+import java.math.BigDecimal;
+
+/** 鎵撳崱淇濆瓨缁撴灉鍜屾湰娆℃牎楠屼娇鐢ㄧ殑瑙勫垯蹇収銆� */
+public record HrmAttendanceClockResult(HrmAttendanceRecordDO record, HrmAttendanceRuleDO rule,
+                                       BigDecimal distanceMeters) {
+}
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 7f6d87e..2d10ac7 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
@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.hrm.service.attendance;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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;
@@ -49,20 +50,24 @@
      *
      * @param userId 鍛樺伐ID
      * @param clockTime 鎵撳崱鏃堕棿
-     * @param location 鎵撳崱鍦扮偣
-     * @return 鑰冨嫟璁板綍
+     * @param reqVO 鍘熷瀹氫綅淇℃伅锛圵GS84锛�
+     * @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 鍘熷瀹氫綅淇℃伅锛圵GS84锛�
+     * @param legacyLocation 鏃х増鍦扮偣鏂囨湰锛屼粎鍦ㄦ湭鍚敤浣嶇疆闄愬埗鏃跺吋瀹逛娇鐢�
+     * @return 鎵撳崱璁板綍銆佽鍒欏拰璺濈
      */
-    HrmAttendanceRecordDO clockOut(Long userId, LocalDateTime clockTime, String location);
+    HrmAttendanceClockResult clockOut(Long userId, LocalDateTime clockTime,
+                                      HrmAttendanceClockReqVO reqVO, String legacyLocation);
 
     /**
      * 鑾峰彇鍛樺伐浠婃棩鎵撳崱鐘舵��
@@ -79,4 +84,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 1c84a33..a2d41f8 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.dal.dataobject.attendance.HrmAttendanceRecordDO;
@@ -149,7 +151,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)) {
@@ -169,6 +172,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;
@@ -185,24 +190,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)) {
@@ -219,8 +224,11 @@
         }
         // 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())
@@ -228,10 +236,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();
@@ -244,9 +248,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);
@@ -254,8 +256,92 @@
                     .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
@@ -284,6 +370,13 @@
             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());
@@ -350,4 +443,4 @@
         attendanceRecordMapper.updateById(record);
     }
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java
index ecc82a7..aaa1ccb 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java
@@ -1,5 +1,6 @@
 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.HrmAttendanceRulePageReqVO;
@@ -30,6 +31,7 @@
 
     @Override
     public Long createAttendanceRule(HrmAttendanceRuleSaveReqVO createReqVO) {
+        validateLocation(createReqVO);
         // 鏍¢獙瑙勫垯鍚嶇О鏄惁閲嶅
         validateAttendanceRuleNameUnique(null, createReqVO.getName());
         // 鏍¢獙閮ㄩ棬鏄惁宸查厤缃鍒欙紙濡傛灉鎸囧畾浜嗛儴闂級
@@ -56,7 +58,21 @@
 
     @Override
     public void updateAttendanceRule(HrmAttendanceRuleSaveReqVO updateReqVO) {
-        validateAttendanceRuleExists(updateReqVO.getId());
+        HrmAttendanceRuleDO currentRule = attendanceRuleMapper.selectById(updateReqVO.getId());
+        if (currentRule == null) {
+            throw exception(ATTENDANCE_RULE_NOT_EXISTS);
+        }
+        // 鍏煎鏃х鐞嗙锛氭病鏈夋彁浜ゅ湴鐐瑰瓧娈垫椂锛屼笉娓呯┖宸查厤缃殑浣嶇疆闄愬埗銆�
+        if (updateReqVO.getLocationRequired() == null) {
+            updateReqVO.setLocationRequired(currentRule.getLocationRequired());
+            if (updateReqVO.getLocationName() == null) updateReqVO.setLocationName(currentRule.getLocationName());
+            if (updateReqVO.getLocationAddress() == null) updateReqVO.setLocationAddress(currentRule.getLocationAddress());
+            if (updateReqVO.getLongitude() == null) updateReqVO.setLongitude(currentRule.getLongitude());
+            if (updateReqVO.getLatitude() == null) updateReqVO.setLatitude(currentRule.getLatitude());
+            if (updateReqVO.getRadiusMeters() == null) updateReqVO.setRadiusMeters(currentRule.getRadiusMeters());
+            if (updateReqVO.getCoordinateSystem() == null) updateReqVO.setCoordinateSystem(currentRule.getCoordinateSystem());
+        }
+        validateLocation(updateReqVO);
         // 鏍¢獙瑙勫垯鍚嶇О鏄惁閲嶅
         validateAttendanceRuleNameUnique(updateReqVO.getId(), updateReqVO.getName());
         // 鏍¢獙閮ㄩ棬鏄惁宸查厤缃鍒欙紙濡傛灉鎸囧畾浜嗛儴闂級
@@ -76,6 +92,39 @@
     private void validateAttendanceRuleExists(Long id) {
         if (attendanceRuleMapper.selectById(id) == null) {
             throw exception(ATTENDANCE_RULE_NOT_EXISTS);
+        }
+    }
+
+    private void validateLocation(HrmAttendanceRuleSaveReqVO reqVO) {
+        if (reqVO.getLocationRequired() == null) {
+            reqVO.setLocationRequired(false);
+        }
+        if (reqVO.getRadiusMeters() == null) {
+            reqVO.setRadiusMeters(10);
+        }
+        if (StrUtil.isBlank(reqVO.getCoordinateSystem())) {
+            reqVO.setCoordinateSystem("WGS84");
+        } else {
+            reqVO.setCoordinateSystem(reqVO.getCoordinateSystem().trim().toUpperCase(java.util.Locale.ROOT));
+        }
+        if (!"WGS84".equals(reqVO.getCoordinateSystem())) {
+            throw exception(ATTENDANCE_LOCATION_SYSTEM_INVALID);
+        }
+        if (reqVO.getLongitude() != null && (reqVO.getLongitude().compareTo(BigDecimal.valueOf(-180)) < 0
+                || reqVO.getLongitude().compareTo(BigDecimal.valueOf(180)) > 0)) {
+            throw exception(ATTENDANCE_LOCATION_COORDINATE_INVALID);
+        }
+        if (reqVO.getLatitude() != null && (reqVO.getLatitude().compareTo(BigDecimal.valueOf(-90)) < 0
+                || reqVO.getLatitude().compareTo(BigDecimal.valueOf(90)) > 0)) {
+            throw exception(ATTENDANCE_LOCATION_COORDINATE_INVALID);
+        }
+        if (reqVO.getRadiusMeters() < 10 || reqVO.getRadiusMeters() > 5000) {
+            throw exception(ATTENDANCE_LOCATION_RULE_INVALID);
+        }
+        if (Boolean.TRUE.equals(reqVO.getLocationRequired())
+                && (StrUtil.isBlank(reqVO.getLocationName()) || reqVO.getLongitude() == null
+                || reqVO.getLatitude() == null)) {
+            throw exception(ATTENDANCE_LOCATION_RULE_INVALID);
         }
     }
 
@@ -138,4 +187,4 @@
         return null;
     }
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-hrm/src/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockLocationTest.java b/yudao-module-hrm/src/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockLocationTest.java
new file mode 100644
index 0000000..53fe048
--- /dev/null
+++ b/yudao-module-hrm/src/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockLocationTest.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.hrm.service.attendance;
+
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceClockReqVO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO;
+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.dal.mysql.attendance.HrmAttendanceRecordMapper;
+import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+class HrmAttendanceClockLocationTest {
+
+    private final HrmAttendanceRecordServiceImpl service = new HrmAttendanceRecordServiceImpl();
+
+    private HrmAttendanceRuleDO requiredRule() {
+        return HrmAttendanceRuleDO.builder().locationRequired(true).locationName("鍏徃鍓嶅彴")
+                .longitude(BigDecimal.ZERO).latitude(BigDecimal.ZERO)
+                .radiusMeters(100).coordinateSystem("WGS84").build();
+    }
+
+    private HrmAttendanceClockReqVO position(String latitude, String accuracy) {
+        HrmAttendanceClockReqVO reqVO = new HrmAttendanceClockReqVO();
+        reqVO.setLongitude(BigDecimal.ZERO);
+        reqVO.setLatitude(new BigDecimal(latitude));
+        reqVO.setAccuracy(new BigDecimal(accuracy));
+        reqVO.setCoordinateSystem("WGS84");
+        return reqVO;
+    }
+
+    @Test
+    void oldRuleWithoutLocationStillAcceptsClock() {
+        HrmAttendanceRuleDO rule = HrmAttendanceRuleDO.builder().locationRequired(false).build();
+        assertNull(service.validateClockLocation(rule, null));
+        assertNull(service.validateClockLocation(rule, position("0.0001", "20")).distanceMeters());
+    }
+
+    @Test
+    void requiredLocationValidatesCompletenessCoordinatesAndAccuracy() {
+        HrmAttendanceRuleDO rule = requiredRule();
+        assertThrows(ServiceException.class, () -> service.validateClockLocation(rule, null));
+        assertThrows(ServiceException.class, () -> service.validateClockLocation(rule, position("91", "20")));
+        assertThrows(ServiceException.class, () -> service.validateClockLocation(rule, position("0", "100.01")));
+        assertNotNull(service.validateClockLocation(rule, position("0", "100")));
+    }
+
+    @Test
+    void comparesUnroundedDistanceWithRadius() {
+        HrmAttendanceRuleDO rule = requiredRule();
+        assertTrue(HrmAttendanceRecordServiceImpl.haversineMeters(0, 0, 0, 0.00089) < 100);
+        assertNotNull(service.validateClockLocation(rule, position("0.00089", "20")));
+        assertThrows(ServiceException.class, () -> service.validateClockLocation(rule, position("0.00091", "20")));
+    }
+
+    @Test
+    void invalidClockOutLocationDoesNotCreateMissingClockInRecord() {
+        HrmAttendanceHolidayService holidayService = mock(HrmAttendanceHolidayService.class);
+        HrmEmployeeService employeeService = mock(HrmEmployeeService.class);
+        HrmAttendanceRuleService ruleService = mock(HrmAttendanceRuleService.class);
+        HrmAttendanceRecordMapper mapper = mock(HrmAttendanceRecordMapper.class);
+        ReflectionTestUtils.setField(service, "attendanceHolidayService", holidayService);
+        ReflectionTestUtils.setField(service, "employeeService", employeeService);
+        ReflectionTestUtils.setField(service, "attendanceRuleService", ruleService);
+        ReflectionTestUtils.setField(service, "attendanceRecordMapper", mapper);
+        LocalDateTime clockTime = LocalDateTime.of(2026, 10, 22, 18, 56);
+        when(holidayService.isWorkDay(any(LocalDate.class))).thenReturn(true);
+        HrmEmployeeDO employee = HrmEmployeeDO.builder().id(1L).deptId(2L).build();
+        when(employeeService.getEmployee(1L)).thenReturn(employee);
+        when(ruleService.getAttendanceRuleByDeptId(2L)).thenReturn(requiredRule());
+        assertThrows(ServiceException.class,
+                () -> service.clockOut(1L, clockTime, position("0.00091", "20"), null));
+        verify(mapper, never()).insert(any(HrmAttendanceRecordDO.class));
+        verify(mapper, never()).updateById(any(HrmAttendanceRecordDO.class));
+    }
+}

--
Gitblit v1.9.3