From 82155dd6f2b16fbfb718e636a54e2c9dae31246b Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期五, 30 十月 2026 20:23:20 +0800
Subject: [PATCH] feat(hrm,bi): 移植长辉电力的考勤请假联动与 BI 大屏图表

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java |  329 ++++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 293 insertions(+), 36 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 92f745d..cd53325 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;
@@ -55,6 +57,8 @@
     private HrmAttendanceHolidayService attendanceHolidayService;
     @Resource
     private DeptApi deptApi;
+    @Resource
+    private cn.iocoder.yudao.module.hrm.service.approval.HrmLeaveQueryService leaveQueryService;
 
     @Override
     public PageResult<HrmAttendanceRecordDO> getAttendanceRecordPage(HrmAttendanceRecordPageReqVO pageReqVO) {
@@ -82,13 +86,13 @@
         // 1. 鐧诲綍鐢ㄦ埛 -> 鍛樺伐妗f
         HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
         if (employee == null) {
-            throw exception(EMPLOYEE_NOT_EXISTS,
-                    "褰撳墠鐧诲綍璐﹀彿鏈叧鑱斿憳宸ユ。妗堬紝鏃犳硶鎵撳崱銆傝鑱旂郴绠$悊鍛樺湪銆愬憳宸ョ鐞嗐�戜腑灏嗚鐧诲綍璐﹀彿缁戝畾鍒板憳宸ャ��");
+            throw exception(ATTENDANCE_EMPLOYEE_NOT_BOUND);
         }
         HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO();
         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 +121,140 @@
                 }
             }
             respVO.setRule(ruleRespVO);
+            fillCompatibleRuleFields(respVO, rule);
         }
 
         // 4. 褰撴棩鑰冨嫟璁板綍
-        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(employee.getId(), queryDate);
+        // 鑰冨嫟璁板綍琛ㄥ瓨鐨勬槸绯荤粺鐢ㄦ埛 ID锛岃繖閲岀敤 loginUserId 鑰屼笉鏄憳宸ユ。妗� ID
+        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(loginUserId, queryDate);
         if (record != null) {
             respVO.setRecord(buildRecordRespVO(record, employee));
+            fillCompatibleRecordFields(respVO, record);
+        }
+
+        // 4.5 褰撳ぉ璇峰亣鎯呭喌锛堣鍋囦粛鍏佽鎵撳崱锛屼絾涓嶇畻杩熷埌/鏃╅��/缂哄崱锛�
+        List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves =
+                leaveQueryService.getApprovedLeaveList(loginUserId, queryDate);
+        boolean onLeave = !leaves.isEmpty();
+        java.math.BigDecimal leaveHours = onLeave ? calcLeaveHours(leaves, rule, queryDate) : java.math.BigDecimal.ZERO;
+        respVO.setOnLeave(onLeave);
+        respVO.setLeaveHours(leaveHours);
+        if (onLeave) {
+            List<String> typeNames = leaves.stream()
+                    .map(l -> cn.iocoder.yudao.module.hrm.enums.HrmLeaveTypeEnum.getNameByStatus(l.getLeaveType()))
+                    .distinct().toList();
+            respVO.setLeaveTypeNames(String.join("銆�", typeNames));
         }
 
         // 5. 褰撳墠鍙墦鍗$姸鎬�
