重复上传文件处理逻辑说明.md 9.0 KB

重复上传文件处理逻辑分析

概述

当前系统对重复上传文件有完整的处理机制,分为同一机构重复上传不同机构重复上传两种场景。


一、同一机构重复上传同一个文件

1.1 文件层面

行为: 文件会被重新保存,不会覆盖旧文件

原因:

  • 文件名使用UUID生成:IdUtil.simpleUUID()
  • 存储路径:{institutionId}/{date}/{uuid}.dcm
  • 每次上传都会生成新的UUID

代码位置: DicomServiceImpl.java:80-82

String fileId = IdUtil.simpleUUID();  // 每次都是新的UUID
String relativePath = institutionId + "/" + LocalDate.now().toString() + "/" + fileId + fileExtension;

结果:

  • ✅ 不会报错
  • ✅ 旧文件保留
  • ⚠️ 产生磁盘空间冗余

1.2 患者信息层面

行为: 根据 patient_id 查找,如果存在则更新,不存在则新建

代码位置: DicomServiceImpl.java:428-458

逻辑:

// 通过 patient_id 查询
LambdaQueryWrapper<PatientInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PatientInfo::getPatientId, dicomData.getPatientId());
PatientInfo patientInfo = patientInfoMapper.selectOne(wrapper);

if (patientInfo == null) {
    // 新建患者记录
} else {
    // 更新患者信息(只更新空字段)
}

结果:

  • ✅ 不会报错
  • ✅ 同一患者信息合并
  • ✅ 不会创建重复患者

1.3 检查(Study)信息层面

行为: 根据 study_instance_uid 查找,如果存在则更新,不存在则新建

代码位置: DicomServiceImpl.java:468-535

逻辑:

// 通过 study_instance_uid 查询
LambdaQueryWrapper<StudyInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StudyInfo::getStudyInstanceUid, dicomData.getStudyInstanceUid());
StudyInfo studyInfo = studyInfoMapper.selectOne(wrapper);

if (studyInfo == null) {
    // 新建检查记录
} else {
    // 更新:更新文件路径和上传时间
    studyInfo.setDicomFilePath(dicomFilePath);
    studyInfo.setUploadTime(LocalDateTime.now());
    studyInfo.setUpdateTime(LocalDateTime.now());
    studyInfoMapper.updateById(studyInfo);
}

结果:

  • ✅ 不会报错
  • ✅ 同一检查记录更新
  • ⚠️ 旧的 dicom_file_path 被覆盖(但旧文件仍在磁盘上)

1.4 序列/实例信息层面

行为: 在异步解析时,根据 sop_instance_uid 检查,如果存在则跳过

代码位置: DicomAsyncServiceImpl.java:162-170

逻辑:

// 检查是否已存在(通过sop_instance_uid)
LambdaQueryWrapper<SeriesInfo> query = new LambdaQueryWrapper<>();
query.eq(SeriesInfo::getSopInstanceUid, dicomData.getSopInstanceUID());
SeriesInfo existing = seriesInfoMapper.selectOne(query);

if (existing != null) {
    log.info("片子已存在,跳过: sopInstanceUid={}", dicomData.getSopInstanceUID());
    continue;  // 跳过保存
}

结果:

  • ✅ 不会报错
  • ✅ 不会产生重复的 series_info 记录
  • ✅ 数据库约束 uk_series_sop (series_instance_uid + sop_instance_uid) 保证唯一性

二、不同机构重复上传同一个文件

2.1 文件层面

行为: 文件保存到不同机构目录

存储路径:

  • 机构A:INST_A/2025-12-30/uuid1.dcm
  • 机构B:INST_B/2025-12-30/uuid2.dcm

结果:

  • ✅ 完全独立,互不影响
  • ✅ 不会报错

2.2 患者信息层面

行为: 创建不同机构的患者记录

查询条件: patient_id + institution_id

结果:

  • ✅ 不会报错
  • ✅ 每个机构有独立的患者记录
  • ℹ️ 虽然是同一个患者(相同 patient_id),但在数据库中是多条记录

2.3 检查(Study)信息层面

行为: 创建不同机构的检查记录

查询条件: study_instance_uid + institution_id

重要说明:

  • 理论上 study_instance_uid 应该全局唯一
  • 但实际中,不同机构可能产生相同的 UID(如果设备配置不当)
  • 当前代码只通过 study_instance_uid 查询,没有加 institution_id 条件

代码位置: DicomServiceImpl.java:471-473

LambdaQueryWrapper<StudyInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StudyInfo::getStudyInstanceUid, dicomData.getStudyInstanceUid());
// ⚠️ 注意:这里没有加 institutionId 条件
StudyInfo studyInfo = studyInfoMapper.selectOne(wrapper);

