- 完成需求分析和架构设计 - 定义批量预验证方案 - 详述数据流和代码实现 - 列出边界情况和测试场景 - 分析性能影响范围 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
11 KiB
11 KiB
员工调动导入员工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 依赖注入
@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 错误信息格式
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
/**
* 批量验证员工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
/**
* 检查某行数据是否已在失败列表中
*
* @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 行开始:
// 原有代码
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 行之后插入:
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
// 在提取时过滤null
.filter(Objects::nonNull)
// 在预验证时跳过,留给后续validateTransferData处理
if (staffId == null) {
continue;
}
5.2 Excel为空或所有员工ID为null
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字节 × mexistingStaffIds: 约 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 实施步骤
- ✅ 完成设计方案
- ⏳ 修改
CcdiStaffTransferImportServiceImpl - ⏳ 编写单元测试
- ⏳ 本地测试验证
- ⏳ 提交代码并生成API文档
- ⏳ 同步修改其他导入服务(可选)
9.2 验收标准
- 不存在的员工ID被正确识别并记录错误
- 错误信息包含正确的行号
- 有效数据正常导入
- 日志记录完整
- 性能无明显下降
- 与现有导入逻辑保持一致
10. 附录
10.1 相关文档
10.2 设计决策记录
-
Q1: 为什么选择批量预验证而非逐条验证?
- A: 批量验证只需1次数据库查询,性能更好,且符合现有部门验证的模式
-
Q2: 为什么不验证员工在职状态?
- A: 需求明确仅验证员工ID存在性,避免过度设计
-
Q3: 为什么选择跳过无效记录而非停止导入?
- A: 与现有导入逻辑一致,最大化导入成功率
10.3 版本历史
- v1.0 (2026-02-11): 初始设计版本