# 导入逻辑优化设计文档

## 文档信息

- **创建日期**：2026-02-05
- **版本**：1.0
- **作者**：Claude Code
- **状态**：待实施

---

## 1. 背景和目标

### 1.1 背景

当前系统中的导入功能采用"存在则更新，不存在则插入"的逻辑：

- 需要区分新增和更新两种操作
- 使用复杂的条件判断和数据分类逻辑
- 批量更新操作依赖特殊的 SQL 语法（CASE WHEN），容易出现语法错误
- 代码逻辑复杂，维护成本高

### 1.2 目标

优化导入逻辑，简化代码实现：

- 统一采用"先删除后插入"的策略
- 移除复杂的更新操作和条件判断
- 提高代码可维护性和可读性
- 保证数据一致性和事务完整性

---

## 2. 需求分析

### 2.1 功能需求

#### 核心需求

1. **导入策略变更**：将"存在则更新"改为"先删后插"
2. **删除范围**：只删除导入数据中已存在的记录
3. **唯一性判断**：使用业务唯一键判断记录是否存在
4. **审计字段**：重新插入的数据，所有审计字段使用当前时间
5. **冲突处理**：批量删除所有使用相同业务键的记录

#### 影响模块

- 员工信息管理（`ccdi_employee`）
- 中介库个人管理（`ccdi_biz_intermediary`）
- 中介库实体管理（`ccdi_enterprise_base_info`）
- 员工招聘信息管理（`ccdi_staff_recruitment`）

### 2.2 非功能需求

- **性能**：批量操作，2-3次数据库往返
- **事务性**：所有操作在同一事务中，保证原子性
- **兼容性**：前端调用方式保持不变

---

## 3. 设计方案

### 3.1 整体架构

新的导入逻辑采用三阶段流程：

#### 阶段 1：数据验证与收集

- 遍历所有导入数据，验证必填字段和数据格式
- 收集所有业务唯一键
- 检查导入数据内部的重复性
- 验证通过的数据放入待处理列表

#### 阶段 2：批量删除

- 根据收集的业务唯一键列表，执行批量删除操作
- SQL：`DELETE FROM table WHERE unique_key IN (...)`
- 删除所有匹配的旧记录，包括重复的记录

#### 阶段 3：批量插入

- 批量插入所有验证通过的数据
- SQL：`INSERT INTO table (...) VALUES (...), (...), ...`
- 所有审计字段使用当前时间

### 3.2 数据流图

```
导入数据（Excel）
    ↓
【阶段 1】数据验证与收集
    ├→ 验证必填字段和数据格式
    ├→ 检查导入数据内部重复
    ├→ 收集业务唯一键
    └→ 构建待插入列表
    ↓
【阶段 2】批量删除已存在记录
    └→ DELETE FROM table WHERE unique_key IN (...)
    ↓
【阶段 3】批量插入所有数据
    └→ INSERT INTO table (...) VALUES (...)
    ↓
返回导入结果（成功数量、失败详情）
```

### 3.3 各模块业务键定义

| 模块    | 表名                          | 业务键                  | 说明       |
|-------|-----------------------------|----------------------|----------|
| 员工信息  | `ccdi_employee`             | `id_card`            | 身份证号     |
| 中介库个人 | `ccdi_biz_intermediary`     | `person_id`          | 个人证件号    |
| 中介库实体 | `ccdi_enterprise_base_info` | `social_credit_code` | 统一社会信用代码 |
| 招聘信息  | `ccdi_staff_recruitment`    | `recruit_id`         | 招聘项目编号   |

---

## 4. 详细设计

### 4.1 数据库层设计

#### 4.1.1 新增 Mapper 方法

每个模块需要添加对应的批量删除方法：

**员工信息模块**：

```java
// CcdiEmployeeMapper.java
int deleteBatchByIdCard(@Param("list") List<String> idCards);
```

**中介库个人模块**：

```java
// CcdiBizIntermediaryMapper.java
int deleteBatchByPersonId(@Param("list") List<String> personIds);
```

**中介库实体模块**：

```java
// CcdiEnterpriseBaseInfoMapper.java
int deleteBatchBySocialCreditCode(@Param("list") List<String> socialCreditCodes);
```

**招聘信息模块**：

```java
// CcdiStaffRecruitmentMapper.java
int deleteBatchByRecruitId(@Param("list") List<String> recruitIds);
```

#### 4.1.2 Mapper XML 实现

