Explorar el Código

1、 修改登录接口,如果是管理员关联的菜单返回全部
2、 新增报告详情接口(点击书写报告进入书写报告页面)

gengjunfang hace 1 mes
padre
commit
316da363ca
Se han modificado 22 ficheros con 1748 adiciones y 20 borrados
  1. 385 0
      doc/api/报告详情接口说明.md
  2. 2 2
      src/main/java/com/zskk/pacsonline/modules/doctor/dto/DoctorDTO.java
  3. 2 2
      src/main/java/com/zskk/pacsonline/modules/doctor/entity/Doctors.java
  4. 4 1
      src/main/java/com/zskk/pacsonline/modules/exam/dto/ExamListDTO.java
  5. 61 1
      src/main/java/com/zskk/pacsonline/modules/exam/service/impl/ExamsServiceImpl.java
  6. 36 0
      src/main/java/com/zskk/pacsonline/modules/report/controller/ReportController.java
  7. 202 0
      src/main/java/com/zskk/pacsonline/modules/report/dto/ReportDetailDTO.java
  8. 37 0
      src/main/java/com/zskk/pacsonline/modules/report/dto/ReportTraceDTO.java
  9. 70 0
      src/main/java/com/zskk/pacsonline/modules/report/entity/ReportRecord.java
  10. 64 0
      src/main/java/com/zskk/pacsonline/modules/report/entity/ReportTemp.java
  11. 44 0
      src/main/java/com/zskk/pacsonline/modules/report/mapper/ReportDetailMapper.java
  12. 15 0
      src/main/java/com/zskk/pacsonline/modules/report/mapper/ReportTempMapper.java
  13. 228 0
      src/main/java/com/zskk/pacsonline/modules/report/mapper/xml/ReportDetailMapper.xml
  14. 21 0
      src/main/java/com/zskk/pacsonline/modules/report/service/ReportDetailService.java
  15. 468 0
      src/main/java/com/zskk/pacsonline/modules/report/service/impl/ReportDetailServiceImpl.java
  16. 29 0
      src/main/java/com/zskk/pacsonline/modules/report/vo/ReportDetailQueryVO.java
  17. 7 0
      src/main/java/com/zskk/pacsonline/modules/system/mapper/MenuMapper.java
  18. 16 4
      src/main/java/com/zskk/pacsonline/modules/system/mapper/xml/MenuMapper.xml
  19. 7 0
      src/main/java/com/zskk/pacsonline/modules/system/service/MenuService.java
  20. 21 4
      src/main/java/com/zskk/pacsonline/modules/system/service/impl/LoginServiceImpl.java
  21. 5 0
      src/main/java/com/zskk/pacsonline/modules/system/service/impl/MenuServiceImpl.java
  22. 24 6
      src/main/java/com/zskk/pacsonline/utils/DataMaskUtil.java

+ 385 - 0
doc/api/报告详情接口说明.md

