feat(esb): T8.2 CDA临床文档 - AppService/Controller/Frontend

This commit is contained in:
2026-06-18 12:56:18 +08:00
parent b6a521db29
commit 2d67395228
5 changed files with 274 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
package com.healthlink.his.web.esbmanage.appservice;
import com.healthlink.his.esb.domain.CdaDocument;
import java.util.List;
public interface ICdaDocumentAppService {
CdaDocument generateCda(Long encounterId, Long patientId, String documentType, String documentTitle, String clinicalData);
List<CdaDocument> getCdaDocuments(Long encounterId);
}

View File

@@ -0,0 +1,90 @@
package com.healthlink.his.web.esbmanage.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.esb.domain.CdaDocument;
import com.healthlink.his.esb.service.ICdaDocumentService;
import com.healthlink.his.web.esbmanage.appservice.ICdaDocumentAppService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Service
@Slf4j
@RequiredArgsConstructor
public class CdaDocumentAppServiceImpl implements ICdaDocumentAppService {
private final ICdaDocumentService cdaDocumentService;
@Override
public CdaDocument generateCda(Long encounterId, Long patientId, String documentType, String documentTitle, String clinicalData) {
String docId = UUID.randomUUID().toString();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
String cdaXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<ClinicalDocument xmlns=\"urn:hl7-org:v3\">\n" +
" <typeId root=\"2.16.840.1.113883.1.3\" extension=\"POCD_HD000040\"/>\n" +
" <id root=\"" + docId + "\"/>\n" +
" <code code=\"" + getDocumentTypeCode(documentType) + "\" codeSystem=\"2.16.840.1.113883.5.4\"/>\n" +
" <title>" + escapeXml(documentTitle) + "</title>\n" +
" <effectiveTime value=\"" + sdf.format(new Date()).replace("-", "").replace(":", "") + "\"/>\n" +
" <recordTarget>\n" +
" <patientRole>\n" +
" <id extension=\"" + patientId + "\" root=\"2.16.156.10011\"/>\n" +
" </patientRole>\n" +
" </recordTarget>\n" +
" <component>\n" +
" <structuredBody>\n" +
" <component>\n" +
" <section>\n" +
" <code code=\"48767-8\" codeSystem=\"2.16.840.1.113883.6.1\"/>\n" +
" <text>" + escapeXml(clinicalData) + "</text>\n" +
" </section>\n" +
" </component>\n" +
" </structuredBody>\n" +
" </component>\n" +
"</ClinicalDocument>";
CdaDocument doc = new CdaDocument();
doc.setDocumentType(documentType);
doc.setDocumentTitle(documentTitle);
doc.setEncounterId(encounterId);
doc.setPatientId(patientId);
doc.setCdaXml(cdaXml);
doc.setStatus("DRAFT");
doc.setVersionId(1);
doc.setCreateTime(new Date());
cdaDocumentService.save(doc);
log.info("CDA文档已生成: type={}, title={}, id={}", documentType, documentTitle, doc.getId());
return doc;
}
@Override
public List<CdaDocument> getCdaDocuments(Long encounterId) {
LambdaQueryWrapper<CdaDocument> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CdaDocument::getEncounterId, encounterId)
.orderByDesc(CdaDocument::getCreateTime);
return cdaDocumentService.list(wrapper);
}
private String getDocumentTypeCode(String documentType) {
switch (documentType) {
case "admission": return "34133-9";
case "discharge": return "18842-5";
case "lab_report": return "11502-2";
case "referral": return "57133-2";
default: return "34133-9";
}
}
private String escapeXml(String text) {
if (text == null) return "";
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace("\"", "&quot;").replace("'", "&apos;");
}
}

View File

@@ -0,0 +1,47 @@
package com.healthlink.his.web.esbmanage.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.esb.domain.CdaDocument;
import com.healthlink.his.web.esbmanage.appservice.ICdaDocumentAppService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* CDA临床文档 Controller — 生成/查询CDA文档
*/
@RestController
@RequestMapping("/esb/cda")
@Slf4j
@RequiredArgsConstructor
public class CdaDocumentController {
private final ICdaDocumentAppService cdaDocumentAppService;
@PostMapping("/generate")
@PreAuthorize("hasAuthority('infection:esb:edit')")
public R<?> generateCda(@RequestBody Map<String, Object> params) {
Long encounterId = params.get("encounterId") != null ? Long.valueOf(String.valueOf(params.get("encounterId"))) : null;
Long patientId = params.get("patientId") != null ? Long.valueOf(String.valueOf(params.get("patientId"))) : null;
String documentType = (String) params.get("documentType");
String documentTitle = (String) params.get("documentTitle");
String clinicalData = (String) params.get("clinicalData");
if (encounterId == null || documentType == null) {
return R.fail("encounterId和documentType不能为空");
}
CdaDocument doc = cdaDocumentAppService.generateCda(encounterId, patientId, documentType, documentTitle, clinicalData);
return R.ok(doc);
}
@GetMapping("/list/{encounterId}")
@PreAuthorize("hasAuthority('infection:esb:list')")
public R<?> getCdaDocuments(@PathVariable Long encounterId) {
List<CdaDocument> docs = cdaDocumentAppService.getCdaDocuments(encounterId);
return R.ok(docs);
}
}

