接口路径: POST /api/report/details
功能说明: 获取报告详细信息,用于报告书写页面。支持本地报告和远程报告两种模式。
examId: 检查ID(必填)isRemote: 报告类型,1=远程报告,2=本地报告(必填)raId: 远程申请ID(远程报告时必填)SELECT
-- 检查信息(exams表)
e.id AS exam_id, e.patient_num, e.name, e.sex, e.age, e.phone,
e.exam_class, e.exam_datetime, e.exam_status, e.exam_project,
-- 登记信息(register表)
reg.body_part, reg.exam_sub_class,
-- 报告信息(report表,type='1'表示本地报告)
r.id AS report_id, r.description, r.impression, r.diagnose,
r.report_result, r.qr_code, r.hr_status,
-- 医生信息(只查询医生ID和姓名,签名信息单独批量查询)
r.report_doctor_id, r.report_doctor_name, r.report_datetime,
r.review_doctor_id, r.review_doctor_name, r.review_datetime,
r.confirm_doctor_id, r.confirm_doctor_name, r.confirm_datetime
FROM exams e
LEFT JOIN register reg ON reg.exam_id = e.id
LEFT JOIN report r ON e.id = r.exam_id AND r.type = '1'
WHERE e.id = #{examId}
性能优化说明:移除了3次doctors表LEFT JOIN,改为在Service层批量查询医生签名信息,降低SQL复杂度。
SELECT
-- 远程申请信息(remote_application表)
ra.exam_id, ra.patient_num, ra.name, ra.sex, ra.age,
ra.report_status, ra.local_institution_name, ra.req_doctor_name,
-- 报告信息(通过 remote_application_id 关联)
r.id AS report_id, r.description, r.impression, r.report_result,
-- 医生信息(只查询医生ID和姓名,签名信息单独批量查询)
r.report_doctor_id, r.report_doctor_name, r.report_datetime,
r.review_doctor_id, r.review_doctor_name, r.review_datetime,
r.confirm_doctor_id, r.confirm_doctor_name, r.confirm_datetime
FROM remote_application ra
LEFT JOIN report r ON r.remote_application_id = ra.id
WHERE ra.id = #{raId}
性能优化说明:同本地报告,移除了3次doctors表LEFT JOIN。
如果本地报告不存在(report_id 为空),自动创建一条报告记录:
Report report = new Report();
report.setExamId(examId);
report.setType("1"); // 本地报告
report.setCreateTime(new Date());
// MyBatis-Plus 会自动生成 UUID(实体类配置了 @TableId(type = IdType.ASSIGN_UUID))
reportMapper.insert(report);
根据 institution_id 查询机构的报告标题、副标题、医院简介:
SysOrg org = sysOrgMapper.selectById(institutionId);
detail.setReportTitle(org.getReportTitle());
detail.setReportSubtitle(org.getReportSubtitle());
detail.setHrInfo(org.getHrInfo());
// TODO: 实现逻辑
// 1. 根据 study_id 查询 dcm_path 表
// 2. 检查 effective_date 是否过期
// 3. 根据 dcm_type 生成访问 URL
// - dcm_type=1: 本地路径,直接返回
// - dcm_type=2/4: 云存储,调用云存储SDK生成临时访问URL
根据每个医生的签名配置生成访问URL:
// 判断逻辑:
// 1. 是否启用签名?(is_use_autograph == 1)
// 2. 签名文件是否存在?(autograph != null)
// 3. 根据存储类型生成URL
// - autograph_type=1: 本地存储,直接返回路径
// - autograph_type=2: 云存储,生成临时访问URL
if (useAutograph == 1 && StringUtils.isNotBlank(autograph)) {
if (autographType == 2) {
// TODO: 调用云存储SDK生成签名URL
url = cloudStorageService.generateSignedUrl(autograph);
} else {
url = autograph; // 本地路径
}
} else {
url = ""; // 不使用签名
}
性能优化: 不在SQL中LEFT JOIN 3次doctors表,改为批量查询:
// 收集需要查询的医生ID(报告医生、审核医生、确认医生)
List<String> doctorIds = new ArrayList<>();
if (reportDoctorId != null) doctorIds.add(reportDoctorId);
if (reviewDoctorId != null) doctorIds.add(reviewDoctorId);
if (confirmDoctorId != null) doctorIds.add(confirmDoctorId);
// 批量查询医生信息(1次数据库查询)
List<Doctors> doctors = doctorsMapper.selectBatchIds(doctorIds);
// 转换为Map,方便查找
Map<String, Doctors> doctorMap = doctors.stream()
.collect(Collectors.toMap(Doctors::getId, d -> d));
// 填充各医生的签名信息
detail.setReportUseAutograph(doctorMap.get(reportDoctorId).getIsUseAutograph());
detail.setReportAutograph(doctorMap.get(reportDoctorId).getAutograph());
// ... 审核医生和确认医生同理
优化效果:
// 查询报告医生的机构信息
if (StringUtils.isNotBlank(reportDoctorId)) {
Doctors doctor = doctorsMapper.selectById(reportDoctorId);
detail.setIntroduce(doctor.getIntroduce());
SysOrg org = sysOrgMapper.selectById(doctor.getOid());
detail.setInstitution(org.getName());
}
从 report_record 表查询报告的所有修改历史:
SELECT
rr.description,
rr.impression,
rr.report_result,
rr.create_time AS handle_time,
d.realname,
rr.type
FROM report_record rr
LEFT JOIN doctors d ON rr.doctor_id = d.id
WHERE rr.report_id = #{reportId}
ORDER BY rr.create_time ASC
如果报告未完成(exam_status != 9 且 report_status != 9),从 report_temp 表加载当前医生的草稿:
// 1. 判断报告是否已完成
if (examStatus == 9 || reportStatus == 9) {
return; // 已完成,不加载草稿
}
// 2. 获取当前登录医生ID
// 从 Spring Security 上下文获取当前用户,然后查询医生ID
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String sysUserId = loginUser.getUser().getId();
// 根据系统用户ID查询医生ID(doctors.uid = sys_user.id)
Doctors doctor = doctorsMapper.selectOne(
new LambdaQueryWrapper<Doctors>()
.eq(Doctors::getUid, sysUserId)
.last("LIMIT 1")
);
String currentDoctorId = doctor.getId();
// 3. 查询草稿
ReportTemp draft = reportTempMapper.selectOne(
new LambdaQueryWrapper<ReportTemp>()
.eq(ReportTemp::getReportId, reportId)
.eq(ReportTemp::getDoctorId, currentDoctorId)
.orderByDesc(ReportTemp::getUpdateTime)
.last("LIMIT 1")
);
// 4. 用草稿覆盖数据库内容
if (draft != null) {
detail.setImpression(draft.getImpression());
detail.setDescription(draft.getDescription());
}
| 表名 | 用途 |
|---|---|
exams |
检查信息(患者、检查项目、状态等) |
register |
登记信息(检查部位、检查子类等) |
report |
报告信息(印象、描述、医生、时间等) |
remote_application |
远程申请信息 |
doctors |
医生信息(姓名、签名、所属机构等) |
sys_org |
机构信息(报告标题、副标题等) |
report_record |
报告操作记录(历史痕迹) |
report_temp |
报告草稿(暂存内容) |
dcm_path |
DICOM文件路径(TODO) |
POST /api/report/details
{
"examId": "exam-uuid-xxx",
"isRemote": 2,
"raId": ""
}
POST /api/report/details
{
"examId": "exam-uuid-xxx",
"isRemote": 1,
"raId": "ra-uuid-xxx"
}
{
"code": 200,
"message": "查询成功",
"data": {
// 检查基础信息
"examId": "exam-uuid-xxx",
"isRemote": 2,
"name": "张三",
"sex": "男",
"age": "45",
"phone": "138****5678",
"examClass": "CT",
"examDatetime": "20251216103000",
// 报告信息
"reportId": "report-uuid-xxx",
"description": "影像所见内容...",
"impression": "诊断印象内容...",
"reportResult": "1",
// 医生信息
"reportDoctorId": "doc-001",
"reportDoctorName": "李医生",
"reportDatetime": "2025-12-16 10:30:00",
"reviewDoctorId": "doc-002",
"reviewDoctorName": "王医生",
"reviewDatetime": "2025-12-16 11:00:00",
// 医生签名
"reportUseAutograph": 1,
"reportAutograph": "https://cloud.com/sign/doc001.png?sign=xxx",
"reviewUseAutograph": 0,
"reviewAutograph": "",
// 机构信息
"institutionId": "org-001",
"institution": "北京协和医院",
"introduce": "影像科主任医师,从事影像诊断30年",
"reportTitle": "医学影像检查报告",
"reportSubtitle": "北京协和医院放射科",
// DICOM信息
"studyId": "study-uuid-xxx&node_type=1",
"dcmPath": "", // TODO
"dcmType": null, // TODO
// 操作痕迹
"trace": [
{
"description": "初步描述...",
"impression": "初步印象...",
"reportResult": "1",
"handleTime": "2025-12-16 10:30:00",
"realname": "李医生",
"type": 1
},
{
"description": "修改后描述...",
"impression": "修改后印象...",
"reportResult": "1",
"handleTime": "2025-12-16 11:00:00",
"realname": "王医生",
"type": 2
}
]
}
}
| 功能点 | PHP项目 | Java项目 |
|---|---|---|
| 草稿存储 | Redis缓存 | 数据库表 report_temp |
| 医生签名查询 | SQL中LEFT JOIN 3次doctors表 | Service层批量查询(selectBatchIds) |
| 性能优化 | SQL复杂JOIN | 拆分为主查询 + 批量查询,适合大表(2000万+) |
| DICOM路径 | 查询 dcm_path + 生成URL | TODO(待实现) |
| 云存储URL | uploadToCloud类 | TODO(待实现) |
| 事务处理 | 无 | @Transactional注解 |
| ID生成 | 手动调用UUID工具类 | MyBatis-Plus自动生成 |
DICOM文件路径获取
dcm_path 表云存储签名URL生成
ReportDetailQueryVO.java - 查询参数VOReportDetailDTO.java - 返回结果DTOReportTraceDTO.java - 操作痕迹DTOReportTemp.java - 报告草稿实体ReportRecord.java - 报告记录实体ReportDetailMapper.java - Mapper接口ReportDetailMapper.xml - SQL映射文件ReportTempMapper.java - 草稿MapperReportDetailService.java - 服务接口ReportDetailServiceImpl.java - 服务实现ReportController.java - 添加 /details 接口