@@ -0,0 +1,385 @@
+# 报告详情接口实现说明
+
+## 接口信息
+
+**接口路径**: `POST /api/report/details`
+
+**功能说明**: 获取报告详细信息,用于报告书写页面。支持本地报告和远程报告两种模式。
+
+---
+
+## 实现逻辑
+
+### 1. 参数验证
+- `examId`: 检查ID(必填)
+- `isRemote`: 报告类型,1=远程报告,2=本地报告(必填)
+- `raId`: 远程申请ID(远程报告时必填)
+
+### 2. 查询报告基础信息(SQL实现)
+
+#### 本地报告查询
+```sql
+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复杂度。
+
+#### 远程报告查询
+```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。
+
+### 3. 自动创建报告记录
+如果本地报告不存在(report_id 为空),自动创建一条报告记录:
+```java
+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);
+```
+
+### 4. 补充机构信息
+根据 `institution_id` 查询机构的报告标题、副标题、医院简介:
+```java
+SysOrg org = sysOrgMapper.selectById(institutionId);
+detail.setReportTitle(org.getReportTitle());
+detail.setReportSubtitle(org.getReportSubtitle());
+detail.setHrInfo(org.getHrInfo());
+```
+
+### 5. TODO: 获取 DICOM 文件路径
+```java
+// TODO: 实现逻辑
+// 1. 根据 study_id 查询 dcm_path 表
+// 2. 检查 effective_date 是否过期
+// 3. 根据 dcm_type 生成访问 URL
+//    - dcm_type=1: 本地路径,直接返回
+//    - dcm_type=2/4: 云存储,调用云存储SDK生成临时访问URL
+```
+
+### 6. 处理医生签名URL
+根据每个医生的签名配置生成访问URL:
+```java
+// 判断逻辑:
+// 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 = ""; // 不使用签名
+}
+```
+
+### 7. 查询医生签名信息(批量查询优化)
+**性能优化**: 不在SQL中LEFT JOIN 3次doctors表,改为批量查询:
+```java
+// 收集需要查询的医生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());
+// ... 审核医生和确认医生同理
+```
+
+**优化效果**:
+- 原方案:SQL中LEFT JOIN 3次doctors表(1次主查询 = 1次DB访问)
+- 新方案:主查询 + 批量查询医生(1次主查询 + 1次批量查询 = 2次DB访问)
+- 对于2000万+数据的exams表,减少JOIN复杂度,提高查询稳定性
+
+### 8. 查询医生所属机构
+```java
+// 查询报告医生的机构信息
+if (StringUtils.isNotBlank(reportDoctorId)) {
+    Doctors doctor = doctorsMapper.selectById(reportDoctorId);
+    detail.setIntroduce(doctor.getIntroduce());
+
+    SysOrg org = sysOrgMapper.selectById(doctor.getOid());
+    detail.setInstitution(org.getName());
+}
+```
+
+### 9. 查询操作痕迹
+从 `report_record` 表查询报告的所有修改历史:
+```sql
+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
+```
+
+### 10. 加载草稿内容
+如果报告未完成(exam_status != 9 且 report_status != 9),从 `report_temp` 表加载当前医生的草稿:
+```java
+// 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) |
+
+---
+
+## 请求示例
+
+### 本地报告
+```json
+POST /api/report/details
+{
+  "examId": "exam-uuid-xxx",
+  "isRemote": 2,
+  "raId": ""
+}
+```
+
+### 远程报告
+```json
+POST /api/report/details
+{
+  "examId": "exam-uuid-xxx",
+  "isRemote": 1,
+  "raId": "ra-uuid-xxx"
+}
+```
+
+---
+
+## 返回数据示例
+
+```json
+{
+  "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项目的差异
+
+| 功能点 | 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自动生成 |
+
+---
+
+## 待实现功能(TODO)
+
+1. **DICOM文件路径获取**
+   - 查询 `dcm_path` 表
+   - 检查有效期
+   - 根据存储类型生成访问URL
+
+2. **云存储签名URL生成**
+   - 集成云存储SDK
+   - 实现临时访问URL生成(12小时有效期)
+
+---
+
+## 文件清单
+
+### 新增文件
+- `ReportDetailQueryVO.java` - 查询参数VO
+- `ReportDetailDTO.java` - 返回结果DTO
+- `ReportTraceDTO.java` - 操作痕迹DTO
+- `ReportTemp.java` - 报告草稿实体
+- `ReportRecord.java` - 报告记录实体
+- `ReportDetailMapper.java` - Mapper接口
+- `ReportDetailMapper.xml` - SQL映射文件
+- `ReportTempMapper.java` - 草稿Mapper
+- `ReportDetailService.java` - 服务接口
+- `ReportDetailServiceImpl.java` - 服务实现
+
+### 修改文件
+- `ReportController.java` - 添加 `/details` 接口
+
+---
+
+## 测试要点
+
+1. ✅ 本地报告查询(有报告记录)
+2. ✅ 本地报告查询(无报告记录,自动创建)
+3. ✅ 远程报告查询
+4. ✅ 草稿加载(报告未完成时)
+5. ✅ 草稿不加载(报告已完成时)
+6. ✅ 操作痕迹查询
+7. ✅ 医生签名处理(启用/未启用)
+8. ⏳ DICOM路径获取(待实现)
+9. ⏳ 云存储URL生成(待实现)

+ 2 - 2
src/main/java/com/zskk/pacsonline/modules/doctor/dto/DoctorDTO.java

@@ -105,10 +105,10 @@ public class DoctorDTO {
     private String introduce;
 
     @Schema(description = "签名存储1 本地  2移动云", example = "1")
-    private String autographType;
+    private Integer autographType;
 
     @Schema(description = "附件存储 1本地 2移动云", example = "1")
-    private String attachmentType;
+    private Integer attachmentType;
 
     @Schema(description = "创建人", example = "admin")
     private String createBy;

+ 2 - 2
src/main/java/com/zskk/pacsonline/modules/doctor/entity/Doctors.java

@@ -179,7 +179,7 @@ public class Doctors implements Serializable {
     @TableField("autograph_type")
     @Schema(description = "签名存储方式", example = "1")
     @Size(max = 255, message = "签名存储方式长度不能超过255个字符")
-    private String autographType;
+    private Integer autographType;
 
     /**
      * 附件存储 1本地 2移动云
@@ -187,7 +187,7 @@ public class Doctors implements Serializable {
     @TableField("attachment_type")
     @Schema(description = "附件存储方式", example = "1")
     @Size(max = 255, message = "附件存储方式长度不能超过255个字符")
-    private String attachmentType;
+    private Integer attachmentType;
 
     /**
      * 创建人

+ 4 - 1
src/main/java/com/zskk/pacsonline/modules/exam/dto/ExamListDTO.java

@@ -25,9 +25,12 @@ public class ExamListDTO {
     @Schema(description = "机构名称")
     private String institutionName;
 
-    @Schema(description = "患者姓名")
+    @Schema(description = "患者姓名(脱敏后)")
     private String name;
 
+    @Schema(description = "患者真实姓名(用于编辑时展示,不脱敏)")
+    private String realName;
+
     @Schema(description = "病历号")
     private String patientNum;
 

+ 61 - 1
src/main/java/com/zskk/pacsonline/modules/exam/service/impl/ExamsServiceImpl.java

@@ -50,7 +50,14 @@ public class ExamsServiceImpl extends ServiceImpl<ExamsMapper, Exams> implements
         if (queryVO.getPageNum() != null && queryVO.getPageSize() != null) {
             PageHelper.startPage(queryVO.getPageNum(), queryVO.getPageSize());
         }
-        return examsMapper.selectExamList(queryVO);
+
+        // 查询数据
+        List<ExamListDTO> list = examsMapper.selectExamList(queryVO);
+
+        // 数据脱敏处理
+        maskExamListSensitiveData(list);
+
+        return list;
     }
 
     @Override
@@ -255,4 +262,57 @@ public class ExamsServiceImpl extends ServiceImpl<ExamsMapper, Exams> implements
         log.debug("敏感数据脱敏处理完成,共处理{}条记录", list.size());
     }
 
+    /**
+     * 管理端检查列表敏感数据脱敏处理
+     * 对姓名、手机号、身份证号进行脱敏,并保存真实姓名用于编辑
+     *
+     * 脱敏规则(与PHP项目保持一致):
+     * - 手机号:保留前3位和后4位,中间用****替换(138****5678)
+     * - 姓名:保留前3个字符,后面用**替换(欧阳娜**)
+     *        特殊机构(04500002)不脱敏姓名
+     * - 身份证:保留前6位和最后1位,中间用*替换(110101***********4)
+     *
+     * 真实姓名保存:
+     * - 在脱敏前将真实姓名保存到 realName 字段
+     * - 用于编辑弹框时展示,避免再次查询数据库
+     *
+     * 内网访问不脱敏:
+     * - 检查HTTP请求头 ZSKK-CRB-NETWORK 是否为 INNER
+     *
+     * @param list 检查列表
+     */
+    private void maskExamListSensitiveData(List<ExamListDTO> list) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+
+        // 特殊机构ID,不脱敏姓名(与PHP项目保持一致)
+        final String SPECIAL_INSTITUTION_ID = "04500002";
+
+        for (ExamListDTO dto : list) {
+            // 0. 保存真实姓名(脱敏前)
+            if (StringUtils.isNotBlank(dto.getName())) {
+                dto.setRealName(dto.getName());
+            }
+
+            // 1. 姓名脱敏(特殊机构除外)
+            if (StringUtils.isNotBlank(dto.getName())
+                    && !SPECIAL_INSTITUTION_ID.equals(dto.getInstitutionId())) {
+                dto.setName(DataMaskUtil.maskName(dto.getName()));
+            }
+
+            // 2. 身份证脱敏(所有机构都脱敏)
+            if (StringUtils.isNotBlank(dto.getCardNum())) {
+                dto.setCardNum(DataMaskUtil.maskIdCard(dto.getCardNum()));
+            }
+
+            // 3. 手机号脱敏(所有机构都脱敏)
+            if (StringUtils.isNotBlank(dto.getPhone())) {
+                dto.setPhone(DataMaskUtil.maskPhone(dto.getPhone()));
+            }
+        }
+
+        log.debug("管理端检查列表敏感数据脱敏处理完成,共处理{}条记录", list.size());
+    }
+
 }

+ 36 - 0
src/main/java/com/zskk/pacsonline/modules/report/controller/ReportController.java

@@ -3,8 +3,11 @@ package com.zskk.pacsonline.modules.report.controller;
 import com.zskk.pacsonline.component.aop.SystemLogHandler;
 import com.zskk.pacsonline.component.response.RestResult;
 import com.zskk.pacsonline.modules.report.dto.ReportDTO;
+import com.zskk.pacsonline.modules.report.dto.ReportDetailDTO;
+import com.zskk.pacsonline.modules.report.service.ReportDetailService;
 import com.zskk.pacsonline.modules.report.service.ReportService;
 import com.zskk.pacsonline.modules.report.vo.ReportCreateVO;
+import com.zskk.pacsonline.modules.report.vo.ReportDetailQueryVO;
 import com.zskk.pacsonline.modules.report.vo.ReportUpdateVO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
@@ -38,6 +41,9 @@ public class ReportController {
     @Resource
     private ReportService reportService;
 
+    @Resource
+    private ReportDetailService reportDetailService;
+
     /**
      * 创建报告
      */
@@ -154,4 +160,34 @@ public class ReportController {
             return RestResult.error("查询失败:" + e.getMessage());
         }
     }
+
+    /**
+     * 获取报告详情(用于报告书写)
+     * 支持本地报告和远程报告
+     */
+    @Operation(
+        summary = "获取报告详情",
+        description = "获取报告详细信息,用于报告书写页面。" +
+                      "支持本地报告(is_remote=2)和远程报告(is_remote=1)。" +
+                      "如果报告不存在会自动创建。包含检查信息、报告内容、医生信息、操作痕迹等完整数据"
+    )
+    @ApiResponses(value = {
+        @ApiResponse(responseCode = "200", description = "查询成功"),
+        @ApiResponse(responseCode = "400", description = "参数错误"),
+        @ApiResponse(responseCode = "500", description = "服务器内部错误")
+    })
+    @SystemLogHandler("获取报告详情|查询")
+    @PostMapping("/details")
+    public RestResult<?> getReportDetails(
+            @Parameter(description = "查询参数", required = true, schema = @Schema(implementation = ReportDetailQueryVO.class))
+            @RequestBody @Valid ReportDetailQueryVO queryVO) {
+        try {
+            ReportDetailDTO detail = reportDetailService.getReportDetail(queryVO);
+            return RestResult.ok("查询成功", detail);
+        } catch (RuntimeException e) {
+            return RestResult.error(e.getMessage());
+        } catch (Exception e) {
+            return RestResult.error("查询失败:" + e.getMessage());
+        }
+    }
 }

