# 员工导入Excel内双字段重复检测功能实现报告

## 功能概述

为员工导入模块添加Excel内双字段(柜员号和身份证号)重复检测功能,防止同一Excel文件中出现重复数据导入到数据库。

## 实现时间

2026-02-09

## 实现位置

-
文件: `D:\ccdi\ccdi\ruoyi-info-collection\src\main\java\com\ruoyi\ccdi\service\impl\CcdiEmployeeImportServiceImpl.java`
- 方法: `importEmployeeAsync` (第43-126行)

## 核心功能

### 1. 批量查询已存在的身份证号

在数据分类前,批量查询数据库中已存在的身份证号:

```java
Set<Long> existingIds = getExistingEmployeeIds(excelList);
Set<String> existingIdCards = getExistingIdCards(excelList);
```

**优点**:

- 减少数据库查询次数,提高性能
- 避免逐条查询导致的N+1问题

### 2. 添加Excel内处理跟踪集合

```java
Set<Long> processedEmployeeIds = new HashSet<>();
Set<String> processedIdCards = new HashSet<>();
```

**作用**:

- 跟踪Excel文件中已处理的柜员号
- 跟踪Excel文件中已处理的身份证号
- 用于检测Excel内部的重复数据

### 3. 双字段重复检测逻辑

在逐条处理时,按以下顺序检查:

```java
if (existingIds.contains(excel.getEmployeeId())) {
    // 柜员号在数据库中已存在
    if (isUpdateSupport) {
        updateRecords.add(employee);
    } else {
        throw new RuntimeException("柜员号已存在且未启用更新支持");
    }
} else if (processedEmployeeIds.contains(excel.getEmployeeId())) {
    // 柜员号在Excel文件中重复
    throw new RuntimeException(String.format("柜员号[%d]在导入文件中重复,已跳过此条记录", excel.getEmployeeId()));
} else if (StringUtils.isNotEmpty(excel.getIdCard()) &&
           processedIdCards.contains(excel.getIdCard())) {
    // 身份证号在Excel文件中重复
    throw new RuntimeException(String.format("身份证号[%s]在导入文件中重复,已跳过此条记录", excel.getIdCard()));
} else {
    // 无重复,添加到新记录
    newRecords.add(employee);
    // 只在成功时标记
    if (excel.getEmployeeId() != null) {
        processedEmployeeIds.add(excel.getEmployeeId());
    }
    if (StringUtils.isNotEmpty(excel.getIdCard())) {
        processedIdCards.add(excel.getIdCard());
    }
}
```

**检查顺序**:

1. 先检查柜员号是否在数据库中存在
2. 再检查柜员号是否在Excel文件内重复
3. 最后检查身份证号是否在Excel文件内重复
4. 只在记录成功添加到newRecords后才标记为已处理

### 4. 更新validateEmployeeData方法

**修改前**:

```java
public void validateEmployeeData(CcdiEmployeeAddDTO addDTO, Boolean isUpdateSupport, Set<Long> existingIds)
```

**修改后**:

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

**身份证号唯一性检查优化**:

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

**优点**:

- 使用批量查询结果,避免逐条查询
- 提高导入性能

## 技术特点

### 1. 双字段同时检测

同时检测柜员号(Long类型)和身份证号(String类型)的Excel内重复

### 2. 检查顺序合理

- 先检查数据库重复(避免无效数据处理)
- 再检查Excel内重复(防止重复导入)
- 最后标记已处理(只在成功后标记)

### 3. 空值处理

使用`StringUtils.isNotEmpty`和`Objects::nonNull`进行空值检查,避免空指针异常

### 4. 错误消息明确

- 柜员号重复: "柜员号[XXX]在导入文件中重复,已跳过此条记录"
- 身份证号重复: "身份证号[XXX]在导入文件中重复,已跳过此条记录"

### 5. 性能优化

- 批量查询数据库中已存在的柜员号和身份证号
- 使用HashSet进行O(1)复杂度的重复检测
- 减少数据库查询次数

## 测试场景

### 场景1: 柜员号在Excel内重复

**输入**:

```
柜员号  姓名  身份证号
1001   张三  110101199001011234
1001   李四  110101199001011235
```

**期望结果**:

- 第一条记录成功导入
- 第二条记录失败,错误信息: "柜员号[1001]在导入文件中重复,已跳过此条记录"

