# 员工导入Excel内双字段重复检测 - 代码流程说明 ## 方法签名 ```java public void importEmployeeAsync(List excelList, Boolean isUpdateSupport, String taskId) ``` ## 完整流程图 ``` 开始 │ ├─ 1. 初始化集合 │ ├─ newRecords = new ArrayList<>() // 新增记录 │ ├─ updateRecords = new ArrayList<>() // 更新记录 │ └─ failures = new ArrayList<>() // 失败记录 │ ├─ 2. 批量查询数据库 │ ├─ getExistingEmployeeIds(excelList) │ │ └─ 返回: Set existingIds // 数据库中已存在的柜员号 │ │ │ └─ getExistingIdCards(excelList) │ └─ 返回: Set existingIdCards // 数据库中已存在的身份证号 │ ├─ 3. 初始化Excel内跟踪集合 │ ├─ processedEmployeeIds = new HashSet<>() // Excel内已处理的柜员号 │ └─ processedIdCards = new HashSet<>() // Excel内已处理的身份证号 │ ├─ 4. 遍历Excel数据 │ │ │ └─ FOR EACH excel IN excelList │ │ │ ├─ 4.1 数据转换 │ │ ├─ addDTO = new CcdiEmployeeAddDTO() │ │ ├─ BeanUtils.copyProperties(excel, addDTO) │ │ └─ employee = new CcdiEmployee() │ │ └─ BeanUtils.copyProperties(excel, employee) │ │ │ ├─ 4.2 数据验证 │ │ └─ validateEmployeeData(addDTO, isUpdateSupport, existingIds, existingIdCards) │ │ ├─ 验证必填字段(姓名、柜员号、部门、身份证号、电话、状态) │ │ ├─ 验证身份证号格式 │ │ └─ 验证柜员号和身份证号唯一性 │ │ │ ├─ 4.3 重复检测与分类 │ │ │ │ │ ├─ IF existingIds.contains(excel.getEmployeeId()) │ │ │ ├─ 柜员号在数据库中已存在 │ │ │ ├─ IF isUpdateSupport │ │ │ │ └─ updateRecords.add(employee) // 添加到更新列表 │ │ │ └─ ELSE │ │ │ └─ throw RuntimeException("柜员号已存在且未启用更新支持") │ │ │ │ │ ├─ ELSE IF processedEmployeeIds.contains(excel.getEmployeeId()) │ │ │ └─ throw RuntimeException("柜员号[XXX]在导入文件中重复,已跳过此条记录") │ │ │ │ │ ├─ ELSE IF processedIdCards.contains(excel.getIdCard()) │ │ │ └─ throw RuntimeException("身份证号[XXX]在导入文件中重复,已跳过此条记录") │ │ │ │ │ └─ ELSE │ │ ├─ newRecords.add(employee) // 添加到新增列表 │ │ ├─ IF excel.getEmployeeId() != null │ │ │ └─ processedEmployeeIds.add(excel.getEmployeeId()) // 标记柜员号 │ │ └─ IF StringUtils.isNotEmpty(excel.getIdCard()) │ │ └─ processedIdCards.add(excel.getIdCard()) // 标记身份证号 │ │ │ └─ 4.4 异常处理 │ └─ CATCH Exception │ ├─ failure = new ImportFailureVO() │ ├─ BeanUtils.copyProperties(excel, failure) │ ├─ failure.setErrorMessage(e.getMessage()) │ └─ failures.add(failure) │ ├─ 5. 批量操作数据库 │ ├─ IF !newRecords.isEmpty() │ │ └─ saveBatch(newRecords, 500) // 批量插入新数据 │ │ │ └─ IF !updateRecords.isEmpty() && isUpdateSupport │ └─ employeeMapper.insertOrUpdateBatch(updateRecords) // 批量更新已有数据 │ ├─ 6. 保存失败记录到Redis │ └─ IF !failures.isEmpty() │ └─ redisTemplate.opsForValue().set("import:employee:" + taskId + ":failures", failures, 7, TimeUnit.DAYS) │ ├─ 7. 生成导入结果 │ ├─ result = new ImportResult() │ ├─ result.setTotalCount(excelList.size()) │ ├─ result.setSuccessCount(newRecords.size() + updateRecords.size()) │ └─ result.setFailureCount(failures.size()) │ └─ 8. 更新导入状态 └─ updateImportStatus("employee", taskId, finalStatus, result) └─ IF result.getFailureCount() == 0 └─ finalStatus = "SUCCESS" └─ ELSE └─ finalStatus = "PARTIAL_SUCCESS" 结束 ``` ## 关键逻辑说明 ### 1. 重复检测优先级 ``` 数据库柜员号重复 > Excel内柜员号重复 > Excel内身份证号重复 ``` **原因**: - 数据库检查优先: 避免处理已经存在且不允许更新的数据 - Excel内柜员号检查: 柜员号是主键,优先检查 - Excel内身份证号检查: 身份证号也需要唯一性 ### 2. 标记时机 ``` 只在记录成功添加到newRecords后才标记为已处理 ``` **原因**: - 避免将验证失败的记录标记为已处理 - 确保只有成功插入数据库的记录才会占用柜员号和身份证号 - 防止因前一条记录失败导致后一条有效记录被误判为重复 ### 3. 空值处理 ```java // 柜员号空值检查 if (excel.getEmployeeId() != null) { processedEmployeeIds.add(excel.getEmployeeId()); } // 身份证号空值检查 if (StringUtils.isNotEmpty(excel.getIdCard())) { processedIdCards.add(excel.getIdCard()); } ``` **原因**: - 防止空指针异常 - 确保只有有效的柜员号和身份证号才会被检查重复 ### 4. 批量查询优化 ```java // 批量查询柜员号 Set existingIds = getExistingEmployeeIds(excelList); // 批量查询身份证号 Set existingIdCards = getExistingIdCards(excelList); ``` **优点**: - 一次性查询所有需要的数据 - 避免逐条查询导致的N+1问题 - 使用HashSet实现O(1)复杂度的查找 ## 错误消息说明 ### 1. 柜员号在数据库中已存在 ```java "柜员号已存在且未启用更新支持" ``` ### 2. 柜员号在Excel内重复 ```java String.format("柜员号[%d]在导入文件中重复,已跳过此条记录", excel.getEmployeeId()) ``` **示例**: "柜员号[1001]在导入文件中重复,已跳过此条记录" ### 3. 身份证号在Excel内重复 ```java String.format("身份证号[%s]在导入文件中重复,已跳过此条记录", excel.getIdCard()) ``` **示例**: "身份证号[110101199001011234]在导入文件中重复,已跳过此条记录" ## validateEmployeeData方法说明 ### 方法签名 ```java public void validateEmployeeData(CcdiEmployeeAddDTO addDTO, Boolean isUpdateSupport, Set existingIds, Set existingIdCards) ``` ### 验证流程 ``` 1. 验证必填字段 ├─ 姓名不能为空 ├─ 柜员号不能为空 ├─ 所属部门不能为空 ├─ 身份证号不能为空 ├─ 电话不能为空 └─ 状态不能为空 2. 验证身份证号格式 └─ IdCardUtil.getErrorMessage(addDTO.getIdCard()) 3. 验证唯一性 ├─ IF existingIds == null (单条新增场景) │ ├─ 检查柜员号唯一性(数据库查询) │ └─ 检查身份证号唯一性(数据库查询) │ └─ ELSE (导入场景) ├─ IF 柜员号不存在于数据库 │ └─ 检查身份证号唯一性(使用批量查询结果) └─ ELSE (柜员号已存在,允许更新) └─ 跳过身份证号检查(更新模式下不检查身份证号重复) 4. 验证状态 └─ 状态只能填写'0'(在职)或'1'(离职) ``` ### 导入场景的身份证号唯一性检查优化 ```java // 导入场景:如果柜员号不存在,才检查身份证号唯一性 if (!existingIds.contains(addDTO.getEmployeeId())) { // 使用批量查询的结果检查身份证号唯一性 if (existingIdCards != null && existingIdCards.contains(addDTO.getIdCard())) { throw new RuntimeException("该身份证号已存在"); } } ``` **优化点**: - 使用批量查询结果`existingIdCards`,避免逐条查询数据库 - 只在柜员号不存在时才检查身份证号(因为柜员号存在时是更新模式) ## 批量查询方法说明 ### getExistingEmployeeIds ```java private Set getExistingEmployeeIds(List excelList) { List employeeIds = excelList.stream() .map(CcdiEmployeeExcel::getEmployeeId) .filter(Objects::nonNull) .collect(Collectors.toList()); if (employeeIds.isEmpty()) { return Collections.emptySet(); } List existingEmployees = employeeMapper.selectBatchIds(employeeIds); return existingEmployees.stream() .map(CcdiEmployee::getEmployeeId) .collect(Collectors.toSet()); } ``` ### getExistingIdCards ```java private Set getExistingIdCards(List excelList) { List idCards = excelList.stream() .map(CcdiEmployeeExcel::getIdCard) .filter(StringUtils::isNotEmpty) .collect(Collectors.toList()); if (idCards.isEmpty()) { return Collections.emptySet(); } LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(CcdiEmployee::getIdCard, idCards); List existingEmployees = employeeMapper.selectList(wrapper); return existingEmployees.stream() .map(CcdiEmployee::getIdCard) .collect(Collectors.toSet()); } ``` **特点**: - 使用Stream API进行数据提取和过滤 - 过滤空值,避免无效查询 - 使用MyBatis Plus的批量查询方法 - 返回Set集合,实现O(1)复杂度的查找 ## 性能分析 ### 时间复杂度 - 批量查询: O(n), n为Excel记录数 - 重复检测: O(1), 使用HashSet - 总体复杂度: O(n) ### 空间复杂度 - existingIds: O(m), m为数据库中已存在的柜员号数量 - existingIdCards: O(k), k为数据库中已存在的身份证号数量 - processedEmployeeIds: O(n), n为Excel记录数 - processedIdCards: O(n), n为Excel记录数 - 总体空间复杂度: O(m + k + n) ### 数据库查询次数 - 修改前: 1次(批量查询柜员号) + n次(逐条查询身份证号) = O(n) - 修改后: 2次(批量查询柜员号 + 批量查询身份证号) = O(1) **性能提升**: 减少n-1次数据库查询 ## 总结 本实现通过以下技术手段实现了Excel内双字段重复检测: 1. 批量查询优化,减少数据库访问 2. 使用HashSet进行O(1)复杂度的重复检测 3. 合理的检查顺序和标记时机 4. 完善的空值处理和错误提示 5. 遵循若依框架编码规范,使用MyBatis Plus进行数据操作