+ 202 - 0
src/main/java/com/zskk/pacsonline/modules/report/dto/ReportDetailDTO.java

@@ -0,0 +1,202 @@
+package com.zskk.pacsonline.modules.report.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 报告详情返回DTO
+ *
+ * @author system
+ * @since 2025-12-16
+ */
+@Data
+@Schema(description = "报告详情")
+public class ReportDetailDTO {
+
+    // ==================== 检查基础信息 ====================
+    @Schema(description = "检查ID")
+    private String examId;
+
+    @Schema(description = "报告类型:1=远程,2=本地")
+    private Integer isRemote;
+
+    @Schema(description = "病历号")
+    private String patientNum;
+
+    @Schema(description = "患者姓名")
+    private String name;
+
+    @Schema(description = "性别")
+    private String sex;
+
+    @Schema(description = "年龄")
+    private String age;
+
+    @Schema(description = "手机号")
+    private String phone;
+
+    @Schema(description = "检查类别")
+    private String examClass;
+
+    @Schema(description = "检查时间")
+    private String examDatetime;
+
+    @Schema(description = "检查状态")
+    private Integer examStatus;
+
+    @Schema(description = "检查项目")
+    private String examProject;
+
+    @Schema(description = "检查号")
+    private String accessionNum;
+
+    @Schema(description = "检查部位")
+    private String bodyPart;
+
+    @Schema(description = "住院号")
+    private String hopitalizedNo;
+
+    @Schema(description = "检查子类")
+    private String examSubClass;
+
+    @Schema(description = "互认状态:0=不互认,1=互认")
+    private Integer hrStatus;
+
+    // ==================== 报告信息 ====================
+    @Schema(description = "报告ID")
+    private String reportId;
+
+    @Schema(description = "影像所见(报告描述)")
+    private String description;
+
+    @Schema(description = "诊断印象(诊断结论)")
+    private String impression;
+
+    @Schema(description = "诊断")
+    private String diagnose;
+
+    @Schema(description = "报告结果:1=阴性,2=阳性")
+    private String reportResult;
+
+    @Schema(description = "二维码")
+    private String qrCode;
+
+    // ==================== 医生信息 ====================
+    @Schema(description = "报告医生ID")
+    private String reportDoctorId;
+
+    @Schema(description = "报告医生姓名")
+    private String reportDoctorName;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    @Schema(description = "报告时间")
+    private Date reportDatetime;
+
+    @Schema(description = "审核医生ID")
+    private String reviewDoctorId;
+
+    @Schema(description = "审核医生姓名")
+    private String reviewDoctorName;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    @Schema(description = "审核时间")
+    private Date reviewDatetime;
+
+    @Schema(description = "确认医生ID")
+    private String confirmDoctorId;
+
+    @Schema(description = "确认医生姓名")
+    private String confirmDoctorName;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    @Schema(description = "确认时间")
+    private Date confirmDatetime;
+
+    // ==================== 医生签名信息 ====================
+    @Schema(description = "报告医生是否使用签名:0=否,1=是")
+    private Integer reportUseAutograph;
+
+    @Schema(description = "报告医生签名URL")
+    private String reportAutograph;
+
+    @Schema(description = "报告医生签名存储类型:1=本地,2=云存储")
+    private Integer reportAutographType;
+
+    @Schema(description = "审核医生是否使用签名:0=否,1=是")
+    private Integer reviewUseAutograph;
+
+    @Schema(description = "审核医生签名URL")
+    private String reviewAutograph;
+
+    @Schema(description = "审核医生签名存储类型:1=本地,2=云存储")
+    private Integer reviewAutographType;
+
+    @Schema(description = "确认医生是否使用签名:0=否,1=是")
+    private Integer confirmUseAutograph;
+
+    @Schema(description = "确认医生签名URL")
+    private String confirmAutograph;
+
+    @Schema(description = "确认医生签名存储类型:1=本地,2=云存储")
+    private Integer confirmAutographType;
+
+    // ==================== 机构信息 ====================
+    @Schema(description = "机构ID")
+    private String institutionId;
+
+    @Schema(description = "报告医生所属机构名称")
+    private String institution;
+
+    @Schema(description = "医生简介")
+    private String introduce;
+
+    @Schema(description = "报告标题")
+    private String reportTitle;
+
+    @Schema(description = "报告副标题")
+    private String reportSubtitle;
+
+    @Schema(description = "医院简介")
+    private String hrInfo;
+
+    // ==================== DICOM 信息 ====================
+    @Schema(description = "Study ID")
+    private String studyId;
+
+    @Schema(description = "DICOM 文件路径")
+    private String dcmPath;
+
+    @Schema(description = "DICOM 存储类型:1=本地,2=云存储")
+    private Integer dcmType;
+
+    @Schema(description = "节点类型")
+    private Integer nodeType;
+
+    // ==================== 远程报告特有字段 ====================
+    @Schema(description = "远程申请ID")
+    private String remoteApplicationId;
+
+    @Schema(description = "本地机构名称(远程报告)")
+    private String localInstitutionName;
+
+    @Schema(description = "申请医生姓名(远程报告)")
+    private String reqDoctorName;
+
+    @Schema(description = "远程科室(远程报告)")
+    private String remoteDepartment;
+
+    @Schema(description = "报告状态(远程报告)")
+    private Integer reportStatus;
+
+    // ==================== 操作痕迹 ====================
+    @Schema(description = "操作痕迹列表")
+    private List<ReportTraceDTO> trace;
+
+    // ==================== 其他 ====================
+    @Schema(description = "是否需要书写报告:0=否,1=是")
+    private Integer writeReport;
+}

+ 37 - 0
src/main/java/com/zskk/pacsonline/modules/report/dto/ReportTraceDTO.java