View File

@@ -0,0 +1,6 @@
import request from '@/utils/request'
export function generateCda(data) { return request({ url: '/esb/cda/generate', method: 'post', data }) }
export function getCdaDocuments(encounterId) { return request({ url: '/esb/cda/list/' + encounterId, method: 'get' }) }
export function getCdaPage(params) { return request({ url: '/fhir-cda/cda/page', method: 'get', params }) }
export function createCdaDocument(data) { return request({ url: '/fhir-cda/cda/create', method: 'post', data }) }
export function publishCdaDocument(id) { return request({ url: '/fhir-cda/cda/publish', method: 'post', params: { id } }) }

View File

@@ -0,0 +1,121 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">CDA临床文档管理</span>
<div>
<el-button type="primary" @click="showGenerate = true">生成CDA文档</el-button>
<el-button @click="loadPage">刷新</el-button>
</div>
</div>
<el-table :data="docList" border stripe>
<el-table-column prop="documentType" label="文档类型" width="120">
<template #default="{row}">
<el-tag size="small">{{ typeMap[row.documentType] || row.documentType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="documentTitle" label="文档标题" min-width="180" show-overflow-tooltip />
<el-table-column prop="encounterId" label="就诊ID" width="100" />
<el-table-column prop="patientId" label="患者ID" width="100" />
<el-table-column prop="status" label="状态" width="90">
<template #default="{row}">
<el-tag :type="row.status==='PUBLISHED'?'success':row.status==='DRAFT'?'warning':'info'" size="small">
{{ row.status === 'DRAFT' ? '草稿' : row.status === 'PUBLISHED' ? '已发布' : row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="versionId" label="版本" width="60" align="center" />
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="160">
<template #default="{row}">
<el-button type="primary" link size="small" @click="viewCda(row)">查看XML</el-button>
<el-button v-if="row.status==='DRAFT'" type="success" link size="small" @click="doPublish(row.id)">发布</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page.pageNo"
v-model:page-size="page.pageSize"
style="margin-top:12px;justify-content:flex-end"
:total="pageTotal"
layout="total,prev,pager,next"
@current-change="loadPage"
/>
<el-dialog v-model="showGenerate" title="生成CDA文档" width="600px">
<el-form :model="genForm" label-width="100px">
<el-form-item label="就诊ID">
<el-input v-model="genForm.encounterId" />
</el-form-item>
<el-form-item label="患者ID">
<el-input v-model="genForm.patientId" />
</el-form-item>
<el-form-item label="文档类型">
<el-select v-model="genForm.documentType" style="width:100%">
<el-option label="入院记录" value="admission" />
<el-option label="出院记录" value="discharge" />
<el-option label="检验报告" value="lab_report" />
<el-option label="转诊记录" value="referral" />
</el-select>
</el-form-item>
<el-form-item label="文档标题">
<el-input v-model="genForm.documentTitle" />
</el-form-item>
<el-form-item label="临床数据">
<el-input v-model="genForm.clinicalData" type="textarea" :rows="6" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showGenerate = false">取消</el-button>
<el-button type="primary" @click="doGenerate">生成</el-button>
</template>
</el-dialog>
<el-dialog v-model="showXml" title="CDA文档XML" width="800px">
<el-input v-model="xmlContent" type="textarea" :rows="20" readonly />
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { generateCda, getCdaPage, publishCdaDocument } from './api'
const typeMap = { admission: '入院记录', discharge: '出院记录', lab_report: '检验报告', referral: '转诊记录' }
const docList = ref([])
const pageTotal = ref(0)
const page = ref({ pageNo: 1, pageSize: 20 })
const showGenerate = ref(false)
const genForm = ref({ encounterId: '', patientId: '', documentType: 'admission', documentTitle: '', clinicalData: '' })
const showXml = ref(false)
const xmlContent = ref('')
const loadPage = async () => {
const r = await getCdaPage(page.value)
docList.value = r.data?.records || []
pageTotal.value = r.data?.total || 0
}
const doGenerate = async () => {
if (!genForm.value.encounterId || !genForm.value.documentType) {
ElMessage.warning('就诊ID和文档类型必填'); return
}
const r = await generateCda(genForm.value)
if (r.code === 200) {
ElMessage.success('CDA文档已生成')
showGenerate.value = false
loadPage()
} else {
ElMessage.error(r.msg || '生成失败')
}
}
const doPublish = async (id) => {
const r = await publishCdaDocument(id)
if (r.code === 200) { ElMessage.success('已发布'); loadPage() }
else ElMessage.error(r.msg || '发布失败')
}
const viewCda = (row) => { xmlContent.value = row.cdaXml || ''; showXml.value = true }
onMounted(() => { loadPage() })
</script>