所有删除 SQL 使用统一的模式：

```xml
<delete id="deleteBatchByXxx">
    DELETE FROM {table_name}
    WHERE {unique_key_column} IN
    <foreach collection="list" item="item" open="(" separator="," close=")">
        #{item}
    </foreach>
</delete>
```

**示例（员工信息）**：

```xml
<!-- CcdiEmployeeMapper.xml -->
<delete id="deleteBatchByIdCard">
    DELETE FROM ccdi_employee
    WHERE id_card IN
    <foreach collection="list" item="item" open="(" separator="," close=")">
        #{item}
    </foreach>
</delete>
```

### 4.2 服务层设计

#### 4.2.1 通用导入方法模板

所有模块的导入方法遵循统一的实现模式：

```java
@Override
@Transactional(rollbackFor = Exception.class)
public String importXxx(List<XxxExcel> excelList, Boolean isUpdateSupport) {
    // 参数校验
    if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
        return "至少需要一条数据";
    }

    // 第一阶段：数据验证和收集
    List<XxxEntity> validList = new ArrayList<>();
    List<String> errorMessages = new ArrayList<>();
    Set<String> uniqueKeys = new HashSet<>();

    for (XxxExcel excel : excelList) {
        try {
            // 转换并验证
            XxxAddDTO addDTO = new XxxAddDTO();
            BeanUtils.copyProperties(excel, addDTO);
            validateXxxDataBasic(addDTO);

            // 检查导入数据内部是否重复
            String uniqueKey = getUniqueKey(addDTO);
            if (!uniqueKeys.add(uniqueKey)) {
                throw new RuntimeException("导入文件中该" + getUniqueKeyName() + "重复");
            }

            // 转换为实体，设置审计字段
            XxxEntity entity = new XxxEntity();
            BeanUtils.copyProperties(addDTO, entity);
            entity.setCreateBy("导入");
            entity.setUpdateBy("导入");

            validList.add(entity);

        } catch (Exception e) {
            errorMessages.add(String.format("%s 导入失败：%s",
                getDisplayName(excel), e.getMessage()));
        }
    }

    // 第二阶段：批量删除已存在的记录
    if (!validList.isEmpty()) {
        List<String> uniqueKeyList = new ArrayList<>(uniqueKeys);
        mapper.deleteBatchByUniqueKey(uniqueKeyList);
    }

    // 第三阶段：批量插入所有数据
    if (!validList.isEmpty()) {
        mapper.insertBatch(validList);
    }

    // 第四阶段：返回结果
    if (!errorMessages.isEmpty()) {
        throw buildFailureException(validList.size(), errorMessages);
    }

    return buildSuccessMessage(validList.size());
}
```

#### 4.2.2 员工信息导入方法（示例）

```java
// CcdiEmployeeServiceImpl.java
@Override
@Transactional(rollbackFor = Exception.class)
public String importEmployee(List<CcdiEmployeeExcel> excelList, Boolean isUpdateSupport) {
    if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
        return "至少需要一条数据";
    }

    // 第一阶段：数据验证和收集
    List<CcdiEmployee> validEmployees = new ArrayList<>();
    List<String> errorMessages = new ArrayList<>();
    Set<String> idCards = new HashSet<>();

    for (CcdiEmployeeExcel excel : excelList) {
        try {
            // 转换并验证
            CcdiEmployeeAddDTO addDTO = new CcdiEmployeeAddDTO();
            BeanUtils.copyProperties(excel, addDTO);
            validateEmployeeDataBasic(addDTO);

            // 检查导入数据内部是否重复
            if (!idCards.add(addDTO.getIdCard())) {
                throw new RuntimeException("导入文件中该身份证号重复");
            }

            // 转换为实体，设置审计字段
            CcdiEmployee employee = new CcdiEmployee();
            BeanUtils.copyProperties(addDTO, employee);
            employee.setCreateBy("导入");
            employee.setUpdateBy("导入");

            validEmployees.add(employee);

        } catch (Exception e) {
            errorMessages.add(String.format("%s 导入失败：%s",
                excel.getName(), e.getMessage()));
        }
    }

    // 第二阶段：批量删除已存在的记录
    if (!validEmployees.isEmpty()) {
        employeeMapper.deleteBatchByIdCard(new ArrayList<>(idCards));
    }

    // 第三阶段：批量插入所有数据
    if (!validEmployees.isEmpty()) {
        employeeMapper.insertBatch(validEmployees);
    }

    // 第四阶段：返回结果
    if (!errorMessages.isEmpty()) {
        StringBuilder failureMsg = new StringBuilder();
        failureMsg.append("很抱歉，导入完成！成功 ")
                  .append(validEmployees.size())
                  .append(" 条，失败 ")
                  .append(errorMessages.size())
                  .append(" 条，错误如下：");

        for (int i = 0; i < errorMessages.size(); i++) {
            failureMsg.append("<br/>")
                      .append(i + 1)
                      .append("、")
                      .append(errorMessages.get(i));
        }

        throw new RuntimeException(failureMsg.toString());
    }

    return "恭喜您，数据已全部导入成功！共 " + validEmployees.size() + " 条";
}
```