@@ -0,0 +1,37 @@
+package com.zskk.pacsonline.modules.report.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 报告操作痕迹DTO
+ *
+ * @author system
+ * @since 2025-12-16
+ */
+@Data
+@Schema(description = "报告操作痕迹")
+public class ReportTraceDTO {
+
+    @Schema(description = "影像所见(报告描述)")
+    private String description;
+
+    @Schema(description = "诊断印象(诊断结论)")
+    private String impression;
+
+    @Schema(description = "报告结果:1=阴性,2=阳性")
+    private String reportResult;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    @Schema(description = "操作时间")
+    private Date handleTime;
+
+    @Schema(description = "操作医生姓名")
+    private String realname;
+
+    @Schema(description = "操作类型:1=书写,2=审核,3=确认")
+    private Integer type;
+}

+ 70 - 0
src/main/java/com/zskk/pacsonline/modules/report/entity/ReportRecord.java

@@ -0,0 +1,70 @@
+package com.zskk.pacsonline.modules.report.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 报告操作记录表
+ *
+ * @author system
+ * @since 2025-12-16
+ */
+@Getter
+@Setter
+@TableName("report_record")
+public class ReportRecord implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.ASSIGN_UUID)
+    private String id;
+
+    /**
+     * 报告ID
+     */
+    @TableField("report_id")
+    private String reportId;
+
+    /**
+     * 医生ID
+     */
+    @TableField("doctor_id")
+    private String doctorId;
+
+    /**
+     * 影像所见
+     */
+    @TableField("description")
+    private String description;
+
+    /**
+     * 诊断印象
+     */
+    @TableField("impression")
+    private String impression;
+
+    /**
+     * 报告结果:1=阴性,2=阳性
+     */
+    @TableField("report_result")
+    private String reportResult;
+
+    /**
+     * 操作类型:1=书写,2=审核,3=确认
+     */
+    @TableField("type")
+    private Integer type;
+
+    /**
+     * 创建时间
+     */
+    @TableField("create_time")
+    private Date createTime;
+}

+ 64 - 0
src/main/java/com/zskk/pacsonline/modules/report/entity/ReportTemp.java

@@ -0,0 +1,64 @@
+package com.zskk.pacsonline.modules.report.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 报告暂存表
+ *
+ * @author system
+ * @since 2025-12-16
+ */
+@Getter
+@Setter
+@TableName("report_temp")
+public class ReportTemp implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.ASSIGN_UUID)
+    private String id;
+
+    /**
+     * 报告ID
+     */
+    @TableField("report_id")
+    private String reportId;
+
+    /**
+     * 医生ID
+     */
+    @TableField("doctor_id")
+    private String doctorId;
+
+    /**
+     * 影像所见
+     */
+    @TableField("description")
+    private String description;
+
+    /**
+     * 诊断印象
+     */
+    @TableField("impression")
+    private String impression;
+
+    /**
+     * 创建时间
+     */
+    @TableField("create_time")
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField("update_time")
+    private Date updateTime;
+}

+ 44 - 0
src/main/java/com/zskk/pacsonline/modules/report/mapper/ReportDetailMapper.java

@@ -0,0 +1,44 @@
+package com.zskk.pacsonline.modules.report.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zskk.pacsonline.modules.report.dto.ReportDetailDTO;
+import com.zskk.pacsonline.modules.report.dto.ReportTraceDTO;
+import com.zskk.pacsonline.modules.report.entity.Report;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 报告详情查询 Mapper
+ *
+ * @author system
+ * @since 2025-12-16
+ */
+@Mapper
+public interface ReportDetailMapper extends BaseMapper<Report> {
+
+    /**
+     * 查询本地报告详情
+     *
+     * @param examId 检查ID
+     * @return 报告详情
+     */
+    ReportDetailDTO selectLocalReportDetail(@Param("examId") String examId);
+
+    /**
+     * 查询远程报告详情
+     *
+     * @param raId 远程申请ID
+     * @return 报告详情
+     */
+    ReportDetailDTO selectRemoteReportDetail(@Param("raId") String raId);
+
+    /**
+     * 查询报告操作痕迹
+     *
+     * @param reportId 报告ID
+     * @return 操作痕迹列表
+     */
+    List<ReportTraceDTO> selectReportTrace(@Param("reportId") String reportId);
+}

+ 15 - 0
src/main/java/com/zskk/pacsonline/modules/report/mapper/ReportTempMapper.java

@@ -0,0 +1,15 @@
+package com.zskk.pacsonline.modules.report.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zskk.pacsonline.modules.report.entity.ReportTemp;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 报告暂存 Mapper
+ *
+ * @author system
+ * @since 2025-12-16
+ */
+@Mapper
+public interface ReportTempMapper extends BaseMapper<ReportTemp> {
+}

+ 228 - 0
src/main/java/com/zskk/pacsonline/modules/report/mapper/xml/ReportDetailMapper.xml

