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

**日期**: 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<String> 存在的身份证号集合

### 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-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<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行后），添加检查:

```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): 初始设计版本，包含三个导入服务的身份证号校验
