# 重复上传文件处理逻辑分析 ## 概述 当前系统对重复上传文件有完整的处理机制,分为**同一机构重复上传**和**不同机构重复上传**两种场景。 --- ## 一、同一机构重复上传同一个文件 ### 1.1 文件层面 **行为:** 文件会被重新保存,**不会覆盖旧文件** **原因:** - 文件名使用UUID生成:`IdUtil.simpleUUID()` - 存储路径:`{institutionId}/{date}/{uuid}.dcm` - 每次上传都会生成新的UUID **代码位置:** `DicomServiceImpl.java:80-82` ```java String fileId = IdUtil.simpleUUID(); // 每次都是新的UUID String relativePath = institutionId + "/" + LocalDate.now().toString() + "/" + fileId + fileExtension; ``` **结果:** - ✅ 不会报错 - ✅ 旧文件保留 - ⚠️ 产生磁盘空间冗余 ### 1.2 患者信息层面 **行为:** 根据 `patient_id` 查找,如果存在则**更新**,不存在则**新建** **代码位置:** `DicomServiceImpl.java:428-458` **逻辑:** ```java // 通过 patient_id 查询 LambdaQueryWrapper 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` **逻辑:** ```java // 通过 study_instance_uid 查询 LambdaQueryWrapper 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` **逻辑:** ```java // 检查是否已存在(通过sop_instance_uid) LambdaQueryWrapper 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` ```java LambdaQueryWrapper 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` ```java LambdaQueryWrapper 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查询 ```java // 当前代码 wrapper.eq(StudyInfo::getStudyInstanceUid, dicomData.getStudyInstanceUid()); // 建议修改为 wrapper.eq(StudyInfo::getStudyInstanceUid, dicomData.getStudyInstanceUid()); wrapper.eq(StudyInfo::getInstitutionId, institutionId); // 添加这行 ``` **修改点2:** Series查询 ```java // 当前代码 query.eq(SeriesInfo::getSopInstanceUid, dicomData.getSopInstanceUID()); // 建议修改为 query.eq(SeriesInfo::getSopInstanceUid, dicomData.getSopInstanceUID()); query.eq(SeriesInfo::getInstitutionId, institutionId); // 添加这行 ``` ### 方案2:文件MD5去重(可选) 在上传前计算文件MD5,如果相同文件已存在,则跳过保存: ```java // 计算上传文件的MD5 String fileMd5 = DigestUtils.md5Hex(file.getInputStream()); // 检查是否已存在相同文件 LambdaQueryWrapper 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文件: ```sql -- 查找僵尸文件(数据库中不存在的文件) 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的文件 → 验证系统行为