### 4.3 事务管理

#### 事务边界

整个导入操作使用 `@Transactional` 注解，确保原子性：

```java
@Transactional(rollbackFor = Exception.class)
public String importXxx(List<XxxExcel> excelList, Boolean isUpdateSupport) {
    // 所有数据库操作在一个事务中
}
```

#### 事务保证

| 场景     | 处理方式     | 结果       |
|--------|----------|----------|
| 批量删除失败 | 自动回滚     | 不影响现有数据  |
| 批量插入失败 | 自动回滚     | 已删除的数据恢复 |
| 数据验证失败 | 不执行数据库操作 | 直接返回错误信息 |

### 4.4 错误处理

#### 分层错误处理策略

**1. 数据验证层**

- 捕获单条数据的验证错误（必填字段、格式校验）
- 记录到失败列表，不影响其他数据
- 验证通过的数据继续处理

**2. 数据库操作层**

- 删除/插入失败时抛出异常，触发事务回滚
- 捕获 `DuplicateKeyException`、`DataIntegrityViolationException` 等
- 转换为用户友好的错误消息

**3. 统一返回**

- 全部成功：返回成功消息 + 统计信息
- 部分失败（验证阶段）：返回详细错误列表
- 数据库失败：事务回滚，返回系统错误提示

### 4.5 数据一致性保障

#### 场景 1：导入数据中业务键重复

**示例**：导入文件中有两条记录的身份证号都是 `110101199001011234`

**处理结果**：

- 数据库中的旧记录被删除（如果存在）
- 导入文件中的最后一条记录被插入
- 第一条记录在验证阶段被检测为重复，记录到错误列表

#### 场景 2：数据库中存在重复记录

**示例**：数据库中有两条记录的身份证号都是 `110101199001011234`（历史数据问题）

**处理结果**：

- 批量删除操作会删除所有身份证号匹配的记录
- 插入新的记录
- 自动修复了数据不一致问题

#### 场景 3：并发导入

**示例**：用户 A 和用户 B 同时导入包含相同身份证号的数据

**处理结果**：

- 依赖数据库事务隔离级别和锁机制
- 后提交的事务可能产生 `DuplicateKeyException`
- 事务回滚，返回错误提示

---

## 5. 实施计划

### 5.1 修改文件清单（11 个文件）

#### 员工信息管理模块

1. `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java`
2. `ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml`
3. `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java`

#### 中介库管理模块（个人和实体）

4. `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiBizIntermediaryMapper.java`
5. `ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiBizIntermediaryMapper.xml`
6. `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiEnterpriseBaseInfoMapper.java`
7. `ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiEnterpriseBaseInfoMapper.xml`
8. `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java`
    - 修改 `importIntermediaryPerson` 方法
    - 修改 `importIntermediaryEntity` 方法

#### 员工招聘信息管理模块

9. `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiStaffRecruitmentMapper.java`
10. `ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiStaffRecruitmentMapper.xml`
11. `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentServiceImpl.java`

### 5.2 实施步骤

#### 步骤 1：员工信息模块（验证方案）

1. 添加 `deleteBatchByIdCard` 方法到 Mapper 接口
2. 在 Mapper XML 中实现删除 SQL
3. 重构 `importEmployee` 方法
4. 生成测试脚本并验证功能
5. **验证通过后，继续其他模块**

#### 步骤 2：中介库模块

1. 添加个人表的批量删除方法
2. 添加实体表的批量删除方法
3. 重构两个导入方法
4. 测试验证

#### 步骤 3：招聘信息模块

1. 添加批量删除方法
2. 重构导入方法
3. 测试验证

#### 步骤 4：清理和优化