@@ -0,0 +1,228 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.zskk.pacsonline.modules.report.mapper.ReportDetailMapper">
+
+    <!-- 本地报告详情结果映射 -->
+    <resultMap id="LocalReportDetailMap" type="com.zskk.pacsonline.modules.report.dto.ReportDetailDTO">
+        <!-- 检查基础信息 -->
+        <result column="exam_id" property="examId"/>
+        <result column="patient_num" property="patientNum"/>
+        <result column="name" property="name"/>
+        <result column="sex" property="sex"/>
+        <result column="age" property="age"/>
+        <result column="phone" property="phone"/>
+        <result column="exam_class" property="examClass"/>
+        <result column="exam_datetime" property="examDatetime"/>
+        <result column="exam_status" property="examStatus"/>
+        <result column="exam_project" property="examProject"/>
+        <result column="accession_num" property="accessionNum"/>
+        <result column="hopitalized_no" property="hopitalizedNo"/>
+        <result column="body_part" property="bodyPart"/>
+        <result column="exam_sub_class" property="examSubClass"/>
+        <result column="write_report" property="writeReport"/>
+        <result column="study_id" property="studyId"/>
+        <result column="node_type" property="nodeType"/>
+        <result column="institution_id" property="institutionId"/>
+
+        <!-- 报告信息 -->
+        <result column="report_id" property="reportId"/>
+        <result column="description" property="description"/>
+        <result column="impression" property="impression"/>
+        <result column="diagnose" property="diagnose"/>
+        <result column="report_result" property="reportResult"/>
+        <result column="qr_code" property="qrCode"/>
+        <result column="remote_application_id" property="remoteApplicationId"/>
+        <result column="hr_status" property="hrStatus"/>
+
+        <!-- 医生信息 -->
+        <result column="report_doctor_id" property="reportDoctorId"/>
+        <result column="report_doctor_name" property="reportDoctorName"/>
+        <result column="report_datetime" property="reportDatetime"/>
+        <result column="review_doctor_id" property="reviewDoctorId"/>
+        <result column="review_doctor_name" property="reviewDoctorName"/>
+        <result column="review_datetime" property="reviewDatetime"/>
+        <result column="confirm_doctor_id" property="confirmDoctorId"/>
+        <result column="confirm_doctor_name" property="confirmDoctorName"/>
+        <result column="confirm_datetime" property="confirmDatetime"/>
+    </resultMap>
+
+    <!-- 远程报告详情结果映射 -->
+    <resultMap id="RemoteReportDetailMap" type="com.zskk.pacsonline.modules.report.dto.ReportDetailDTO">
+        <!-- 检查基础信息 -->
+        <result column="exam_id" property="examId"/>
+        <result column="patient_num" property="patientNum"/>
+        <result column="name" property="name"/>
+        <result column="sex" property="sex"/>
+        <result column="age" property="age"/>
+        <result column="phone" property="phone"/>
+        <result column="exam_class" property="examClass"/>
+        <result column="exam_datetime" property="examDatetime"/>
+        <result column="exam_project" property="examProject"/>
+        <result column="accession_num" property="accessionNum"/>
+        <result column="hopitalized_no" property="hopitalizedNo"/>
+        <result column="body_part" property="bodyPart"/>
+        <result column="study_id" property="studyId"/>
+        <result column="institution_id" property="institutionId"/>
+
+        <!-- 远程报告特有字段 -->
+        <result column="report_status" property="reportStatus"/>
+        <result column="local_institution_name" property="localInstitutionName"/>
+        <result column="req_doctor_name" property="reqDoctorName"/>
+        <result column="remote_department" property="remoteDepartment"/>
+
+        <!-- 报告信息 -->
+        <result column="report_id" property="reportId"/>
+        <result column="description" property="description"/>
+        <result column="impression" property="impression"/>
+        <result column="diagnose" property="diagnose"/>
+        <result column="report_result" property="reportResult"/>
+        <result column="qr_code" property="qrCode"/>
+        <result column="remote_application_id" property="remoteApplicationId"/>
+
+        <!-- 医生信息 -->
+        <result column="report_doctor_id" property="reportDoctorId"/>
+        <result column="report_doctor_name" property="reportDoctorName"/>
+        <result column="report_datetime" property="reportDatetime"/>
+        <result column="review_doctor_id" property="reviewDoctorId"/>
+        <result column="review_doctor_name" property="reviewDoctorName"/>
+        <result column="review_datetime" property="reviewDatetime"/>
+        <result column="confirm_doctor_id" property="confirmDoctorId"/>
+        <result column="confirm_doctor_name" property="confirmDoctorName"/>
+        <result column="confirm_datetime" property="confirmDatetime"/>
+    </resultMap>
+
+    <!-- 操作痕迹结果映射 -->
+    <resultMap id="ReportTraceMap" type="com.zskk.pacsonline.modules.report.dto.ReportTraceDTO">
+        <result column="description" property="description"/>
+        <result column="impression" property="impression"/>
+        <result column="report_result" property="reportResult"/>
+        <result column="handle_time" property="handleTime"/>
+        <result column="realname" property="realname"/>
+        <result column="type" property="type"/>
+    </resultMap>
+
+    <!-- 查询本地报告详情 -->
+    <select id="selectLocalReportDetail" resultMap="LocalReportDetailMap">
+        SELECT
+
+            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,
+            e.accession_num,
+            e.hopitalized_no,
+            e.write_report,
+            CONCAT(e.studyid, '&amp;node_type=', e.node_type) AS study_id,
+            e.node_type,
+            e.institution_id,
+
+
+            reg.body_part,
+            reg.exam_sub_class,
+
+
+            r.id AS report_id,
+            r.description,
+            r.impression,
+            r.diagnose,
+            r.report_result,
+            r.qr_code,
+            r.remote_application_id,
+            r.hr_status,
+
+
+            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}
+    </select>
+
+    <!-- 查询远程报告详情 -->
+    <select id="selectRemoteReportDetail" resultMap="RemoteReportDetailMap">
+        SELECT
+            -- 远程申请信息(remote_application表)
+            ra.exam_id,
+            ra.patient_num,
+            ra.name,
+            ra.sex,
+            ra.age,
+            ra.phone,
+            ra.exam_class,
+            ra.exam_datetime,
+            ra.exam_project,
+            ra.accession_num,
+            ra.hopitalized_no,
+            ra.body_part,
+            ra.study_id,
+            ra.local_institution_id AS institution_id,
+            ra.report_status,
+            ra.local_institution_name,
+            ra.req_doctor_name,
+            ra.remote_department,
+
+            -- 报告信息(report表,通过 remote_application_id 关联)
+            r.id AS report_id,
+            r.description,
+            r.impression,
+            r.diagnose,
+            r.report_result,
+            r.qr_code,
+            r.remote_application_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}
+    </select>
+
+    <!-- 查询报告操作痕迹 -->
+    <select id="selectReportTrace" resultMap="ReportTraceMap">
+        SELECT
+            rr.description,
+            rr.impression,
+            rr.report_result,
+            rr.create_time AS handle_time,
+            u.realname,
+            rr.type
+        FROM
+            report_record rr
+            LEFT JOIN doctors d ON rr.doctor_id = d.id
+            LEFT JOIN sys_user u on u.id=d.uid
+        WHERE
+            rr.report_id = #{reportId}
+        ORDER BY
+            rr.create_time ASC
+    </select>
+
+</mapper>

+ 21 - 0
src/main/java/com/zskk/pacsonline/modules/report/service/ReportDetailService.java

@@ -0,0 +1,21 @@
+package com.zskk.pacsonline.modules.report.service;
+
+import com.zskk.pacsonline.modules.report.dto.ReportDetailDTO;
+import com.zskk.pacsonline.modules.report.vo.ReportDetailQueryVO;
+
+/**
+ * 报告详情服务接口
+ *
+ * @author system
+ * @since 2025-12-16
+ */
+public interface ReportDetailService {
+
+    /**
+     * 获取报告详情
+     *
+     * @param queryVO 查询参数
+     * @return 报告详情
+     */
+    ReportDetailDTO getReportDetail(ReportDetailQueryVO queryVO);
+}

+ 468 - 0
src/main/java/com/zskk/pacsonline/modules/report/service/impl/ReportDetailServiceImpl.java

