|
|
@@ -1,6 +1,7 @@
|
|
|
package com.zskk.qcns.modules.qc.service.impl;
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import com.zskk.qcns.modules.dicom.entity.SeriesInfo;
|
|
|
@@ -523,37 +524,72 @@ public class QcExecutionServiceImpl implements QcExecutionService {
|
|
|
DicomMetadataParser.ParsedExamInfo examInfo =
|
|
|
dicomMetadataParser.parseExamInfo(firstInstance.getFilePath());
|
|
|
|
|
|
- if (examInfo == null) {
|
|
|
- String error = "解析DICOM元数据失败,无法自动匹配质控标准";
|
|
|
- log.error(error);
|
|
|
- saveExecutionLog(taskId, study.getStudyId(), "ERROR", error, null);
|
|
|
- return new StudyQcResult(study.getStudyId(), null, false, error, 0, 0);
|
|
|
- }
|
|
|
+ String originalModality;
|
|
|
+ String mappedModality;
|
|
|
+ QcStandard matchedStandard;
|
|
|
|
|
|
- // 2. 应用 modality 映射(将 DX 等映射到 DR)
|
|
|
- String originalModality = examInfo.getModality();
|
|
|
- String mappedModality = ModalityMapping.mapModality(originalModality);
|
|
|
+ if (examInfo != null) {
|
|
|
+ // DICOM解析成功,使用原有逻辑
|
|
|
+ // 2. 应用 modality 映射(将 DX 等映射到 DR)
|
|
|
+ originalModality = examInfo.getModality();
|
|
|
+ mappedModality = ModalityMapping.mapModality(originalModality);
|
|
|
|
|
|
- if (!mappedModality.equals(originalModality)) {
|
|
|
- log.info("Modality 映射:{} -> {}", originalModality, mappedModality);
|
|
|
- saveExecutionLog(taskId, study.getStudyId(), "INFO",
|
|
|
- String.format("Modality 映射:%s -> %s", originalModality, mappedModality), null);
|
|
|
- }
|
|
|
+ if (!mappedModality.equals(originalModality)) {
|
|
|
+ log.info("Modality 映射:{} -> {}", originalModality, mappedModality);
|
|
|
+ saveExecutionLog(taskId, study.getStudyId(), "INFO",
|
|
|
+ String.format("Modality 映射:%s -> %s", originalModality, mappedModality), null);
|
|
|
+ }
|
|
|
|
|
|
- // 3. 自动匹配质控标准(优先使用examItemName)
|
|
|
- QcStandard matchedStandard = qcStandardMatcher.matchStandard(
|
|
|
- mappedModality, // 使用映射后的 modality
|
|
|
- examInfo.getBodyPart(),
|
|
|
- examInfo.getExamItem(), // DICOM解析的项目名称(备用)
|
|
|
- examItemName // 传入的检查项目名称(优先)
|
|
|
- );
|
|
|
+ // 3. 自动匹配质控标准(优先使用examItemName)
|
|
|
+ matchedStandard = qcStandardMatcher.matchStandard(
|
|
|
+ mappedModality,
|
|
|
+ examInfo.getBodyPart(),
|
|
|
+ examInfo.getExamItem(),
|
|
|
+ examItemName
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ // DICOM解析失败,降级使用数据库中的信息匹配质控标准
|
|
|
+ log.warn("解析DICOM元数据失败,降级使用数据库信息匹配质控标准");
|
|
|
+ saveExecutionLog(taskId, study.getStudyId(), "WARN",
|
|
|
+ "解析DICOM元数据失败,降级使用数据库信息匹配质控标准", null);
|
|
|
+
|
|
|
+ // 使用study_info中的modality
|
|
|
+ originalModality = study.getModality();
|
|
|
+ mappedModality = ModalityMapping.mapModality(originalModality);
|
|
|
+
|
|
|
+ if (!mappedModality.equals(originalModality)) {
|
|
|
+ log.info("Modality 映射:{} -> {}", originalModality, mappedModality);
|
|
|
+ saveExecutionLog(taskId, study.getStudyId(), "INFO",
|
|
|
+ String.format("Modality 映射:%s -> %s", originalModality, mappedModality), null);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 优先使用series_info的examItemName,为空则使用study_info的examItemName
|
|
|
+ String fallbackExamItemName = examItemName;
|
|
|
+ if (!StringUtils.hasText(fallbackExamItemName)) {
|
|
|
+ fallbackExamItemName = study.getExamItemName();
|
|
|
+ log.info("series_info的examItemName为空,使用study_info的examItemName: {}", fallbackExamItemName);
|
|
|
+ }
|
|
|
+
|
|
|
+ matchedStandard = qcStandardMatcher.matchStandard(
|
|
|
+ mappedModality,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ fallbackExamItemName
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
if (matchedStandard == null) {
|
|
|
+ String bodyPartInfo = examInfo != null ? examInfo.getBodyPart() : "";
|
|
|
+ String dicomExamItem = examInfo != null ? examInfo.getExamItem() : "";
|
|
|
String error = String.format("未能匹配到质控标准:original_modality=%s, mapped_modality=%s, bodyPart=%s, examItemName=%s, dicom_exam_item=%s",
|
|
|
- originalModality, mappedModality, examInfo.getBodyPart(),
|
|
|
- examItemName, examInfo.getExamItem());
|
|
|
+ originalModality, mappedModality, bodyPartInfo,
|
|
|
+ examItemName, dicomExamItem);
|
|
|
log.error(error);
|
|
|
saveExecutionLog(taskId, study.getStudyId(), "ERROR", error, null);
|
|
|
+
|
|
|
+ // 插入一条"未匹配"的整体结果记录,使列表页能查到该检查
|
|
|
+ saveUnmatchedOverallResult(taskId, study.getStudyId(), examItemName, instances.size());
|
|
|
+
|
|
|
return new StudyQcResult(study.getStudyId(), null, false, error, 0, 0);
|
|
|
}
|
|
|
|
|
|
@@ -1351,6 +1387,34 @@ public class QcExecutionServiceImpl implements QcExecutionService {
|
|
|
qcExecutionLogMapper.insert(log);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 保存未匹配到质控标准的整体结果记录
|
|
|
+ * 使列表页能查到该检查,详情页展示"未匹配到标准检查项目"
|
|
|
+ */
|
|
|
+ private void saveUnmatchedOverallResult(String taskId, String studyId, String examItemName, int imageCount) {
|
|
|
+ QcTaskOverallResult overallResult = new QcTaskOverallResult();
|
|
|
+ overallResult.setId(UUID.randomUUID().toString().replace("-", ""));
|
|
|
+ overallResult.setTaskId(taskId);
|
|
|
+ overallResult.setStudyId(studyId);
|
|
|
+ overallResult.setExamItemName(examItemName);
|
|
|
+ overallResult.setStandardId("");
|
|
|
+ overallResult.setFactorId("");
|
|
|
+ overallResult.setFactorName("未匹配到标准检查项目");
|
|
|
+ overallResult.setIsPass(0);
|
|
|
+ overallResult.setQualityLevel("UNMATCHED");
|
|
|
+ overallResult.setQualityLevelValue(0);
|
|
|
+ overallResult.setScore(BigDecimal.ZERO);
|
|
|
+ overallResult.setTotalImageCount(imageCount);
|
|
|
+ overallResult.setSuccessImageCount(0);
|
|
|
+ overallResult.setPassImageCount(0);
|
|
|
+ overallResult.setFailImageCount(imageCount);
|
|
|
+ overallResult.setRemark("未匹配到对应的质控标准,请在质控标准管理中添加对应的检查项目标准后重新执行质控");
|
|
|
+ overallResult.setCreateTime(LocalDateTime.now());
|
|
|
+
|
|
|
+ qcTaskOverallResultMapper.insert(overallResult);
|
|
|
+ log.info("已保存未匹配质控标准记录:taskId={}, studyId={}, examItemName={}", taskId, studyId, examItemName);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 检查图像指标是否满足阈值配置
|
|
|
* 优化后的方案2:部分匹配策略
|