-        respVO.setClockable(buildClockable(workDay, rule, record, queryDate));
+        HrmAttendanceMyStatusRespVO.Clockable clockable = buildClockable(workDay, rule, record, queryDate, onLeave);
+
+        // 5.1 璇峰亣褰撳ぉ锛氭妸銆屽彲鎵撳崱銆嶇殑鎻愮ず鎹㈡垚璇峰亣璇存槑锛屼絾渚濈劧鍏佽鎵撳崱
+        if (onLeave) {
+            clockable.setMessage("浠婃棩宸茶鍋囷紙" + respVO.getLeaveTypeNames() + "锛夛紝濡傞渶涓婄彮鍙甯告墦鍗�");
+        }
+        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());
+    }
+
+    /**
+     * 鎶婅鍋囦俊鎭啓鍏ヨ�冨嫟璁板綍锛坙eave_flag / leave_hours / leave_types锛�
+     *
+     * @param record 鑰冨嫟璁板綍
+     * @param leaves 褰撳ぉ瀹℃壒閫氳繃鐨勮鍋囧崟
+     * @param rule   鑰冨嫟瑙勫垯锛堢敤浜庤绠楀伐浣滄椂娈典氦闆嗭級
+     * @param date   鑰冨嫟鏃ユ湡
+     */
+    private void applyLeaveInfo(HrmAttendanceRecordDO record,
+                                List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves,
+                                HrmAttendanceRuleDO rule, LocalDate date) {
+        if (leaves == null || leaves.isEmpty()) {
+            record.setLeaveFlag(false);
+            record.setLeaveHours(BigDecimal.ZERO);
+            record.setLeaveTypes(null);
+            return;
+        }
+        record.setLeaveFlag(true);
+        record.setLeaveHours(calcLeaveHours(leaves, rule, date));
+        record.setLeaveTypes(leaves.stream()
+                .map(l -> String.valueOf(l.getLeaveType()))
+                .distinct()
+                .collect(java.util.stream.Collectors.joining(",")));
+    }
+
+    /**
+     * 璁$畻鏌愬ぉ鐨勮鍋囧伐鏃�
+     *
+     * <p>鎶婃瘡寮犺鍋囧崟鐨� [start_time, end_time] 涓庤�冨嫟瑙勫垯鐨勫伐浣滄椂娈垫眰浜ら泦鍚庣疮鍔狅紝
+     * 绮剧‘鍒� 0.01 灏忔椂锛�36 绉掞級銆傛棤瑙勫垯鏃舵寜鏁村ぉ鐨勮鍋囨椂闀挎姌绠椼��</p>
+     *
+     * <p>渚嬶細瑙勫垯 08:30-17:30锛岃鍋囧崟 14:00-18:00 鈫� 浜ら泦 14:00-17:30 = 3.5 灏忔椂銆�</p>
+     */
+    private java.math.BigDecimal calcLeaveHours(
+            List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves,
+            HrmAttendanceRuleDO rule, LocalDate date) {
+        LocalDateTime dayStart = date.atStartOfDay();
+        LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
+        // 宸ヤ綔鏃ユ椂娈碉細浼樺厛鐢ㄨ�冨嫟瑙勫垯锛屾病閰嶈鍒欏氨鎸夋暣澶�
+        LocalDateTime workStart;
+        LocalDateTime workEnd;
+        if (rule != null && rule.getWorkStartTime() != null && rule.getWorkEndTime() != null) {
+            workStart = LocalDateTime.of(date, rule.getWorkStartTime());
+            workEnd = LocalDateTime.of(date, rule.getWorkEndTime());
+            // 璺ㄩ浂鐐圭彮娆★紙濡� 20:00-娆℃棩 04:00锛�
+            if (!workEnd.isAfter(workStart)) {
+                workEnd = workEnd.plusDays(1);
+            }
+        } else {
+            workStart = dayStart;
+            workEnd = dayEnd;
+        }
+        java.math.BigDecimal total = java.math.BigDecimal.ZERO;
+        for (var leave : leaves) {
+            if (leave.getStartTime() == null || leave.getEndTime() == null) {
+                continue;
+            }
+            LocalDateTime from = leave.getStartTime().isAfter(workStart) ? leave.getStartTime() : workStart;
+            LocalDateTime to = leave.getEndTime().isBefore(workEnd) ? leave.getEndTime() : workEnd;
+            if (!to.isAfter(from)) {
+                continue;
+            }
+            long minutes = java.time.Duration.between(from, to).toMinutes();
+            total = total.add(java.math.BigDecimal.valueOf(minutes)
+                    .divide(java.math.BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP));
+        }
+        return total;
     }
 
     /**
@@ -174,7 +301,8 @@
      * 鏍规嵁宸ヤ綔鏃ャ�佽�冨嫟瑙勫垯涓庡綋鏃ヨ褰曡绠楀彲鎵撳崱鐘舵��
      */
     private HrmAttendanceMyStatusRespVO.Clockable buildClockable(boolean workDay, HrmAttendanceRuleDO rule,
-                                                                 HrmAttendanceRecordDO record, LocalDate date) {
+                                                                 HrmAttendanceRecordDO record, LocalDate date,
+                                                                 boolean onLeave) {
         HrmAttendanceMyStatusRespVO.Clockable clockable = new HrmAttendanceMyStatusRespVO.Clockable();
         if (!workDay) {
             clockable.setClockIn(false);
@@ -204,9 +332,9 @@
             clockable.setClockOut(false);
             clockable.setNextAction("in");
             clockable.setMessage("鍙笂鐝墦鍗�");
-            // 浠呭綋澶╂煡璇㈡椂鎻愮ず宸茶繜鍒版椂闀�
+            // 浠呭綋澶╂煡璇㈡椂鎻愮ず宸茶繜鍒版椂闀匡紱璇峰亣褰撳ぉ涓嶆彁绀鸿繜鍒�
             LocalDateTime now = LocalDateTime.now();
-            if (rule.getWorkStartTime() != null && date.equals(now.toLocalDate())) {
+            if (!onLeave && rule.getWorkStartTime() != null && date.equals(now.toLocalDate())) {
                 LocalDateTime workStart = LocalDateTime.of(date, rule.getWorkStartTime());
                 int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
                 if (now.isAfter(workStart.plusMinutes(lateAllowMinutes))) {
@@ -314,60 +442,80 @@
 
     @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)) {
             throw exception(ATTENDANCE_CLOCK_FAIL_NOT_WORKDAY);
         }
         // 2. 鑾峰彇鍛樺伐淇℃伅鍜岃�冨嫟瑙勫垯
+        //    娉ㄦ剰锛氬叆鍙� userId 瀹為檯浼犵殑鏄�屽憳宸ユ。妗� ID銆嶏紝
+        //    鑰岃�冨嫟璁板綍琛� / 璇峰亣琛ㄧ殑 user_id 瀛樼殑鏄�岀郴缁熺敤鎴� ID銆嶏紝蹇呴』鍖哄垎銆�
         HrmEmployeeDO employee = employeeService.getEmployee(userId);
         if (employee == null) {
             throw exception(EMPLOYEE_NOT_EXISTS);
         }
+        Long systemUserId = employee.getUserId();
         HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
         if (rule == null) {
             throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE);
         }
-        // 3. 鏌ヨ褰撴棩鑰冨嫟璁板綍
-        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate);
+        // 3. 鏌ヨ褰撴棩鑰冨嫟璁板綍锛堢敤绯荤粺鐢ㄦ埛 ID锛�
+        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(systemUserId, clockDate);
         if (record != null && record.getClockInTime() != null) {
             throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
         }
+        // 鍦ㄤ换浣曞啓搴撴搷浣滃墠瀹屾垚瀹氫綅鏍¢獙锛屾棫鐗堝湴鐐瑰瓧绗︿覆涓嶈兘缁曡繃鑼冨洿闄愬埗銆�
+        ClockLocation clockLocation = validateClockLocation(rule, reqVO);
+        // 3.1 褰撳ぉ璇峰亣鎯呭喌锛氳鍋囧綋澶╁厑璁告墦鍗★紝浣嗕笉鍒ゅ畾涓鸿繜鍒�
+        //     娉ㄦ剰锛氭柟娉曞叆鍙� userId 瀹炰负銆屽憳宸ユ。妗� ID銆嶏紝鑰岃鍋囪〃瀛樼殑鏄�岀郴缁熺敤鎴� ID銆嶏紝
+        //     蹇呴』鐢� employee.getUserId() 鎵嶈兘瀵逛笂銆�
+        List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves =
+                leaveQueryService.getApprovedLeaveList(employee.getUserId(), clockDate);
+        boolean onLeave = !leaves.isEmpty();
         // 4. 鍒ゆ柇鎵撳崱绫诲瀷
         LocalTime workStartTime = rule.getWorkStartTime();
         int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
         LocalTime lateThreshold = workStartTime.plusMinutes(lateAllowMinutes);
         LocalTime clockTimeOnly = clockTime.toLocalTime();
-        Integer clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold)
-                ? HrmAttendanceClockTypeEnum.NORMAL.getType()
-                : HrmAttendanceClockTypeEnum.LATE.getType();
+        Integer clockInType;
+        if (onLeave) {
+            // 璇峰亣褰撳ぉ锛氬彧瑕佹湁鎵撳崱灏辩畻姝e父锛岃繜鍒颁笌鍚︿笉褰卞搷鑰冨嫟
+            clockInType = HrmAttendanceClockTypeEnum.NORMAL.getType();
+        } else {
+            clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold)
+                    ? HrmAttendanceClockTypeEnum.NORMAL.getType()
+                    : HrmAttendanceClockTypeEnum.LATE.getType();
+        }
         // 5. 鍒涘缓鎴栨洿鏂拌�冨嫟璁板綍
         if (record == null) {
             record = HrmAttendanceRecordDO.builder()
-                    .userId(userId)
+                    .userId(systemUserId)
                     .deptId(employee.getDeptId())
                     .date(clockDate)
                     .clockInTime(clockTime)
                     .clockInType(clockInType)
-                    .location(location)
                     .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType())
                     .build();
