Files
ccdi/doc/requirements/plans/2026-02-05-导入逻辑优化设计.md
wkc 1cd87d2695 refactor: 重命名 ruoyi-ccdi 模块为 ruoyi-info-collection
- Maven 模块从 ruoyi-ccdi 重命名为 ruoyi-info-collection
- Java 包名从 com.ruoyi.ccdi 改为 com.ruoyi.info.collection
- MyBatis XML 命名空间同步更新
- 保留数据库表名、API URL、权限标识中的 ccdi 前缀
- 更新项目文档中的模块引用
2026-02-24 17:12:11 +08:00

17 KiB
Raw Blame History

导入逻辑优化设计文档

文档信息

  • 创建日期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批量删除

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

阶段 3批量插入

  • 批量插入所有验证通过的数据
  • SQLINSERT 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 方法

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

员工信息模块

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

中介库个人模块

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

中介库实体模块

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

招聘信息模块

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

4.1.2 Mapper XML 实现

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

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

示例(员工信息)

<!-- 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 通用导入方法模板

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

@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 员工信息导入方法(示例)

// 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 注解,确保原子性:

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

事务保证

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

4.4 错误处理

分层错误处理策略

1. 数据验证层

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

2. 数据库操作层

  • 删除/插入失败时抛出异常,触发事务回滚
  • 捕获 DuplicateKeyExceptionDataIntegrityViolationException
  • 转换为用户友好的错误消息

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

中介库管理模块(个人和实体)

  1. ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiBizIntermediaryMapper.java
  2. ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiBizIntermediaryMapper.xml
  3. ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiEnterpriseBaseInfoMapper.java
  4. ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiEnterpriseBaseInfoMapper.xml
  5. ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java
    • 修改 importIntermediaryPerson 方法
    • 修改 importIntermediaryEntity 方法

员工招聘信息管理模块

  1. ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiStaffRecruitmentMapper.java
  2. ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiStaffRecruitmentMapper.xml
  3. 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_timecreate_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 参考资料


文档结束