修复重复上传问题说明.md 9.8 KB

修复 TooManyResultsException 和重复上传问题

问题现象

nested exception is org.apache.ibatis.exceptions.TooManyResultsException:
Expected one result (or null) to be returned by selectOne(), but found: 2

原因: 同一机构存在多条相同的 study_instance_uid 记录,导致 selectOne() 报错。


解决方案总览

✅ 已完成的修改

  1. 修改查询逻辑 - 使用 selectList() 代替 selectOne()
  2. 添加重复检查 - 在上传时检查是否已存在记录
  3. 添加 institution_id 条件 - 确保不同机构数据隔离
  4. 实现智能更新 - 检测到重复时更新而不是报错
  5. 创建数据库约束 - 防止未来再出现重复数据
  6. 创建清理脚本 - 清理现有的重复数据

一、代码修改详情

1.1 DicomServiceImpl.java - 单文件上传

文件位置: DicomServiceImpl.java:468-494

修改前:

// 只用 selectOne,遇到多条记录会报错
LambdaQueryWrapper<StudyInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StudyInfo::getStudyInstanceUid, dicomData.getStudyInstanceUid());
StudyInfo studyInfo = studyInfoMapper.selectOne(wrapper);  // ❌ TooManyResultsException

修改后:

// 使用 selectList,可以处理多条记录
LambdaQueryWrapper<StudyInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StudyInfo::getStudyInstanceUid, dicomData.getStudyInstanceUid());
wrapper.eq(StudyInfo::getInstitutionId, institutionId);  // ✅ 添加机构ID条件
List<StudyInfo> existingStudies = studyInfoMapper.selectList(wrapper);  // ✅ 使用 selectList

StudyInfo studyInfo = null;

if (existingStudies.isEmpty()) {
    // 不存在,创建新记录
    studyInfo = null;
} else if (existingStudies.size() == 1) {
    // 存在一条,使用这条记录
    studyInfo = existingStudies.get(0);
} else {
    // 存在多条,记录警告并使用第一条
    log.warn("检测到同一机构有多个相同的 study_instance_uid记录!...");
    studyInfo = existingStudies.get(0);
}

优点:

  • ✅ 不会因为重复数据而报错
  • ✅ 自动检测并记录重复情况
  • ✅ 使用第一条记录进行更新

1.2 DicomServiceImpl.java - 批量上传

文件位置: DicomServiceImpl.java:259-304

新增逻辑:

// 检查是否已存在该study
LambdaQueryWrapper<StudyInfo> studyQuery = new LambdaQueryWrapper<>();
studyQuery.eq(StudyInfo::getStudyInstanceUid, studyInstanceUid);
studyQuery.eq(StudyInfo::getInstitutionId, institutionId);  // ✅ 添加机构ID条件
List<StudyInfo> existingStudies = studyInfoMapper.selectList(studyQuery);

if (!existingStudies.isEmpty()) {
    // 已存在,更新记录而不是新建
    studyInfo = existingStudies.get(0);

    if (existingStudies.size() > 1) {
        log.warn("检测到同一机构有多个相同的 study_instance_uid记录!...");
    }

    // 更新统计信息
    studyInfo.setSeriesCount(studyInfo.getSeriesCount() + newSeriesCount);
    studyInfo.setImageCount(studyInfo.getImageCount() + newFileCount);
    studyInfo.setUploadTime(LocalDateTime.now());
    studyInfo.setUpdateTime(LocalDateTime.now());

    studyInfoMapper.updateById(studyInfo);  // ✅ 更新而不是插入
} else {
    // 不存在,创建新记录
    studyInfo = new StudyInfo();
    studyInfoMapper.insert(studyInfo);
}

优点:

  • ✅ 重复上传时更新已有记录,而不是创建新记录
  • ✅ 累加统计信息(序列数、图像数)
  • ✅ 避免数据冗余

1.3 DicomAsyncServiceImpl.java - 异步解析

文件位置: DicomAsyncServiceImpl.java:162-166

修改前:

LambdaQueryWrapper<SeriesInfo> query = new LambdaQueryWrapper<>();
query.eq(SeriesInfo::getSopInstanceUid, dicomData.getSopInstanceUID());
SeriesInfo existing = seriesInfoMapper.selectOne(query);  // ❌ 可能报错

修改后:

LambdaQueryWrapper<SeriesInfo> query = new LambdaQueryWrapper<>();
query.eq(SeriesInfo::getSopInstanceUid, dicomData.getSopInstanceUID());
query.eq(SeriesInfo::getInstitutionId, studyInfo.getInstitutionId());  // ✅ 添加机构ID条件
SeriesInfo existing = seriesInfoMapper.selectOne(query);  // 现在可以安全使用

优点:

  • ✅ 添加 institution_id 条件,确保不同机构数据隔离
  • ✅ 防止不同机构的相同 UID 冲突

二、数据库约束

2.1 添加唯一约束

SQL文件: doc/sql/add_study_unique_constraint.sql

约束定义:

ALTER TABLE study_info
ADD UNIQUE KEY uk_study_instance (institution_id, study_instance_uid);

作用:

  • ✅ 从数据库层面防止同一机构产生重复的 study_instance_uid
  • ✅ 如果尝试插入重复数据,数据库会直接拒绝

