# 报告保存接口实现说明 ## 接口信息 **接口路径**: - 本地报告: `POST /api/report/save` - 远程报告: `POST /api/report/remote-save` **功能说明**: 正式保存报告(书写、审核、确认),更新报告内容、状态和操作痕迹。 --- ## PHP项目核心逻辑分析 ### 1. 主流程 (updateReport 方法) ```php public function updateReport($examId, $data, $type, $reportType, $token, $ra_id = null) { // 1. 权限验证 $user = $this->reportDao->getUser($token); $result = $this->reportAuthentication($user_id, $type, $exam['institution_id'], $reportType); if (!$result) { throw new Exception('权限未分配'); } // 2. 获取或创建报告记录 $report = $this->reportDao->getReport($examId, $rt, $ra_id); if (empty($reportId)) { if ($type == 'save') { // 自动创建 report 记录 $insert_data = [ 'id' => UUIDUtils::uuid(), 'exam_id' => $examId, 'createdAt' => date('Y-m-d H:i:s'), 'report_result' => 1, 'type' => $rt, // 1=本地, 2=远程 'remote_application_id' => $ra_id ]; $this->reportDao->insertReport($insert_data); $reportId = $insert_data['id']; } } else { // 3. 防止并发冲突 switch ($type) { case 'save': if ($user_id !== $report['report_doctor_id'] && !empty($report['report_doctor_id'])) { throw new Exception('该报告已被其他医师书写'); } break; case 'audit': if ($user_id !== $report['review_doctor_id'] && !empty($report['review_doctor_id'])) { throw new Exception('该报告已被其他医师审核'); } break; } } // 4. 更新患者基本信息(save和audit时) if ($type != 'confirm') { $update = [ 'name' => $data['name'], 'sex' => $data['sex'], 'age' => $data['age'], 'phone' => $data['phone'], 'exam_class' => $data['exam_class'], 'exam_project' => $data['exam_project'] ]; if ($rt == 1) { // 本地报告:更新 exams 表 $this->reportDao->updateExamMsg($examId, $exam['patient_id'], $update); } else { // 远程报告:更新 remote_application 表 $this->reportDao->updateRemote($ra_id, $update); } } // 5. 更新报告数据 $report_up = [ 'impression' => $data['impression'], 'description' => $data['description'], 'report_result' => $data['report_result'], 'hr_status' => $data['hr_status'] ]; $this->updateReportData($examId, $report_up, $type, $exam['exam_status'], $user, $reportType, $ra_id); // 6. 确认报告时的特殊处理 if ($type == 'confirm') { // 删除Redis缓存 $this->reportDao->delCache($reportId . '_local'); // 发送短信通知 if ($sms == '1' && !empty($phone)) { $this->sendSms($phone, $reportId, $examId, $studyId, $insName, $user['institution_id']); } // 生成二维码 $url = $wechat_url . '/wx_patient/api/unifyGetWxQrcode?reportId=' . $reportId . '&version=3'; $this->curl_get($url); // 远程报告:处理订单和费用 if ($reportType == 1 && !empty($ra_id)) { $money = $this->reportDao->getOrderMoney($ra_id); // 增加机构余额 $this->reportDao->updateInstititonMoney($user['institution_id'], $current_money + $money); // 更新订单状态 $this->reportDao->updateOrder($ra_id, ORDER_STATUS_CONFIRM); // 微信推送 $wechat->pushWechatComplete($order_id); } } // 7. 删除草稿 if (isset($reportId) && !empty($reportId)) { $key = $this->getStageKey($reportId, $user_id); $stage = $this->reportDao->getStage($key); if ($stage) { $this->reportDao->delStage($key); } } return true; } ``` ### 2. 更新报告数据 (updateReportData 方法) ```php public function updateReportData($exam_id, $data, $type, $exam_status, $user, $report_type, $id = null) { // 根据操作类型设置不同的更新字段 switch ($type) { case 'save': // 书写报告 // 状态验证 if ($report_type == 2) { // 本地 if ($exam_status != 3 && $exam_status != 7 && $exam_status != 12) { throw new Exception('流程顺序错误,无法书写报告'); } } else { // 远程 if ($status != 6 && $status != 7) { throw new Exception('流程顺序错误,无法书写报告'); } } $update = [ 'impression' => $data['impression'], 'description' => $data['description'], 'hr_status' => $data['hr_status'], 'report_result' => $data['report_result'], 'report_datetime' => date('Y-m-d H:i:s'), 'report_doctor_id' => $user['id'], 'report_doctor_name' => $user['realname'] ]; $exam_up_status = 7; // 已写报告 $trace_type = 1; // 痕迹类型:书写 break; case 'audit': // 审核报告 if ($report_type == 2) { if ($exam_status != 8 && $exam_status != 7) { throw new Exception('流程顺序错误,无法审核报告'); } } $update = [ 'impression' => $data['impression'], 'description' => $data['description'], 'hr_status' => $data['hr_status'], 'report_result' => $data['report_result'], 'review_datetime' => date('Y-m-d H:i:s'), 'review_doctor_id' => $user['id'], 'review_doctor_name' => $user['realname'] ]; $exam_up_status = 8; // 已审报告 $trace_type = 2; // 痕迹类型:审核 break; case 'confirm': // 确认报告 if ($report_type == 2) { if ($exam_status != 8) { throw new Exception('流程顺序错误,无法确认报告'); } } $update = [ 'confirm_datetime' => date('Y-m-d H:i:s'), 'confirm_doctor_id' => $user['id'], 'confirm_doctor_name' => $user['realname'] ]; $exam_up_status = 9; // 已确认报告 $trace_type = 3; // 痕迹类型:确认 break; } // 更新 report 表 if ($report_type == '2') { // 本地 $this->reportDao->updateReport($exam_id, 1, $update); // 更新 exams 表的 exam_status $this->reportDao->updateExamStatus($exam_id, $exam_up_status); } else { // 远程 $this->reportDao->updateRemoteReport($id, $update); // 更新 remote_application 表的 report_status $this->reportDao->updateRemoteStatus($id, $exam_up_status); } // 添加报告操作痕迹 $trace = [ 'id' => UUIDUtils::uuid(), 'impression' => $update['impression'] ?? '', 'description' => $update['description'] ?? '', 'report_result' => $update['report_result'] ?? '', 'report_id' => $rid, 'createdAt' => date('Y-m-d H:i:s'), 'doctor_id' => $user['id'], 'type' => $trace_type // 1=书写,2=审核,3=确认 ]; $this->reportDao->insertReportRecord($trace); return true; } ``` --- ## Java项目实现方案 ### 1. 请求参数 #### ReportSaveVO (书写/审核) ```java @Data public class ReportSaveVO { @NotBlank(message = "检查ID不能为空") private String examId; @NotNull(message = "报告类型不能为空") private Integer isRemote; // 1=远程,2=本地 private String raId; // 远程申请ID(远程报告时必填) // 患者基本信息 @NotBlank(message = "患者姓名不能为空") private String name; @NotBlank(message = "性别不能为空") private String sex; @NotBlank(message = "年龄不能为空") private String age; private String phone; @NotBlank(message = "检查类型不能为空") private String examClass; private String examProject; // 报告内容 @NotBlank(message = "诊断印象不能为空") private String impression; @NotBlank(message = "影像所见不能为空") private String description; @NotNull(message = "报告结果不能为空") private Integer reportResult; private Integer hrStatus; // HR状态,默认1 } ``` #### ReportConfirmVO (确认) ```java @Data public class ReportConfirmVO { @NotBlank(message = "检查ID不能为空") private String examId; @NotNull(message = "报告类型不能为空") private Integer isRemote; private String raId; } ``` ### 2. Service 实现 ```java @Override @Transactional(rollbackFor = Exception.class) public boolean saveReport(ReportSaveVO saveVO, String type) { // type: "save"=书写, "audit"=审核, "confirm"=确认 String examId = saveVO.getExamId(); Integer isRemote = saveVO.getIsRemote(); String raId = saveVO.getRaId(); // 1. 获取当前登录用户 String currentUserId = getCurrentUserId(); SysUser currentUser = sysUserService.getById(currentUserId); Doctors doctor = doctorsMapper.selectOne( new LambdaQueryWrapper() .eq(Doctors::getUid, currentUserId) .last("LIMIT 1") ); // 2. 权限验证 boolean hasPermission = checkReportPermission(currentUserId, type, isRemote); if (!hasPermission) { throw new RuntimeException("权限未分配,请联系管理员"); } // 3. 获取或创建报告记录 Report report; if (isRemote == 2) { // 本地报告 report = reportMapper.selectOne( new LambdaQueryWrapper() .eq(Report::getExamId, examId) .eq(Report::getType, "1") .last("LIMIT 1") ); } else { // 远程报告 report = reportMapper.selectOne( new LambdaQueryWrapper() .eq(Report::getRemoteApplicationId, raId) .last("LIMIT 1") ); } String reportId; if (report == null) { // 自动创建报告记录(仅在书写时) if ("save".equals(type)) { report = new Report(); report.setExamId(examId); report.setCreateTime(new Date()); report.setReportResult(1); report.setType(isRemote == 2 ? "1" : "2"); if (isRemote == 1) { report.setRemoteApplicationId(raId); } reportMapper.insert(report); reportId = report.getId(); } else { throw new RuntimeException("报告不存在,无法进行" + getTypeDesc(type)); } } else { reportId = report.getId(); // 4. 防止并发冲突 checkConcurrentConflict(report, doctor.getId(), type); } // 5. 更新患者基本信息(书写和审核时) if (!"confirm".equals(type)) { updatePatientInfo(examId, isRemote, raId, saveVO); } // 6. 更新报告数据 updateReportData(examId, reportId, isRemote, raId, saveVO, type, doctor); // 7. 确认报告时的特殊处理 if ("confirm".equals(type)) { handleConfirm(reportId, examId, isRemote, raId, doctor); } // 8. 删除草稿 deleteDraft(reportId, doctor.getId()); return true; } /** * 检查并发冲突 */ private void checkConcurrentConflict(Report report, String doctorId, String type) { switch (type) { case "save": if (StringUtils.isNotBlank(report.getReportDoctorId()) && !doctorId.equals(report.getReportDoctorId())) { throw new RuntimeException("该报告已被其他医师书写,请刷新页面进行查看"); } break; case "audit": if (StringUtils.isNotBlank(report.getReviewDoctorId()) && !doctorId.equals(report.getReviewDoctorId())) { throw new RuntimeException("该报告已被其他医师审核,请刷新页面进行查看"); } break; } } /** * 更新患者基本信息 */ private void updatePatientInfo(String examId, Integer isRemote, String raId, ReportSaveVO saveVO) { // 性别转换 String sex = saveVO.getSex(); if ("男".equals(sex)) { sex = "M"; } else if ("女".equals(sex)) { sex = "F"; } if (isRemote == 2) { // 本地报告:更新 exams 表 Exam exam = new Exam(); exam.setId(examId); exam.setName(saveVO.getName()); exam.setSex(sex); exam.setAge(saveVO.getAge()); exam.setPhone(saveVO.getPhone()); exam.setExamClass(saveVO.getExamClass()); exam.setExamProject(saveVO.getExamProject()); examMapper.updateById(exam); } else { // 远程报告:更新 remote_application 表 RemoteApplication ra = new RemoteApplication(); ra.setId(raId); ra.setName(saveVO.getName()); ra.setSex(sex); ra.setAge(saveVO.getAge()); ra.setPhone(saveVO.getPhone()); ra.setExamClass(saveVO.getExamClass()); ra.setExamProject(saveVO.getExamProject()); remoteApplicationMapper.updateById(ra); } } /** * 更新报告数据 */ private void updateReportData(String examId, String reportId, Integer isRemote, String raId, ReportSaveVO saveVO, String type, Doctors doctor) { Report updateReport = new Report(); updateReport.setId(reportId); Integer examStatus; Integer traceType; switch (type) { case "save": // 书写 // 验证流程状态 validateStatus(examId, isRemote, raId, type); updateReport.setImpression(saveVO.getImpression()); updateReport.setDescription(saveVO.getDescription()); updateReport.setReportResult(saveVO.getReportResult()); updateReport.setHrStatus(saveVO.getHrStatus() != null ? saveVO.getHrStatus() : 1); updateReport.setReportDatetime(new Date()); updateReport.setReportDoctorId(doctor.getId()); updateReport.setReportDoctorName(doctor.getRealname()); examStatus = 7; // 已写报告 traceType = 1; // 书写痕迹 break; case "audit": // 审核 validateStatus(examId, isRemote, raId, type); updateReport.setImpression(saveVO.getImpression()); updateReport.setDescription(saveVO.getDescription()); updateReport.setReportResult(saveVO.getReportResult()); updateReport.setHrStatus(saveVO.getHrStatus() != null ? saveVO.getHrStatus() : 1); updateReport.setReviewDatetime(new Date()); updateReport.setReviewDoctorId(doctor.getId()); updateReport.setReviewDoctorName(doctor.getRealname()); examStatus = 8; // 已审报告 traceType = 2; // 审核痕迹 break; case "confirm": // 确认 validateStatus(examId, isRemote, raId, type); updateReport.setConfirmDatetime(new Date()); updateReport.setConfirmDoctorId(doctor.getId()); updateReport.setConfirmDoctorName(doctor.getRealname()); examStatus = 9; // 已确认报告 traceType = 3; // 确认痕迹 break; default: throw new RuntimeException("未知的操作类型: " + type); } // 更新 report 表 reportMapper.updateById(updateReport); // 更新状态 updateExamStatus(examId, isRemote, raId, examStatus); // 添加操作痕迹 addReportTrace(reportId, saveVO, doctor.getId(), traceType); } /** * 添加报告操作痕迹 */ private void addReportTrace(String reportId, ReportSaveVO saveVO, String doctorId, Integer type) { ReportRecord trace = new ReportRecord(); trace.setReportId(reportId); trace.setImpression(saveVO.getImpression()); trace.setDescription(saveVO.getDescription()); trace.setReportResult(saveVO.getReportResult()); trace.setCreateTime(new Date()); trace.setDoctorId(doctorId); trace.setType(type); // 1=书写,2=审核,3=确认 reportRecordMapper.insert(trace); } /** * 删除草稿 */ private void deleteDraft(String reportId, String doctorId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ReportTemp::getReportId, reportId) .eq(ReportTemp::getDoctorId, doctorId); reportTempMapper.delete(wrapper); } /** * 确认报告的特殊处理 */ private void handleConfirm(String reportId, String examId, Integer isRemote, String raId, Doctors doctor) { // TODO: 发送短信通知 // TODO: 生成二维码 // TODO: 远程报告处理订单和费用 // TODO: 微信推送通知 } ``` ### 3. Controller 实现 ```java /** * 书写报告 */ @PostMapping("/save") @Operation(summary = "书写报告", description = "保存报告内容,更新为已写状态") @SystemLogHandler("书写报告|保存") public RestResult saveReport(@RequestBody @Valid ReportSaveVO saveVO) { try { boolean result = reportService.saveReport(saveVO, "save"); return RestResult.ok("保存成功"); } catch (Exception e) { return RestResult.error("保存失败:" + e.getMessage()); } } /** * 审核报告 */ @PostMapping("/audit") @Operation(summary = "审核报告", description = "审核报告内容,更新为已审状态") @SystemLogHandler("审核报告|保存") public RestResult auditReport(@RequestBody @Valid ReportSaveVO saveVO) { try { boolean result = reportService.saveReport(saveVO, "audit"); return RestResult.ok("审核成功"); } catch (Exception e) { return RestResult.error("审核失败:" + e.getMessage()); } } /** * 确认报告 */ @PostMapping("/confirm") @Operation(summary = "确认报告", description = "确认报告,更新为已确认状态") @SystemLogHandler("确认报告|保存") public RestResult confirmReport(@RequestBody @Valid ReportConfirmVO confirmVO) { try { boolean result = reportService.confirmReport(confirmVO); return RestResult.ok("确认成功"); } catch (Exception e) { return RestResult.error("确认失败:" + e.getMessage()); } } ``` --- ## 状态流转 ### 本地报告状态 (exam_status) ``` 3(已登记) → 7(已写报告) → 8(已审报告) → 9(已确认报告) ↑______________| (可重新书写/审核) ``` ### 远程报告状态 (report_status) ``` 6(待书写) → 7(已写报告) → 8(已审报告) → 10(待确认) → 9(已确认报告) ``` --- ## 报告操作痕迹类型 | type | 操作类型 | 说明 | |------|---------|------| | 1 | 书写 | 报告医生书写报告 | | 2 | 审核 | 审核医生审核报告 | | 3 | 确认 | 确认医生确认报告 | --- ## 待实现功能 1. ✅ 基础保存逻辑 2. ✅ 状态流转控制 3. ✅ 并发冲突检测 4. ✅ 操作痕迹记录 5. ⏳ 权限验证逻辑 6. ⏳ 短信通知 7. ⏳ 二维码生成 8. ⏳ 远程报告费用结算 9. ⏳ 微信推送通知