Bläddra i källkod

质控演示版代码优化

gengjunfang 2 dagar sedan
förälder
incheckning
cea7983c9d

+ 26 - 0
.gitignore

@@ -298,3 +298,29 @@
 /target/QConline-1.0.0-SNAPSHOT.jar
 /target/QConline-1.0.0-SNAPSHOT.jar.original
 /hs_err_pid50673.log
+/logs/debug/log-debug-2026-01-04.0.log
+/logs/debug/log-debug-2026-01-05.0.log
+/logs/debug/log-debug-2026-01-06.0.log
+/logs/debug/log-debug-2026-01-07.0.log
+/logs/debug/log-debug-2026-01-08.0.log
+/logs/debug/log-debug-2026-01-09.0.log
+/logs/debug/log-debug-2026-01-10.0.log
+/logs/error/log-error-2026-01-04.0.log
+/logs/error/log-error-2026-01-07.0.log
+/logs/error/log-error-2026-01-08.0.log
+/logs/error/log-error-2026-01-09.0.log
+/logs/info/log-info-2026-01-04.0.log
+/logs/info/log-info-2026-01-05.0.log
+/logs/info/log-info-2026-01-06.0.log
+/logs/info/log-info-2026-01-07.0.log
+/logs/info/log-info-2026-01-08.0.log
+/logs/info/log-info-2026-01-09.0.log
+/logs/info/log-info-2026-01-10.0.log
+/logs/info/log-info-2026-01-11.0.log
+/logs/warn/log-warn-2026-01-04.0.log
+/logs/warn/log-warn-2026-01-05.0.log
+/logs/warn/log-warn-2026-01-06.0.log
+/logs/warn/log-warn-2026-01-07.0.log
+/logs/warn/log-warn-2026-01-08.0.log
+/logs/warn/log-warn-2026-01-09.0.log
+/lib.zip

+ 14 - 0
doc/sql/alter_study_info_add_is_deleted.sql

