# 报告暂存接口实现说明 ## 接口信息 **接口路径**: `POST /api/report/stage` **功能说明**: 暂存报告内容(草稿保存),用于报告书写时临时保存数据,避免数据丢失。 --- ## 与PHP项目的差异 | 功能点 | PHP项目 | Java项目 | |--------|---------|----------| | **草稿存储** | Redis缓存,有效期7天 | 数据库表 `report_temp` | | **缓存Key生成** | `"stage" + report_id + user_id` | 无需key,直接用 report_id + doctor_id 查询 | | **数据持久化** | 临时存储,服务重启丢失 | 永久存储,不会丢失 | | **查询方式** | Redis GET | 数据库 SELECT | --- ## PHP项目原逻辑分析 ### 1. 暂存报告 (stageReport) ```php public function stageReport($exam_id, $token, $data, $isRemote, $report_id) { // 1. 如果 report_id 不存在,自动创建 report 记录 if (!$report_id) { $insert_data = [ 'id' => UUIDUtils::uuid(), 'exam_id' => $exam_id, 'createdAt' => date('Y-m-d H:i:s'), 'report_result' => 1, 'type' => $remote // 1=本地, 2=远程 ]; if ($remote == 2) { // 远程报告 $applicationId = $this->reportDao->getApplicationId($exam_id); $insert_data['remote_application_id'] = $applicationId; } $this->reportDao->insertReport($insert_data); $report_id = $insert_data['id']; } // 2. 生成缓存 key $user_id = $this->reportDao->getUserId($token); $key = $this->getStageKey($report_id, $user_id); // "stage" + report_id + user_id // 3. 保存到 Redis,有效期7天 $this->reportDao->saveCache($key, $data, 604800); return $key; } ``` **缓存内容 ($data)**: - `impression`: 诊断印象 - `description`: 影像所见 - `report_result`: 报告结果 - `examId`: 检查ID - `isRemote`: 报告类型 - `report_id`: 报告ID - 其他患者基本信息 ### 2. 加载草稿 (在 getReportDetail 中) ```php // 获取报告详情时,如果存在草稿,用草稿覆盖数据库内容 $cacheToken = $this->getStageKey($detail['report_id'], $userId); $stage = $this->reportDao->getStage($cacheToken); // 从 Redis 读取 if ($stage) { // 用草稿覆盖 $detail['impression'] = $stage['impression']; $detail['description'] = $stage['description']; // ... } ``` ### 3. 删除草稿 (在 updateReport 中) ```php // 正式保存报告时,删除草稿 if (isset($reportId) && !empty($reportId)) { $key = $this->getStageKey($reportId, $user_id); $stage = $this->reportDao->getStage($key); if ($stage) { $this->reportDao->delStage($key); // 从 Redis 删除 } } ``` --- ## Java项目实现方案 ### 1. 数据库表设计 已有 `report_temp` 表(草稿表),字段应包括: ```sql CREATE TABLE report_temp ( id VARCHAR(36) PRIMARY KEY, -- 主键ID report_id VARCHAR(36), -- 关联 report.id doctor_id VARCHAR(36), -- 医生ID impression TEXT, -- 诊断印象 description TEXT, -- 影像所见 report_result INT, -- 报告结果 create_time DATETIME, -- 创建时间 update_time DATETIME, -- 更新时间 UNIQUE KEY uk_report_doctor (report_id, doctor_id) -- 唯一约束 ); ``` ### 2. 接口实现步骤 #### 请求参数 (ReportStageVO) ```java public class ReportStageVO { @NotBlank(message = "检查ID不能为空") private String examId; @NotNull(message = "报告类型不能为空") private Integer isRemote; // 1=远程,2=本地 private String reportId; // 报告ID(首次可能为空) private String raId; // 远程申请ID(远程报告时需要) // 报告内容 private String impression; // 诊断印象 private String description; // 影像所见 private Integer reportResult; // 报告结果 } ``` #### Service 实现逻辑 ```java @Override @Transactional(rollbackFor = Exception.class) public String stageReport(ReportStageVO stageVO) { String reportId = stageVO.getReportId(); // 1. 如果 reportId 不存在,自动创建 report 记录 if (StringUtils.isBlank(reportId)) { Report report = new Report(); report.setExamId(stageVO.getExamId()); report.setCreateTime(new Date()); report.setReportResult(1); // 判断本地/远程 if (stageVO.getIsRemote() == 2) { // 本地报告 report.setType("1"); } else { // 远程报告 report.setType("2"); // 查询 remote_application_id String applicationId = reportMapper.selectApplicationIdByExamId(stageVO.getExamId()); report.setRemoteApplicationId(applicationId); } // MyBatis-Plus 自动生成 UUID reportMapper.insert(report); reportId = report.getId(); } // 2. 获取当前登录医生ID String currentDoctorId = getCurrentDoctorId(); // 3. 保存或更新草稿 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ReportTemp::getReportId, reportId) .eq(ReportTemp::getDoctorId, currentDoctorId); ReportTemp existingTemp = reportTempMapper.selectOne(wrapper); if (existingTemp != null) { // 更新现有草稿 existingTemp.setImpression(stageVO.getImpression()); existingTemp.setDescription(stageVO.getDescription()); existingTemp.setReportResult(stageVO.getReportResult()); existingTemp.setUpdateTime(new Date()); reportTempMapper.updateById(existingTemp); } else { // 新增草稿 ReportTemp newTemp = new ReportTemp(); newTemp.setReportId(reportId); newTemp.setDoctorId(currentDoctorId); newTemp.setImpression(stageVO.getImpression()); newTemp.setDescription(stageVO.getDescription()); newTemp.setReportResult(stageVO.getReportResult()); newTemp.setCreateTime(new Date()); newTemp.setUpdateTime(new Date()); reportTempMapper.insert(newTemp); } // 4. 返回 reportId return reportId; } ``` #### Controller 实现 ```java @PostMapping("/stage") @Operation(summary = "暂存报告", description = "保存报告草稿,避免数据丢失") @SystemLogHandler("暂存报告|保存") public RestResult stageReport(@RequestBody @Valid ReportStageVO stageVO) { try { String reportId = reportService.stageReport(stageVO); Map result = new HashMap<>(); result.put("reportId", reportId); return RestResult.ok("暂存成功", result); } catch (Exception e) { return RestResult.error("暂存失败:" + e.getMessage()); } } ``` ### 3. 草稿加载逻辑(已在 ReportDetailServiceImpl 中实现) ```java // 在 getReportDetail 方法中已实现 private void loadDraftIfNotCompleted(ReportDetailDTO detail) { // 检查报告是否已完成 if (isReportCompleted(detail)) { return; } // 获取当前登录用户ID String currentUserId = getCurrentUserId(); // 查询草稿 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()); } } } ``` ### 4. 草稿删除逻辑(在正式保存时) 在 `ReportService.saveReport()` 或 `updateReport()` 方法中: ```java // 正式保存成功后,删除草稿 if (StringUtils.isNotBlank(reportId)) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ReportTemp::getReportId, reportId) .eq(ReportTemp::getDoctorId, currentDoctorId); reportTempMapper.delete(wrapper); } ``` --- ## 接口测试 ### 请求示例 ```json POST /api/report/stage { "examId": "exam-uuid-xxx", "isRemote": 2, "reportId": "", // 首次可为空 "impression": "双肺纹理增强,未见明显实质性病变", "description": "胸部正位片显示:双肺野透亮度尚可...", "reportResult": 1 } ``` ### 返回示例 ```json { "code": 200, "message": "暂存成功", "data": { "reportId": "report-uuid-xxx" } } ``` --- ## 注意事项 1. **唯一约束**: `report_temp` 表需要添加 `(report_id, doctor_id)` 唯一约束,确保每个医生对每份报告只有一个草稿 2. **定期清理**: 建议定期清理超过一定时间(如30天)的草稿数据 3. **并发控制**: 同一医生可能在多个设备上编辑,后保存的会覆盖先保存的 4. **权限验证**: 暂存时也应验证用户是否有权限书写该报告 5. **数据校验**: 虽然是草稿,但仍需基本的数据格式校验 --- ## 与报告保存(save)的关系 | 功能 | 暂存(stage) | 保存(save) | |------|------------|-----------| | **存储位置** | report_temp 表 | report 表 | | **触发时机** | 用户主动暂存或自动保存 | 用户点击"保存"按钮 | | **数据完整性** | 可以不完整 | 需要必填字段 | | **状态变更** | 不改变 exam_status | 改为"已写报告"(7) | | **操作痕迹** | 不记录 | 记录到 report_record | | **草稿处理** | 保存草稿 | 删除草稿 | --- ## 待实现清单 1. ✅ ReportTemp 实体类(已生成) 2. ✅ ReportTempMapper 接口和XML(已生成) 3. ⏳ ReportStageVO 请求参数类 4. ⏳ ReportService.stageReport() 方法 5. ⏳ ReportController.stageReport() 接口 6. ⏳ 在 saveReport() 中添加删除草稿逻辑 7. ⏳ 定时任务清理过期草稿(可选)