From 5db13c2ad6678cf4614b81c95df035c6f47d313d Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期五, 16 十月 2026 21:57:36 +0800
Subject: [PATCH] feat(hrm): 考勤规则放开部门唯一限制,支持同一部门多班次

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java |  104 ++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 77 insertions(+), 27 deletions(-)

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..15c6391 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
@@ -2,6 +2,8 @@
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import com.google.common.annotations.VisibleForTesting;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRulePageReqVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleSaveReqVO;
 import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO;
@@ -11,6 +13,10 @@
 import org.springframework.validation.annotation.Validated;
 
 import java.math.BigDecimal;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Comparator;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -25,6 +31,9 @@
 @Validated
 public class HrmAttendanceRuleServiceImpl implements HrmAttendanceRuleService {
 
+    /** 瑙勫垯鐘舵�侊細0-鍚敤 */
+    private static final Integer ENABLED_STATUS = 0;
+
     @Resource
     private HrmAttendanceRuleMapper attendanceRuleMapper;
 
@@ -32,10 +41,7 @@
     public Long createAttendanceRule(HrmAttendanceRuleSaveReqVO createReqVO) {
         // 鏍¢獙瑙勫垯鍚嶇О鏄惁閲嶅
         validateAttendanceRuleNameUnique(null, createReqVO.getName());
-        // 鏍¢獙閮ㄩ棬鏄惁宸查厤缃鍒欙紙濡傛灉鎸囧畾浜嗛儴闂級
-        if (createReqVO.getDeptId() != null) {
-            validateAttendanceRuleDeptUnique(null, createReqVO.getDeptId());
-        }
+        // 娉細鍚屼竴涓儴闂ㄥ厑璁搁厤缃鏉¤鍒欙紙澶氱彮娆★級锛岃繖閲屼笉鍐嶆牎楠岄儴闂ㄥ敮涓�
         HrmAttendanceRuleDO attendanceRule = BeanUtils.toBean(createReqVO, HrmAttendanceRuleDO.class);
         // 璁剧疆榛樿鍊�
         if (attendanceRule.getLateAllowMinutes() == null) {
@@ -59,10 +65,7 @@
         validateAttendanceRuleExists(updateReqVO.getId());
         // 鏍¢獙瑙勫垯鍚嶇О鏄惁閲嶅
         validateAttendanceRuleNameUnique(updateReqVO.getId(), updateReqVO.getName());
-        // 鏍¢獙閮ㄩ棬鏄惁宸查厤缃鍒欙紙濡傛灉鎸囧畾浜嗛儴闂級
-        if (updateReqVO.getDeptId() != null) {
-            validateAttendanceRuleDeptUnique(updateReqVO.getId(), updateReqVO.getDeptId());
-        }
+        // 娉細鍚屼竴涓儴闂ㄥ厑璁搁厤缃鏉¤鍒欙紙澶氱彮娆★級锛岃繖閲屼笉鍐嶆牎楠岄儴闂ㄥ敮涓�
         HrmAttendanceRuleDO updateObj = BeanUtils.toBean(updateReqVO, HrmAttendanceRuleDO.class);
         attendanceRuleMapper.updateById(updateObj);
     }
@@ -92,18 +95,6 @@
         }
     }
 
-    private void validateAttendanceRuleDeptUnique(Long id, Long deptId) {
-        List<HrmAttendanceRuleDO> rules = attendanceRuleMapper.selectListByDeptId(deptId);
-        if (rules.isEmpty()) {
-            return;
-        }
-        for (HrmAttendanceRuleDO rule : rules) {
-            if (id == null || !rule.getId().equals(id)) {
-                throw exception(ATTENDANCE_RULE_DEPT_EXISTS);
-            }
-        }
-    }
-
     @Override
     public HrmAttendanceRuleDO getAttendanceRule(Long id) {
         return attendanceRuleMapper.selectById(id);
@@ -121,21 +112,80 @@
 
     @Override
     public HrmAttendanceRuleDO getAttendanceRuleByDeptId(Long deptId) {
-        // 1. 鍏堟煡鎵鹃儴闂ㄤ笓灞炶鍒�
-        List<HrmAttendanceRuleDO> deptRules = attendanceRuleMapper.selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<HrmAttendanceRuleDO>()
+        return getAttendanceRuleByDeptId(deptId, null);
+    }
+
+    @Override
+    public HrmAttendanceRuleDO getAttendanceRuleByDeptId(Long deptId, LocalDateTime clockTime) {
+        // 1. 鍏堟煡鎵鹃儴闂ㄤ笓灞炶鍒欙紙鍚屼竴涓儴闂ㄥ彲浠ラ厤缃鏉★紝鐢ㄤ簬澶氱彮娆★級
+        List<HrmAttendanceRuleDO> deptRules = attendanceRuleMapper.selectList(new LambdaQueryWrapperX<HrmAttendanceRuleDO>()
                 .eq(HrmAttendanceRuleDO::getDeptId, deptId)
-                .eq(HrmAttendanceRuleDO::getStatus, 0));
+                .eq(HrmAttendanceRuleDO::getStatus, ENABLED_STATUS));
         if (!deptRules.isEmpty()) {
-            return deptRules.get(0);
+            return matchRuleByClockTime(deptRules, clockTime);
         }
         // 2. 鏌ユ壘鍏ㄥ眬瑙勫垯锛坉eptId 涓� null锛�
-        List<HrmAttendanceRuleDO> globalRules = attendanceRuleMapper.selectList(new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<HrmAttendanceRuleDO>()
+        List<HrmAttendanceRuleDO> globalRules = attendanceRuleMapper.selectList(new LambdaQueryWrapperX<HrmAttendanceRuleDO>()
                 .isNull(HrmAttendanceRuleDO::getDeptId)
-                .eq(HrmAttendanceRuleDO::getStatus, 0));
+                .eq(HrmAttendanceRuleDO::getStatus, ENABLED_STATUS));
         if (!globalRules.isEmpty()) {
-            return globalRules.get(0);
+            return matchRuleByClockTime(globalRules, clockTime);
         }
         return null;
     }
 
