2026-02-09 09:10:35 +08:00
|
|
|
# 员工导入Excel内双字段重复检测 - 代码流程说明
|
|
|
|
|
|
|
|
|
|
## 方法签名
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
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. 重复检测优先级
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```
|
|
|
|
|
数据库柜员号重复 > Excel内柜员号重复 > Excel内身份证号重复
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**原因**:
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
- 数据库检查优先: 避免处理已经存在且不允许更新的数据
|
|
|
|
|
- Excel内柜员号检查: 柜员号是主键,优先检查
|
|
|
|
|
- Excel内身份证号检查: 身份证号也需要唯一性
|
|
|
|
|
|
|
|
|
|
### 2. 标记时机
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```
|
|
|
|
|
只在记录成功添加到newRecords后才标记为已处理
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**原因**:
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
- 避免将验证失败的记录标记为已处理
|
|
|
|
|
- 确保只有成功插入数据库的记录才会占用柜员号和身份证号
|
|
|
|
|
- 防止因前一条记录失败导致后一条有效记录被误判为重复
|
|
|
|
|
|
|
|
|
|
### 3. 空值处理
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
// 柜员号空值检查
|
|
|
|
|
if (excel.getEmployeeId() != null) {
|
|
|
|
|
processedEmployeeIds.add(excel.getEmployeeId());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 身份证号空值检查
|
|
|
|
|
if (StringUtils.isNotEmpty(excel.getIdCard())) {
|
|
|
|
|
processedIdCards.add(excel.getIdCard());
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**原因**:
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
- 防止空指针异常
|
|
|
|
|
- 确保只有有效的柜员号和身份证号才会被检查重复
|
|
|
|
|
|
|
|
|
|
### 4. 批量查询优化
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
// 批量查询柜员号
|
|
|
|
|
Set<Long> existingIds = getExistingEmployeeIds(excelList);
|
|
|
|
|
|
|
|
|
|
// 批量查询身份证号
|
|
|
|
|
Set<String> existingIdCards = getExistingIdCards(excelList);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**优点**:
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
- 一次性查询所有需要的数据
|
|
|
|
|
- 避免逐条查询导致的N+1问题
|
|
|
|
|
- 使用HashSet实现O(1)复杂度的查找
|
|
|
|
|
|
|
|
|
|
## 错误消息说明
|
|
|
|
|
|
|
|
|
|
### 1. 柜员号在数据库中已存在
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
"柜员号已存在且未启用更新支持"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. 柜员号在Excel内重复
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
String.format("柜员号[%d]在导入文件中重复,已跳过此条记录", excel.getEmployeeId())
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**示例**: "柜员号[1001]在导入文件中重复,已跳过此条记录"
|
|
|
|
|
|
|
|
|
|
### 3. 身份证号在Excel内重复
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
String.format("身份证号[%s]在导入文件中重复,已跳过此条记录", excel.getIdCard())
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**示例**: "身份证号[110101199001011234]在导入文件中重复,已跳过此条记录"
|
|
|
|
|
|
|
|
|
|
## validateEmployeeData方法说明
|
|
|
|
|
|
|
|
|
|
### 方法签名
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
public void validateEmployeeData(CcdiEmployeeAddDTO addDTO,
|
|
|
|
|
Boolean isUpdateSupport,
|
|
|
|
|
Set<Long> existingIds,
|
|
|
|
|
Set<String> existingIdCards)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 验证流程
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```
|
|
|
|
|
1. 验证必填字段
|
|
|
|
|
├─ 姓名不能为空
|
|
|
|
|
├─ 柜员号不能为空
|
|
|
|
|
├─ 所属部门不能为空
|
|
|
|
|
├─ 身份证号不能为空
|
|
|
|
|
├─ 电话不能为空
|
|
|
|
|
└─ 状态不能为空
|
|
|
|
|
|
|
|
|
|
2. 验证身份证号格式
|
|
|
|
|
└─ IdCardUtil.getErrorMessage(addDTO.getIdCard())
|
|
|
|
|
|
|
|
|
|
3. 验证唯一性
|
|
|
|
|
├─ IF existingIds == null (单条新增场景)
|
|
|
|
|
│ ├─ 检查柜员号唯一性(数据库查询)
|
|
|
|
|
│ └─ 检查身份证号唯一性(数据库查询)
|
|
|
|
|
│
|
|
|
|
|
└─ ELSE (导入场景)
|
|
|
|
|
├─ IF 柜员号不存在于数据库
|
|
|
|
|
│ └─ 检查身份证号唯一性(使用批量查询结果)
|
|
|
|
|
└─ ELSE (柜员号已存在,允许更新)
|
|
|
|
|
└─ 跳过身份证号检查(更新模式下不检查身份证号重复)
|
|
|
|
|
|
|
|
|
|
4. 验证状态
|
|
|
|
|
└─ 状态只能填写'0'(在职)或'1'(离职)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 导入场景的身份证号唯一性检查优化
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
// 导入场景:如果柜员号不存在,才检查身份证号唯一性
|
|
|
|
|
if (!existingIds.contains(addDTO.getEmployeeId())) {
|
|
|
|
|
// 使用批量查询的结果检查身份证号唯一性
|
|
|
|
|
if (existingIdCards != null && existingIdCards.contains(addDTO.getIdCard())) {
|
|
|
|
|
throw new RuntimeException("该身份证号已存在");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**优化点**:
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
- 使用批量查询结果`existingIdCards`,避免逐条查询数据库
|
|
|
|
|
- 只在柜员号不存在时才检查身份证号(因为柜员号存在时是更新模式)
|
|
|
|
|
|
|
|
|
|
## 批量查询方法说明
|
|
|
|
|
|
|
|
|
|
### getExistingEmployeeIds
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
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
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
```java
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**特点**:
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
- 使用Stream API进行数据提取和过滤
|
|
|
|
|
- 过滤空值,避免无效查询
|
|
|
|
|
- 使用MyBatis Plus的批量查询方法
|
|
|
|
|
- 返回Set集合,实现O(1)复杂度的查找
|
|
|
|
|
|
|
|
|
|
## 性能分析
|
|
|
|
|
|
|
|
|
|
### 时间复杂度
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
- 批量查询: O(n), n为Excel记录数
|
|
|
|
|
- 重复检测: O(1), 使用HashSet
|
|
|
|
|
- 总体复杂度: O(n)
|
|
|
|
|
|
|
|
|
|
### 空间复杂度
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
- existingIds: O(m), m为数据库中已存在的柜员号数量
|
|
|
|
|
- existingIdCards: O(k), k为数据库中已存在的身份证号数量
|
|
|
|
|
- processedEmployeeIds: O(n), n为Excel记录数
|
|
|
|
|
- processedIdCards: O(n), n为Excel记录数
|
|
|
|
|
- 总体空间复杂度: O(m + k + n)
|
|
|
|
|
|
|
|
|
|
### 数据库查询次数
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
- 修改前: 1次(批量查询柜员号) + n次(逐条查询身份证号) = O(n)
|
|
|
|
|
- 修改后: 2次(批量查询柜员号 + 批量查询身份证号) = O(1)
|
|
|
|
|
|
|
|
|
|
**性能提升**: 减少n-1次数据库查询
|
|
|
|
|
|
|
|
|
|
## 总结
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
本实现通过以下技术手段实现了Excel内双字段重复检测:
|
2026-03-03 16:14:16 +08:00
|
|
|
|
2026-02-09 09:10:35 +08:00
|
|
|
1. 批量查询优化,减少数据库访问
|
|
|
|
|
2. 使用HashSet进行O(1)复杂度的重复检测
|
|
|
|
|
3. 合理的检查顺序和标记时机
|
|
|
|
|
4. 完善的空值处理和错误提示
|
|
|
|
|
5. 遵循若依框架编码规范,使用MyBatis Plus进行数据操作
|