diff --git a/doc/plans/2026-02-11-staff-transfer-import-staff-id-validation-design.md b/doc/plans/2026-02-11-staff-transfer-import-staff-id-validation-design.md new file mode 100644 index 0000000..2dfe693 --- /dev/null +++ b/doc/plans/2026-02-11-staff-transfer-import-staff-id-validation-design.md @@ -0,0 +1,384 @@ +# 员工调动导入员工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 allStaffIds + +阶段2: 批量查询(新增) +├─ 如果 allStaffIds 为空,返回空集合 +├─ 构建查询: WHERE staffId IN (...) +├─ 执行: baseStaffMapper.selectList(wrapper) +├─ 提取结果中的 staffId +└─ 得到 Set 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 batchValidateStaffIds(List excelList, + String taskId, + List failures) { + // 1. 提取并去重员工ID + Set 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.select(CcdiBaseStaff::getStaffId) + .in(CcdiBaseStaff::getStaffId, allStaffIds); + + List existingStaff = baseStaffMapper.selectList(wrapper); + Set 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 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 newRecords = new ArrayList<>(); +List failures = new ArrayList<>(); + +// 新增: 批量验证员工ID +ImportLogUtils.logBatchQueryStart(log, taskId, "员工ID预验证", excelList.size()); +Set existingStaffIds = batchValidateStaffIds(excelList, taskId, failures); + +// 原有代码继续 +// 批量查询已存在的唯一键组合 +ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size()); +Set 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): 初始设计版本