Files
ccdi/doc/implementation-reports/employee-duplicate-detection-flow.md
2026-02-09 14:34:27 +08:00

11 KiB

员工导入Excel内双字段重复检测 - 代码流程说明

方法签名

public void importEmployeeAsync(List<CcdiEmployeeExcel> excelList, Boolean isUpdateSupport, String taskId)

完整流程图

开始
  │
  ├─ 1. 初始化集合
  │   ├─ newRecords = new ArrayList<>()      // 新增记录
  │   ├─ updateRecords = new ArrayList<>()   // 更新记录
  │   └─ failures = new ArrayList<>()        // 失败记录
  │
  ├─ 2. 批量查询数据库
  │   ├─ getExistingEmployeeIds(excelList)
  │   │   └─ 返回: Set<Long> existingIds     // 数据库中已存在的柜员号
  │   │
  │   └─ getExistingIdCards(excelList)
  │       └─ 返回: Set<String> 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. 空值处理

// 柜员号空值检查
if (excel.getEmployeeId() != null) {
    processedEmployeeIds.add(excel.getEmployeeId());
}

// 身份证号空值检查
if (StringUtils.isNotEmpty(excel.getIdCard())) {
    processedIdCards.add(excel.getIdCard());
}

原因:

  • 防止空指针异常
  • 确保只有有效的柜员号和身份证号才会被检查重复

4. 批量查询优化

// 批量查询柜员号
Set<Long> existingIds = getExistingEmployeeIds(excelList);

// 批量查询身份证号
Set<String> existingIdCards = getExistingIdCards(excelList);

优点:

  • 一次性查询所有需要的数据
  • 避免逐条查询导致的N+1问题
  • 使用HashSet实现O(1)复杂度的查找

错误消息说明

1. 柜员号在数据库中已存在

"柜员号已存在且未启用更新支持"

2. 柜员号在Excel内重复

String.format("柜员号[%d]在导入文件中重复,已跳过此条记录", excel.getEmployeeId())

示例: "柜员号[1001]在导入文件中重复,已跳过此条记录"

3. 身份证号在Excel内重复

String.format("身份证号[%s]在导入文件中重复,已跳过此条记录", excel.getIdCard())

示例: "身份证号[110101199001011234]在导入文件中重复,已跳过此条记录"

validateEmployeeData方法说明

方法签名

public void validateEmployeeData(CcdiEmployeeAddDTO addDTO,
                                 Boolean isUpdateSupport,
                                 Set<Long> existingIds,
                                 Set<String> existingIdCards)

验证流程

1. 验证必填字段
   ├─ 姓名不能为空
   ├─ 柜员号不能为空
   ├─ 所属部门不能为空
   ├─ 身份证号不能为空
   ├─ 电话不能为空
   └─ 状态不能为空

2. 验证身份证号格式
   └─ IdCardUtil.getErrorMessage(addDTO.getIdCard())

3. 验证唯一性
   ├─ IF existingIds == null (单条新增场景)
   │   ├─ 检查柜员号唯一性(数据库查询)
   │   └─ 检查身份证号唯一性(数据库查询)
   │
   └─ ELSE (导入场景)
       ├─ IF 柜员号不存在于数据库
       │   └─ 检查身份证号唯一性(使用批量查询结果)
       └─ ELSE (柜员号已存在,允许更新)
           └─ 跳过身份证号检查(更新模式下不检查身份证号重复)

4. 验证状态
   └─ 状态只能填写'0'(在职)或'1'(离职)

导入场景的身份证号唯一性检查优化

// 导入场景:如果柜员号不存在,才检查身份证号唯一性
if (!existingIds.contains(addDTO.getEmployeeId())) {
    // 使用批量查询的结果检查身份证号唯一性
    if (existingIdCards != null && existingIdCards.contains(addDTO.getIdCard())) {
        throw new RuntimeException("该身份证号已存在");
    }
}

优化点:

  • 使用批量查询结果existingIdCards,避免逐条查询数据库
  • 只在柜员号不存在时才检查身份证号(因为柜员号存在时是更新模式)

批量查询方法说明

getExistingEmployeeIds

private Set<Long> getExistingEmployeeIds(List<CcdiEmployeeExcel> excelList) {
    List<Long> employeeIds = excelList.stream()
            .map(CcdiEmployeeExcel::getEmployeeId)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());

    if (employeeIds.isEmpty()) {
        return Collections.emptySet();
    }

    List<CcdiEmployee> existingEmployees = employeeMapper.selectBatchIds(employeeIds);
    return existingEmployees.stream()
            .map(CcdiEmployee::getEmployeeId)
            .collect(Collectors.toSet());
}

getExistingIdCards

private Set<String> getExistingIdCards(List<CcdiEmployeeExcel> excelList) {
    List<String> idCards = excelList.stream()
            .map(CcdiEmployeeExcel::getIdCard)
            .filter(StringUtils::isNotEmpty)
            .collect(Collectors.toList());

    if (idCards.isEmpty()) {
        return Collections.emptySet();
    }

    LambdaQueryWrapper<CcdiEmployee> wrapper = new LambdaQueryWrapper<>();
    wrapper.in(CcdiEmployee::getIdCard, idCards);
    List<CcdiEmployee> 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进行数据操作