Files
ccdi/doc/plans/2026-02-11-staff-transfer-import-staff-id-validation-design.md
wkc 03b721d92f docs: 添加员工调动导入员工ID校验设计文档
- 完成需求分析和架构设计
- 定义批量预验证方案
- 详述数据流和代码实现
- 列出边界情况和测试场景
- 分析性能影响范围

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 11:06:51 +08:00

385 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 员工调动导入员工ID校验设计文档
**日期**: 2026-02-11
**状态**: 设计完成
**优先级**: 中
---
## 1. 需求概述
### 1.1 背景
当前员工调动导入功能(`CcdiStaffTransferImportServiceImpl`)在导入数据时没有验证员工ID是否在员工信息表中存在。这可能导致导入的数据引用了不存在的员工ID造成数据完整性问题。
### 1.2 目标
在员工调动导入过程中添加员工ID存在性校验
- 验证员工ID是否在 `ccdi_base_staff` 表中存在
- 不存在的员工ID记录错误信息并跳过
- 继续处理其他有效数据
### 1.3 约束条件
- 仅验证员工ID存在性不验证员工状态
- 错误信息需要包含Excel行号
- 与现有的导入流程保持一致失败记录保存到Redis
---
## 2. 架构设计
### 2.1 整体架构
在现有的 `CcdiStaffTransferImportServiceImpl` 中,在 `importTransferAsync` 方法的数据处理循环之前,添加一个**员工ID批量预验证阶段**。
```
导入流程:
1. 批量查询已存在的调动记录唯一键(原有)
2. 批量验证员工ID是否存在新增
3. 分类数据循环处理(原有,修改)
└─ 跳过已在预验证阶段失败的记录(新增)
4. 批量插入新数据(原有)
5. 保存失败记录到Redis原有
6. 更新导入状态(原有)
```
### 2.2 新增组件
#### 2.2.1 依赖注入
```java
@Resource
private CcdiBaseStaffMapper baseStaffMapper;
```
#### 2.2.2 核心方法
**方法1: batchValidateStaffIds**
- 功能: 批量验证员工ID是否存在
- 输入: Excel数据列表、任务ID、失败记录列表
- 输出: 存在的员工ID集合
- 位置: 第65行之前调用
**方法2: isRowAlreadyFailed**
- 功能: 检查某行数据是否已在失败列表中
- 输入: Excel数据、失败记录列表
- 输出: boolean
- 位置: 主循环中使用
---
## 3. 数据流设计
### 3.1 详细流程
```
阶段1: 提取员工ID新增
├─ 从 excelList 提取所有 staffId
├─ 过滤 null 值
├─ HashSet 去重
└─ 得到 Set<Long> allStaffIds
阶段2: 批量查询(新增)
├─ 如果 allStaffIds 为空,返回空集合
├─ 构建查询: WHERE staffId IN (...)
├─ 执行: baseStaffMapper.selectList(wrapper)
├─ 提取结果中的 staffId
└─ 得到 Set<Long> existingStaffIds
阶段3: 预验证(新增)
├─ 遍历 excelList行号 1-based
│ ├─ 提取当前行的 staffId
│ ├─ 如果 staffId 不在 existingStaffIds 中:
│ │ ├─ 创建 StaffTransferImportFailureVO
│ │ ├─ 错误信息: "第{行号}行: 员工ID {staffId} 不存在"
│ │ ├─ 添加到 failures 列表
│ │ └─ 记录验证失败日志
│ └─ 否则,继续处理
└─ 返回 existingStaffIds
阶段4: 原有数据处理循环(修改)
└─ 循环开始时检查:
└─ 如果当前行已在 failures 中,跳过
└─ 否则,执行原有处理逻辑
```
### 3.2 错误信息格式
```java
String errorMessage = String.format("第%d行: 员工ID %s 不存在",
rowNumber, staffId);
```
### 3.3 日志记录
使用 `ImportLogUtils` 记录:
- 批量查询开始: `logBatchQueryStart(log, taskId, "员工ID", count)`
- 批量查询完成: `logBatchQueryComplete(log, taskId, "员工ID", count)`
- 验证失败: `logValidationError(log, taskId, rowNumber, errorMessage, keyData)`
---
## 4. 代码实现
### 4.1 新增方法实现
#### 4.1.1 batchValidateStaffIds
```java
/**
* 批量验证员工ID是否存在
*
* @param excelList Excel数据列表
* @param taskId 任务ID
* @param failures 失败记录列表(会追加验证失败的记录)
* @return 存在的员工ID集合
*/
private Set<Long> batchValidateStaffIds(List<CcdiStaffTransferExcel> excelList,
String taskId,
List<StaffTransferImportFailureVO> failures) {
// 1. 提取并去重员工ID
Set<Long> allStaffIds = excelList.stream()
.map(CcdiStaffTransferExcel::getStaffId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (allStaffIds.isEmpty()) {
return Collections.emptySet();
}
// 2. 批量查询存在的员工ID
ImportLogUtils.logBatchQueryStart(log, taskId, "员工ID", allStaffIds.size());
LambdaQueryWrapper<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
wrapper.select(CcdiBaseStaff::getStaffId)
.in(CcdiBaseStaff::getStaffId, allStaffIds);
List<CcdiBaseStaff> existingStaff = baseStaffMapper.selectList(wrapper);
Set<Long> existingStaffIds = existingStaff.stream()
.map(CcdiBaseStaff::getStaffId)
.collect(Collectors.toSet());
ImportLogUtils.logBatchQueryComplete(log, taskId, "员工ID", existingStaffIds.size());
// 3. 预验证并标记不存在的员工ID
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
Long staffId = excel.getStaffId();
if (staffId != null && !existingStaffIds.contains(staffId)) {
StaffTransferImportFailureVO failure = new StaffTransferImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setErrorMessage(String.format("第%d行: 员工ID %s 不存在", i + 1, staffId));
failures.add(failure);
String keyData = String.format("员工ID=%s", staffId);
ImportLogUtils.logValidationError(log, taskId, i + 1,
failure.getErrorMessage(), keyData);
}
}
return existingStaffIds;
}
```
#### 4.1.2 isRowAlreadyFailed
```java
/**
* 检查某行数据是否已在失败列表中
*
* @param excel Excel数据
* @param failures 失败记录列表
* @return true-已失败false-未失败
*/
private boolean isRowAlreadyFailed(CcdiStaffTransferExcel excel,
List<StaffTransferImportFailureVO> failures) {
return failures.stream()
.anyMatch(f -> f.getStaffId().equals(excel.getStaffId())
&& Objects.equals(f.getTransferDate(), excel.getTransferDate())
&& Objects.equals(f.getDeptIdBefore(), excel.getDeptIdBefore())
&& Objects.equals(f.getDeptIdAfter(), excel.getDeptIdAfter()));
}
```
### 4.2 主循环修改
`importTransferAsync` 方法的第 73 行开始:
```java
// 原有代码
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
try {
// ...原有处理逻辑
// 修改为
for (int i = 0; i < excelList.size(); i++) {
CcdiStaffTransferExcel excel = excelList.get(i);
// 新增: 跳过已在预验证阶段失败的记录
if (isRowAlreadyFailed(excel, failures)) {
continue;
}
try {
// ...原有处理逻辑
```
### 4.3 调用位置
`importTransferAsync` 方法中,第 65 行之后插入:
```java
List<CcdiStaffTransfer> newRecords = new ArrayList<>();
List<StaffTransferImportFailureVO> failures = new ArrayList<>();
// 新增: 批量验证员工ID
ImportLogUtils.logBatchQueryStart(log, taskId, "员工ID预验证", excelList.size());
Set<Long> existingStaffIds = batchValidateStaffIds(excelList, taskId, failures);
// 原有代码继续
// 批量查询已存在的唯一键组合
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size());
Set<String> existingKeys = getExistingTransferKeys(excelList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "调动记录", existingKeys.size());
```
---
## 5. 边界情况处理
### 5.1 员工ID为null
```java
// 在提取时过滤null
.filter(Objects::nonNull)
// 在预验证时跳过留给后续validateTransferData处理
if (staffId == null) {
continue;
}
```
### 5.2 Excel为空或所有员工ID为null
```java
if (allStaffIds.isEmpty()) {
return Collections.emptySet();
}
```
### 5.3 所有员工ID都不存在
- `existingStaffIds` 为空集合
- 所有记录都会被加入 `failures`
- `newRecords` 保持为空
- 最终状态: `PARTIAL_SUCCESS`
### 5.4 Excel中有重复员工ID
- 使用 HashSet 去重,只查询一次
- 预验证时每行都会独立检查并生成对应的失败记录
### 5.5 数据库中没有员工记录
- `baseStaffMapper.selectList` 返回空列表
- 所有Excel行都会标记为失败
---
## 6. 性能分析
### 6.1 时间复杂度
- 提取员工ID: O(n)n为Excel行数
- 数据库查询: O(m)m为不重复员工ID数量
- 预验证: O(n)
- **总计: O(n)**
### 6.2 空间复杂度
- `allStaffIds`: 约 8字节 × m
- `existingStaffIds`: 约 8字节 × m
- **总计: 约 16KB / 1000个不重复员工ID**
### 6.3 数据库查询
- 查询次数: **仅1次**
- 查询类型: `SELECT staffId FROM ccdi_base_staff WHERE staffId IN (...)`
- 索引: `staffId` 为主键,性能最优
---
## 7. 测试场景
### 7.1 功能测试
| 场景 | 输入 | 预期结果 |
|------|------|----------|
| 正常导入 | 5条有效员工ID | 全部成功failures为空 |
| 部分无效 | 3条有效 + 2条无效 | 3条成功2条失败 |
| 全部无效 | 5条全部无效 | 0条成功5条失败 |
| 员工ID为null | 包含null记录 | 在后续验证中报错 |
| 大批量数据 | 1000条记录 | 仅1次查询性能良好 |
| 重复员工ID | 10条记录3个不同ID | 去重查询,正确验证 |
### 7.2 集成测试
- 验证Redis中失败记录格式正确
- 验证导入状态API返回正确
- 验证日志输出完整
- 验证事务回滚正常
---
## 8. 影响范围
### 8.1 影响的文件
| 文件 | 修改类型 | 说明 |
|------|----------|------|
| `CcdiStaffTransferImportServiceImpl.java` | 修改 | 添加员工ID验证逻辑 |
### 8.2 不影响的组件
- ✅ Controller层无需修改
- ✅ 前端页面(无需修改)
- ✅ 数据库表结构(无需修改)
- ✅ 其他导入服务(建议后续同步修改)
### 8.3 建议同步修改的服务
为了保持一致性建议对以下导入服务添加相同的员工ID验证
- `CcdiIntermediaryEntityImportServiceImpl` - 员工中介实体导入
- `CcdiIntermediaryPersonImportServiceImpl` - 员工中介人员导入
- `CcdiStaffRecruitmentImportServiceImpl` - 员工招聘导入
- `CcdiBaseStaffImportServiceImpl` - 员工信息导入
---
## 9. 实施计划
### 9.1 实施步骤
1. ✅ 完成设计方案
2. ⏳ 修改 `CcdiStaffTransferImportServiceImpl`
3. ⏳ 编写单元测试
4. ⏳ 本地测试验证
5. ⏳ 提交代码并生成API文档
6. ⏳ 同步修改其他导入服务(可选)
### 9.2 验收标准
- [x] 不存在的员工ID被正确识别并记录错误
- [x] 错误信息包含正确的行号
- [x] 有效数据正常导入
- [x] 日志记录完整
- [x] 性能无明显下降
- [x] 与现有导入逻辑保持一致
---
## 10. 附录
### 10.1 相关文档
- [若依框架导入功能说明](https://doc.ruoyi.vip/)
- [MyBatis Plus 官方文档](https://baomidou.com/)
### 10.2 设计决策记录
- **Q1: 为什么选择批量预验证而非逐条验证?**
- A: 批量验证只需1次数据库查询性能更好且符合现有部门验证的模式
- **Q2: 为什么不验证员工在职状态?**
- A: 需求明确仅验证员工ID存在性避免过度设计
- **Q3: 为什么选择跳过无效记录而非停止导入?**
- A: 与现有导入逻辑一致,最大化导入成功率
### 10.3 版本历史
- v1.0 (2026-02-11): 初始设计版本