报告暂存接口说明.md 9.7 KB

报告暂存接口实现说明

接口信息

接口路径: 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)

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 中)

// 获取报告详情时,如果存在草稿,用草稿覆盖数据库内容
$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 中)

// 正式保存报告时,删除草稿
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 表(草稿表),字段应包括:

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)

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 实现逻辑

@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<ReportTemp> 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 实现

@PostMapping("/stage")
@Operation(summary = "暂存报告", description = "保存报告草稿,避免数据丢失")
@SystemLogHandler("暂存报告|保存")
public RestResult<?> stageReport(@RequestBody @Valid ReportStageVO stageVO) {
    try {
        String reportId = reportService.stageReport(stageVO);
        Map<String, String> result = new HashMap<>();
        result.put("reportId", reportId);
        return RestResult.ok("暂存成功", result);
    } catch (Exception e) {
        return RestResult.error("暂存失败:" + e.getMessage());
    }
}

3. 草稿加载逻辑(已在 ReportDetailServiceImpl 中实现)

// 在 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() 方法中:

// 正式保存成功后,删除草稿
if (StringUtils.isNotBlank(reportId)) {
    LambdaQueryWrapper<ReportTemp> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(ReportTemp::getReportId, reportId)
           .eq(ReportTemp::getDoctorId, currentDoctorId);
    reportTempMapper.delete(wrapper);
}

接口测试

请求示例

POST /api/report/stage
{
  "examId": "exam-uuid-xxx",
  "isRemote": 2,
  "reportId": "",  // 首次可为空
  "impression": "双肺纹理增强,未见明显实质性病变",
  "description": "胸部正位片显示:双肺野透亮度尚可...",
  "reportResult": 1
}

返回示例

{
  "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. ⏳ 定时任务清理过期草稿(可选)