2.2 清理现有重复数据

SQL文件: doc/sql/cleanup_duplicate_studies.sql

提供的方案:

  1. 方案1 - 查看重复数据(不删除)
  2. 方案2 - 标记要删除的记录(不删除)
  3. 方案3 - 备份重复记录到历史表(推荐)
  4. 方案4 - 删除重复记录(谨慎使用)
  5. 方案5 - 智能合并重复记录(高级)

三、执行步骤

第一步:修复代码(已完成)

✅ 代码修改已全部完成,包括:

  • DicomServiceImpl.java - 上传逻辑
  • DicomAsyncServiceImpl.java - 异步解析逻辑
  • 添加了 institution_id 条件
  • 使用 selectList() 处理多条记录

第二步:清理现有重复数据

执行清理脚本:

mysql -u root -p qconline < doc/sql/cleanup_duplicate_studies.sql

建议步骤:

  1. 先查看重复数据(方案1)
  2. 备份重复记录(方案3)
  3. 确认无误后删除(方案4)
  4. 验证无重复

第三步:添加数据库约束

执行约束脚本:

mysql -u root -p qconline < doc/sql/add_study_unique_constraint.sql

验证约束:

SHOW INDEX FROM study_info WHERE Key_name = 'uk_study_instance';

第四步:重启应用测试

# 重启 Spring Boot 应用
mvn spring-boot:run

测试场景:

  1. 上传同一个文件两次 → 应该更新而不是报错
  2. 不同机构上传相同文件 → 应该分别保存
  3. 查询接口 → 正常返回数据

四、效果对比

修复前

场景 结果
同一机构重复上传 ❌ TooManyResultsException
不同机构重复上传 ⚠️ 数据可能混乱
查询接口 ❌ 报错

修复后

场景 结果
同一机构重复上传 ✅ 更新已有记录,不报错
不同机构重复上传 ✅ 数据完全隔离
查询接口 ✅ 正常返回数据
数据库约束 ✅ 防止新的重复数据

五、重复数据说明

5.1 重复数据来源

可能的原因:

  1. 并发上传 - 多个线程同时上传同一个检查
  2. 网络重试 - 上传失败后重试,导致重复
  3. 设备问题 - DICOM设备产生了相同的 UID
  4. 数据导入 - 从其他系统导入数据时未去重

5.2 如何避免重复

代码层面(已完成):

  • ✅ 使用 selectList() 而不是 selectOne()
  • ✅ 检查是否已存在再决定新建或更新
  • ✅ 添加 institution_id 条件确保数据隔离

数据库层面(待执行):

  • ⏳ 添加唯一约束 uk_study_instance
  • ⏳ 清理现有重复数据

六、日志说明

正常日志

INFO  - 找到已存在的检查记录,将更新: studyId=STY001, studyInstanceUid=1.2.3...
INFO  - 更新检查记录: studyId=STY001, 新的seriesCount=5, 新的imageCount=120

警告日志(表示有重复数据)

WARN  - 检测到同一机构有多个相同的 study_instance_uid记录!
       studyInstanceUid=1.2.3..., institutionId=INST001, count=2
WARN  - 这可能需要数据清理,将使用第一条记录进行更新

看到警告日志时:

  1. 说明数据库中存在重复数据
  2. 系统会自动使用第一条记录
  3. 建议执行清理脚本
  4. 添加唯一约束防止以后再出现

七、常见问题

Q1: 为什么会 TooManyResultsException?

A: 因为同一机构有多个相同的 study_instance_uid 记录,selectOne() 期望返回0或1条记录,但实际返回了多条。

Q2: 修改后还会报错吗?

A: 不会。修改后使用 selectList(),可以处理多条记录的情况。如果有多条,会记录警告并使用第一条。

Q3: 不同机构上传相同的UID会冲突吗?

A: 不会。查询时添加了 institution_id 条件,不同机构的数据是完全隔离的。

Q4: 如何清理现有的重复数据?

A: 执行 cleanup_duplicate_studies.sql,提供了5个方案,建议先备份再删除。

Q5: 添加唯一约束后会怎样?

A: 从数据库层面防止重复,如果尝试插入重复的 institution_id + study_instance_uid,数据库会直接拒绝。


八、文件清单

修改的文件

  • DicomServiceImpl.java - 上传逻辑
  • DicomAsyncServiceImpl.java - 异步解析逻辑

新增的SQL文件

  • doc/sql/add_study_unique_constraint.sql - 添加唯一约束
  • doc/sql/cleanup_duplicate_studies.sql - 清理重复数据
  • doc/重复上传文件处理逻辑说明.md - 详细说明文档

九、总结

核心改进

  1. 健壮性 - 不再因为重复数据而报错
  2. 数据隔离 - 不同机构数据完全独立
  3. 智能更新 - 检测到重复时更新而不是报错
  4. 预防机制 - 数据库约束防止新重复

执行建议

  1. ✅ 代码已修改完成
  2. ⏳ 执行清理脚本(可选,但强烈推荐)
  3. ⏳ 执行约束脚本(必须)
  4. ✅ 重启应用测试

预期结果

  • ✅ 不会再出现 TooManyResultsException
  • ✅ 同一机构重复上传会智能更新
  • ✅ 不同机构数据完全隔离
  • ✅ 数据库层面防止新的重复