# 员工关系导入身份证号校验设计文档 **日期**: 2026-02-11 **状态**: 设计完成 **优先级**: 中 --- ## 1. 需求概述 ### 1.1 背景 当前员工实体关系和员工亲属关系的导入功能在导入数据时,没有验证员工身份证号是否在员工信息表中存在。这可能导致导入的数据引用了不存在的员工,造成数据完整性问题。 ### 1.2 目标 在员工实体关系和员工亲属关系的导入过程中,添加员工身份证号存在性校验: - 验证员工身份证号是否在 `ccdi_base_staff` 表中存在 - 不存在的身份证号记录错误信息并跳过 - 继续处理其他有效数据 ### 1.3 约束条件 - 仅验证员工身份证号(`person_id`)存在性,不验证关系人身份证号 - 不验证员工状态(在职/离职) - 错误信息需要包含Excel行号 - 与现有的导入流程保持一致(失败记录保存到Redis) ### 1.4 优化范围 同时优化员工调动导入的身份证号验证逻辑,从2次遍历优化为1次遍历。 --- ## 2. 架构设计 ### 2.1 整体架构 在两个导入服务中添加**员工身份证号批量预验证阶段**,流程如下: ``` 导入流程: 1. 批量查询已存在的身份证号(新增)⭐ 2. 数据处理循环(原有,修改) └─ 循环开始时检查身份证号是否存在(新增) 3. 批量插入新数据(原有) 4. 保存失败记录到Redis(原有) 5. 更新导入状态(原有) ``` ### 2.2 新增组件 #### 2.2.1 依赖注入 在三个导入服务中添加: ```java @Resource private CcdiBaseStaffMapper baseStaffMapper; ``` #### 2.2.2 核心逻辑 - **位置**: 在数据处理循环之前 - **功能**: 批量查询所有Excel中出现的身份证号,构建存在性集合 - **输入**: Excel数据列表、任务ID - **输出**: Set 存在的身份证号集合 ### 2.3 影响的服务 | 服务 | 文件 | 说明 | |------|------|------| | 员工实体关系导入 | `CcdiStaffEnterpriseRelationImportServiceImpl.java` | 添加身份证号验证 | | 员工亲属关系导入 | `CcdiStaffFmyRelationImportServiceImpl.java` | 添加身份证号验证 | | 员工调动导入 | `CcdiStaffTransferImportServiceImpl.java` | 优化为1次遍历 | --- ## 3. 数据流设计 ### 3.1 详细流程 ``` 阶段1: 提取身份证号 ├─ 从 excelList 提取所有 personId ├─ 过滤 null 值和空字符串 ├─ HashSet 去重 └─ 得到 Set allPersonIds 阶段2: 批量查询 ├─ 如果 allPersonIds 为空,返回空集合 ├─ 构建查询: SELECT id_card FROM ccdi_base_staff WHERE id_card IN (...) ├─ 执行: baseStaffMapper.selectList(wrapper) ├─ 提取结果中的 idCard └─ 得到 Set existingPersonIds 阶段3: 数据处理循环(原有循环增强) ├─ 遍历 excelList(行号 1-based,i为索引) │ ├─ 【新增】首先检查: personId 是否在 existingPersonIds 中? │ │ ├─ 如果不在: │ │ │ ├─ 创建 ImportFailureVO 对象 │ │ │ ├─ 错误信息: "第{i+1}行: 身份证号[{personId}]不存在于员工信息表中" │ │ │ ├─ 添加到 failures 列表 │ │ │ ├─ 记录验证失败日志 │ │ │ └─ continue(跳过后续处理) │ │ └─ 如果在:继续执行原有逻辑 │ ├─ 转换并验证数据(原有) │ ├─ 检查重复(原有) │ └─ 添加到 newRecords(原有) ``` ### 3.2 错误信息格式 ```java String errorMessage = String.format("第%d行: 身份证号[%s]不存在于员工信息表中", rowNumber, personId); ``` ### 3.3 日志记录 使用 `ImportLogUtils` 记录: - 批量查询开始: `logBatchQueryStart(log, taskId, "员工身份证号", count)` - 批量查询完成: `logBatchQueryComplete(log, taskId, "员工身份证号", count)` - 验证失败: `logValidationError(log, taskId, rowNumber, errorMessage, keyData)` --- ## 4. 边界情况处理 ### 4.1 personId 为 null 或空字符串 - 在提取阶段过滤掉: `.filter(StringUtils::isNotEmpty)` - 这些记录会在原有的 `validateRelationData` 方法中报错 ### 4.2 Excel 为空或所有 personId 为空 - `allPersonIds` 为空集合 - 直接返回空集合,跳过批量查询 - 所有记录会在后续验证中报错 ### 4.3 所有身份证号都不存在 - `existingPersonIds` 为空集合 - 所有记录都会在第一次检查时抛出异常 - `newRecords` 保持为空 - 最终状态: `PARTIAL_SUCCESS` ### 4.4 Excel 中有重复身份证号 - 使用 HashSet 去重,只查询一次 - 每行独立检查,如果不存在则各自生成失败记录 ### 4.5 数据库中没有员工记录 - `baseStaffMapper.selectList` 返回空列表 - 所有 Excel 行都会在检查时失败 ### 4.6 身份证号格式错误 - 先检查身份证号是否存在 - 如果不存在,直接报错"身份证号不存在" - 如果存在但格式错误,在后续的 `validateRelationData` 中会报错 --- ## 5. 性能分析 ### 5.1 时间复杂度 - 提取身份证号: O(n),n为Excel行数 - 数据库查询: O(m),m为不重复身份证号数量 - 数据处理循环: O(n) - **总计: O(n)**,线性复杂度 ### 5.2 空间复杂度 - `allPersonIds`: 约 20字节 × m - `existingPersonIds`: 约 20字节 × m - **总计: 约 40KB / 1000个不重复身份证号** ### 5.3 数据库查询 - 查询次数: **仅1次** - 查询类型: `SELECT id_card FROM ccdi_base_staff WHERE id_card IN (...)` - 索引: `id_card` 字段需要添加索引 ### 5.4 性能对比 | 方案 | 数据库查询次数 | 1000行耗时 | 10000行耗时 | |------|---------------|-----------|------------| | 批量预验证(本设计) | 1次 | ~50ms | ~200ms | | 逐条验证 | N次 | ~5000ms | ~50000ms | **结论**: 批量预验证方案性能提升约**100倍**。 --- ## 6. 代码实现 ### 6.1 员工实体关系导入服务 **文件**: `CcdiStaffEnterpriseRelationImportServiceImpl.java` #### 6.1.1 添加依赖注入(第44行后) ```java @Resource private CcdiBaseStaffMapper baseStaffMapper; ``` #### 6.1.2 在 importRelationAsync 方法中(第55行后),添加批量查询: ```java // 【新增】批量验证员工身份证号是否存在 Set excelPersonIds = excelList.stream() .map(CcdiStaffEnterpriseRelationExcel::getPersonId) .filter(StringUtils::isNotEmpty) .collect(Collectors.toSet()); Set existingPersonIds = new HashSet<>(); if (!excelPersonIds.isEmpty()) { ImportLogUtils.logBatchQueryStart(log, taskId, "员工身份证号", excelPersonIds.size()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.select(CcdiBaseStaff::getIdCard) .in(CcdiBaseStaff::getIdCard, excelPersonIds); List existingStaff = baseStaffMapper.selectList(wrapper); existingPersonIds = existingStaff.stream() .map(CcdiBaseStaff::getIdCard) .collect(Collectors.toSet()); ImportLogUtils.logBatchQueryComplete(log, taskId, "员工身份证号", existingPersonIds.size()); } ``` #### 6.1.3 在数据处理循环开始处(第71行后),添加检查: ```java try { // 【新增】身份证号存在性检查 if (!existingPersonIds.contains(excel.getPersonId())) { throw new RuntimeException(String.format( "第%d行: 身份证号[%s]不存在于员工信息表中", i + 1, excel.getPersonId())); } // 原有逻辑继续... CcdiStaffEnterpriseRelationAddDTO addDTO = new CcdiStaffEnterpriseRelationAddDTO(); BeanUtils.copyProperties(excel, addDTO); // ... } ``` ### 6.2 员工亲属关系导入服务 **文件**: `CcdiStaffFmyRelationImportServiceImpl.java` 相同的修改步骤: 1. 添加 `CcdiBaseStaffMapper` 依赖注入 2. 在第57行后添加批量查询身份证号逻辑 3. 在第96行后(数据处理循环开始处)添加身份证号检查 ### 6.3 员工调动导入服务优化 **文件**: `CcdiStaffTransferImportServiceImpl.java` **优化前**: 2次遍历(预验证 + 主循环) **优化后**: 1次遍历(主循环中检查) **修改步骤**: 1. 移除 `batchValidateStaffIds` 方法 2. 移除 `isRowAlreadyFailed` 方法 3. 在主循环开始处添加员工ID存在性检查 4. 使用 `existingStaffIds` 替代原有的预验证逻辑 ### 6.4 需要导入的类 ```java import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.ccdi.mapper.CcdiBaseStaffMapper; import com.ruoyi.ccdi.domain.CcdiBaseStaff; import java.util.HashSet; import java.util.stream.Collectors; ``` --- ## 7. 测试场景 ### 7.1 功能测试用例 | 场景 | 输入 | 预期结果 | |------|------|----------| | 正常导入 | 5条有效身份证号 | 全部成功,failures为空 | | 部分无效 | 3条有效 + 2条无效身份证号 | 3条成功,2条失败 | | 全部无效 | 5条全部无效身份证号 | 0条成功,5条失败 | | 身份证号为null | 包含null或空字符串 | 在后续验证中报错 | | 大批量数据 | 1000条记录,全部有效 | 仅1次查询,全部成功 | | 重复身份证号 | 10条记录,3个不同身份证号 | 去重查询,正确验证 | | 混合场景 | 有效、无效、null、重复 | 各自正确处理 | ### 7.2 员工实体关系导入专项测试 ``` 测试数据1: 有效身份证号 personId: "110101199001011234" (存在于ccdi_base_staff) 预期: 导入成功 测试数据2: 无效身份证号 personId: "999999999999999999" (不存在于ccdi_base_staff) 预期: 导入失败,错误信息: "第N行: 身份证号[xxx]不存在于员工信息表中" ``` ### 7.3 员工亲属关系导入专项测试 ``` 测试数据1: 有效员工身份证号 personId: "110101199001011234" (存在) relationCertNo: "110101199001011235" (可以不存在) 预期: 导入成功 测试数据2: 无效员工身份证号 personId: "999999999999999999" (不存在) 预期: 导入失败 ``` ### 7.4 性能测试 | 数据量 | 预期查询次数 | 预期耗时 | 内存占用 | |--------|------------|---------|---------| | 100条 | 1次 | < 20ms | < 10KB | | 1000条 | 1次 | < 100ms | < 50KB | | 10000条 | 1次 | < 500ms | < 500KB | --- ## 8. 影响范围和实施计划 ### 8.1 影响的文件 | 文件 | 修改类型 | 说明 | |------|----------|------| | `CcdiStaffEnterpriseRelationImportServiceImpl.java` | 修改 | 添加员工身份证号验证 | | `CcdiStaffFmyRelationImportServiceImpl.java` | 修改 | 添加员工身份证号验证 | | `CcdiStaffTransferImportServiceImpl.java` | 优化 | 从2次遍历优化为1次遍历 | ### 8.2 不影响的组件 - ✅ Controller层(无需修改) - ✅ 前端页面(无需修改) - ✅ 数据库表结构(无需修改) - ✅ Mapper接口(无需修改) - ✅ VO/DTO/Excel类(无需修改) ### 8.3 数据库索引建议 ```sql -- 检查索引是否存在 SHOW INDEX FROM ccdi_base_staff WHERE Column_name = 'id_card'; -- 如果不存在,创建索引 CREATE INDEX idx_ccdi_base_staff_id_card ON ccdi_base_staff(id_card); ``` ### 8.4 实施步骤 1. ✅ 完成设计方案 2. ⏳ 修改 `CcdiStaffEnterpriseRelationImportServiceImpl` 3. ⏳ 修改 `CcdiStaffFmyRelationImportServiceImpl` 4. ⏳ 优化 `CcdiStaffTransferImportServiceImpl` 5. ⏳ 检查并创建数据库索引(如需要) 6. ⏳ 编写单元测试 7. ⏳ 本地测试验证 8. ⏳ 更新API文档(如需要) ### 8.5 验收标准 - [ ] 不存在的员工身份证号被正确识别并记录错误 - [ ] 错误信息包含正确的行号和身份证号 - [ ] 有效数据正常导入 - [ ] 日志记录完整 - [ ] 性能无明显下降(批量查询仅1次) - [ ] 与现有导入逻辑保持一致 - [ ] 三个导入服务功能一致 ### 8.6 风险评估 - **风险等级**: 低 - **影响范围**: 仅影响导入功能,不影响其他模块 - **回滚方案**: 直接移除新增的验证代码即可 - **数据安全**: 只读查询,无数据风险 --- ## 9. 设计总结 ### 9.1 核心优势 1. **性能优异**: 批量查询,仅1次数据库访问 2. **代码简洁**: 仅1次遍历,逻辑清晰 3. **一致性高**: 三个导入服务使用相同的设计模式 4. **易于维护**: 遵循现有框架规范 ### 9.2 与原员工调动导入设计的对比 | 对比项 | 原设计 | 新设计 | |--------|--------|--------| | 遍历次数 | 2次 | **1次** ⭐ | | 代码复杂度 | 需要额外方法 | 更简洁 | | 性能 | 优秀 | **更优** | | 可维护性 | 良好 | **更好** | ### 9.3 设计亮点 - ✅ **批量预验证**: 充分利用数据库 IN 查询性能 - ✅ **单次遍历**: 减少不必要的循环,代码更清晰 - ✅ **统一模式**: 三个导入服务使用一致的验证逻辑 - ✅ **错误友好**: 详细的错误信息包含行号 - ✅ **性能监控**: 完整的日志记录便于排查问题 --- ## 10. 附录 ### 10.1 相关文档 - [员工调动导入员工ID校验设计文档](2026-02-11-staff-transfer-import-staff-id-validation-design.md) - [若依框架导入功能说明](https://doc.ruoyi.vip/) - [MyBatis Plus 官方文档](https://baomidou.com/) ### 10.2 设计决策记录 **Q1: 为什么选择批量预验证而非逐条验证?** A: 批量验证只需1次数据库查询,性能提升约100倍。 **Q2: 为什么优化为1次遍历?** A: 减少不必要的循环,代码更简洁,性能更好。 **Q3: 为什么不验证员工在职状态?** A: 需求明确仅验证身份证号存在性,避免过度设计。 **Q4: 为什么不验证关系人身份证号?** A: 关系人可能不是系统员工,验证会限制使用场景。 ### 10.3 版本历史 - v1.0 (2026-02-11): 初始设计版本,包含三个导入服务的身份证号校验