tuqin
2 天以前 531b50cab6e42e6146a419bc71c5b494fff774b2
feat(hrm): 考勤规则支持 Excel 导入;修复定位打卡测试类编译错误

新增(同事 9aa810ea 未覆盖的部分):
- 考勤规则 Excel 导入:模板下载 + 导入(按名称判重,支持 updateSupport 覆盖,部门按名称匹配)
- 补充 LocalTimeConvert(框架缺失,导入模板依赖 LocalTime)
- 导入相关错误码:ATTENDANCE_RULE_IMPORT_LIST_IS_EMPTY / ATTENDANCE_RULE_EXISTS / ATTENDANCE_RULE_DEPT_NOT_EXISTS
- SQL:hrm_attendance_rule_import_menu.sql(import 权限点 + 全角色授权,幂等)

修复:
- HrmAttendanceClockLocationTest 中 getAttendanceRuleByDeptId 调用与签名不符(9aa810ea 为单参数)。
该分支的方法签名已含考勤日期参数,改为 getAttendanceRuleByDeptId(eq(2L), any(LocalDate.class)),
否则 mvn package 编译失败。

未包含(已在 9aa810ea 实现,本提交不重复、不覆盖):
- 考勤规则地点字段与 validateLocation 校验
- 打卡接口 JSON Body + legacyLocation 兼容、记录定位字段

验证:导入三态(创建/不覆盖/覆盖)、部门名称匹配、全局规则(dept_id NULL)、
普通角色权限点、HrmAttendanceRuleImportExcelVO 解析均通过。
已修改5个文件
已添加5个文件
362 ■■■■■ 文件已修改
sql/mysql/hrm_attendance_rule_import_menu.sql 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/LocalTimeConvert.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRuleController.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportExcelVO.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportReqVO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRuleImportRespVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRuleServiceImpl.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/test/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceClockLocationTest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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(MAX(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;
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(cn.idev.excel)内置不支持 LocalTime,未指定 converter æ—¶è¯»å†™éƒ½ä¼šæŠ›å‡º
 * Can not find Converter support class LocalTime。这里补上「HH:mm:ss å­—符串」与
 * ã€ŒExcel æ—¶é—´æ•°å€¼ï¼ˆä¸€å¤©çš„占比)」两种写法的读写支持。
 *
 * @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) {
        // æƒ…况一:Excel é‡Œå¡«çš„æ˜¯æ—¶é—´æ•°å€¼ï¼ˆä¸€å¤©çš„占比)
        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));
    }
}
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, "结束日期【{}】不能早于开始日期【{}】,无法保存。请重新选择正确的日期区间");
    // ========== è€ƒå‹¤è§„则:导入 ==========
    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, "打卡经纬度超出有效范围");
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));
    }
}
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;
}
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;
}
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 = "导入失败的考勤规则集合,key ä¸ºè§„则名称,value ä¸ºå¤±è´¥åŽŸå› ", requiredMode = Schema.RequiredMode.REQUIRED)
    private Map<String, String> failureNames;
}
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);
}
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 è§£æžéƒ¨é—¨
                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;
    }
    /**
     * å¯¼å…¥æ—¶çš„默认值补全(与页面新增保持一致的默认口径;不覆盖地点字段,避免导入清空已配置的地点)
     */
    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);
        }
    }
}
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));