报告保存接口说明.md 19 KB

报告保存接口实现说明

接口信息

接口路径:

  • 本地报告: POST /api/report/save
  • 远程报告: POST /api/report/remote-save

功能说明: 正式保存报告(书写、审核、确认),更新报告内容、状态和操作痕迹。


PHP项目核心逻辑分析

1. 主流程 (updateReport 方法)

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 方法)

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 (书写/审核)

@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 (确认)

@Data
public class ReportConfirmVO {
    @NotBlank(message = "检查ID不能为空")
    private String examId;

    @NotNull(message = "报告类型不能为空")
    private Integer isRemote;

    private String raId;
}

2. Service 实现

@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<Doctors>()
            .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<Report>()
                .eq(Report::getExamId, examId)
                .eq(Report::getType, "1")
                .last("LIMIT 1")
        );
    } else {
        // 远程报告
        report = reportMapper.selectOne(
            new LambdaQueryWrapper<Report>()
                .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<ReportTemp> 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 实现

/**
 * 书写报告
 */
@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. ⏳ 微信推送通知