# 员工调动导入员工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): 初始设计版本