查询接口支持按检查项目返回对应部位.md 7.3 KB

修改 /api/dicom/query 接口支持按检查项目查询对应部位

需求说明

当查询接口传入 exam_item_name 参数时(如"胸部CT"),返回的 partExamined 应该是该检查项目对应的部位(如"CHEST"),而不是拼接后的多部位(如"CHEST+KNEE")。


修改内容

文件:StudyQueryServiceImpl.java

修改1:传递 examItemName 参数(第88行)

// 修改前
StudyQueryResponseVO.StudyDetailVO studyDetail = buildStudyDetail(
    studyInfo, patientInfo, institution, seriesList);

// 修改后
StudyQueryResponseVO.StudyDetailVO studyDetail = buildStudyDetail(
    studyInfo, patientInfo, institution, seriesList, examItemName);

修改2:动态决定 partExamined 的值(第180-190行)

// ⭐ 新增:根据examItemName参数决定partExamined的值
if (examItemName != null && !examItemName.isEmpty() && seriesList != null && !seriesList.isEmpty()) {
    // 如果传入了exam_item_name查询参数,从series_info中提取对应的部位
    String partExamined = extractPartExaminedFromSeries(seriesList);
    detail.setPartExamined(partExamined);
    log.info("使用查询参数对应的部位: examItemName={}, partExamined={}", examItemName, partExamined);
} else {
    // 没有传入exam_item_name,使用study_info中的bodyPart(可能是拼接的)
    detail.setPartExamined(studyInfo.getBodyPart());
    log.info("使用study_info的部位: bodyPart={}", studyInfo.getBodyPart());
}

修改3:新增 extractPartExaminedFromSeries 方法(第310-338行)

/**
 * 从series_info列表中提取对应的部位
 * 因为已经通过exam_item_name过滤,所以所有片子的部位应该相同
 * 取第一个片子的部位即可
 */
private String extractPartExaminedFromSeries(List<SeriesInfo> seriesList) {
    if (seriesList == null || seriesList.isEmpty()) {
        log.warn("series_list为空,无法提取部位");
        return null;
    }

    // 从第一个片子中提取部位
    String partExamined = seriesList.get(0).getBodyPartExamined();

    // 可选:验证所有片子的部位是否一致(用于调试)
    long uniquePartCount = seriesList.stream()
        .map(SeriesInfo::getBodyPartExamined)
        .distinct()
        .count();

    if (uniquePartCount > 1) {
        log.warn("检测到series_list中存在多个不同的部位: count={}, parts={}",
            uniquePartCount,
            seriesList.stream()
                .map(SeriesInfo::getBodyPartExamined)
                .distinct()
                .collect(java.util.stream.Collectors.toList()));
    }

    log.info("从series_info中提取部位: partExamined={}, seriesCount={}",
        partExamined, seriesList.size());

    return partExamined;
}

功能说明

查询逻辑

场景1:不带 exam_item_name 参数查询

请求:

GET /api/dicom/query?study_instance_uid=1.2.3...&institution_id=INST001

返回:

{
  "partExamined": "CHEST+KNEE",  // study_info 中的拼接值
  "studyDescribe": "胸部CT+膝关节CT"
}

说明: 返回所有部位的拼接值


场景2:带 exam_item_name 参数查询(胸部CT)

请求:

GET /api/dicom/query?study_instance_uid=1.2.3...&institution_id=INST001&exam_item_name=胸部CT

处理流程:

  1. 查询 series_info,过滤条件:exam_item_name = '胸部CT'
  2. 得到的 series_list 中所有片子的 exam_item_name 都是 "胸部CT"
  3. 从第一个片子提取 body_part_examined,假设是 "CHEST"
  4. 返回 partExamined = "CHEST"

返回:

{
  "partExamined": "CHEST",  // ⭐ 从 series_info 中提取的对应部位
  "studyDescribe": "胸部CT+膝关节CT"
}

说明: 只返回查询参数对应的部位


场景3:带 exam_item_name 参数查询(膝关节CT)

请求:

GET /api/dicom/query?study_instance_uid=1.2.3...&institution_id=INST001&exam_item_name=膝关节CT

返回:

{
  "partExamined": "KNEE",  // ⭐ 从 series_info 中提取的对应部位
  "studyDescribe": "胸部CT+膝关节CT"
}

数据表关系

study_info 表(检查级别)

study_id body_part exam_item_name
STY001 CHEST+KNEE 胸部CT+膝关节CT

series_info 表(片子级别)

instance_number body_part_examined exam_item_name
1 CHEST 胸部CT
2 CHEST 胸部CT
3 KNEE 膝关节CT
4 KNEE 膝关节CT

查询逻辑详解

不带 exam_item_name 查询

SELECT * FROM series_info WHERE study_id = 'STY001';
-- 结果:返回所有片子(胸部2张+膝关节2张)

返回值:

{
  "partExamined": "CHEST+KNEE"  // study_info.body_part(拼接值)
}

带 exam_item_name=胸部CT 查询

SELECT * FROM series_info
WHERE study_id = 'STY001'
  AND exam_item_name = '胸部CT';
-- 结果:只返回胸部片子(2张)

提取逻辑:

  1. series_list 有2条记录
  2. 所有记录的 body_part_examined 都是 "CHEST"
  3. 从第一条记录提取:partExamined = "CHEST"

返回值:

{
  "partExamined": "CHEST"  // ⭐ 从 series_info 提取
}

日志输出

不带 exam_item_name

INFO - 使用study_info的部位: bodyPart=CHEST+KNEE

带 exam_item_name=胸部CT

INFO - 使用查询参数对应的部位: examItemName=胸部CT, partExamined=CHEST
INFO - 从series_info中提取部位: partExamined=CHEST, seriesCount=2

检测到多个不同部位(警告)

WARN - 检测到series_list中存在多个不同的部位: count=2, parts=[CHEST, KNEE]

这种情况理论上不应该出现,因为已经通过 exam_item_name 过滤了。


测试用例

测试1:查询所有数据(不带参数)

curl -X GET "http://localhost:8080/api/dicom/query?study_instance_uid=1.2.3...&institution_id=INST001"

预期:

{
  "partExamined": "CHEST+KNEE",
  "studyDescribe": "胸部CT+膝关节CT"
}

测试2:查询胸部CT

curl -X GET "http://localhost:8080/api/dicom/query?study_instance_uid=1.2.3...&institution_id=INST001&exam_item_name=胸部CT"

预期:

{
  "partExamined": "CHEST",
  "studyDescribe": "胸部CT+膝关节CT"
}

测试3:查询膝关节CT

curl -X GET "http://localhost:8080/api/dicom/query?study_instance_uid=1.2.3...&institution_id=INST001&exam_item_name=膝关节CT"

预期:

{
  "partExamined": "KNEE",
  "studyDescribe": "胸部CT+膝关节CT"
}

总结

核心改进

  1. ✅ 修改 buildStudyDetail 方法签名,增加 examItemName 参数
  2. ✅ 动态决定 partExamined 的来源:
    • 有 exam_item_name 参数:从 series_info 提取
    • 无 exam_item_name 参数:使用 study_info.body_part
  3. ✅ 新增 extractPartExaminedFromSeries 方法
  4. ✅ 添加详细日志输出

数据一致性

  • partExamined 与查询参数 exam_item_name 对应
  • studyDescribe 保持完整(仍然是拼接值)
  • seriesList 只包含查询参数对应的序列

效果

  • 不传参数: 返回拼接的部位(如 "CHEST+KNEE")
  • 传参数: 返回对应的单个部位(如 "CHEST" 或 "KNEE")

重启应用后即可生效! 🎯