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/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImplTest.java | 108 ++++++++++++++++++
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java | 1
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java | 55 +++++----
docs/attendance_multi_shift_dept_rules.md | 54 +++++++++
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleService.java | 13 ++
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java | 104 ++++++++++++----
6 files changed, 284 insertions(+), 51 deletions(-)
diff --git a/docs/attendance_multi_shift_dept_rules.md b/docs/attendance_multi_shift_dept_rules.md
new file mode 100644
index 0000000..c85cbb7
--- /dev/null
+++ b/docs/attendance_multi_shift_dept_rules.md
@@ -0,0 +1,54 @@
+# 鑰冨嫟瑙勫垯鏀寔銆屽悓涓�涓儴闂ㄥ鏉¤鍒欙紙澶氱彮娆★級銆�
+
+> 鍙樻洿鏃ユ湡锛�2026-10-16銆�妯″潡锛欻RM 鈫� 鑰冨嫟绠$悊 鈫� 鑰冨嫟瑙勫垯閰嶇疆
+
+## 娑夊強椤甸潰
+
+- HRM 鈫� 鑰冨嫟绠$悊 鈫� 鑰冨嫟瑙勫垯锛堟柊澧� / 缂栬緫锛�
+- HRM 鈫� 鑰冨嫟绠$悊 鈫� 鑰冨嫟璁板綍锛堟墦鍗°�佸鍏ャ�佹湀搴︽眹鎬讳緷璧栫殑瑙勫垯鍖归厤锛�
+- 绉诲姩绔�/澶у睆鐨勬墦鍗″叆鍙o紙璧板悓涓�濂� clock-in / clock-out 鎺ュ彛锛�
+
+## 鍙樻洿鐐�
+
+| 椤� | 鍙樻洿鍓� | 鍙樻洿鍚� |
+|----|--------|--------|
+| 閮ㄩ棬涓庤鍒欑殑鍏崇郴 | 涓�涓儴闂ㄥ彧鑳介厤缃竴鏉¤鍒欙紝鏂板/缂栬緫鏃惰嫢璇ラ儴闂ㄥ凡鏈夎鍒欙紝鎶ラ敊銆岃閮ㄩ棬宸查厤缃�冨嫟瑙勫垯銆� | 涓�涓儴闂ㄥ彲浠ラ厤缃鏉¤鍒欙紝鐢ㄤ簬澶氱彮娆★紱鏂板/缂栬緫涓嶅啀鎶ヨ閿� |
+| 瑙勫垯鍚嶇О | 鍏ㄥ眬鍞竴锛坄hrm_attendance_rule.name` 涓嶅厑璁搁噸澶嶏級 | **涓嶅彉**锛屼粛鐒跺叏灞�鍞竴锛岃鐢ㄣ�屾棭鐝鍒� / 澶滅彮瑙勫垯銆嶈繖绫诲彲鍖哄垎鐨勫悕绉� |
+| 鎵撳崱/瀵煎叆鏃剁敤鍝潯瑙勫垯 | 璇ラ儴闂ㄥ惎鐢ㄨ鍒欎腑鐨勪换鎰忎竴鏉★紙鎸夋暟鎹簱杩斿洖椤哄簭锛� | 鎸夋墦鍗℃椂闂村尮閰嶇彮娆★紝瑙佷笅琛� |
+| 鏈厤缃儴闂ㄨ鍒欐椂 | 鍥為��鍒板叏灞�瑙勫垯锛坄dept_id` 涓虹┖锛� | **涓嶅彉** |
+
+## 鐝鍖归厤瑙勫垯锛堝悓涓�涓儴闂ㄦ湁澶氭潯鍚敤瑙勫垯鏃讹級
+
+| 鍦烘櫙 | 鍙栧摢鏉¤鍒� |
+|------|------------|
+| 鎵撳崱鏃堕棿钀藉湪鏌愭潯瑙勫垯鐨� [涓婄彮鏃堕棿, 涓嬬彮鏃堕棿] 鍐� | 鍙栬繖鏉� |
+| 鎵撳崱鏃堕棿涓嶅湪浠讳綍鐝绐楀彛鍐咃紙濡傛彁鍓� 2 灏忔椂鍒板矖锛� | 鍙栫彮娆$獥鍙g鎵撳崱鏃堕棿鏈�杩戠殑閭f潯 |
+| 鏈夊鏉¢兘绗﹀悎锛堝苟鍒楋級 | 鎸変笂鐝椂闂存棭鐨勪紭鍏堬紝鍐嶆寜瑙勫垯缂栧彿锛岀粨鏋滅ǔ瀹氫笉婕傜Щ |
+| 璺ㄥぉ鐝锛堝 22:00-06:00锛屽嵆涓嬬彮鏃堕棿 鈮� 涓婄彮鏃堕棿锛� | 鍑屾櫒鐨勬墦鍗★紙00:00-06:00锛夊垽缁欒璺ㄥぉ鐝 |
+| 涓嶅叧蹇冪彮娆★紙鍐呴儴璋冪敤鏈紶鎵撳崱鏃堕棿锛� | 鍙栦笂鐝椂闂存渶鏃╃殑涓�鏉� |
+
+鍖归厤鍙湪璇ラ儴闂ㄥ惎鐢ㄧ殑瑙勫垯涓繘琛岋紱鑻ヨ閮ㄩ棬涓�鏉″惎鐢ㄨ鍒欓兘娌℃湁锛屼粛鍥為��鍒板叏灞�瑙勫垯锛堝悓鏍锋寜涓婅堪瑙勫垯鍖归厤锛夈��
+
+## API
+
+鎺ュ彛璺緞銆佽姹傚弬鏁般�佸搷搴旂粨鏋�**鍧囨棤鍙樺寲**锛�
+
+| 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|
+| POST | /hrm/attendance-rule/create | 鏂板鑰冨嫟瑙勫垯锛堜笉鍐嶅洜閮ㄩ棬閲嶅鑰屽け璐ワ級 |
+| PUT | /hrm/attendance-rule/update | 淇敼鑰冨嫟瑙勫垯锛堜笉鍐嶅洜閮ㄩ棬閲嶅鑰屽け璐ワ級 |
+| POST | /hrm/attendance-clock/clock-in | 涓婄彮鎵撳崱锛堟寜鎵撳崱鏃堕棿鍖归厤鐝鍒よ繜鍒帮級 |
+| POST | /hrm/attendance-clock/clock-out | 涓嬬彮鎵撳崱锛堟寜鎵撳崱鏃堕棿鍖归厤鐝鍒ゆ棭閫�銆佺畻鍔犵彮锛� |
+
+閿欒鐮� `1_030_004_007銆岃閮ㄩ棬宸查厤缃�冨嫟瑙勫垯銆峘 宸蹭笉鍐嶆湁瑙﹀彂鐐癸紝鍓嶇鏃犻渶鍐嶉拡瀵瑰畠鍋氱壒娈婂鐞嗭紙鑻ユ湁鎸夐敊璇爜鍐欐鐨勬枃妗堬紝鍙垹闄わ級銆�
+
+## 鍓嶇淇敼鐐�
+
+鏃犮�傚墠绔殑閮ㄩ棬涓嬫媺鏈潵灏辨槸鎷夊彇鍏ㄩ噺閮ㄩ棬銆佷笉鍋氥�屽凡閰嶇疆鍒欑鐢ㄣ�嶇殑杩囨护锛涜鎻愮ず鏉ヨ嚜鍚庣寮傚父锛屽悗绔笉鍐嶈繑鍥炲嵆鍙��
+
+## 娉ㄦ剰浜嬮」
+
+- 鍚屼竴涓儴闂ㄩ厤澶氭潯瑙勫垯鏃讹紝**鐝鏃堕棿蹇呴』鑳藉尯鍒嗗紑**锛堜緥濡傛棭鐝� 08:00-16:00銆佹櫄鐝� 16:00-24:00锛夈�傝嫢涓ゆ潯瑙勫垯鐨勭彮娆$獥鍙e畬鍏ㄩ噸鍙狅紝鎵撳崱鍙細鍛戒腑鍏朵腑涓�鏉★紙涓婄彮鏃堕棿鏃╃殑閭f潯锛夛紝璇烽伩鍏嶉厤缃噸鍙犵彮娆°��
+- 瑙勫垯鍚嶇О浠嶅叏灞�鍞竴锛屼袱涓儴闂ㄥ悇閰嶃�岀櫧鐝�嶄細鎻愮ず銆岃�冨嫟瑙勫垯鍚嶇О宸插瓨鍦ㄣ�嶏紝璇锋敼鍚嶅尯鍒嗐��
+- 宸叉湁鐨勮�冨嫟璁板綍涓嶅洖婧噸绠楋細鏈鍙樻洿鍙奖鍝嶅彉鏇翠箣鍚庢柊鎵撳崱銆佹柊瀵煎叆/缂栬緫鐨勮褰曘�傚巻鍙茶褰曞闇�鎸夋柊鐝閲嶇畻锛岄渶瑕佸崟鐙鐞嗭紙閲嶆柊瀵煎叆鎴栨彁渚涢噸绠楄剼鏈級銆�
+- 鏃犺〃缁撴瀯鍙樻洿锛屾棤闇�鎵ц SQL锛沗hrm_attendance_rule.dept_id` 鏈潵灏辨槸鏅�氱储寮曪紙`idx_dept_id`锛夛紝涓嶅瓨鍦ㄥ敮涓�绱㈠紩銆�
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..11e1ef3 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
@@ -83,6 +83,7 @@
ErrorCode ATTENDANCE_EXCEPTION_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_030_004_004, "鑰冨嫟寮傚父鐢宠鎻愪氦澶辫触锛屽師鍥狅細涓嶆槸鑽夌鐘舵��");
ErrorCode ATTENDANCE_RULE_NOT_EXISTS = new ErrorCode(1_030_004_005, "鑰冨嫟瑙勫垯涓嶅瓨鍦�");
ErrorCode ATTENDANCE_RULE_NAME_EXISTS = new ErrorCode(1_030_004_006, "鑰冨嫟瑙勫垯鍚嶇О宸插瓨鍦�");
+ // 宸插簾寮冿紙2026-10-16锛夛細鍚屼竴涓儴闂ㄥ厑璁搁厤缃鏉¤�冨嫟瑙勫垯锛堝鐝锛夛紝涓嶅啀鏍¢獙閮ㄩ棬鍞竴锛屼繚鐣欒閿欒鐮佷粎涓哄吋瀹瑰巻鍙插紩鐢�
ErrorCode ATTENDANCE_RULE_DEPT_EXISTS = new ErrorCode(1_030_004_007, "璇ラ儴闂ㄥ凡閰嶇疆鑰冨嫟瑙勫垯");
ErrorCode ATTENDANCE_HOLIDAY_NOT_EXISTS = new ErrorCode(1_030_004_008, "鑺傚亣鏃ラ厤缃笉瀛樺湪锛圛D={}锛夛紝鍙兘宸茶浠栦汉鍒犻櫎銆傝鍒锋柊鍒楄〃鍚庨噸璇�");
ErrorCode ATTENDANCE_HOLIDAY_RANGE_OVERLAP = new ErrorCode(1_030_004_009, "鏃ユ湡鍖洪棿銆恵} ~ {}銆戜笌宸叉湁閰嶇疆銆寋}銆嶏紙{} ~ {}锛夐噸鍙狅紝鏃犳硶淇濆瓨銆傝璋冩暣鏈鏃ユ湡鍖洪棿閬垮紑璇ラ厤缃紝鎴栫洿鎺ョ紪杈�/鍒犻櫎宸叉湁閰嶇疆鍚庡啀淇濆瓨");
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 60ba60d..2fa76d3 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
@@ -88,27 +88,31 @@
if (record.getDeptId() == null) {
return;
}
- HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
- if (rule == null) {
- return;
+ // 涓婄彮鎵撳崱绫诲瀷锛氭櫄浜� 涓婄彮鏃堕棿+杩熷埌鍏佽 璁颁负杩熷埌锛堟寜涓婄彮鎵撳崱鏃堕棿鍖归厤鐝锛�
+ if (record.getClockInTime() != null) {
+ HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(
+ record.getDeptId(), record.getClockInTime());
+ if (rule != null && rule.getWorkStartTime() != null) {
+ int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
+ LocalTime lateThreshold = rule.getWorkStartTime().plusMinutes(lateAllowMinutes);
+ LocalTime clockInOnly = record.getClockInTime().toLocalTime();
+ record.setClockInType(clockInOnly.isAfter(lateThreshold)
+ ? HrmAttendanceClockTypeEnum.LATE.getType()
+ : HrmAttendanceClockTypeEnum.NORMAL.getType());
+ }
}
- // 涓婄彮鎵撳崱绫诲瀷锛氭櫄浜� 涓婄彮鏃堕棿+杩熷埌鍏佽 璁颁负杩熷埌
- if (record.getClockInTime() != null && rule.getWorkStartTime() != null) {
- int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
- LocalTime lateThreshold = rule.getWorkStartTime().plusMinutes(lateAllowMinutes);
- LocalTime clockInOnly = record.getClockInTime().toLocalTime();
- record.setClockInType(clockInOnly.isAfter(lateThreshold)
- ? HrmAttendanceClockTypeEnum.LATE.getType()
- : HrmAttendanceClockTypeEnum.NORMAL.getType());
- }
- // 涓嬬彮鎵撳崱绫诲瀷锛氭棭浜� 涓嬬彮鏃堕棿-鏃╅��鍏佽 璁颁负鏃╅��
- if (record.getClockOutTime() != null && rule.getWorkEndTime() != null) {
- int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
- LocalTime earlyThreshold = rule.getWorkEndTime().minusMinutes(earlyAllowMinutes);
- LocalTime clockOutOnly = record.getClockOutTime().toLocalTime();
- record.setClockOutType(clockOutOnly.isBefore(earlyThreshold)
- ? HrmAttendanceClockTypeEnum.EARLY.getType()
- : HrmAttendanceClockTypeEnum.NORMAL.getType());
+ // 涓嬬彮鎵撳崱绫诲瀷锛氭棭浜� 涓嬬彮鏃堕棿-鏃╅��鍏佽 璁颁负鏃╅��锛堟寜涓嬬彮鎵撳崱鏃堕棿鍖归厤鐝锛�
+ if (record.getClockOutTime() != null) {
+ HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(
+ record.getDeptId(), record.getClockOutTime());
+ if (rule != null && rule.getWorkEndTime() != null) {
+ int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
+ LocalTime earlyThreshold = rule.getWorkEndTime().minusMinutes(earlyAllowMinutes);
+ LocalTime clockOutOnly = record.getClockOutTime().toLocalTime();
+ record.setClockOutType(clockOutOnly.isBefore(earlyThreshold)
+ ? HrmAttendanceClockTypeEnum.EARLY.getType()
+ : HrmAttendanceClockTypeEnum.NORMAL.getType());
+ }
}
}
@@ -123,9 +127,10 @@
.divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
record.setWorkHours(workHours);
}
- // 鍔犵彮鏃堕暱锛氭寜鑰冨嫟瑙勫垯鐨勬爣鍑嗕笅鐝椂闂磋绠楄秴棰濋儴鍒�
+ // 鍔犵彮鏃堕暱锛氭寜鑰冨嫟瑙勫垯鐨勬爣鍑嗕笅鐝椂闂磋绠楄秴棰濋儴鍒嗭紙鎸変笅鐝墦鍗℃椂闂村尮閰嶇彮娆★級
if (record.getClockOutTime() != null && record.getDeptId() != null) {
- HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
+ HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(
+ record.getDeptId(), record.getClockOutTime());
if (rule != null && rule.getWorkEndTime() != null) {
LocalTime workEnd = rule.getWorkEndTime();
LocalDate outDate = record.getClockOutTime().toLocalDate();
@@ -159,7 +164,8 @@
if (employee == null) {
throw exception(EMPLOYEE_NOT_EXISTS);
}
- HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
+ // 鎸夋湰娆℃墦鍗℃椂闂村尮閰嶇彮娆★紙鍚屼竴涓儴闂ㄥ彲鑳介厤缃簡澶氭潯瑙勫垯锛�
+ HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId(), clockTime);
if (rule == null) {
throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE);
}
@@ -212,7 +218,8 @@
if (employee == null) {
throw exception(EMPLOYEE_NOT_EXISTS);
}
- HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
+ // 鎸夋湰娆℃墦鍗℃椂闂村尮閰嶇彮娆★紙鍚屼竴涓儴闂ㄥ彲鑳介厤缃簡澶氭潯瑙勫垯锛�
+ HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId(), clockTime);
if (rule == null) {
throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE);
}
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleService.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleService.java
index 69e9b70..30b3ccf 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleService.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleService.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO;
import jakarta.validation.Valid;
+import java.time.LocalDateTime;
import java.util.List;
/**
@@ -69,4 +70,16 @@
*/
HrmAttendanceRuleDO getAttendanceRuleByDeptId(Long deptId);
+ /**
+ * 鏍规嵁閮ㄩ棬ID + 鎵撳崱鏃堕棿鑾峰彇鑰冨嫟瑙勫垯
+ *
+ * 鍚屼竴涓儴闂ㄥ厑璁搁厤缃鏉¤�冨嫟瑙勫垯锛堝鐝锛夛紝姝ゆ椂鎸夋墦鍗℃椂闂村尮閰嶇彮娆★細
+ * 浼樺厛鍙栫彮娆℃椂闂寸獥鍙e寘鍚鎵撳崱鏃堕棿鐨勮鍒欙紝閮戒笉鍖呭惈鏃跺彇鏃堕棿绐楀彛鏈�鎺ヨ繎鐨勪竴鏉°��
+ *
+ * @param deptId 閮ㄩ棬ID
+ * @param clockTime 鎵撳崱鏃堕棿锛涗负绌烘椂鍙栬閮ㄩ棬涓婄彮鏃堕棿鏈�鏃╃殑涓�鏉¤鍒�
+ * @return 鑰冨嫟瑙勫垯
+ */
+ HrmAttendanceRuleDO getAttendanceRuleByDeptId(Long deptId, LocalDateTime clockTime);
+
}
\ 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..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
diff --git a/yudao-module-hrm/src/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImplTest.java b/yudao-module-hrm/src/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImplTest.java
new file mode 100644
index 0000000..559e8ad
--- /dev/null
+++ b/yudao-module-hrm/src/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImplTest.java
@@ -0,0 +1,108 @@
+package cn.iocoder.yudao.module.hrm.service.attendance;
+
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * {@link HrmAttendanceRuleServiceImpl} 鐨勫崟鍏冩祴璇�
+ *
+ * 鍚屼竴涓儴闂ㄥ厑璁搁厤缃鏉¤�冨嫟瑙勫垯锛堝鐝锛夛紝杩欓噷瑕嗙洊銆屾寜鎵撳崱鏃堕棿鍖归厤鐝銆嶇殑閫夎鍒欓�昏緫銆�
+ * 娉ㄦ剰锛氭柇瑷�鍖归厤鍒扮殑瑙勫垯缂栧彿锛岃�屼笉鏄鍒欏璞℃湰韬� 鈥斺�� 杩斿洖鐨勬槸浼犲叆鐨勫悓涓�涓璞★紝姣旇緝缂栧彿鏇磋兘璇存槑閫夌殑鏄摢涓�鏉°��
+ */
+public class HrmAttendanceRuleServiceImplTest {
+
+ private static HrmAttendanceRuleDO rule(Long id, String name, LocalTime start, LocalTime end) {
+ return HrmAttendanceRuleDO.builder().id(id).name(name)
+ .workStartTime(start).workEndTime(end)
+ .lateAllowMinutes(0).earlyAllowMinutes(0)
+ .overtimeThreshold(BigDecimal.ONE).status(0).build();
+ }
+
+ /** 鏃╃彮 08:00-16:00銆佹櫄鐝� 16:00-24:00 鈥斺�� 鐢ㄦ潵瑕嗙洊鍚屼竴涓儴闂ㄥ涓彮娆� */
+ private static List<HrmAttendanceRuleDO> dayAndNightRules() {
+ return Stream.of(
+ rule(1L, "鏃╃彮", LocalTime.of(8, 0), LocalTime.of(16, 0)),
+ rule(2L, "鏅氱彮", LocalTime.of(16, 0), LocalTime.of(23, 59)))
+ .collect(Collectors.toList());
+ }
+
+ @Test
+ public void testMatchRuleByClockTime_singleRule() {
+ // 鍙湁涓�鏉¤鍒欐椂锛屾棤璁烘墦鍗℃椂闂存槸澶氬皯閮界敤瀹�
+ List<HrmAttendanceRuleDO> rules = Stream.of(rule(1L, "鏍囧噯鐝�", LocalTime.of(9, 0), LocalTime.of(18, 0)))
+ .collect(Collectors.toList());
+ assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 2, 0)).getId());
+ }
+
+ /** 鎵撳崱鏃堕棿钀藉湪鍝釜鐝鐨勬椂闂寸獥鍙e唴锛屽氨鐢ㄥ摢涓彮娆� 鈥斺�� 涓嶈兘鍥犱负杩斿洖椤哄簭鍙樹簡灏遍�夐敊 */
+ @Test
+ public void testMatchRuleByClockTime_clockTimeInWindow() {
+ List<HrmAttendanceRuleDO> rules = dayAndNightRules();
+ assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 8, 5)).getId());
+ assertEquals(2L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 16, 5)).getId());
+ assertEquals(2L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 23, 30)).getId());
+ }
+
+ /** 鐝椤哄簭棰犲�掓椂缁撴灉涓嶅彉锛堜繚璇佷笉闅忔暟鎹簱杩斿洖椤哄簭婕傜Щ锛� */
+ @Test
+ public void testMatchRuleByClockTime_orderIndependent() {
+ List<HrmAttendanceRuleDO> reversed = Stream.of(
+ rule(2L, "鏅氱彮", LocalTime.of(16, 0), LocalTime.of(23, 59)),
+ rule(1L, "鏃╃彮", LocalTime.of(8, 0), LocalTime.of(16, 0)))
+ .collect(Collectors.toList());
+ assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(reversed, LocalDateTime.of(2026, 10, 16, 7, 50)).getId());
+ }
+
+ /** 鎵撳崱鏃堕棿涓嶅湪浠讳綍绐楀彛鍐咃紙渚嬪鎻愬墠寰堜箙鍒板矖锛夛紝鍙栫獥鍙g寰楁渶杩戠殑涓�鏉� */
+ @Test
+ public void testMatchRuleByClockTime_outOfWindow() {
+ List<HrmAttendanceRuleDO> rules = dayAndNightRules();
+ assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 6, 0)).getId()); // 绂绘棭鐝� 2 灏忔椂
+ assertEquals(2L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 17, 1, 0)).getId()); // 绂绘櫄鐝笅鐝� 1 灏忔椂
+ }
+
+ /** 璺ㄥぉ澶滅彮锛�22:00-06:00锛屽噷鏅ㄧ殑鎵撳崱灞炰簬杩欎釜鐝 */
+ @Test
+ public void testMatchRuleByClockTime_crossDay() {
+ List<HrmAttendanceRuleDO> rules = Stream.of(
+ rule(1L, "鐧界彮", LocalTime.of(8, 0), LocalTime.of(16, 0)),
+ rule(3L, "澶滅彮", LocalTime.of(22, 0), LocalTime.of(6, 0)))
+ .collect(Collectors.toList());
+ assertEquals(3L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 22, 10)).getId());
+ assertEquals(3L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 17, 5, 40)).getId());
+ assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 9, 0)).getId());
+ }
+
+ /** 涓嶅叧蹇冪彮娆★紙鎵撳崱鏃堕棿涓虹┖锛夋椂锛屽彇涓婄彮鏃堕棿鏈�鏃╃殑涓�鏉★紝缁撴灉绋冲畾 */
+ @Test
+ public void testMatchRuleByClockTime_nullClockTime() {
+ List<HrmAttendanceRuleDO> rules = Stream.of(
+ rule(2L, "鏅氱彮", LocalTime.of(16, 0), LocalTime.of(23, 59)),
+ rule(1L, "鏃╃彮", LocalTime.of(8, 0), LocalTime.of(16, 0)))
+ .collect(Collectors.toList());
+ assertEquals(1L, HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, null).getId());
+ }
+
+ /** 鐝鏃堕棿娌¢厤濂斤紙涓虹┖锛夋椂涓嶈兘鎶涘紓甯革紝浠嶈鑳介�夊嚭涓�鏉¤鍒� */
+ @Test
+ public void testMatchRuleByClockTime_ruleWithoutTime() {
+ List<HrmAttendanceRuleDO> rules = Stream.of(
+ rule(9L, "鏈厤缃彮娆℃椂闂寸殑瑙勫垯", null, null),
+ rule(1L, "鏃╃彮", LocalTime.of(8, 0), LocalTime.of(16, 0)))
+ .collect(Collectors.toList());
+ HrmAttendanceRuleDO matched = HrmAttendanceRuleServiceImpl.matchRuleByClockTime(rules, LocalDateTime.of(2026, 10, 16, 9, 0));
+ assertNotNull(matched);
+ assertEquals(1L, matched.getId());
+ }
+
+}
--
Gitblit v1.9.3