@@ -0,0 +1,468 @@
+package com.zskk.pacsonline.modules.report.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zskk.pacsonline.modules.doctor.entity.Doctors;
+import com.zskk.pacsonline.modules.doctor.mapper.DoctorsMapper;
+import com.zskk.pacsonline.modules.org.entity.SysOrg;
+import com.zskk.pacsonline.modules.org.mapper.SysOrgMapper;
+import com.zskk.pacsonline.modules.report.dto.ReportDetailDTO;
+import com.zskk.pacsonline.modules.report.dto.ReportTraceDTO;
+import com.zskk.pacsonline.modules.report.entity.Report;
+import com.zskk.pacsonline.modules.report.entity.ReportTemp;
+import com.zskk.pacsonline.modules.report.mapper.ReportDetailMapper;
+import com.zskk.pacsonline.modules.report.mapper.ReportMapper;
+import com.zskk.pacsonline.modules.report.mapper.ReportTempMapper;
+import com.zskk.pacsonline.modules.report.service.ReportDetailService;
+import com.zskk.pacsonline.modules.report.vo.ReportDetailQueryVO;
+import com.zskk.pacsonline.security.LoginUser;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import javax.annotation.Resource;
+import java.util.*;
+
+/**
+ * 报告详情服务实现类
+ *
+ * @author system
+ * @since 2025-12-16
+ */
+@Slf4j
+@Service
+public class ReportDetailServiceImpl implements ReportDetailService {
+
+    @Resource
+    private ReportDetailMapper reportDetailMapper;
+
+    @Resource
+    private ReportMapper reportMapper;
+
+    @Resource
+    private ReportTempMapper reportTempMapper;
+
+    @Resource
+    private SysOrgMapper sysOrgMapper;
+
+    @Resource
+    private DoctorsMapper doctorsMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ReportDetailDTO getReportDetail(ReportDetailQueryVO queryVO) {
+        String examId = queryVO.getExamId();
+        Integer isRemote = queryVO.getIsRemote();
+        String raId = queryVO.getRaId();
+
+        ReportDetailDTO detail;
+
+        // 1. 根据 raId 判断查询本地报告还是远程报告
+        if (StringUtils.isBlank(raId)) {
+            // 本地报告详情
+            detail = reportDetailMapper.selectLocalReportDetail(examId);
+
+            // 如果报告不存在,自动创建一条报告记录
+            if (detail == null || StringUtils.isBlank(detail.getReportId())) {
+                createReportIfNotExists(examId, detail);
+            }
+        } else {
+            // 远程报告详情
+            detail = reportDetailMapper.selectRemoteReportDetail(raId);
+        }
+
+        if (detail == null) {
+            throw new RuntimeException("检查信息不存在");
+        }
+
+        // 2. 补充机构信息(报告标题、副标题、医院简介)
+        supplementInstitutionInfo(detail);
+
+        // 3. TODO: 获取 DICOM 文件路径
+        // supplementDcmPath(detail);
+
+        // 4. 查询医生签名信息(批量查询3个医生)
+        queryDoctorSignatures(detail);
+
+        // 5. 处理医生签名 URL(根据存储类型生成访问路径)
+        processAutographUrls(detail);
+
+        // 6. 查询报告医生所属机构信息
+        queryDoctorInstitution(detail);
+
+        // 7. 设置基础字段
+        detail.setExamId(examId);
+        detail.setIsRemote(isRemote);
+
+        // 8. 查询操作痕迹
+        List<ReportTraceDTO> trace = getReportTrace(detail.getReportId());
+        detail.setTrace(trace);
+
+        // 9. 如果报告未完成,加载草稿内容
+        loadDraftIfNotCompleted(detail);
+
+        return detail;
+    }
+
+    /**
+     * 如果报告不存在,自动创建
+     * MyBatis-Plus 会自动生成 UUID(实体类配置了 @TableId(type = IdType.ASSIGN_UUID))
+     *
+     * @param examId 检查ID
+     * @param detail 报告详情
+     */
+    private void createReportIfNotExists(String examId, ReportDetailDTO detail) {
+        Report report = new Report();
+        report.setExamId(examId);
+        report.setType("1"); // 本地报告
+        report.setCreateTime(new Date());
+
+        // MyBatis-Plus 会自动生成 ID
+        int rows = reportMapper.insert(report);
+        if (rows > 0) {
+            log.info("自动创建报告成功,reportId={}", report.getId());
+            if (detail != null) {
+                detail.setReportId(report.getId());
+            }
+        }
+    }
+
+    /**
+     * 补充机构信息(报告标题、副标题、医院简介)
+     *
+     * @param detail 报告详情
+     */
+    private void supplementInstitutionInfo(ReportDetailDTO detail) {
+        if (StringUtils.isBlank(detail.getInstitutionId())) {
+            return;
+        }
+
+        SysOrg org = sysOrgMapper.selectById(detail.getInstitutionId());
+        if (org != null) {
+            detail.setReportTitle(org.getReportTitle());
+            detail.setReportSubtitle(org.getReportSubtitle());
+            detail.setHrInfo(org.getHrInfo());
+        }
+    }
+
+    /**
+     * 查询医生签名信息(批量查询3个医生)
+     * 性能优化:使用批量查询代替SQL中的3次LEFT JOIN
+     *
+     * @param detail 报告详情
+     */
+    private void queryDoctorSignatures(ReportDetailDTO detail) {
+        // 收集需要查询的医生ID
+        List<String> doctorIds = new ArrayList<>();
+        if (StringUtils.isNotBlank(detail.getReportDoctorId())) {
+            doctorIds.add(detail.getReportDoctorId());
+        }
+        if (StringUtils.isNotBlank(detail.getReviewDoctorId())) {
+            doctorIds.add(detail.getReviewDoctorId());
+        }
+        if (StringUtils.isNotBlank(detail.getConfirmDoctorId())) {
+            doctorIds.add(detail.getConfirmDoctorId());
+        }
+
+        // 如果没有医生ID,直接返回
+        if (doctorIds.isEmpty()) {
+            return;
+        }
+
+        // 批量查询医生信息
+        List<Doctors> doctors = doctorsMapper.selectBatchIds(doctorIds);
+        if (doctors == null || doctors.isEmpty()) {
+            return;
+        }
+
+        // 将医生列表转换为Map,方便按ID查找
+        Map<String, Doctors> doctorMap = new HashMap<>();
+        for (Doctors doctor : doctors) {
+            doctorMap.put(doctor.getId(), doctor);
+        }
+
+        // 填充报告医生签名信息
+        if (StringUtils.isNotBlank(detail.getReportDoctorId())) {
+            Doctors reportDoctor = doctorMap.get(detail.getReportDoctorId());
+            if (reportDoctor != null) {
+                detail.setReportUseAutograph(reportDoctor.getIsUseAutograph());
+                detail.setReportAutograph(reportDoctor.getAutograph());
+                detail.setReportAutographType(reportDoctor.getAutographType());
+            }
+        }
+
+        // 填充审核医生签名信息
+        if (StringUtils.isNotBlank(detail.getReviewDoctorId())) {
+            Doctors reviewDoctor = doctorMap.get(detail.getReviewDoctorId());
+            if (reviewDoctor != null) {
+                detail.setReviewUseAutograph(reviewDoctor.getIsUseAutograph());
+                detail.setReviewAutograph(reviewDoctor.getAutograph());
+                detail.setReviewAutographType(reviewDoctor.getAutographType());
+            }
+        }
+
+        // 填充确认医生签名信息
+        if (StringUtils.isNotBlank(detail.getConfirmDoctorId())) {
+            Doctors confirmDoctor = doctorMap.get(detail.getConfirmDoctorId());
+            if (confirmDoctor != null) {
+                detail.setConfirmUseAutograph(confirmDoctor.getIsUseAutograph());
+                detail.setConfirmAutograph(confirmDoctor.getAutograph());
+                detail.setConfirmAutographType(confirmDoctor.getAutographType());
+            }
+        }
+    }
+
+    /**
+     * TODO: 获取 DICOM 文件路径
+     *
+     * 实现逻辑:
+     * 1. 根据 study_id 查询 dcm_path 表
+     * 2. 检查 effective_date 是否过期
+     * 3. 根据 dcm_type 生成访问 URL
+     *
+     * @param detail 报告详情
+     */
+    // private void supplementDcmPath(ReportDetailDTO detail) {
+    //     // TODO: 实现 DICOM 路径查询和 URL 生成
+    // }
+
+    /**
+     * 处理医生签名 URL
+     * 根据存储类型(本地/云存储)生成对应的访问路径
+     *
+     * @param detail 报告详情
+     */
+    private void processAutographUrls(ReportDetailDTO detail) {
+        // 报告医生签名
+        detail.setReportAutograph(
+            generateAutographUrl(
+                detail.getReportUseAutograph(),
+                detail.getReportAutograph(),
+                detail.getReportAutographType()
+            )
+        );
+
+        // 审核医生签名
+        detail.setReviewAutograph(
+            generateAutographUrl(
+                detail.getReviewUseAutograph(),
+                detail.getReviewAutograph(),
+                detail.getReviewAutographType()
+            )
+        );
+
+        // 确认医生签名
+        detail.setConfirmAutograph(
+            generateAutographUrl(
+                detail.getConfirmUseAutograph(),
+                detail.getConfirmAutograph(),
+                detail.getConfirmAutographType()
+            )
+        );
+    }
+
+    /**
+     * 生成签名访问 URL
+     *
+     * @param useAutograph 是否使用签名:0=否,1=是
+     * @param autograph 签名文件路径
+     * @param autographType 存储类型:1=本地,2=云存储
+     * @return 签名 URL,如果不使用签名或路径为空,返回空字符串
+     */
+    private String generateAutographUrl(Integer useAutograph, String autograph, Integer autographType) {
+        // 如果不使用签名,返回空
+        if (useAutograph == null || useAutograph != 1) {
+            return "";
+        }
+
+        // 如果签名路径为空,返回空
+        if (StringUtils.isBlank(autograph)) {
+            return "";
+        }
+
+        // 根据存储类型生成 URL
+        if (autographType != null && autographType == 2) {
+            // TODO: 云存储,调用云存储 SDK 生成临时访问 URL
+            // return cloudStorageService.generateSignedUrl(autograph);
+            return autograph; // 暂时直接返回路径
+        } else {
+            // 本地存储,直接返回路径
+            return autograph;
+        }
+    }
+
+    /**
+     * 查询报告医生所属机构信息
+     *
+     * @param detail 报告详情
+     */
+    private void queryDoctorInstitution(ReportDetailDTO detail) {
+        if (StringUtils.isBlank(detail.getReportDoctorId())) {
+            detail.setInstitution("");
+            detail.setIntroduce("");
+            return;
+        }
+
+        // 查询医生信息(包含机构ID和简介)
+        Doctors doctor = doctorsMapper.selectById(detail.getReportDoctorId());
+        if (doctor == null) {
+            detail.setInstitution("");
+            detail.setIntroduce("");
+            return;
+        }
+
+        // 设置医生简介
+        detail.setIntroduce(StringUtils.defaultString(doctor.getIntroduce(), ""));
+
+        // 查询机构名称
+        if (StringUtils.isNotBlank(doctor.getOid())) {
+            SysOrg org = sysOrgMapper.selectById(doctor.getOid());
+            if (org != null) {
+                detail.setInstitution(org.getName());
+            } else {
+                detail.setInstitution("");
+            }
+        } else {
+            detail.setInstitution("");
+        }
+    }
+
+    /**
+     * 获取报告操作痕迹
+     *
+     * @param reportId 报告ID
+     * @return 操作痕迹列表
+     */
+    private List<ReportTraceDTO> getReportTrace(String reportId) {
+        if (StringUtils.isBlank(reportId)) {
+            return List.of();
+        }
+
+        return reportDetailMapper.selectReportTrace(reportId);
+    }
+
+    /**
+     * 如果报告未完成,加载草稿内容
+     * 当报告处于完成状态(exam_status=9 或 report_status=9)时,不加载草稿
+     *
+     * @param detail 报告详情
+     */
+    private void loadDraftIfNotCompleted(ReportDetailDTO detail) {
+        // 检查报告是否已完成
+        if (isReportCompleted(detail)) {
+            return;
+        }
+
+        // 获取当前登录用户ID
+        String currentUserId = getCurrentUserId();
+        if (StringUtils.isBlank(currentUserId)) {
+            return;
+        }
+
+        // 查询草稿
+        ReportTemp draft = queryDraft(detail.getReportId(), currentUserId);
+        if (draft != null) {
+            // 用草稿内容覆盖数据库内容
+            if (StringUtils.isNotBlank(draft.getImpression())) {
+                detail.setImpression(draft.getImpression());
+            }
+            if (StringUtils.isNotBlank(draft.getDescription())) {
+                detail.setDescription(draft.getDescription());
+            }
+            log.debug("加载草稿成功,reportId={}, userId={}", detail.getReportId(), currentUserId);
+        }
+    }
+
+    /**
+     * 判断报告是否已完成
+     *
+     * @param detail 报告详情
+     * @return true=已完成,false=未完成
+     */
+    private boolean isReportCompleted(ReportDetailDTO detail) {
+        // exam_status = 9 表示检查已完成
+        if (detail.getExamStatus() != null && detail.getExamStatus() == 9) {
+            return true;
+        }
+
+        // report_status = 9 表示报告已完成(远程报告)
+        if (detail.getReportStatus() != null && detail.getReportStatus() == 9) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 获取当前登录医生ID
+     * 从 Spring Security 上下文获取当前登录用户,然后查询对应的医生ID
+     *
+     * @return 医生ID,如果获取失败返回null
+     */
+    private String getCurrentUserId() {
+        try {
+            // 1. 从 Spring Security 获取当前登录用户
+            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+            if (authentication == null || authentication.getPrincipal() == null) {
+                log.warn("未找到登录用户信息");
+                return null;
+            }
+
+            // 2. 获取 LoginUser 对象
+            Object principal = authentication.getPrincipal();
+            if (!(principal instanceof LoginUser)) {
+                log.warn("Principal 不是 LoginUser 类型: {}", principal.getClass().getName());
+                return null;
+            }
+
+            LoginUser loginUser = (LoginUser) principal;
+            if (loginUser.getUser() == null || StringUtils.isBlank(loginUser.getUser().getId())) {
+                log.warn("LoginUser 中未找到用户信息");
+                return null;
+            }
+
+            String sysUserId = loginUser.getUser().getId();
+            log.debug("当前登录系统用户ID: {}", sysUserId);
+
+            // 3. 根据系统用户ID查询医生ID(doctors 表的 uid 字段关联 sys_user 的 id)
+            LambdaQueryWrapper<Doctors> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(Doctors::getUid, sysUserId)
+                   .last("LIMIT 1");
+
+            Doctors doctor = doctorsMapper.selectOne(wrapper);
+            if (doctor == null) {
+                log.warn("未找到系统用户对应的医生信息,sysUserId={}", sysUserId);
+                return null;
+            }
+
+            log.debug("当前登录医生ID: {}", doctor.getId());
+            return doctor.getId();
+
+        } catch (Exception e) {
+            log.error("获取当前医生ID失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 查询草稿
+     *
+     * @param reportId 报告ID
+     * @param userId 用户ID
+     * @return 草稿内容
+     */
+    private ReportTemp queryDraft(String reportId, String userId) {
+        if (StringUtils.isBlank(reportId) || StringUtils.isBlank(userId)) {
+            return null;
+        }
+
+        LambdaQueryWrapper<ReportTemp> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ReportTemp::getReportId, reportId)
+               .eq(ReportTemp::getDoctorId, userId)
+               .orderByDesc(ReportTemp::getUpdateTime)
+               .last("LIMIT 1");
+
+        return reportTempMapper.selectOne(wrapper);
+    }
+}

+ 29 - 0
src/main/java/com/zskk/pacsonline/modules/report/vo/ReportDetailQueryVO.java

@@ -0,0 +1,29 @@
+package com.zskk.pacsonline.modules.report.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 报告详情查询参数
+ *
+ * @author system
+ * @since 2025-12-16
+ */
+@Data
+@Schema(description = "报告详情查询参数")
+public class ReportDetailQueryVO {
+
+    @Schema(description = "检查ID", required = true, example = "exam-uuid-xxx")
+    @NotBlank(message = "检查ID不能为空")
+    private String examId;
+
+    @Schema(description = "是否远程报告:1=远程,2=本地", required = true, example = "2")
+    @NotNull(message = "报告类型不能为空")
+    private Integer isRemote;
+
+    @Schema(description = "远程申请ID(远程报告时必填)", example = "ra-uuid-xxx")
+    private String raId;
+}

+ 7 - 0
src/main/java/com/zskk/pacsonline/modules/system/mapper/MenuMapper.java

@@ -48,4 +48,11 @@ public interface MenuMapper extends BaseMapper<Menu> {
      * @return 权限列表
      */
     List<String> selectPermissionsByUserId(@Param("userId") String userId);
+
+    /**
+     * 查询所有权限列表(管理员使用)
+     *
+     * @return 所有权限列表
+     */
+    List<String> selectAllPermissions();
 }

+ 16 - 4
src/main/java/com/zskk/pacsonline/modules/system/mapper/xml/MenuMapper.xml

@@ -17,10 +17,13 @@
 
     <!-- 根据用户ID查询菜单列表(通过用户的所有角色) -->
     <select id="selectMenusByUserId" parameterType="String" resultType="com.zskk.pacsonline.modules.system.entity.Menu">
-        SELECT DISTINCT m.* FROM sys_menu m
-        INNER JOIN sys_role_menu rm ON m.id = rm.menu_id
-        INNER JOIN sys_user_role ur ON rm.role_id = ur.role_id
-        WHERE ur.user_id = #{userId} AND m.status = 1
+        SELECT
+            DISTINCT m.*
+        FROM sys_menu m
+            INNER JOIN sys_role_menu rm ON m.id = rm.menu_id
+            INNER JOIN sys_user_role ur ON rm.role_id = ur.role_id
+        WHERE ur.user_id = #{userId}
+            AND m.status = 1
         ORDER BY m.order_num
     </select>
 
@@ -35,4 +38,13 @@
           AND m.perms != ''
     </select>
 
+    <!-- 查询所有权限列表(管理员使用) -->
+    <select id="selectAllPermissions" resultType="java.lang.String">
+        SELECT DISTINCT perms FROM sys_menu
+        WHERE status = 1
+          AND perms IS NOT NULL
+          AND perms != ''
+        ORDER BY perms
+    </select>
+
 </mapper>

+ 7 - 0
src/main/java/com/zskk/pacsonline/modules/system/service/MenuService.java

@@ -54,4 +54,11 @@ public interface MenuService extends IService<Menu> {
      * @return 权限列表
      */
     List<String> getPermissionsByUserId(String userId);
+
+    /**
+     * 获取所有权限列表(管理员使用)
+     *
+     * @return 所有权限列表
+     */
+    List<String> getAllPermissions();
 }

+ 21 - 4
src/main/java/com/zskk/pacsonline/modules/system/service/impl/LoginServiceImpl.java

@@ -723,13 +723,30 @@ public class LoginServiceImpl implements LoginService {
                 .collect(Collectors.toList());
         manageInfo.setRoles(roles);
 
+        // 判断是否为管理员(role id=1)
+        boolean isAdmin = roles.stream().anyMatch(role -> role.getId()==1 );
+
         // 获取权限列表
-        List<String> permissions = menuService.getPermissionsByUserId(userId);
+        List<String> permissions;
+        if (isAdmin) {
+            // 管理员:获取所有权限
+            permissions = menuService.getAllPermissions();
+            permissions = List.of("*:*:*");
+        } else {
+            // 普通用户:根据用户ID获取权限
+            permissions = menuService.getPermissionsByUserId(userId);
+        }
         manageInfo.setPermissions(permissions);
 
-        // 获取菜单树,只保留id、name和children
-        List<MenuTreeDTO> menuTrees = menuService.getMenuTreeByUserId(userId);
-        //List<MenuSimpleDTO> menus = convertToSimpleMenuTree(menuTrees);
+        // 获取菜单树
+        List<MenuTreeDTO> menuTrees;
+        if (isAdmin) {
+            // 管理员:返回所有菜单
+            menuTrees = menuService.getAllMenuTree();
+        } else {
+            // 普通用户:根据用户权限返回菜单
+            menuTrees = menuService.getMenuTreeByUserId(userId);
+        }
         manageInfo.setMenus(menuTrees);
 
         // 获取管理端关联的机构列表

+ 5 - 0
src/main/java/com/zskk/pacsonline/modules/system/service/impl/MenuServiceImpl.java

@@ -84,4 +84,9 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
     public List<String> getPermissionsByUserId(String userId) {
         return baseMapper.selectPermissionsByUserId(userId);
     }
+
+    @Override
+    public List<String> getAllPermissions() {
+        return baseMapper.selectAllPermissions();
+    }
 }

+ 24 - 6
src/main/java/com/zskk/pacsonline/utils/DataMaskUtil.java

@@ -66,12 +66,14 @@ public class DataMaskUtil {
 
     /**
      * 姓名脱敏
-     * 保留前3个字符,后面用**替换
+     * 根据姓名长度采用不同的脱敏策略:
+     * - 1个字:不脱敏(李 -> 李)
+     * - 2个字:保留姓,名用*替换(张三 -> 张*)
+     * - 3个字:保留前2个字,最后1个字用*替换(张小三 -> 张小*)
+     * - 4个字及以上:保留前2个字,后面用**替换(欧阳娜娜 -> 欧阳**)
      *
      * @param name 原始姓名
      * @return 脱敏后的姓名
-     * @example 张三 -> 张三**
-     * @example 欧阳娜娜 -> 欧阳娜**
      */
     public static String maskName(String name) {
         // 空值检查
@@ -84,9 +86,25 @@ public class DataMaskUtil {
             return name;
         }
 
-        // 保留前3个字符(如果不足3个字符,则全部保留)
-        int keepLength = Math.min(name.length(), 3);
-        return name.substring(0, keepLength) + "**";
+        int length = name.length();
+
+        // 1个字:不脱敏
+        if (length == 1) {
+            return name;
+        }
+
+        // 2个字:保留姓,名用*替换(张三 -> 张*)
+        if (length == 2) {
+            return name.substring(0, 1) + "*";
+        }
+
+        // 3个字:保留前2个字,最后1个字用*替换(张小三 -> 张小*)
+        if (length == 3) {
+            return name.substring(0, 2) + "*";
+        }
+
+        // 4个字及以上:保留前2个字,后面用**替换(欧阳娜娜 -> 欧阳**)
+        return name.substring(0, 2) + "**";
     }
 
     /**