### 场景2: 身份证号在Excel内重复

**输入**:

```
柜员号  姓名  身份证号
1001   张三  110101199001011234
1002   李四  110101199001011234
```

**期望结果**:

- 第一条记录成功导入
- 第二条记录失败,错误信息: "身份证号[110101199001011234]在导入文件中重复,已跳过此条记录"

### 场景3: 柜员号和身份证号同时重复

**输入**:

```
柜员号  姓名  身份证号
1001   张三  110101199001011234
1001   张三  110101199001011234
```

**期望结果**:

- 第一条记录成功导入
- 第二条记录失败,错误信息: "柜员号[1001]在导入文件中重复,已跳过此条记录"

### 场景4: 正常导入(无重复)

**输入**:

```
柜员号  姓名  身份证号
1001   张三  110101199001011234
1002   李四  110101199001011235
1003   王五  110101199001011236
```

**期望结果**:

- 所有记录都成功导入

## 代码对比

### 修改前

```java
// 批量查询已存在的柜员号
Set<Long> existingIds = getExistingEmployeeIds(excelList);

// 分类数据
for (int i = 0; i < excelList.size(); i++) {
    // ...
    validateEmployeeData(addDTO, isUpdateSupport, existingIds);

    if (existingIds.contains(excel.getEmployeeId())) {
        if (isUpdateSupport) {
            updateRecords.add(employee);
        } else {
            throw new RuntimeException("柜员号已存在且未启用更新支持");
        }
    } else {
        newRecords.add(employee);
    }
}
```

### 修改后

```java
// 批量查询已存在的柜员号和身份证号
Set<Long> existingIds = getExistingEmployeeIds(excelList);
Set<String> existingIdCards = getExistingIdCards(excelList);

// 用于跟踪Excel文件内已处理的主键
Set<Long> processedEmployeeIds = new HashSet<>();
Set<String> processedIdCards = new HashSet<>();

// 分类数据
for (int i = 0; i < excelList.size(); i++) {
    // ...
    validateEmployeeData(addDTO, isUpdateSupport, existingIds, existingIdCards);

    if (existingIds.contains(excel.getEmployeeId())) {
        if (isUpdateSupport) {
            updateRecords.add(employee);
        } else {
            throw new RuntimeException("柜员号已存在且未启用更新支持");
        }
    } else if (processedEmployeeIds.contains(excel.getEmployeeId())) {
        throw new RuntimeException(String.format("柜员号[%d]在导入文件中重复,已跳过此条记录", excel.getEmployeeId()));
    } else if (StringUtils.isNotEmpty(excel.getIdCard()) &&
               processedIdCards.contains(excel.getIdCard())) {
        throw new RuntimeException(String.format("身份证号[%s]在导入文件中重复,已跳过此条记录", excel.getIdCard()));
    } else {
        newRecords.add(employee);
        // 只在成功时标记
        if (excel.getEmployeeId() != null) {
            processedEmployeeIds.add(excel.getEmployeeId());
        }
        if (StringUtils.isNotEmpty(excel.getIdCard())) {
            processedIdCards.add(excel.getIdCard());
        }
    }
}
```

## 参考实现

本功能参考了中介人员导入模块的双字段重复检测实现:

- 文件: `CcdiIntermediaryEntityImportServiceImpl.java`
- 关键方法: `importEntityAsync`

## 编译验证

已通过Maven编译验证,无语法错误:

```bash
mvn clean compile -DskipTests
```

编译结果: BUILD SUCCESS

## 测试脚本

测试脚本位置: `D:\ccdi\ccdi\doc\test-scripts\test_employee_duplicate_detection.py`

## 总结

本次实现成功为员工导入模块添加了Excel内双字段重复检测功能,主要改进包括:

1. **批量查询优化**: 添加`getExistingIdCards`方法批量查询已存在的身份证号
2. **双字段检测**: 同时检测柜员号和身份证号的Excel内重复
3. **性能优化**: 使用批量查询减少数据库访问次数
4. **错误处理**: 提供明确的错误提示信息
5. **代码规范**: 遵循若依框架编码规范,使用MyBatis Plus进行数据操作

该功能可以有效防止Excel文件内部的重复数据导入到数据库,提高数据质量和导入可靠性。
