# 员工调动导入员工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<Long> allStaffIds

阶段2: 批量查询（新增）
├─ 如果 allStaffIds 为空，返回空集合
├─ 构建查询: WHERE staffId IN (...)
├─ 执行: baseStaffMapper.selectList(wrapper)
├─ 提取结果中的 staffId
└─ 得到 Set<Long> 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<Long> batchValidateStaffIds(List<CcdiStaffTransferExcel> excelList,
                                        String taskId,
                                        List<StaffTransferImportFailureVO> failures) {
    // 1. 提取并去重员工ID
    Set<Long> 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<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
    wrapper.select(CcdiBaseStaff::getStaffId)
           .in(CcdiBaseStaff::getStaffId, allStaffIds);

    List<CcdiBaseStaff> existingStaff = baseStaffMapper.selectList(wrapper);
    Set<Long> 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<StaffTransferImportFailureVO> 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<CcdiStaffTransfer> newRecords = new ArrayList<>();
List<StaffTransferImportFailureVO> failures = new ArrayList<>();

// 新增: 批量验证员工ID
ImportLogUtils.logBatchQueryStart(log, taskId, "员工ID预验证", excelList.size());
Set<Long> existingStaffIds = batchValidateStaffIds(excelList, taskId, failures);

// 原有代码继续
// 批量查询已存在的唯一键组合
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size());
Set<String> 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): 初始设计版本