潜在问题:

  • ⚠️ 如果不同机构有相同的 study_instance_uid,会视为同一个检查
  • ⚠️ 后上传的机构会更新先上传机构的记录
  • ❌ 数据混乱

2.4 序列/实例信息层面

行为: 创建不同机构的序列/实例记录

查询条件: sop_instance_uid 全局查询(无 institution_id)

代码位置: DicomAsyncServiceImpl.java:163-165

LambdaQueryWrapper<SeriesInfo> query = new LambdaQueryWrapper<>();
query.eq(SeriesInfo::getSopInstanceUid, dicomData.getSopInstanceUID());
// ⚠️ 注意:这里也没有加 institutionId 条件
SeriesInfo existing = seriesInfoMapper.selectOne(query);

潜在问题:

  • ⚠️ 如果不同机构有相同的 sop_instance_uid,后上传的会跳过
  • ⚠️ 导致某些机构的片子无法保存

三、可能出现的错误场景

3.1 同一机构重复上传

场景 会不会报错 说明
文件保存 ❌ 不会 生成新文件,不覆盖
患者信息 ❌ 不会 合并更新
检查信息 ❌ 不会 更新文件路径
序列/实例 ❌ 不会 跳过已存在的片子
数据库约束 ❌ 不会 uk_series_sop 允许同一序列多张片子

3.2 不同机构重复上传

场景 会不会报错 条件
文件保存 ❌ 不会 路径不同,完全独立
患者信息 ❌ 不会 不同机构的记录
检查信息 ⚠️ 可能 如果 study_instance_uid 相同
序列/实例 ⚠️ 可能 如果 sop_instance_uid 相同
数据库约束 ⚠️ 可能 违反唯一约束

四、当前存在的问题

问题1:不同机构查询条件不完整

位置:

  • DicomServiceImpl.java:471-473 - Study查询
  • DicomAsyncServiceImpl.java:163-165 - Series查询

问题: 只用UID查询,没有加 institution_id 条件

影响:

  • 不同机构如果UID相同,会互相干扰
  • 数据可能被覆盖或跳过

问题2:文件冗余

问题: 同一机构重复上传会保存多份相同的文件

影响:

  • 浪费磁盘空间
  • 旧文件成为"僵尸文件"(数据库中无引用)

问题3:Study记录更新逻辑

问题: Study记录更新时,只更新 dicom_file_path,但不检查该路径是否正确

影响:

  • 如果新上传的文件和旧文件不属于同一个检查,会导致数据不一致

五、建议改进方案

方案1:添加 institution_id 条件(强烈推荐)

修改点1: Study查询

// 当前代码
wrapper.eq(StudyInfo::getStudyInstanceUid, dicomData.getStudyInstanceUid());

// 建议修改为
wrapper.eq(StudyInfo::getStudyInstanceUid, dicomData.getStudyInstanceUid());
wrapper.eq(StudyInfo::getInstitutionId, institutionId);  // 添加这行

修改点2: Series查询

// 当前代码
query.eq(SeriesInfo::getSopInstanceUid, dicomData.getSopInstanceUID());

// 建议修改为
query.eq(SeriesInfo::getSopInstanceUid, dicomData.getSopInstanceUID());
query.eq(SeriesInfo::getInstitutionId, institutionId);  // 添加这行

方案2:文件MD5去重(可选)

在上传前计算文件MD5,如果相同文件已存在,则跳过保存:

// 计算上传文件的MD5
String fileMd5 = DigestUtils.md5Hex(file.getInputStream());

// 检查是否已存在相同文件
LambdaQueryWrapper<SeriesInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SeriesInfo::getFileMd5, fileMd5);
SeriesInfo existing = seriesInfoMapper.selectOne(wrapper);

if (existing != null) {
    log.info("文件已存在,跳过上传: md5={}, existingPath={}",
        fileMd5, existing.getFilePath());
    return existing; // 返回已存在的记录
}

方案3:定期清理僵尸文件(运维层面)

定期扫描并清理数据库中没有引用的DICOM文件:

-- 查找僵尸文件(数据库中不存在的文件)
SELECT file_path FROM series_info
WHERE file_path NOT IN (
    SELECT CONCAT(dicom_root_path, dicom_file_path, '/', file_id, '.dcm')
    FROM study_info
);

六、总结

当前状态

同一机构重复上传: 完全支持,不会报错 ⚠️ 不同机构重复上传: 基本支持,但存在UID冲突风险

优先级建议

  1. 高优先级: 添加 institution_id 到查询条件(防止数据混乱)
  2. 中优先级: 实现文件MD5去重(节省空间)
  3. 低优先级: 定期清理僵尸文件(维护脚本)

测试建议

  1. 同一机构重复上传相同文件 → 验证不报错
  2. 不同机构上传相同文件 → 验证数据隔离
  3. 人为制造相同UID的文件 → 验证系统行为