@@ -0,0 +1,14 @@
+-- 为 study_info 表添加逻辑删除字段
+-- 创建时间: 2026-01-08
+
+ALTER TABLE `study_info`
+ADD COLUMN `is_deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除:0未删除 1已删除'
+AFTER `batch_id`;
+
+-- 为删除标记字段添加索引,提升查询性能
+CREATE INDEX `idx_is_deleted` ON `study_info` (`is_deleted`);
+
+-- 更新说明:
+-- 1. is_deleted = 0: 未删除(默认值)
+-- 2. is_deleted = 1: 已删除(逻辑删除)
+-- 3. 添加索引提升查询未删除数据的性能

+ 8 - 2
src/main/java/com/zskk/qconline/config/DicomConfig.java

@@ -165,8 +165,14 @@ public class DicomConfig {
                 platform = "macosx-x86-64";
             }
         } else if (osName.contains("linux")) {
-            // Linux: linux-x86-64 或 linux-x86 (用连字符)
-            platform = osArch.contains("64") ? "linux-x86-64" : "linux-x86";
+            // Linux: 支持 x86-64, aarch64 等架构
+            if (osArch.contains("aarch64") || osArch.contains("arm64")) {
+                platform = "linux-aarch64";
+            } else if (osArch.contains("64")) {
+                platform = "linux-x86-64";
+            } else {
+                platform = "linux-x86";
+            }
         } else {
             platform = "unknown";
         }

+ 21 - 17
src/main/java/com/zskk/qconline/modules/api/DcmViewerController.java

@@ -57,7 +57,7 @@ public class DcmViewerController {
             // 使用 XML 中定义的连表查询(直接返回最多10条)
             java.util.List<StudyInfo> studyList = studyInfoMapper.selectExamList(
                     request.getSearch(),
-                    request.getStudyId()
+                    null
             );
 
             // 转换为ExamListResponseDTO格式
@@ -185,6 +185,8 @@ public class DcmViewerController {
 
     /**
      * 历史查询接口(根据当前study_id查询患者历史记录)
+     * 查询逻辑:1. 用studyId查询对应检查,获取patient_id
+     *          2. 用patient_id查询该患者的所有检查记录(排除当前studyId)
      */
     @Operation(summary = "历史查询", description = "根据当前检查ID查询患者的历史检查记录")
     @PostMapping("/butt/anotherSearch")
@@ -197,33 +199,35 @@ public class DcmViewerController {
                 return ExamListApiResponse.success("success", new java.util.ArrayList<>());
             }
 
-            // 1. 根据study_id查询当前检查信息
+            // 1. 根据study_id查询当前检查信息,获取patient_id
             LambdaQueryWrapper<StudyInfo> studyQuery = new LambdaQueryWrapper<>();
             studyQuery.eq(StudyInfo::getStudyId, studyId);
             StudyInfo currentStudy = studyInfoMapper.selectOne(studyQuery);
 
             if (currentStudy == null) {
+                log.warn("历史查询失败:检查不存在, studyId={}", studyId);
                 return ExamListApiResponse.error("检查不存在");
             }
 
-            // 2. 查询患者信息
-            LambdaQueryWrapper<PatientInfo> patientQuery = new LambdaQueryWrapper<>();
-            patientQuery.eq(PatientInfo::getPatientId, currentStudy.getPatientId());
-            PatientInfo patient = patientInfoMapper.selectOne(patientQuery);
-
-            if (patient == null) {
-                return ExamListApiResponse.error("患者信息不存在");
+            String patientId = currentStudy.getPatientId();
+            if (patientId == null || patientId.trim().isEmpty()) {
+                log.warn("历史查询失败:患者ID为空, studyId={}", studyId);
+                return ExamListApiResponse.error("患者ID为空");
             }
 
-            // 3. 使用患者姓名、手机号、身份证号检索历史记录(排除当前study_id)
-            java.util.List<StudyInfo> studyList = studyInfoMapper.selectHistorySearch(
-                    studyId,
-                    patient.getPatientName(),
-                    patient.getPhone(),
-                    patient.getIdCard()
-            );
+            log.info("历史查询:studyId={}, patientId={}", studyId, patientId);
+
+            // 2. 用patient_id查询该患者的所有检查记录(排除当前study_id)
+            LambdaQueryWrapper<StudyInfo> historyQuery = new LambdaQueryWrapper<>();
+            historyQuery.eq(StudyInfo::getPatientId, patientId)
+                    .ne(StudyInfo::getStudyId, studyId)
+                    .orderByDesc(StudyInfo::getStudyDate, StudyInfo::getStudyTime)
+                    .last("LIMIT 10");
+            java.util.List<StudyInfo> studyList = studyInfoMapper.selectList(historyQuery);
+
+            log.info("历史查询结果:找到{}条历史记录", studyList.size());
 
-            // 4. 转换为ExamListResponseDTO格式
+            // 3. 转换为ExamListResponseDTO格式
             java.util.List<ExamListResponseDTO> resultList = studyList.stream().map(study -> {
                 ExamListResponseDTO dto = new ExamListResponseDTO();
 

+ 9 - 2
src/main/java/com/zskk/qconline/modules/dicom/service/impl/DataManageServiceImpl.java

@@ -47,6 +47,9 @@ public class DataManageServiceImpl implements DataManageService {
         Page<StudyInfo> page = new Page<>(queryVO.getPageNum(), queryVO.getPageSize());
         LambdaQueryWrapper<StudyInfo> query = new LambdaQueryWrapper<>();
 
+        // 只查询未删除的数据
+        query.eq(StudyInfo::getIsDeleted, 0);
+
         // 机构筛选
         if (queryVO.getInstitutionId() != null && !queryVO.getInstitutionId().isEmpty()) {
             query.eq(StudyInfo::getInstitutionId, queryVO.getInstitutionId());
@@ -354,8 +357,12 @@ public class DataManageServiceImpl implements DataManageService {
             return false;
         }
 
-        int result = studyInfoMapper.deleteById(studyInfo.getId());
-        log.info("删除检查数据: studyId={}", studyId);
+        // 逻辑删除:设置 is_deleted = 1
+        studyInfo.setIsDeleted(1);
+        studyInfo.setUpdateTime(LocalDateTime.now());
+
+        int result = studyInfoMapper.updateById(studyInfo);
+        log.info("逻辑删除检查数据: studyId={}, result={}", studyId, result);
         return result > 0;
     }
 

+ 11 - 6
src/main/java/com/zskk/qconline/modules/dicom/service/impl/DicomServiceImpl.java

@@ -320,32 +320,35 @@ public class DicomServiceImpl implements DicomService {
             // studyInfo.setBodyPart() 在后面设置,需要收集所有文件的部位
 
             // 匹配检查项目名称:遍历所有文件,尝试匹配
+            // ⭐ 修复:每个文件单独匹配,避免多部位拼接导致的误识别
             java.util.Set<String> matchedExamItems = new java.util.LinkedHashSet<>();
-            java.util.Set<String> uniqueBodyParts = new java.util.LinkedHashSet<>();  // ⭐ 新增:收集所有部位
+            java.util.Set<String> uniqueBodyParts = new java.util.LinkedHashSet<>();  // 收集所有部位
             java.util.Set<Integer> uniqueSeriesNumbers = new java.util.HashSet<>();
 
             for (File file : studyFiles) {
                 try {
                     DicomParser.DicomData dicomData = DicomParser.parse(file);
 
-                    // ⭐ 新增:收集所有部位
+                    // 收集所有部位
                     if (dicomData.getBodyPartExamined() != null && !dicomData.getBodyPartExamined().trim().isEmpty()) {
                         uniqueBodyParts.add(dicomData.getBodyPartExamined().trim());
-                        log.info("文件 {} 解析到部位: {}", file.getName(), dicomData.getBodyPartExamined());  // 改为 info 级别
+                        log.info("文件 {} 解析到部位: {}", file.getName(), dicomData.getBodyPartExamined());
                     } else {
                         log.warn("文件 {} 的 body_part_examined 为空或null", file.getName());
                     }
 
-                    // 匹配检查项目
+                    // ⭐ 关键修复:使用当前文件的部位进行匹配,而不是拼接后的部位
+                    // 这样可以避免 "HEAD+CHEST" 误匹配到 "CT_CHEST" 的问题
                     String itemName = matchExamItemName(
                         dicomData.getModality(),
-                        dicomData.getBodyPartExamined(),
+                        dicomData.getBodyPartExamined(),  // 使用单个文件的部位
                         dicomData.getStudyDescription(),
                         dicomData.getViewPosition()
                     );
                     if (itemName != null && !itemName.trim().isEmpty()) {
                         matchedExamItems.add(itemName.trim());
-                        log.info("文件 {} 匹配到检查项目: {}", file.getName(), itemName);
+                        log.info("文件 {} 匹配到检查项目: {} (部位: {})",
+                            file.getName(), itemName, dicomData.getBodyPartExamined());
                     }
 
                     // 统计序列号
@@ -390,6 +393,7 @@ public class DicomServiceImpl implements DicomService {
             studyInfo.setGovernStatus(0);
             studyInfo.setConfirmStatus(0);
             studyInfo.setIsSplit(0);
+            studyInfo.setIsDeleted(0);  // 0表示未删除
             studyInfo.setCreateTime(LocalDateTime.now());
             studyInfo.setUpdateTime(LocalDateTime.now());
 
@@ -601,6 +605,7 @@ public class DicomServiceImpl implements DicomService {
             studyInfo.setGovernStatus(0); // 未治理
             studyInfo.setConfirmStatus(0); // 待确认
             studyInfo.setIsSplit(0);       // 未拆分
+            studyInfo.setIsDeleted(0);     // 0表示未删除
             studyInfo.setCreateTime(LocalDateTime.now());
             studyInfo.setUpdateTime(LocalDateTime.now());
 

+ 12 - 1
src/main/java/com/zskk/qconline/modules/dicom/service/impl/StudyQueryServiceImpl.java

@@ -7,6 +7,9 @@ import com.zskk.qconline.modules.dicom.vo.StudyQueryResponseVO;
 import com.zskk.qconline.modules.entity.*;
 import com.zskk.qconline.modules.mapper.*;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.ValueOperations;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -42,6 +45,12 @@ public class StudyQueryServiceImpl implements StudyQueryService {
 
     @Resource
     private StudySplitRecordMapper splitRecordMapper;
+    @Autowired
+    private ValueOperations<String, String> valueOperations;
+
+    @Value("${dcm_file_url_prefix}")
+    private String dcmpath;
+
 
     @Override
     public StudyQueryResponseVO queryStudy(String studyInstanceUid, String institutionId, String examItemName) {
@@ -292,7 +301,9 @@ public class StudyQueryServiceImpl implements StudyQueryService {
                 String fileName = parts[parts.length - 1];
 
                 // 生成访问URL,使用 dicomweb:// 协议
-                return String.format("dicomweb://36.140.148.147:9604/api/dicom/viewer/%s/%s", institutionId, fileName);
+                //return String.format("dicomweb://36.140.148.147:9604/api/dicom/viewer/%s/%s", institutionId, fileName);
+                //return String.format("dicomweb://localhost:8080/api/dicom/viewer/%s/%s", institutionId, fileName);
+                return String.format( dcmpath, institutionId, fileName);
             }
 
         } catch (Exception e) {

+ 6 - 0
src/main/java/com/zskk/qconline/modules/entity/StudyInfo.java

@@ -175,6 +175,12 @@ public class StudyInfo implements Serializable {
     @TableField("batch_id")
     private String batchId;
 
+    /**
+     * 是否删除:0未删除 1已删除
+     */
+    @TableField("is_deleted")
+    private Integer isDeleted;
+
     /**
      * 创建时间
      */

+ 20 - 13
src/main/java/com/zskk/qconline/modules/enums/ExamItemEnum.java

@@ -65,6 +65,13 @@ public enum ExamItemEnum {
         Arrays.asList("SHOULDER", "肩关节", "肩"),
         Arrays.asList("AP", "ANTEROPOSTERIOR", "正位", "前后位")),
 
+    /**
+     * CT - 颅脑平扫
+     */
+    CT_HEAD("CT", "颅脑", "平扫", "CT 颅脑平扫",
+        Arrays.asList("HEAD", "BRAIN", "SKULL", "颅脑", "头部", "脑"),
+        Arrays.asList("PLAIN", "NON-CONTRAST", "平扫", "非增强")),
+
     /**
      * CT - 胸部平扫
      */
@@ -79,13 +86,6 @@ public enum ExamItemEnum {
         Arrays.asList("ABDOMEN", "UPPER ABDOMEN", "ABD", "腹部", "上腹部", "腹"),
         Arrays.asList("PLAIN", "NON-CONTRAST", "平扫", "非增强")),
 
-    /**
-     * CT - 颅脑平扫
-     */
-    CT_HEAD("CT", "颅脑", "平扫", "CT 颅脑平扫",
-        Arrays.asList("HEAD", "BRAIN", "SKULL", "颅脑", "头部", "脑"),
-        Arrays.asList("PLAIN", "NON-CONTRAST", "平扫", "非增强")),
-
     /**
      * CT - 腰椎间盘平扫
      */
@@ -1058,19 +1058,26 @@ public enum ExamItemEnum {
             boolean bodyPartMatch = false;
             boolean scanTypeMatch = false;
 
-            // 部位匹配(在别名列表中查找)
+            // 部位匹配(在别名列表中查找)- 精确匹配优先
             if (normalizedBodyPart != null && !normalizedBodyPart.isEmpty()) {
                 for (String alias : item.getBodyPartAliases()) {
                     String normalizedAlias = normalizeString(alias);
-                    if (normalizedBodyPart.contains(normalizedAlias) ||
-                        normalizedAlias.contains(normalizedBodyPart) ||
-                        normalizedBodyPart.equalsIgnoreCase(normalizedAlias)) {
+                    // 1. 精确匹配 - 高分
+                    if (normalizedBodyPart.equalsIgnoreCase(normalizedAlias)) {
                         bodyPartMatch = true;
-                        score += 10; // 部位匹配权重较
-                        System.out.println("[ExamItemEnum] 部位匹配成功: item=" + item.getItemName() +
+                        score += 20; // 精确匹配权重更
+                        System.out.println("[ExamItemEnum] 部位精确匹配: item=" + item.getItemName() +
                             ", alias=" + alias + ", score=" + score);
                         break;
                     }
+                    // 2. 包含匹配 - 低分(避免误匹配)
+                    else if (normalizedBodyPart.contains(normalizedAlias) ||
+                             normalizedAlias.contains(normalizedBodyPart)) {
+                        bodyPartMatch = true;
+                        score += 5; // 模糊匹配权重降低
+                        System.out.println("[ExamItemEnum] 部位模糊匹配: item=" + item.getItemName() +
+                            ", alias=" + alias + ", score=" + score);
+                    }
                 }
             } else {
                 // 如果没有部位信息,只匹配检查类型

+ 20 - 0
src/main/java/com/zskk/qconline/modules/mapper/StudyInfoMapper.java

@@ -46,4 +46,24 @@ public interface StudyInfoMapper extends BaseMapper<StudyInfo> {
             @Param("idCard") String idCard
     );
 
+    /**
+     * 统计符合条件的检查数据数量
+     *
+     * @param modalities 检查类型数组(如 CT, MR, DR)
+     * @return 数据总数
+     */
+    Long countByModalities(@Param("modalities") List<String> modalities);
+
+    /**
+     * 随机查询符合条件的检查数据
+     *
+     * @param modalities 检查类型数组(如 CT, MR, DR)
+     * @param limit      查询数量限制
+     * @return 检查数据列表
+     */
+    List<StudyInfo> selectRandomByModalities(
+            @Param("modalities") List<String> modalities,
+            @Param("limit") Integer limit
+    );
+
 }

+ 1 - 0
src/main/java/com/zskk/qconline/modules/pacs/service/impl/PacsDataServiceImpl.java

@@ -122,6 +122,7 @@ public class PacsDataServiceImpl implements PacsDataService {
             study.setImageCount(studyData.getImageCount());
             study.setSeriesCount(studyData.getSeriesCount());
             study.setInstitutionId(studyData.getInstitutionId());
+            study.setIsDeleted(0);  // 0表示未删除
             study.setCreateTime(LocalDateTime.now());
             study.setUpdateTime(LocalDateTime.now());
             studyInfoMapper.insert(study);

+ 131 - 4
src/main/java/com/zskk/qconline/modules/qc/controller/QcTaskManageController.java

@@ -245,7 +245,15 @@ public class QcTaskManageController {
                 map.put("taskType", task.getTaskType());
                 map.put("standardId", task.getStandardId());
                 map.put("institutionId", task.getInstitutionId());  // 添加 institutionId
-                map.put("modality", task.getModality());
+
+                // 处理检查类型显示:如果为空数组或null,显示原始值(前端会处理为"全部")
+                String modalityValue = task.getModality();
+                if (modalityValue == null || modalityValue.equals("[]") || modalityValue.isEmpty()) {
+                    map.put("modality", "[]");  // 空数组表示全部类型
+                } else {
+                    map.put("modality", modalityValue);
+                }
+
                 map.put("planCount", task.getPlanCount());
                 map.put("sampleType", task.getSampleType());
                 map.put("totalCount", task.getTotalCount());
@@ -261,10 +269,14 @@ public class QcTaskManageController {
                 map.put("patientIds", task.getPatientIds());  // 添加 patientIds
                 map.put("examIds", task.getExamIds());  // 添加 examIds
 
-                // 查询机构名称
+                // 查询机构名称:如果为"all"显示"全部"
                 if (task.getInstitutionId() != null) {
-                    Institution institution = institutionMapper.selectById(task.getInstitutionId());
-                    map.put("institutionName", institution != null ? institution.getInstitutionName() : "");
+                    if ("all".equalsIgnoreCase(task.getInstitutionId())) {
+                        map.put("institutionName", "全部");
+                    } else {
+                        Institution institution = institutionMapper.selectById(task.getInstitutionId());
+                        map.put("institutionName", institution != null ? institution.getInstitutionName() : "");
+                    }
                 } else {
                     map.put("institutionName", "");
                 }
@@ -448,6 +460,121 @@ public class QcTaskManageController {
         }
     }
 
+    /**
+     * 一键质控
+     * 快速创建并执行质控任务
+     */
+    @Operation(summary = "一键质控", description = "快速创建并执行质控任务,自动配置参数")
+    @PostMapping("/quick-qc")
+    public RestResult<?> quickQc() {
+        try {
+            log.info("执行一键质控");
+
+            // 1. 生成任务名称:演示任务 + 4位随机字母数字组合
+            String randomSuffix = generateRandomString(4);
+            String taskName = "演示任务" + randomSuffix;
+
+            // 2. 查询"影像质控标准"
+            LambdaQueryWrapper<QcStandard> standardQuery = new LambdaQueryWrapper<>();
+            standardQuery.eq(QcStandard::getStatus, 1) // 启用状态
+                    .eq(QcStandard::getStandardName, "影像质控标准")
+                    .last("LIMIT 1");
+            QcStandard standard = qcStandardMapper.selectOne(standardQuery);
+
+            if (standard == null) {
+                return RestResult.error("未找到影像质控标准,请先创建该标准");
+            }
+
+            // 3. 查询所有检查数据总数(所有类型)
+            List<String> modalities = new java.util.ArrayList<>(); // 空列表,查询所有类型
+            Long totalCount = studyInfoMapper.countByModalities(modalities);
+
+            if (totalCount == null || totalCount == 0) {
+                return RestResult.error("未找到可质控的检查数据");
+            }
+
+            // 4. 随机抽取检查数据(最多100条,如果总数少于100则全部选择)
+            int sampleCount = Math.min(totalCount.intValue(), 100);
+            List<StudyInfo> randomStudies = studyInfoMapper.selectRandomByModalities(modalities, sampleCount);
+
+            if (randomStudies == null || randomStudies.isEmpty()) {
+                return RestResult.error("未能获取检查数据");
+            }
+
+            // 5. 提取studyId列表并转为JSON字符串
+            List<String> studyIds = randomStudies.stream()
+                    .map(StudyInfo::getStudyId)
+                    .collect(java.util.stream.Collectors.toList());
+            String examIdsJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(studyIds);
+
+            log.info("一键质控自动选择了 {} 条检查数据:{}", studyIds.size(), studyIds);
+
+            // 6. 构建任务对象
+            QcTask task = new QcTask();
+            task.setTaskName(taskName);
+            task.setTaskCode("QC" + System.currentTimeMillis());
+            task.setTaskType("auto"); // 自动任务
+            task.setStandardId(standard.getId());
+            task.setInstitutionId("all"); // 所有机构
+            task.setModality("[]"); // 所有检查类型(空数组表示不限制)
+            task.setPlanCount(studyIds.size()); // 实际选择的数据数量
+            task.setSampleType("random"); // 随机抽样
+            task.setDataRangeType(1); // 全部数据
+            task.setStartDate(null); // 不限制时间范围
+            task.setEndDate(null);
+            task.setQcTypes("[2]"); // 影像质控
+            task.setPatientIds(null);
+            task.setExamIds(examIdsJson); // 设置选中的检查数据ID列表
+            task.setIsPreset(0); // 不使用预制结果
+            task.setStatus(0); // 待执行
+
+            // 7. 保存任务
+            int result = qcTaskMapper.insert(task);
+
+            if (result <= 0) {
+                return RestResult.error("创建任务失败");
+            }
+
+            log.info("一键质控任务创建成功:{}, ID: {}, 检查数据数量: {}", taskName, task.getId(), studyIds.size());
+
+            // 8. 异步执行任务
+            try {
+                qcTaskService.executeTask(task.getId());
+                log.info("一键质控任务开始执行:{}", task.getId());
+            } catch (Exception e) {
+                log.error("任务已创建但执行失败:{}", e.getMessage(), e);
+                // 不抛出异常,让任务保持待执行状态,用户可以手动执行
+            }
+
+            // 9. 返回任务信息
+            Map<String, Object> resultData = new HashMap<>();
+            resultData.put("taskId", task.getId());
+            resultData.put("taskName", taskName);
+            resultData.put("sampleCount", studyIds.size());
+            resultData.put("totalCount", totalCount);
+            resultData.put("message", String.format("已自动选择 %d 条检查数据并开始执行质控", studyIds.size()));
+
+            return RestResult.ok("一键质控任务创建成功", resultData);
+
+        } catch (Exception e) {
+            log.error("一键质控失败:{}", e.getMessage(), e);
+            return RestResult.error("一键质控失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 生成随机字符串(数字+字母)
+     */
+    private String generateRandomString(int length) {
+        String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+        StringBuilder sb = new StringBuilder();
+        java.util.Random random = new java.util.Random();
+        for (int i = 0; i < length; i++) {
+            sb.append(chars.charAt(random.nextInt(chars.length())));
+        }
+        return sb.toString();
+    }
+
     /**
      * 患者姓名脱敏
      * 保留姓氏,名字用*代替,保持原名字长度

+ 91 - 91
src/main/resources/application.yaml

@@ -1,91 +1,91 @@
-server:
-  port: 9603
-spring:
-  autoconfigure:
-    # 去掉自动装配
-    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
-  mvc:
-    pathmatch:
-      matching-strategy: ANT_PATH_MATCHER
-  redis:
-    database: 3
-    host: 127.0.0.1
-    port: 6379
-    password: zskk@2025
-    jedis:
-      pool:
-        max-active: 8
-        max-wait: PT15M
-        max-idle: 8
-        min-idle: 0
-    timeout: 5000
-  datasource:
-    type: com.alibaba.druid.pool.DruidDataSource
-    dynamic:
-      primary: mysql
-      strict: false
-      datasource:
-        mysql:
-          url: jdbc:mysql://127.0.0.1:3306/qconline?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
-          username: root
-          password: Zskk@2025
-          driver-class-name: com.mysql.cj.jdbc.Driver
-    druid:
-      initial-size: 10
-      max-active: 500
-      min-idle: 10
-      max-wait: 600000
-      pool-prepared-statements: true
-      max-pool-prepared-statement-per-connection-size: 20
-      max-open-prepared-statements: 50
-      validation-query: SELECT 1
-      validation-query-timeout: 10000
-      test-on-borrow: true
-      test-on-return: false
-      test-while-idle: true
-      time-between-eviction-runs-millis: 60000
-      min-evictable-idle-time-millis: 300000
-      filters: stat
-logging:
-  config: classpath:logback-spring.xml
-  level:
-    org.apache.ibatis.builder.xml: DEBUG  # 打印 XML 加载日志
-    org.springframework.security: DEBUG # 打印认证相关详细日志
-    # 打印 SQL 语句和参数(推荐方式)
-    com.zskk.qconline.modules.exam.mapper: DEBUG  # 打印 exam 模块 SQL
-    com.zskk.qconline.modules.api.mapper: DEBUG   # 打印 api 模块 SQL
-    com.zskk.qconline.modules: DEBUG  # 打印所有模块的 SQL
-mybatis-plus:
-  mapper-locations:
-    - classpath:mapper/*.xml
-    - classpath*:com/**/mapper/**/*.xml
-  global-config:
-    db-config:
-      uuid-type: SIMPLE  # 生成32位无分隔符UUID(默认是36位带-)
-      logic-delete-value: 0     # 删除时设置的值
-      logic-not-delete-value: 1 # 未删除时的值
-    banner: false  # 关闭 MyBatis-Plus 启动图标日志(核心配置)
-  # SQL 日志配置(开启后会在控制台输出完整的 SQL 语句和参数)
-  configuration:
-    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 开启 SQL 控制台输出
-    # log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl  # 关闭 SQL 日志(不使用)
-# SpringDoc OpenAPI 3 配置
-springdoc:
-  swagger-ui:
-    path: /swagger-ui.html
-    operationsSorter: method
-    tagsSorter: alpha
-    disable-swagger-default-url: true
-    defaultModelsExpandDepth: -1
-    defaultModelExpandDepth: -1
-    docExpansion: none
-    tryItOutEnabled: false
-    filter: true
-    displayRequestDuration: false
-    showExtensions: false
-    showCommonExtensions: false
-  # 完全禁用Schemas
-  default-consumes: application/json
-  default-produces: application/json
-  cache:
-      disabled: true
+#server:
+#  port: 9603
+#spring:
+#  autoconfigure:
+#    # 去掉自动装配
+#    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
+#  mvc:
+#    pathmatch:
+#      matching-strategy: ANT_PATH_MATCHER
+#  redis:
+#    database: 3
+#    host: 127.0.0.1
+#    port: 6379
+#    password: zskk@2025
+#    jedis:
+#      pool:
+#        max-active: 8
+#        max-wait: PT15M
+#        max-idle: 8
+#        min-idle: 0
+#    timeout: 5000
+#  datasource:
+#    type: com.alibaba.druid.pool.DruidDataSource
+#    dynamic:
+#      primary: mysql
+#      strict: false
+#      datasource:
+#        mysql:
+#          url: jdbc:mysql://127.0.0.1:3306/qconline?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
+#          username: root
+#          password: Zskk@2025
+#          driver-class-name: com.mysql.cj.jdbc.Driver
+#    druid:
+#      initial-size: 10
+#      max-active: 500
+#      min-idle: 10
+#      max-wait: 600000
+#      pool-prepared-statements: true
+#      max-pool-prepared-statement-per-connection-size: 20
+#      max-open-prepared-statements: 50
+#      validation-query: SELECT 1
+#      validation-query-timeout: 10000
+#      test-on-borrow: true
+#      test-on-return: false
+#      test-while-idle: true
+#      time-between-eviction-runs-millis: 60000
+#      min-evictable-idle-time-millis: 300000
+#      filters: stat
+#logging:
+#  config: classpath:logback-spring.xml
+#  level:
+#    org.apache.ibatis.builder.xml: DEBUG  # 打印 XML 加载日志
+#    org.springframework.security: DEBUG # 打印认证相关详细日志
+#    # 打印 SQL 语句和参数(推荐方式)
+#    com.zskk.qconline.modules.exam.mapper: DEBUG  # 打印 exam 模块 SQL
+#    com.zskk.qconline.modules.api.mapper: DEBUG   # 打印 api 模块 SQL
+#    com.zskk.qconline.modules: DEBUG  # 打印所有模块的 SQL
+#mybatis-plus:
+#  mapper-locations:
+#    - classpath:mapper/*.xml
+#    - classpath*:com/**/mapper/**/*.xml
+#  global-config:
+#    db-config:
+#      uuid-type: SIMPLE  # 生成32位无分隔符UUID(默认是36位带-)
+#      logic-delete-value: 0     # 删除时设置的值
+#      logic-not-delete-value: 1 # 未删除时的值
+#    banner: false  # 关闭 MyBatis-Plus 启动图标日志(核心配置)
+#  # SQL 日志配置(开启后会在控制台输出完整的 SQL 语句和参数)
+#  configuration:
+#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 开启 SQL 控制台输出
+#    # log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl  # 关闭 SQL 日志(不使用)
+## SpringDoc OpenAPI 3 配置
+#springdoc:
+#  swagger-ui:
+#    path: /swagger-ui.html
+#    operationsSorter: method
+#    tagsSorter: alpha
+#    disable-swagger-default-url: true
+#    defaultModelsExpandDepth: -1
+#    defaultModelExpandDepth: -1
+#    docExpansion: none
+#    tryItOutEnabled: false
+#    filter: true
+#    displayRequestDuration: false
+#    showExtensions: false
+#    showCommonExtensions: false
+#  # 完全禁用Schemas
+#  default-consumes: application/json
+#  default-produces: application/json
+#  cache:
+#      disabled: true

+ 6 - 0
src/main/resources/application.yml

@@ -107,3 +107,9 @@ springdoc:
     - group: 'default'
       paths-to-match: '/**'
       packages-to-scan: com.zskk.qconline.modules
+
+#dcm_file_url_prefix: dicomweb://192.168.1.10:9604/api/dicom/viewer/%s/%s
+#dcm_file_url_prefix: dicomweb://36.140.148.147:9604/api/dicom/viewer/%s/%s
+dcm_file_url_prefix: dicomweb://127.0.0.1:8080/api/dicom/viewer/%s/%s
+#dcm_file_url_prefix: dicomweb://192.168.110.91:8080/api/dicom/viewer/%s/%s
+

+ 58 - 0
src/main/resources/mapper/StudyInfoMapper.xml

@@ -109,4 +109,62 @@
         LIMIT 10
     </select>
 
+    <!-- 统计符合条件的检查数据数量 -->
+    <select id="countByModalities" resultType="java.lang.Long">
+        SELECT COUNT(*)
+        FROM study_info
+        <where>
+            <!-- 只统计未删除的数据 -->
+            AND is_deleted = 0
+            <if test="modalities != null and !modalities.isEmpty()">
+                AND modality IN
+                <foreach collection="modalities" item="modality" open="(" separator="," close=")">
+                    #{modality}
+                </foreach>
+            </if>
+        </where>
+    </select>
+
+    <!-- 随机查询符合条件的检查数据 -->
+    <select id="selectRandomByModalities" resultType="com.zskk.qconline.modules.entity.StudyInfo">
+        SELECT
+            id,
+            institution_id,
+            study_id,
+            study_instance_uid,
+            study_dicom_id,
+            patient_id,
+            study_date,
+            study_time,
+            modality,
+            body_part,
+            exam_item_name,
+            study_description,
+            accession_number,
+            check_status,
+            govern_status,
+            confirm_status,
+            is_split,
+            image_count,
+            series_count,
+            position,
+            create_time,
+            update_time,
+            upload_time,
+            dicom_file_path
+        FROM study_info
+        <where>
+            <!-- 只查询未删除的数据 -->
+            AND is_deleted = 0
+            <if test="modalities != null and !modalities.isEmpty()">
+                AND modality IN
+                <foreach collection="modalities" item="modality" open="(" separator="," close=")">
+                    #{modality}
+                </foreach>
+            </if>
+        </where>
+        ORDER BY RAND()
+        LIMIT #{limit}
+    </select>
+
 </mapper>