# 导入逻辑优化设计文档 ## 文档信息 - **创建日期**: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 idCards); ``` **中介库个人模块**: ```java // CcdiBizIntermediaryMapper.java int deleteBatchByPersonId(@Param("list") List personIds); ``` **中介库实体模块**: ```java // CcdiEnterpriseBaseInfoMapper.java int deleteBatchBySocialCreditCode(@Param("list") List socialCreditCodes); ``` **招聘信息模块**: ```java // CcdiStaffRecruitmentMapper.java int deleteBatchByRecruitId(@Param("list") List recruitIds); ``` #### 4.1.2 Mapper XML 实现 所有删除 SQL 使用统一的模式: ```xml DELETE FROM {table_name} WHERE {unique_key_column} IN #{item} ``` **示例(员工信息)**: ```xml DELETE FROM ccdi_employee WHERE id_card IN #{item} ``` ### 4.2 服务层设计 #### 4.2.1 通用导入方法模板 所有模块的导入方法遵循统一的实现模式: ```java @Override @Transactional(rollbackFor = Exception.class) public String importXxx(List excelList, Boolean isUpdateSupport) { // 参数校验 if (StringUtils.isNull(excelList) || excelList.isEmpty()) { return "至少需要一条数据"; } // 第一阶段:数据验证和收集 List validList = new ArrayList<>(); List errorMessages = new ArrayList<>(); Set 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 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 excelList, Boolean isUpdateSupport) { if (StringUtils.isNull(excelList) || excelList.isEmpty()) { return "至少需要一条数据"; } // 第一阶段:数据验证和收集 List validEmployees = new ArrayList<>(); List errorMessages = new ArrayList<>(); Set 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("
") .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 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) --- **文档结束**