接口路径: POST /api/report/stage
功能说明: 暂存报告内容(草稿保存),用于报告书写时临时保存数据,避免数据丢失。
| 功能点 | PHP项目 | Java项目 |
|---|---|---|
| 草稿存储 | Redis缓存,有效期7天 | 数据库表 report_temp |
| 缓存Key生成 | "stage" + report_id + user_id |
无需key,直接用 report_id + doctor_id 查询 |
| 数据持久化 | 临时存储,服务重启丢失 | 永久存储,不会丢失 |
| 查询方式 | Redis GET | 数据库 SELECT |
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: 检查IDisRemote: 报告类型report_id: 报告ID// 获取报告详情时,如果存在草稿,用草稿覆盖数据库内容
$cacheToken = $this->getStageKey($detail['report_id'], $userId);
$stage = $this->reportDao->getStage($cacheToken); // 从 Redis 读取
if ($stage) {
// 用草稿覆盖
$detail['impression'] = $stage['impression'];
$detail['description'] = $stage['description'];
// ...
}
// 正式保存报告时,删除草稿
if (isset($reportId) && !empty($reportId)) {
$key = $this->getStageKey($reportId, $user_id);
$stage = $this->reportDao->getStage($key);
if ($stage) {
$this->reportDao->delStage($key); // 从 Redis 删除
}
}
已有 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) -- 唯一约束
);
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; // 报告结果
}
@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;
}
@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());
}
}
// 在 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());
}
}
}
在 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"
}
}
report_temp 表需要添加 (report_id, doctor_id) 唯一约束,确保每个医生对每份报告只有一个草稿| 功能 | 暂存(stage) | 保存(save) |
|---|---|---|
| 存储位置 | report_temp 表 | report 表 |
| 触发时机 | 用户主动暂存或自动保存 | 用户点击"保存"按钮 |
| 数据完整性 | 可以不完整 | 需要必填字段 |
| 状态变更 | 不改变 exam_status | 改为"已写报告"(7) |
| 操作痕迹 | 不记录 | 记录到 report_record |
| 草稿处理 | 保存草稿 | 删除草稿 |