Pārlūkot izejas kodu

1、匹配不到检查项目的质控任务也可以查看质控结果
2、重置密码默认为 123456

gengjunfang 1 nedēļu atpakaļ
vecāks
revīzija
ceb67ef538

+ 79 - 1
src/main/java/com/zskk/qcns/modules/qc/engine/ExamItemMatcher.java

@@ -209,9 +209,16 @@ public class ExamItemMatcher {
 
     /**
      * 计算两个规范项目的相似度
-     * 使用综合算法:精确匹配 + 关键词匹配 + 编辑距离
+     * 使用综合算法:关键词全匹配 + 精确匹配 + 关键词匹配 + 编辑距离
      */
     private double calculateSimilarity(NormalizedItem item1, NormalizedItem item2) {
+        // 优先判断:提取的核心关键词(模态、部位、扫描方式)是否全部匹配
+        // 例如:"腰椎 MRI 平扫" vs "MR 腰椎平扫",提取出 MR、腰椎、平扫 全部能对上则直接匹配
+        if (isCoreTokensFullMatch(item1, item2)) {
+            log.debug("核心关键词全匹配命中:{} <-> {}", item1.getNormalized(), item2.getNormalized());
+            return 1.0;
+        }
+
         double score = 0.0;
 
         // 1. 模态匹配(权重:0.3)
@@ -242,6 +249,77 @@ public class ExamItemMatcher {
         return Math.max(0.0, Math.min(1.0, score)); // 限制在[0, 1]范围
     }
 
+    /**
+     * 核心关键词全匹配判断
+     * 提取两边的非空核心词(模态、部位、扫描方式),检查一方的所有核心词是否都能在另一方的原始文本中找到
+     * 例如:标准"腰椎MRI平扫" vs 检查"MR腰椎平扫"
+     *   标准核心词:[MR, 腰椎, 平扫]  检查核心词:[MR, 腰椎, 平扫] → 全匹配
+     */
+    private boolean isCoreTokensFullMatch(NormalizedItem item1, NormalizedItem item2) {
+        List<String> tokens1 = extractCoreTokens(item1);
+        List<String> tokens2 = extractCoreTokens(item2);
+
+        // 至少要有2个核心词才做全匹配判断,避免误匹配
+        if (tokens1.size() < 2 && tokens2.size() < 2) {
+            return false;
+        }
+
+        String text1 = item1.getNormalized();
+        String text2 = item2.getNormalized();
+
+        // 检查item1的所有核心词是否都出现在item2的原始文本中
+        boolean allTokens1InText2 = !tokens1.isEmpty() && tokens1.stream().allMatch(t -> containsToken(text2, t));
+        // 检查item2的所有核心词是否都出现在item1的原始文本中
+        boolean allTokens2InText1 = !tokens2.isEmpty() && tokens2.stream().allMatch(t -> containsToken(text1, t));
+
+        return allTokens1InText2 || allTokens2InText1;
+    }
+
+    /**
+     * 提取核心关键词列表(模态、部位、扫描方式)
+     */
+    private List<String> extractCoreTokens(NormalizedItem item) {
+        List<String> tokens = new ArrayList<>();
+        if (StringUtils.hasText(item.getModality())) {
+            tokens.add(item.getModality());
+        }
+        if (StringUtils.hasText(item.getBodyPart())) {
+            tokens.add(item.getBodyPart());
+        }
+        if (StringUtils.hasText(item.getScanMethod())) {
+            tokens.add(item.getScanMethod());
+        }
+        return tokens;
+    }
+
+    /**
+     * 检查文本中是否包含指定关键词(支持同义词匹配)
+     */
+    private boolean containsToken(String text, String token) {
+        // 直接包含
+        if (text.contains(token)) {
+            return true;
+        }
+        // MR/MRI互认
+        if ("MR".equals(token) && text.contains("MRI")) {
+            return true;
+        }
+        if ("MRI".equals(token) && text.contains("MR")) {
+            return true;
+        }
+        // 同义词匹配:如"脑"和"颅脑"
+        for (List<String> synonyms : BODY_PART_SYNONYMS.values()) {
+            boolean tokenInGroup = synonyms.stream().anyMatch(s -> s.equals(token) || token.contains(s) || s.contains(token));
+            if (tokenInGroup) {
+                boolean textHasSynonym = synonyms.stream().anyMatch(text::contains);
+                if (textHasSynonym) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     /**
      * 计算身体部位相似度
      */

+ 87 - 23
src/main/java/com/zskk/qcns/modules/qc/service/impl/QcExecutionServiceImpl.java

@@ -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:部分匹配策略

+ 2 - 2
src/main/java/com/zskk/qcns/modules/user/service/impl/UserServiceImpl.java

@@ -186,8 +186,8 @@ public class UserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impleme
             return null;
         }
 
-        // 生成随机密码:8位,包含大小写字母和数字
-        String newPassword = generateRandomPassword();
+        // 默认重置密码为123456
+        String newPassword = "123456";
         user.setPassword(passwordEncoder.encode(newPassword));
         user.setUpdateTime(LocalDateTime.now());