Files
ccdi/doc/plans/2026-02-11-staff-relation-import-person-id-validation-design.md
wkc fd9e208fa3 docs(staff-enterprise-relation): 更新API文档,添加员工姓名字段说明
- 新增员工实体关系管理API文档
- 在列表接口和详情接口响应中添加personName字段
- 说明personName通过LEFT JOIN ccdi_base_staff表获取
- 如果personId在员工信息表中不存在,personName为null
2026-02-11 15:27:40 +08:00

14 KiB
Raw Blame History

员工关系导入身份证号校验设计文档

日期: 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 依赖注入

在三个导入服务中添加:

@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<String> allPersonIds

阶段2: 批量查询
├─ 如果 allPersonIds 为空,返回空集合
├─ 构建查询: SELECT id_card FROM ccdi_base_staff WHERE id_card IN (...)
├─ 执行: baseStaffMapper.selectList(wrapper)
├─ 提取结果中的 idCard
└─ 得到 Set<String> existingPersonIds

阶段3: 数据处理循环(原有循环增强)
├─ 遍历 excelList行号 1-basedi为索引
│   ├─ 【新增】首先检查: personId 是否在 existingPersonIds 中?
│   │   ├─ 如果不在:
│   │   │   ├─ 创建 ImportFailureVO 对象
│   │   │   ├─ 错误信息: "第{i+1}行: 身份证号[{personId}]不存在于员工信息表中"
│   │   │   ├─ 添加到 failures 列表
│   │   │   ├─ 记录验证失败日志
│   │   │   └─ continue跳过后续处理
│   │   └─ 如果在:继续执行原有逻辑
│   ├─ 转换并验证数据(原有)
│   ├─ 检查重复(原有)
│   └─ 添加到 newRecords原有

3.2 错误信息格式

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行后

@Resource
private CcdiBaseStaffMapper baseStaffMapper;

6.1.2 在 importRelationAsync 方法中第55行后添加批量查询:

// 【新增】批量验证员工身份证号是否存在
Set<String> excelPersonIds = excelList.stream()
        .map(CcdiStaffEnterpriseRelationExcel::getPersonId)
        .filter(StringUtils::isNotEmpty)
        .collect(Collectors.toSet());

Set<String> existingPersonIds = new HashSet<>();
if (!excelPersonIds.isEmpty()) {
    ImportLogUtils.logBatchQueryStart(log, taskId, "员工身份证号", excelPersonIds.size());

    LambdaQueryWrapper<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
    wrapper.select(CcdiBaseStaff::getIdCard)
           .in(CcdiBaseStaff::getIdCard, excelPersonIds);

    List<CcdiBaseStaff> existingStaff = baseStaffMapper.selectList(wrapper);
    existingPersonIds = existingStaff.stream()
            .map(CcdiBaseStaff::getIdCard)
            .collect(Collectors.toSet());

    ImportLogUtils.logBatchQueryComplete(log, taskId, "员工身份证号", existingPersonIds.size());
}

6.1.3 在数据处理循环开始处第71行后添加检查:

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 需要导入的类

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 数据库索引建议

-- 检查索引是否存在
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 相关文档

10.2 设计决策记录

Q1: 为什么选择批量预验证而非逐条验证? A: 批量验证只需1次数据库查询性能提升约100倍。

Q2: 为什么优化为1次遍历 A: 减少不必要的循环,代码更简洁,性能更好。

Q3: 为什么不验证员工在职状态? A: 需求明确仅验证身份证号存在性,避免过度设计。

Q4: 为什么不验证关系人身份证号? A: 关系人可能不是系统员工,验证会限制使用场景。

10.3 版本历史

  • v1.0 (2026-02-11): 初始设计版本,包含三个导入服务的身份证号校验