+            applyLeaveInfo(record, leaves, rule, clockDate);
+            applyClockLocation(record, rule, clockLocation, legacyLocation, true);
             attendanceRecordMapper.insert(record);
         } else {
             record.setClockInTime(clockTime);
             record.setClockInType(clockInType);
-            if (location != null) {
-                record.setLocation(location);
-            }
+            applyLeaveInfo(record, leaves, rule, clockDate);
+            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)) {
@@ -378,40 +526,55 @@
         if (employee == null) {
             throw exception(EMPLOYEE_NOT_EXISTS);
         }
+        Long systemUserId = employee.getUserId();
         HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
         if (rule == null) {
             throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE);
         }
-        // 3. 鏌ヨ褰撴棩鑰冨嫟璁板綍
-        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate);
+        // 3. 鏌ヨ褰撴棩鑰冨嫟璁板綍锛堢郴缁熺敤鎴� ID锛�
+        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(systemUserId, 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)
+                    .userId(systemUserId)
                     .deptId(employee.getDeptId())
                     .date(clockDate)
                     .clockInType(HrmAttendanceClockTypeEnum.MISSING.getType())
                     .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType())
                     .build();
-            attendanceRecordMapper.insert(record);
         }
-        if (record.getClockOutTime() != null) {
-            throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
+        // 3.1 褰撳ぉ璇峰亣鎯呭喌锛氳鍋囧綋澶╁厑璁告墦鍗★紝涓嶅垽瀹氫负鏃╅��锛屼篃涓嶇畻缂轰笂鐝崱
+        //     鍚屼笂锛氱敤绯荤粺鐢ㄦ埛 ID 鏌ヨ鍋囪〃
+        List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves =
+                leaveQueryService.getApprovedLeaveList(employee.getUserId(), clockDate);
+        boolean onLeave = !leaves.isEmpty();
+        if (onLeave && HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) {
+            // 璇峰亣褰撳ぉ娌℃墦涓婄彮鍗″睘浜庢甯革紙浜烘病鏉ワ級锛屼笉搴旇缂哄崱
+            record.setClockInType(HrmAttendanceClockTypeEnum.LEAVE.getType());
         }
         // 4. 鍒ゆ柇鎵撳崱绫诲瀷
         LocalTime workEndTime = rule.getWorkEndTime();
         int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
         LocalTime earlyThreshold = workEndTime.minusMinutes(earlyAllowMinutes);
         LocalTime clockTimeOnly = clockTime.toLocalTime();
-        Integer clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold)
-                ? HrmAttendanceClockTypeEnum.NORMAL.getType()
-                : HrmAttendanceClockTypeEnum.EARLY.getType();
+        Integer clockOutType;
+        if (onLeave) {
+            // 璇峰亣褰撳ぉ锛氭湁鎵撳崱灏辩畻姝e父
+            clockOutType = HrmAttendanceClockTypeEnum.NORMAL.getType();
+        } else {
+            clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold)
+                    ? HrmAttendanceClockTypeEnum.NORMAL.getType()
+                    : HrmAttendanceClockTypeEnum.EARLY.getType();
+        }
         // 5. 鏇存柊鑰冨嫟璁板綍
         record.setClockOutTime(clockTime);
         record.setClockOutType(clockOutType);
-        if (location != null) {
-            record.setLocation(location);
-        }
+        applyLeaveInfo(record, leaves, rule, clockDate);
+        applyClockLocation(record, rule, clockLocation, legacyLocation, false);
         // 6. 璁$畻宸ヤ綔鏃堕暱
         if (record.getClockInTime() != null) {
             Duration duration = Duration.between(record.getClockInTime(), clockTime);
@@ -419,8 +582,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 +726,4 @@
         attendanceRecordMapper.updateById(record);
     }
 
-}
\ No newline at end of file
+}

--
Gitblit v1.9.3