feat(esb): T8.1 HL7 FHIR R4消息转换 - AppService/Controller/Frontend
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
package com.healthlink.his.web.esbmanage.appservice;
|
||||
|
||||
import com.healthlink.his.esb.domain.FhirResource;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface IFhirConversionAppService {
|
||||
FhirResource convertToFhir(Map<String, Object> internalData, String resourceType);
|
||||
Map<String, Object> convertFromFhir(String resourceJson);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.healthlink.his.web.esbmanage.appservice.impl;
|
||||
|
||||
import com.healthlink.his.esb.domain.FhirResource;
|
||||
import com.healthlink.his.esb.service.IFhirResourceService;
|
||||
import com.healthlink.his.web.esbmanage.appservice.IFhirConversionAppService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class FhirConversionAppServiceImpl implements IFhirConversionAppService {
|
||||
|
||||
private final IFhirResourceService fhirResourceService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public FhirResource convertToFhir(Map<String, Object> internalData, String resourceType) {
|
||||
try {
|
||||
Map<String, Object> fhirBundle = new LinkedHashMap<>();
|
||||
fhirBundle.put("resourceType", resourceType);
|
||||
fhirBundle.put("id", UUID.randomUUID().toString());
|
||||
fhirBundle.put("meta", Map.of("lastUpdated", new Date().toString()));
|
||||
|
||||
Map<String, Object> identifier = new LinkedHashMap<>();
|
||||
identifier.put("system", "urn:oid:2.16.156.10011");
|
||||
identifier.put("value", String.valueOf(internalData.getOrDefault("patientId", "")));
|
||||
fhirBundle.put("identifier", List.of(identifier));
|
||||
|
||||
if ("Patient".equals(resourceType)) {
|
||||
Map<String, Object> name = new LinkedHashMap<>();
|
||||
name.put("use", "official");
|
||||
name.put("text", String.valueOf(internalData.getOrDefault("patientName", "")));
|
||||
fhirBundle.put("name", List.of(name));
|
||||
fhirBundle.put("gender", internalData.getOrDefault("gender", "unknown"));
|
||||
fhirBundle.put("birthDate", String.valueOf(internalData.getOrDefault("birthDate", "")));
|
||||
} else if ("Encounter".equals(resourceType)) {
|
||||
fhirBundle.put("status", "in-progress");
|
||||
Map<String, Object> classCode = new LinkedHashMap<>();
|
||||
classCode.put("system", "http://terminology.hl7.org/CodeSystem/v3-ActCode");
|
||||
classCode.put("code", "IMP");
|
||||
fhirBundle.put("class", classCode);
|
||||
fhirBundle.put("period", Map.of("start", internalData.getOrDefault("admissionDate", "")));
|
||||
} else if ("Observation".equals(resourceType)) {
|
||||
fhirBundle.put("status", "final");
|
||||
Map<String, Object> codeMap = new LinkedHashMap<>();
|
||||
codeMap.put("coding", List.of(Map.of("system", "http://loinc.org", "code", internalData.getOrDefault("obsCode", ""))));
|
||||
fhirBundle.put("code", codeMap);
|
||||
Map<String, Object> valueQuantity = new LinkedHashMap<>();
|
||||
valueQuantity.put("value", internalData.getOrDefault("obsValue", 0));
|
||||
valueQuantity.put("unit", internalData.getOrDefault("obsUnit", ""));
|
||||
fhirBundle.put("valueQuantity", valueQuantity);
|
||||
} else if ("Condition".equals(resourceType)) {
|
||||
fhirBundle.put("clinicalStatus", Map.of("coding", List.of(Map.of("code", "active"))));
|
||||
Map<String, Object> condCode = new LinkedHashMap<>();
|
||||
condCode.put("coding", List.of(Map.of("system", "http://snomed.info/sct", "code", internalData.getOrDefault("conditionCode", ""))));
|
||||
fhirBundle.put("code", condCode);
|
||||
} else if ("MedicationRequest".equals(resourceType)) {
|
||||
fhirBundle.put("status", "active");
|
||||
fhirBundle.put("intent", "order");
|
||||
Map<String, Object> medCode = new LinkedHashMap<>();
|
||||
medCode.put("coding", List.of(Map.of("system", "http://www.nmpa.gov.cn", "code", internalData.getOrDefault("drugCode", ""))));
|
||||
fhirBundle.put("medicationCodeableConcept", medCode);
|
||||
fhirBundle.put("dosageInstruction", List.of(Map.of("text", internalData.getOrDefault("dosage", ""))));
|
||||
}
|
||||
|
||||
String resourceJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(fhirBundle);
|
||||
|
||||
FhirResource resource = new FhirResource();
|
||||
resource.setResourceType(resourceType);
|
||||
resource.setResourceId(fhirBundle.get("id").toString());
|
||||
resource.setPatientId(internalData.get("patientId") != null ? Long.valueOf(String.valueOf(internalData.get("patientId"))) : null);
|
||||
resource.setEncounterId(internalData.get("encounterId") != null ? Long.valueOf(String.valueOf(internalData.get("encounterId"))) : null);
|
||||
resource.setResourceJson(resourceJson);
|
||||
resource.setStatus("ACTIVE");
|
||||
resource.setVersionId(1);
|
||||
resource.setCreateTime(new Date());
|
||||
fhirResourceService.save(resource);
|
||||
|
||||
return resource;
|
||||
} catch (Exception e) {
|
||||
log.error("FHIR转换失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("FHIR R4转换失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> convertFromFhir(String resourceJson) {
|
||||
try {
|
||||
Map<String, Object> fhirResource = objectMapper.readValue(resourceJson, Map.class);
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("resourceType", fhirResource.get("resourceType"));
|
||||
result.put("resourceId", fhirResource.get("id"));
|
||||
|
||||
List<Map<String, Object>> identifiers = (List<Map<String, Object>>) fhirResource.get("identifier");
|
||||
if (identifiers != null && !identifiers.isEmpty()) {
|
||||
result.put("patientId", identifiers.get(0).get("value"));
|
||||
}
|
||||
|
||||
if ("Patient".equals(fhirResource.get("resourceType"))) {
|
||||
List<Map<String, Object>> names = (List<Map<String, Object>>) fhirResource.get("name");
|
||||
if (names != null && !names.isEmpty()) {
|
||||
result.put("patientName", names.get(0).get("text"));
|
||||
}
|
||||
result.put("gender", fhirResource.get("gender"));
|
||||
result.put("birthDate", fhirResource.get("birthDate"));
|
||||
} else if ("Encounter".equals(fhirResource.get("resourceType"))) {
|
||||
result.put("status", fhirResource.get("status"));
|
||||
Map<String, Object> cls = (Map<String, Object>) fhirResource.get("class");
|
||||
if (cls != null) result.put("encounterClass", cls.get("code"));
|
||||
Map<String, Object> period = (Map<String, Object>) fhirResource.get("period");
|
||||
if (period != null) result.put("admissionDate", period.get("start"));
|
||||
} else if ("Observation".equals(fhirResource.get("resourceType"))) {
|
||||
Map<String, Object> vq = (Map<String, Object>) fhirResource.get("valueQuantity");
|
||||
if (vq != null) {
|
||||
result.put("obsValue", vq.get("value"));
|
||||
result.put("obsUnit", vq.get("unit"));
|
||||
}
|
||||
} else if ("Condition".equals(fhirResource.get("resourceType"))) {
|
||||
Map<String, Object> code = (Map<String, Object>) fhirResource.get("code");
|
||||
if (code != null) {
|
||||
List<Map<String, Object>> codings = (List<Map<String, Object>>) code.get("coding");
|
||||
if (codings != null && !codings.isEmpty()) {
|
||||
result.put("conditionCode", codings.get(0).get("code"));
|
||||
}
|
||||
}
|
||||
} else if ("MedicationRequest".equals(fhirResource.get("resourceType"))) {
|
||||
Map<String, Object> med = (Map<String, Object>) fhirResource.get("medicationCodeableConcept");
|
||||
if (med != null) {
|
||||
List<Map<String, Object>> codings = (List<Map<String, Object>>) med.get("coding");
|
||||
if (codings != null && !codings.isEmpty()) {
|
||||
result.put("drugCode", codings.get(0).get("code"));
|
||||
}
|
||||
}
|
||||
List<Map<String, Object>> dosage = (List<Map<String, Object>>) fhirResource.get("dosageInstruction");
|
||||
if (dosage != null && !dosage.isEmpty()) {
|
||||
result.put("dosage", dosage.get(0).get("text"));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("FHIR反向转换失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("FHIR R4反向转换失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.healthlink.his.web.esbmanage.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.esb.domain.FhirResource;
|
||||
import com.healthlink.his.web.esbmanage.appservice.IFhirConversionAppService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* FHIR R4消息转换 Controller — 内部数据 ↔ FHIR R4标准格式
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/esb/fhir")
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class FhirConversionController {
|
||||
|
||||
private final IFhirConversionAppService fhirConversionAppService;
|
||||
|
||||
@PostMapping("/convert-to")
|
||||
@PreAuthorize("hasAuthority('infection:esb:edit')")
|
||||
public R<?> convertToFhir(@RequestBody Map<String, Object> params) {
|
||||
String resourceType = (String) params.get("resourceType");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> internalData = (Map<String, Object>) params.get("data");
|
||||
if (resourceType == null || internalData == null) {
|
||||
return R.fail("resourceType和data不能为空");
|
||||
}
|
||||
FhirResource result = fhirConversionAppService.convertToFhir(internalData, resourceType);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/convert-from")
|
||||
@PreAuthorize("hasAuthority('infection:esb:edit')")
|
||||
public R<?> convertFromFhir(@RequestBody Map<String, String> params) {
|
||||
String resourceJson = params.get("resourceJson");
|
||||
if (resourceJson == null || resourceJson.isBlank()) {
|
||||
return R.fail("resourceJson不能为空");
|
||||
}
|
||||
Map<String, Object> result = fhirConversionAppService.convertFromFhir(resourceJson);
|
||||
return R.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
export function convertToFhir(data) { return request({ url: '/esb/fhir/convert-to', method: 'post', data }) }
|
||||
export function convertFromFhir(data) { return request({ url: '/esb/fhir/convert-from', method: 'post', data }) }
|
||||
export function getFhirPage(params) { return request({ url: '/fhir-cda/fhir/page', method: 'get', params }) }
|
||||
export function getFhirTypeStats() { return request({ url: '/fhir-cda/fhir/type-stats', method: 'get' }) }
|
||||
153
healthlink-his-ui/src/views/esbmanage/fhirconversion/index.vue
Normal file
153
healthlink-his-ui/src/views/esbmanage/fhirconversion/index.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<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">HL7 FHIR R4 消息转换</span>
|
||||
<el-button type="primary" @click="loadStats">刷新统计</el-button>
|
||||
</div>
|
||||
<el-row :gutter="12" style="margin-bottom:16px">
|
||||
<el-col :span="4" v-for="(val, key) in typeStats" :key="key">
|
||||
<el-card shadow="hover">
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:20px;font-weight:bold;color:#409eff">{{ val }}</div>
|
||||
<div style="font-size:12px;color:#999">{{ key }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
<el-tab-pane label="内部→FHIR R4" name="toFhir">
|
||||
<el-form :model="toFhirForm" label-width="120px" style="max-width:700px">
|
||||
<el-form-item label="资源类型">
|
||||
<el-select v-model="toFhirForm.resourceType" style="width:100%">
|
||||
<el-option label="Patient" value="Patient" />
|
||||
<el-option label="Encounter" value="Encounter" />
|
||||
<el-option label="Observation" value="Observation" />
|
||||
<el-option label="Condition" value="Condition" />
|
||||
<el-option label="MedicationRequest" value="MedicationRequest" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="患者ID">
|
||||
<el-input v-model="toFhirForm.patientId" />
|
||||
</el-form-item>
|
||||
<el-form-item label="就诊ID">
|
||||
<el-input v-model="toFhirForm.encounterId" />
|
||||
</el-form-item>
|
||||
<el-form-item label="患者姓名">
|
||||
<el-input v-model="toFhirForm.patientName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别">
|
||||
<el-select v-model="toFhirForm.gender" style="width:100%">
|
||||
<el-option label="男" value="male" />
|
||||
<el-option label="女" value="female" />
|
||||
<el-option label="未知" value="unknown" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="出生日期">
|
||||
<el-input v-model="toFhirForm.birthDate" placeholder="YYYY-MM-DD" />
|
||||
</el-form-item>
|
||||
<el-form-item label="诊断编码">
|
||||
<el-input v-model="toFhirForm.conditionCode" />
|
||||
</el-form-item>
|
||||
<el-form-item label="药物编码">
|
||||
<el-input v-model="toFhirForm.drugCode" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用法用量">
|
||||
<el-input v-model="toFhirForm.dosage" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="doConvertToFhir">转换为FHIR R4</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-divider v-if="toFhirResult" content-position="left">转换结果</el-divider>
|
||||
<el-input v-if="toFhirResult" v-model="toFhirResult" type="textarea" :rows="12" readonly />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="FHIR R4→内部" name="fromFhir">
|
||||
<el-form label-width="120px" style="max-width:700px">
|
||||
<el-form-item label="FHIR JSON">
|
||||
<el-input v-model="fromFhirJson" type="textarea" :rows="10" placeholder="粘贴FHIR R4 Resource JSON" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="doConvertFromFhir">转换为内部格式</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-divider v-if="fromFhirResult" content-position="left">转换结果</el-divider>
|
||||
<el-descriptions v-if="fromFhirResult" :column="2" border>
|
||||
<el-descriptions-item v-for="(val, key) in fromFhirResult" :key="key" :label="key">{{ val }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="已有FHIR资源" name="list">
|
||||
<el-table :data="fhirList" border stripe>
|
||||
<el-table-column prop="resourceType" label="资源类型" width="150" />
|
||||
<el-table-column prop="resourceId" label="资源ID" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.status==='ACTIVE'?'success':'info'" size="small">{{ 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>
|
||||
<el-pagination
|
||||
v-model:current-page="listPage.pageNo"
|
||||
v-model:page-size="listPage.pageSize"
|
||||
style="margin-top:12px;justify-content:flex-end"
|
||||
:total="listTotal"
|
||||
layout="total,prev,pager,next"
|
||||
@current-change="loadList"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { convertToFhir, convertFromFhir, getFhirPage, getFhirTypeStats } from './api'
|
||||
|
||||
const activeTab = ref('toFhir')
|
||||
const typeStats = ref({})
|
||||
const toFhirForm = ref({ resourceType: 'Patient', patientId: '', encounterId: '', patientName: '', gender: 'male', birthDate: '', conditionCode: '', drugCode: '', dosage: '' })
|
||||
const toFhirResult = ref('')
|
||||
const fromFhirJson = ref('')
|
||||
const fromFhirResult = ref('')
|
||||
const fhirList = ref([])
|
||||
const listTotal = ref(0)
|
||||
const listPage = ref({ pageNo: 1, pageSize: 20 })
|
||||
|
||||
const loadStats = async () => {
|
||||
const r = await getFhirTypeStats()
|
||||
typeStats.value = r.data || {}
|
||||
}
|
||||
|
||||
const loadList = async () => {
|
||||
const r = await getFhirPage(listPage.value)
|
||||
fhirList.value = r.data?.records || []
|
||||
listTotal.value = r.data?.total || 0
|
||||
}
|
||||
|
||||
const doConvertToFhir = async () => {
|
||||
const data = { ...toFhirForm.value }
|
||||
if (!data.patientId) { ElMessage.warning('患者ID必填'); return }
|
||||
const r = await convertToFhir({ resourceType: data.resourceType, data })
|
||||
if (r.code === 200) {
|
||||
toFhirResult.value = JSON.stringify(JSON.parse(r.data?.resourceJson || '{}'), null, 2)
|
||||
ElMessage.success('转换成功')
|
||||
loadList()
|
||||
} else {
|
||||
ElMessage.error(r.msg || '转换失败')
|
||||
}
|
||||
}
|
||||
|
||||
const doConvertFromFhir = async () => {
|
||||
if (!fromFhirJson.value) { ElMessage.warning('请输入FHIR JSON'); return }
|
||||
const r = await convertFromFhir({ resourceJson: fromFhirJson.value })
|
||||
if (r.code === 200) {
|
||||
fromFhirResult.value = r.data
|
||||
ElMessage.success('转换成功')
|
||||
} else {
|
||||
ElMessage.error(r.msg || '转换失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { loadStats(); loadList() })
|
||||
</script>
|
||||
Reference in New Issue
Block a user