+    /**
+     * 澶氭潯瑙勫垯锛堝悓涓�涓儴闂ㄥ鐝锛夋椂锛屾寜鎵撳崱鏃堕棿閫夌彮娆★細
+     * 1. 鎵撳崱鏃堕棿钀藉湪鏌愪釜鐝鐨� [涓婄彮鏃堕棿, 涓嬬彮鏃堕棿] 鍐咃紝灏卞彇杩欎竴鏉�
+     * 2. 閮戒笉鍦ㄧ獥鍙e唴鏃讹紝鍙栫彮娆℃椂闂寸獥鍙g鎵撳崱鏃堕棿鏈�杩戠殑涓�鏉�
+     * 3. 浠嶇劧骞跺垪鏃讹紝鎸変笂鐝椂闂淬�佺紪鍙锋帓搴忥紝淇濊瘉缁撴灉绋冲畾锛堜笉浼氶殢鏁版嵁搴撹繑鍥為『搴忔紓绉伙級
+     * 鎵撳崱鏃堕棿涓虹┖锛堜笉鍏冲績鐝锛夋椂锛屽彇涓婄彮鏃堕棿鏈�鏃╃殑涓�鏉°��
+     */
+    @VisibleForTesting
+    static HrmAttendanceRuleDO matchRuleByClockTime(List<HrmAttendanceRuleDO> rules, LocalDateTime clockTime) {
+        if (rules.size() == 1) {
+            return rules.get(0);
+        }
+        if (clockTime == null) {
+            return rules.stream().min(Comparator
+                            .comparing(HrmAttendanceRuleDO::getWorkStartTime,
+                                    Comparator.nullsLast(Comparator.<LocalTime>naturalOrder()))
+                            .thenComparing(HrmAttendanceRuleDO::getId))
+                    .orElse(rules.get(0));
+        }
+        LocalTime time = clockTime.toLocalTime();
+        return rules.stream().min(Comparator
+                        .comparingLong((HrmAttendanceRuleDO rule) -> minutesToRuleWindow(rule, time))
+                        .thenComparing(HrmAttendanceRuleDO::getWorkStartTime,
+                                Comparator.nullsLast(Comparator.<LocalTime>naturalOrder()))
+                        .thenComparing(HrmAttendanceRuleDO::getId))
+                .orElse(rules.get(0));
+    }
+
+    /**
+     * 鎵撳崱鏃堕棿鍒拌鍒欑彮娆$獥鍙g殑璺濈锛堝垎閽燂級锛氳惤鍦ㄧ獥鍙e唴涓� 0锛岀彮娆℃椂闂存湭閰嶇疆鏃惰涓烘渶澶с��
+     * 鏀寔璺ㄥぉ鐝锛堝 22:00-06:00锛屽彧瑕� end <= start 灏辨寜璺ㄥぉ澶勭悊锛夈��
+     */
+    private static long minutesToRuleWindow(HrmAttendanceRuleDO rule, LocalTime time) {
+        LocalTime start = rule.getWorkStartTime();
+        LocalTime end = rule.getWorkEndTime();
+        if (start == null || end == null) {
+            return Long.MAX_VALUE;
+        }
+        boolean crossDay = !end.isAfter(start);
+        boolean inWindow = crossDay
+                ? !time.isBefore(start) || !time.isAfter(end)
+                : !time.isBefore(start) && !time.isAfter(end);
+        if (inWindow) {
+            return 0;
+        }
+        return Math.min(minutesBetween(time, start), minutesBetween(time, end));
+    }
+
+    /** 涓や釜鏃跺埢鐨勫垎閽熷樊锛屾寜涓�澶� 24 灏忔椂鍙栨渶杩戠殑鐜洖璺濈锛�23:50 涓� 00:10 鍙樊 20 鍒嗛挓锛� */
+    private static long minutesBetween(LocalTime one, LocalTime other) {
+        long minutes = Math.abs(Duration.between(one, other).toMinutes());
+        return Math.min(minutes, 24 * 60 - minutes);
+    }
+
 }
\ No newline at end of file

--
Gitblit v1.9.3