1. 移除不再使用的 `updateBatch` 方法（如果存在）
2. 更新 API 文档
3. 代码审查

### 5.3 测试计划

#### 单元测试

- 测试批量删除 SQL 语法正确性
- 测试批量插入 SQL 语法正确性
- 测试事务回滚机制

#### 集成测试

- 测试全新数据导入（数据库中不存在）
- 测试更新数据导入（数据库中已存在）
- 测试混合数据导入（部分存在，部分不存在）
- 测试导入数据内部重复
- 测试数据库中存在重复记录的清理

#### 性能测试

- 测试 100 条数据的导入性能
- 测试 1000 条数据的导入性能
- 对比优化前后的性能差异

---

## 6. 风险评估

### 6.1 技术风险

| 风险            | 影响 | 概率 | 缓解措施                |
|---------------|----|----|---------------------|
| 批量删除 SQL 性能问题 | 中  | 低  | 确保 business_key 有索引 |
| 事务超时          | 中  | 低  | 监控事务执行时间，必要时调整超时配置  |
| 并发冲突          | 低  | 中  | 依赖数据库事务隔离机制         |

### 6.2 业务风险

| 风险             | 影响 | 概率 | 缓解措施        |
|----------------|----|----|-------------|
| 历史数据丢失（审计字段重置） | 中  | 低  | 在文档中说明，告知用户 |
| 用户误操作导入错误数据    | 高  | 中  | 前端增加确认提示    |

### 6.3 兼容性风险

| 风险                        | 影响 | 概率 | 缓解措施     |
|---------------------------|----|----|----------|
| 前端依赖 `isUpdateSupport` 参数 | 低  | 低  | 参数保留但不使用 |
| 其他系统调用导入接口                | 低  | 低  | 保持接口签名不变 |

---

## 7. 优势与劣势

### 7.1 优势

1. **代码简化**
    - 移除复杂的条件判断和数据分类逻辑
    - 统一的实现模式，易于维护
    - 代码行数减少约 30%

2. **性能优化**
    - 数据库操作从 3-4 次减少到 2-3 次
    - 不再需要复杂的批量更新 SQL
    - 批量删除和批量插入都使用索引，性能更好

3. **数据一致性**
    - 自动清理重复数据
    - 事务保证原子性
    - 减少数据不一致的可能性

4. **可维护性**
    - 代码逻辑清晰易懂
    - 各模块实现模式统一
    - 新增模块导入功能时可直接复用

### 7.2 劣势

1. **审计字段丢失**
    - `create_time` 和 `create_by` 会被重置为当前值
    - 无法保留原始创建时间
    - **缓解措施**：在文档中明确说明，如果需要保留历史记录，可以考虑使用软删除或历史表

2. **并发性能**
    - 高并发情况下可能产生事务冲突
    - **缓解措施**：导入功能通常是管理员操作，并发概率较低

3. **参数失效**
    - `isUpdateSupport` 参数失去原有意义
    - **缓解措施**：保留参数以保持接口兼容性，内部不再使用

---

## 8. 后续优化建议

### 8.1 短期优化

1. **添加导入进度提示**
    - 对于大量数据导入，前端显示导入进度
    - 避免用户长时间等待

2. **优化错误消息**
    - 提供更详细的错误信息
    - 帮助用户快速定位问题

### 8.2 长期优化

1. **异步导入**
    - 对于超大文件（>10000条），使用异步处理
    - 导入完成后通知用户

2. **导入历史记录**
    - 记录每次导入的操作日志
    - 支持导入历史查询和回滚

3. **数据校验增强**
    - 添加更多业务规则校验
    - 支持自定义校验规则

---

## 9. 附录

### 9.1 术语表

| 术语   | 说明                                                             |
|------|----------------------------------------------------------------|
| 业务键  | 业务层面判断记录唯一性的字段（如身份证号）                                          |
| 审计字段 | 记录数据创建和修改信息的字段（create_time, create_by, update_time, update_by） |
| 批量操作 | 一次数据库操作处理多条记录                                                  |
| 事务   | 保证一组数据库操作原子性的机制                                                |

### 9.2 参考资料

- [MyBatis 官方文档 - 动态 SQL](https://mybatis.org/mybatis-3/zh/dynamic-sql.html)
- [MySQL 批量插入最佳实践](https://dev.mysql.com/doc/refman/8.0/en/insert-optimization.html)
- [Spring 事务管理](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html)

---

**文档结束**
