From 531b50cab6e42e6146a419bc71c5b494fff774b2 Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期二, 27 十月 2026 09:48:33 +0800
Subject: [PATCH] feat(hrm): 考勤规则支持 Excel 导入;修复定位打卡测试类编译错误

---
 yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalTimeConvert.java |   72 ++++++++++
 yudao-module-hrm/src/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockLocationTest.java                 |    3 
 sql/mysql/hrm_attendance_rule_import_menu.sql                                                                                     |   56 ++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportReqVO.java       |   19 ++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportExcelVO.java     |   49 +++++++
 yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java                                      |    4 
 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                   |   94 +++++++++++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRuleController.java           |   28 ++++
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportRespVO.java      |   24 +++
 10 files changed, 361 insertions(+), 1 deletions(-)

diff --git a/sql/mysql/hrm_attendance_rule_import_menu.sql b/sql/mysql/hrm_attendance_rule_import_menu.sql
new file mode 100644
index 0000000..d94bc26
--- /dev/null
+++ b/sql/mysql/hrm_attendance_rule_import_menu.sql
@@ -0,0 +1,56 @@
+-- =============================================
+-- 鑰冨嫟瑙勫垯锛氬鍏ユ寜閽潈闄愮偣
+--
+-- 鑳屾櫙锛氭柊澧� /hrm/attendance/rule/import 涓� /get-import-template 涓や釜鎺ュ彛锛�
+--       鏉冮檺鐮� hrm:attendance-rule:import 鍦� system_menu 涓笉瀛樺湪锛�
+--       浼氬鑷撮潪瓒呯骇绠$悊鍛樿皟鐢ㄦ椂 403銆�
+--
+-- 鍋氭硶锛氳ˉ涓�涓� type=3 鐨勬寜閽潈闄愮偣锛堜笉鏄剧ず鍦ㄤ晶鏍忥級锛屽苟鎺堟潈缁欐墍鏈夌幇鏈夊惎鐢ㄨ鑹诧紝
+--       涓庢棦鏈夈�岃�冨嫟瑙勫垯鏌ヨ/鏂板/淇敼/鍒犻櫎/瀵煎嚭銆嶇殑鎺堟潈鑼冨洿淇濇寔涓�鑷淬��
+--
+-- 鍔ㄦ�� id锛圡AX(id)+1锛夛紝鍙噸澶嶆墽琛岋紙骞傜瓑锛夈��
+-- =============================================
+SET NAMES utf8mb4;
+
+-- 1. 瀹氫綅鐖惰彍鍗曪細鑰冨嫟瑙勫垯锛堜紭鍏堟寜 path锛屽厹搴曟寜鏃㈡湁鏉冮檺鐮佸弽鏌ワ級
+SET @hrm := (SELECT id FROM `system_menu` WHERE `path` IN ('hrm','/hrm') AND `deleted`=b'0' LIMIT 1);
+SET @att := (SELECT id FROM `system_menu` WHERE `path`='attendance' AND `parent_id`=@hrm AND `deleted`=b'0' LIMIT 1);
+SET @att_rule := (SELECT `parent_id` FROM `system_menu`
+    WHERE `permission`='hrm:attendance-rule:query' AND `deleted`=b'0' LIMIT 1);
+SET @att_rule := IFNULL(@att_rule,
+    (SELECT id FROM `system_menu` WHERE `path`='rule' AND `parent_id`=@att AND `deleted`=b'0' LIMIT 1));
+
+-- 2. 鎻掑叆鏉冮檺鐐�
+SET @perm_exists := (SELECT COUNT(*) FROM `system_menu` WHERE `permission`='hrm:attendance-rule:import');
+SET @import_menu_id := IFNULL((SELECT MAX(id) FROM `system_menu`), 0) + 1;
+
+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`)
+SELECT @import_menu_id, '鑰冨嫟瑙勫垯瀵煎叆', 'hrm:attendance-rule:import', 3, 6, @att_rule, '', '', '', NULL,
+       0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
+FROM DUAL
+WHERE @perm_exists = 0 AND @att_rule IS NOT NULL;
+
+-- 3. 鎺堟潈缁欐墍鏈夌幇鏈夊惎鐢ㄨ鑹�
+SET @menu_id := (SELECT id FROM `system_menu` WHERE `permission`='hrm:attendance-rule:import' AND `deleted`=b'0' LIMIT 1);
+
+INSERT INTO `system_role_menu` (`role_id`,`menu_id`,`creator`,`create_time`,`updater`,`update_time`,`deleted`)
+SELECT r.`id`, @menu_id, '1', NOW(), '1', NOW(), b'0'
+FROM `system_role` r
+WHERE @menu_id IS NOT NULL
+  AND r.`deleted` = b'0'
+  AND r.`status` = 0
+  AND NOT EXISTS (SELECT 1 FROM `system_role_menu` rm
+                  WHERE rm.`role_id` = r.`id` AND rm.`menu_id` = @menu_id AND rm.`deleted` = b'0');
+
+-- 4. 鏍稿
+SELECT id, name, permission, type, parent_id FROM `system_menu`
+WHERE permission = 'hrm:attendance-rule:import';
+
+SELECT r.id AS role_id, r.name AS role_name, rm.menu_id
+FROM `system_role` r
+LEFT JOIN `system_role_menu` rm ON rm.role_id = r.id AND rm.menu_id = @menu_id AND rm.deleted = b'0'
+WHERE r.deleted = b'0' AND r.status = 0;
+
+SELECT 'hrm:attendance-rule:import 鏉冮檺鐐瑰垵濮嬪寲瀹屾垚' AS result;
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalTimeConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalTimeConvert.java
new file mode 100644
index 0000000..f963a14
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalTimeConvert.java
@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.framework.excel.core.convert;
+
+import cn.hutool.core.util.StrUtil;
+import cn.idev.excel.converters.Converter;
+import cn.idev.excel.enums.CellDataTypeEnum;
+import cn.idev.excel.metadata.GlobalConfiguration;
+import cn.idev.excel.metadata.data.ReadCellData;
+import cn.idev.excel.metadata.data.WriteCellData;
+import cn.idev.excel.metadata.property.ExcelContentProperty;
+
+import java.math.BigDecimal;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * LocalTime 杞崲鍣�
+ *
+ * FastExcel锛坈n.idev.excel锛夊唴缃笉鏀寔 LocalTime锛屾湭鎸囧畾 converter 鏃惰鍐欓兘浼氭姏鍑�
+ * Can not find Converter support class LocalTime銆傝繖閲岃ˉ涓娿�孒H:mm:ss 瀛楃涓层�嶄笌
+ * 銆孍xcel 鏃堕棿鏁板�硷紙涓�澶╃殑鍗犳瘮锛夈�嶄袱绉嶅啓娉曠殑璇诲啓鏀寔銆�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public class LocalTimeConvert implements Converter<LocalTime> {
+
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
+
+    @Override
+    public Class<LocalTime> supportJavaTypeKey() {
+        return LocalTime.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public LocalTime convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty,
+                                       GlobalConfiguration globalConfiguration) {
+        // 鎯呭喌涓�锛欵xcel 閲屽~鐨勬槸鏃堕棿鏁板�硷紙涓�澶╃殑鍗犳瘮锛�
+        if (CellDataTypeEnum.NUMBER.equals(readCellData.getType())) {
+            BigDecimal numberValue = readCellData.getNumberValue();
+            if (numberValue == null) {
+                return null;
+            }
+            long secondOfDay = Math.round(numberValue.doubleValue() * 24 * 60 * 60) % (24 * 60 * 60);
+            return LocalTime.ofSecondOfDay(secondOfDay);
+        }
+        // 鎯呭喌浜岋細Excel 閲屽~鐨勬槸鏂囨湰锛屽 8:30 / 08:30:00
+        String text = readCellData.getStringValue();
+        if (StrUtil.isBlank(text)) {
+            return null;
+        }
+        try {
+            String[] parts = text.trim().replace('锛�', ':').split(":");
+            int hour = Integer.parseInt(parts[0].trim());
+            int minute = parts.length > 1 ? Integer.parseInt(parts[1].trim()) : 0;
+            int second = parts.length > 2 ? Integer.parseInt(parts[2].trim()) : 0;
+            return LocalTime.of(hour, minute, second);
+        } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public WriteCellData<String> convertToExcelData(LocalTime value, ExcelContentProperty contentProperty,
+                                                    GlobalConfiguration globalConfiguration) {
+        return new WriteCellData<>(value == null ? "" : value.format(TIME_FORMATTER));
+    }
+
+}
\ No newline at end of file
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 525dc39..5b4f221 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
@@ -97,6 +97,10 @@
     ErrorCode ATTENDANCE_CLOCK_ALREADY_CLOCKED = new ErrorCode(1_030_004_016, "宸叉墦杩囧崱锛岃鍕块噸澶嶆墦鍗�");
     ErrorCode ATTENDANCE_RULE_EFFECTIVE_DATE_INVALID = new ErrorCode(1_030_004_017, "鐢熸晥寮�濮嬫棩鏈熶笉鑳芥櫄浜庣敓鏁堢粨鏉熸棩鏈�");
     ErrorCode ATTENDANCE_HOLIDAY_DATE_RANGE_INVALID = new ErrorCode(1_030_004_017, "缁撴潫鏃ユ湡銆恵}銆戜笉鑳芥棭浜庡紑濮嬫棩鏈熴�恵}銆戯紝鏃犳硶淇濆瓨銆傝閲嶆柊閫夋嫨姝g‘鐨勬棩鏈熷尯闂�");
+    // ========== 鑰冨嫟瑙勫垯锛氬鍏� ==========
+    ErrorCode ATTENDANCE_RULE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_030_004_023, "瀵煎叆鑰冨嫟瑙勫垯鏁版嵁涓嶈兘涓虹┖");
+    ErrorCode ATTENDANCE_RULE_EXISTS = new ErrorCode(1_030_004_024, "宸插瓨鍦ㄨ�冨嫟瑙勫垯銆恵}銆戯紝涓旀湭寮�鍚鐩栵紒");
+    ErrorCode ATTENDANCE_RULE_DEPT_NOT_EXISTS = new ErrorCode(1_030_004_025, "閮ㄩ棬銆恵}銆戜笉瀛樺湪");
     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, "鎵撳崱缁忕含搴﹁秴鍑烘湁鏁堣寖鍥�");
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRuleController.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRuleController.java
index dbeab37..41b5a73 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRuleController.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRuleController.java
@@ -7,6 +7,9 @@
 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.HrmAttendanceRuleImportExcelVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleImportReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleImportRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRulePageReqVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleRespVO;
 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleSaveReqVO;
@@ -25,6 +28,8 @@
 import org.springframework.web.bind.annotation.*;
 
 import java.io.IOException;
+import java.math.BigDecimal;
+import java.time.LocalTime;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -129,4 +134,27 @@
         ExcelUtils.write(response, "鑰冨嫟瑙勫垯.xls", "鏁版嵁", HrmAttendanceRuleRespVO.class, data);
     }
 
+    @GetMapping("/get-import-template")
+    @Operation(summary = "鑾峰緱瀵煎叆鑰冨嫟瑙勫垯妯℃澘")
+    @PreAuthorize("@ss.hasPermission('hrm:attendance-rule:import')")
+    public void importTemplate(HttpServletResponse response) throws IOException {
+        // 鎵嬪姩鍒涘缓瀵煎嚭 demo
+        List<HrmAttendanceRuleImportExcelVO> list = Collections.singletonList(
+                HrmAttendanceRuleImportExcelVO.builder().name("鏍囧噯鑰冨嫟瑙勫垯").deptName("鐮斿彂閮�")
+                        .workStartTime(LocalTime.of(9, 0)).workEndTime(LocalTime.of(18, 0))
+                        .lateAllowMinutes(10).earlyAllowMinutes(10).overtimeThreshold(BigDecimal.ONE)
+                        .status(0).remark("").build()
+        );
+        // 杈撳嚭
+        ExcelUtils.write(response, "鑰冨嫟瑙勫垯瀵煎叆妯℃澘.xls", "鑰冨嫟瑙勫垯鍒楄〃", HrmAttendanceRuleImportExcelVO.class, list);
+    }
+
+    @PostMapping("/import")
+    @Operation(summary = "瀵煎叆鑰冨嫟瑙勫垯")
+    @PreAuthorize("@ss.hasPermission('hrm:attendance-rule:import')")
+    public CommonResult<HrmAttendanceRuleImportRespVO> importExcel(@Valid HrmAttendanceRuleImportReqVO importReqVO)
+            throws Exception {
+        List<HrmAttendanceRuleImportExcelVO> list = ExcelUtils.read(importReqVO.getFile(), HrmAttendanceRuleImportExcelVO.class);
+        return success(attendanceRuleService.importAttendanceRuleList(list, importReqVO));
+    }
 }
\ No newline at end of file
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportExcelVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportExcelVO.java
new file mode 100644
index 0000000..29a3b13
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportExcelVO.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
+
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.iocoder.yudao.framework.excel.core.convert.LocalTimeConvert;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalTime;
+
+/**
+ * 鑰冨嫟瑙勫垯閰嶇疆 Excel 瀵煎叆 VO
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class HrmAttendanceRuleImportExcelVO {
+
+    @ExcelProperty("瑙勫垯鍚嶇О")
+    private String name;
+
+    @ExcelProperty("閮ㄩ棬鍚嶇О")
+    private String deptName;
+
+    @ExcelProperty(value = "涓婄彮鏃堕棿", converter = LocalTimeConvert.class)
+    private LocalTime workStartTime;
+
+    @ExcelProperty(value = "涓嬬彮鏃堕棿", converter = LocalTimeConvert.class)
+    private LocalTime workEndTime;
+
+    @ExcelProperty("杩熷埌鍏佽鍒嗛挓鏁�")
+    private Integer lateAllowMinutes;
+
+    @ExcelProperty("鏃╅��鍏佽鍒嗛挓鏁�")
+    private Integer earlyAllowMinutes;
+
+    @ExcelProperty("鍔犵彮璧风畻鏃堕暱锛堝皬鏃讹級")
+    private BigDecimal overtimeThreshold;
+
+    @ExcelProperty("鐘舵�侊紙0-鍚敤锛�1-绂佺敤锛�")
+    private Integer status;
+
+    @ExcelProperty("澶囨敞")
+    private String remark;
+
+}
\ No newline at end of file
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportReqVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportReqVO.java
new file mode 100644
index 0000000..64367d4
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportReqVO.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.springframework.web.multipart.MultipartFile;
+
+@Schema(description = "绠$悊鍚庡彴 - 鑰冨嫟瑙勫垯瀵煎叆 Request VO")
+@Data
+public class HrmAttendanceRuleImportReqVO {
+
+    @Schema(description = "Excel 鏂囦欢", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "Excel 鏂囦欢涓嶈兘涓虹┖")
+    private MultipartFile file;
+
+    @Schema(description = "鏄惁鏀寔鏇存柊锛岄粯璁� false", example = "true")
+    private Boolean updateSupport = false;
+
+}
\ No newline at end of file
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportRespVO.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportRespVO.java
new file mode 100644
index 0000000..54d42c2
--- /dev/null
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportRespVO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Schema(description = "绠$悊鍚庡彴 - 鑰冨嫟瑙勫垯瀵煎叆 Response VO")
+@Data
+@Builder
+public class HrmAttendanceRuleImportRespVO {
+
+    @Schema(description = "鍒涘缓鎴愬姛鐨勮�冨嫟瑙勫垯鍚嶇О鏁扮粍", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<String> createNames;
+
+    @Schema(description = "鏇存柊鎴愬姛鐨勮�冨嫟瑙勫垯鍚嶇О鏁扮粍", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<String> updateNames;
+
+    @Schema(description = "瀵煎叆澶辫触鐨勮�冨嫟瑙勫垯闆嗗悎锛宬ey 涓鸿鍒欏悕绉帮紝value 涓哄け璐ュ師鍥�", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Map<String, String> failureNames;
+
+}
\ No newline at end of file
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 96eaf1b..25c18fe 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
@@ -71,4 +71,17 @@
      */
     HrmAttendanceRuleDO getAttendanceRuleByDeptId(Long deptId, java.time.LocalDate date);
 
+    /**
+     * 瀵煎叆鑰冨嫟瑙勫垯锛堢Щ妞嶈嚜鏂伴珮鐢靛姏锛�
+     *
+     * 鎸夈�岃鍒欏悕绉般�嶅垽閲嶏紱宸插瓨鍦ㄦ椂闇� updateSupport=true 鎵嶈鐩栥��
+     * 閮ㄩ棬鎸夊悕绉板尮閰嶏紝閮ㄩ棬涓虹┖琛ㄧず鍏ㄥ眬瑙勫垯銆�
+     *
+     * @param importList  瀵煎叆鐨勮鍒欏垪琛�
+     * @param importReqVO 瀵煎叆鍙傛暟
+     * @return 瀵煎叆缁撴灉
+     */
+    cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleImportRespVO importAttendanceRuleList(
+            List<cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleImportExcelVO> importList,
+            cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleImportReqVO importReqVO);
 }
\ 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 fbe8cc5..a875825 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,22 +1,34 @@
 package cn.iocoder.yudao.module.hrm.service.attendance;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
 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 cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleImportExcelVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleImportReqVO;
+import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleImportRespVO;
 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;
 import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceRuleMapper;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
 import jakarta.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
 import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
 
 /**
@@ -33,6 +45,9 @@
 
     @Resource
     private HrmAttendanceRuleMapper attendanceRuleMapper;
+
+    @Resource
+    private DeptApi deptApi;
 
     @Override
     public Long createAttendanceRule(HrmAttendanceRuleSaveReqVO createReqVO) {
@@ -208,4 +223,83 @@
         return attendanceRuleMapper.selectList(wrapper);
     }
 
+    /**
+     * 瀵煎叆鑰冨嫟瑙勫垯锛堢Щ妞嶈嚜鏂伴珮鐢靛姏锛�
+     *
+     * 璇存槑锛氬鍏ユ寜銆岃鍒欏悕绉般�嶅垽閲嶏紱宸插瓨鍦ㄦ椂闇� updateSupport=true 鎵嶈鐩栥��
+     * 閮ㄩ棬鎸夊悕绉板尮閰嶏紙deptName -> deptId锛夛紝閮ㄩ棬涓虹┖琛ㄧず鍏ㄥ眬瑙勫垯銆�
+     */
+    @Override
+    public HrmAttendanceRuleImportRespVO importAttendanceRuleList(
+            List<HrmAttendanceRuleImportExcelVO> importList, HrmAttendanceRuleImportReqVO importReqVO) {
+        // 1. 鏍¢獙闈炵┖
+        importList = filterList(importList, item -> StrUtil.isNotBlank(item.getName()));
+        if (CollUtil.isEmpty(importList)) {
+            throw exception(ATTENDANCE_RULE_IMPORT_LIST_IS_EMPTY);
+        }
+        // 2. 棰勫姞杞介儴闂ㄥ悕绉� -> 閮ㄩ棬ID
+        Map<String, Long> deptMap = deptApi.getDeptList().stream()
+                .filter(dept -> StrUtil.isNotBlank(dept.getName()))
+                .collect(Collectors.toMap(DeptRespDTO::getName, DeptRespDTO::getId, (a, b) -> a));
+        // 3. 閫愭潯澶勭悊
+        HrmAttendanceRuleImportRespVO respVO = HrmAttendanceRuleImportRespVO.builder()
+                .createNames(new ArrayList<>())
+                .updateNames(new ArrayList<>())
+                .failureNames(new LinkedHashMap<>())
+                .build();
+        importList.forEach(importRule -> {
+            try {
+                // 3.1 瑙f瀽閮ㄩ棬
+                Long deptId = null;
+                if (StrUtil.isNotBlank(importRule.getDeptName())) {
+                    deptId = deptMap.get(importRule.getDeptName());
+                    if (deptId == null) {
+                        throw exception(ATTENDANCE_RULE_DEPT_NOT_EXISTS, importRule.getDeptName());
+                    }
+                }
+                // 3.2 鍒ゆ柇鏄惁瀛樺湪锛堟寜鍚嶇О鍞竴锛�
+                HrmAttendanceRuleDO existRule = attendanceRuleMapper.selectOne(HrmAttendanceRuleDO::getName, importRule.getName());
+                if (existRule == null) {
+                    // 3.2.1 鏂板
+                    HrmAttendanceRuleDO rule = BeanUtils.toBean(importRule, HrmAttendanceRuleDO.class).setDeptId(deptId);
+                    applyImportDefaults(rule);
+                    attendanceRuleMapper.insert(rule);
+                    respVO.getCreateNames().add(importRule.getName());
+                    return;
+                }
+                // 3.2.2 宸插瓨鍦紝鍒ゆ柇鏄惁鍏佽鏇存柊
+                if (!Boolean.TRUE.equals(importReqVO.getUpdateSupport())) {
+                    respVO.getFailureNames().put(importRule.getName(),
+                            StrUtil.format(ATTENDANCE_RULE_EXISTS.getMsg(), importRule.getName()));
+                    return;
+                }
+                HrmAttendanceRuleDO updateRule = BeanUtils.toBean(importRule, HrmAttendanceRuleDO.class)
+                        .setId(existRule.getId()).setDeptId(deptId);
+                applyImportDefaults(updateRule);
+                attendanceRuleMapper.updateById(updateRule);
+                respVO.getUpdateNames().add(importRule.getName());
+            } catch (ServiceException ex) {
+                respVO.getFailureNames().put(importRule.getName(), ex.getMessage());
+            }
+        });
+        return respVO;
+    }
+
+    /**
+     * 瀵煎叆鏃剁殑榛樿鍊艰ˉ鍏紙涓庨〉闈㈡柊澧炰繚鎸佷竴鑷寸殑榛樿鍙e緞锛涗笉瑕嗙洊鍦扮偣瀛楁锛岄伩鍏嶅鍏ユ竻绌哄凡閰嶇疆鐨勫湴鐐癸級
+     */
+    private void applyImportDefaults(HrmAttendanceRuleDO rule) {
+        if (rule.getLateAllowMinutes() == null) {
+            rule.setLateAllowMinutes(0);
+        }
+        if (rule.getEarlyAllowMinutes() == null) {
+            rule.setEarlyAllowMinutes(0);
+        }
+        if (rule.getOvertimeThreshold() == null) {
+            rule.setOvertimeThreshold(BigDecimal.ONE);
+        }
+        if (rule.getStatus() == null) {
+            rule.setStatus(0);
+        }
+    }
 }
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
index 53fe048..9a9e958 100644
--- 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
@@ -16,6 +16,7 @@
 
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.*;
 
 class HrmAttendanceClockLocationTest {
@@ -75,7 +76,7 @@
         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());
+        when(ruleService.getAttendanceRuleByDeptId(eq(2L), any(LocalDate.class))).thenReturn(requiredRule());
         assertThrows(ServiceException.class,
                 () -> service.clockOut(1L, clockTime, position("0.00091", "20"), null));
         verify(mapper, never()).insert(any(HrmAttendanceRecordDO.class));

--
Gitblit v1.9.3