1. EMR/HIS 整合挑戰
大多數越南醫院使用內部 HIS/EMR 系統和傳統關係資料庫。 FHIR 整合需要處理許多問題。
| 挑戰 | 描述 | 解決方案 |
|---|---|---|
| 不同的架構 | HIS使用自己的表,FHIR使用資源 | 資料映射層 |
| 數據不標準 | 姓名和地址不符合格式 | 資料清洗+標準化 |
| 內碼 | HIS 使用自己的代碼(ICD-10 VN,醫院藥品代碼) | 概念圖翻譯 |
| 遺留協定 | HL7 v2、CDA、XML/CSV 文件 | 轉換器/變壓器 |
| 效能 | HIS 為數千用戶提供服務 | 快取、非同步處理 |
| 停機時間 | 他無法停止 | 非侵入式集成 |
2. 整合模式
模式 1:FHIR 外觀
在現有 HIS 之前放置一個 FHIR API 層, 沒有變化 他的。 Facade直接讀寫HIS資料庫。
┌─────────────┐ ┌──────────────┐ ┌──────────┐
│ FHIR Client │────▶│ FHIR Facade │────▶│ HIS DB │
│ (App/Portal)│◀────│ (Middleware) │◀────│(Oracle/ │
└─────────────┘ └──────────────┘ │ SQL Srv) │
└──────────┘
@Component
public class HISPatientFacade implements IResourceProvider {
@Autowired
private HISPatientRepository hisRepo;
@Autowired
private PatientMapper mapper;
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Read
public Patient read(@IdParam IdType theId) {
// Đọc từ HIS database
HISBenhNhan bn = hisRepo.findByMaBN(theId.getIdPart());
if (bn == null) {
throw new ResourceNotFoundException(theId);
}
// Map sang FHIR Patient
return mapper.toFhirPatient(bn);
}
@Search
public List<Patient> searchByName(
@RequiredParam(name = Patient.SP_NAME) StringParam name) {
List<HISBenhNhan> results = hisRepo.findByTenContaining(
name.getValue());
return results.stream()
.map(mapper::toFhirPatient)
.collect(Collectors.toList());
}
}
資料映射器:HIS → FHIR
@Component
public class PatientMapper {
public Patient toFhirPatient(HISBenhNhan bn) {
Patient patient = new Patient();
// ID
patient.setId(bn.getMaBenhNhan());
// Identifier: Mã BN
patient.addIdentifier()
.setSystem("http://bv-abc.vn/fhir/sid/mabn")
.setValue(bn.getMaBenhNhan());
// Identifier: CCCD
if (bn.getSoCCCD() != null) {
patient.addIdentifier()
.setSystem("http://fhir.vn/sid/cccd")
.setValue(bn.getSoCCCD());
}
// Identifier: BHYT
if (bn.getSoBHYT() != null) {
patient.addIdentifier()
.setSystem("http://fhir.vn/sid/bhyt")
.setValue(bn.getSoBHYT());
}
// Họ tên
HumanName name = patient.addName();
name.setFamily(bn.getHo());
name.addGiven(bn.getTen());
name.setText(bn.getHoTen());
// Giới tính: HIS dùng 0/1, FHIR dùng enum
switch (bn.getGioiTinh()) {
case 0: patient.setGender(AdministrativeGender.FEMALE); break;
case 1: patient.setGender(AdministrativeGender.MALE); break;
default: patient.setGender(AdministrativeGender.UNKNOWN);
}
// Ngày sinh
patient.setBirthDate(bn.getNgaySinh());
// Địa chỉ
Address address = patient.addAddress();
address.addLine(bn.getDiaChi());
address.setDistrict(bn.getQuanHuyen());
address.setCity(bn.getTinhThanh());
// Số điện thoại
if (bn.getSoDienThoai() != null) {
patient.addTelecom()
.setSystem(ContactPoint.ContactPointSystem.PHONE)
.setValue(bn.getSoDienThoai());
}
return patient;
}
public HISBenhNhan toHISBenhNhan(Patient patient) {
HISBenhNhan bn = new HISBenhNhan();
// Map ngược từ FHIR → HIS
HumanName name = patient.getNameFirstRep();
bn.setHo(name.getFamily());
bn.setTen(name.getGivenAsSingleString());
bn.setHoTen(name.getText());
// CCCD
patient.getIdentifier().stream()
.filter(i -> "http://fhir.vn/sid/cccd".equals(i.getSystem()))
.findFirst()
.ifPresent(i -> bn.setSoCCCD(i.getValue()));
// Giới tính
switch (patient.getGender()) {
case MALE: bn.setGioiTinh(1); break;
case FEMALE: bn.setGioiTinh(0); break;
default: bn.setGioiTinh(-1);
}
bn.setNgaySinh(patient.getBirthDate());
return bn;
}
}
模式 2:資料同步 (ETL)
根據計劃(批量)或即時(CDC)將資料從HIS同步到FHIR Server。
┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐
│ HIS DB │────▶│ ETL/CDC │────▶│ FHIR Server │────▶│ FHIR Apps │
│ │ │ Pipeline │ │ (HAPI FHIR) │ │ │
└──────────┘ └───────────┘ └─────────────┘ └───────────┘
# ETL Pipeline: HIS PostgreSQL → FHIR Server
import psycopg2
import requests
from datetime import datetime
FHIR_BASE = "http://localhost:8080/fhir"
def sync_patients(last_sync: datetime):
"""Đồng bộ bệnh nhân mới/cập nhật từ HIS"""
conn = psycopg2.connect(
host="his-db", dbname="his",
user="readonly", password="***"
)
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT * FROM benh_nhan
WHERE ngay_cap_nhat > %s
ORDER BY ngay_cap_nhat
""", (last_sync,))
for row in cur:
patient = map_to_fhir_patient(row)
# Upsert: PUT với ID
resp = requests.put(
f"{FHIR_BASE}/Patient/{row['ma_bn']}",
json=patient,
headers={"Content-Type": "application/fhir+json"}
)
if resp.status_code in (200, 201):
print(f"Synced Patient/{row['ma_bn']}")
else:
print(f"Error: {resp.status_code} - {resp.text}")
conn.close()
def map_to_fhir_patient(row):
return {
"resourceType": "Patient",
"id": row["ma_bn"],
"identifier": [
{
"system": "http://bv-abc.vn/fhir/sid/mabn",
"value": row["ma_bn"]
}
],
"name": [{
"family": row["ho"],
"given": [row["ten"]],
"text": row["ho_ten"]
}],
"gender": "male" if row["gioi_tinh"] == 1 else "female",
"birthDate": row["ngay_sinh"].isoformat()
}
3. HL7 v2 到 FHIR 轉換
許多遺留系統透過 HL7 v2 訊息(ADT、ORM、ORU)進行通訊。 FHIR 提供轉換映射。
MSH|^~\&|HIS|BVDK|FHIR|GATEWAY|20250115103000||ADT^A01|MSG001|P|2.5
EVN|A01|20250115103000
PID|1||BN001^^^BVDK||Nguyen^Van A||19900515|M|||123 Nguyen Hue^^HCM^^700000^VN||0901234567
PV1|1|I|KHOA-NOI^P-301^G-01||||BS001^Tran^Thi B
// HL7 v2 → FHIR converter
public class V2ToFhirConverter {
public Bundle convertADT_A01(Message v2Message) {
ADT_A01 adt = (ADT_A01) v2Message;
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
// PID → Patient
PID pid = adt.getPID();
Patient patient = new Patient();
patient.setId(pid.getPatientID().getIDNumber().getValue());
HumanName name = patient.addName();
name.setFamily(pid.getPatientName(0).getFamilyName()
.getSurname().getValue());
name.addGiven(pid.getPatientName(0).getGivenName().getValue());
String gender = pid.getAdministrativeSex().getValue();
patient.setGender("M".equals(gender)
? AdministrativeGender.MALE
: AdministrativeGender.FEMALE);
bundle.addEntry()
.setResource(patient)
.getRequest()
.setMethod(Bundle.HTTPVerb.PUT)
.setUrl("Patient/" + patient.getId());
// PV1 → Encounter
PV1 pv1 = adt.getPV1();
Encounter encounter = new Encounter();
encounter.setStatus(Encounter.EncounterStatus.INPROGRESS);
encounter.setSubject(new Reference(
"Patient/" + patient.getId()));
// Location
String ward = pv1.getAssignedPatientLocation()
.getPointOfCare().getValue();
encounter.addLocation()
.setLocation(new Reference("Location/" + ward));
bundle.addEntry()
.setResource(encounter)
.getRequest()
.setMethod(Bundle.HTTPVerb.POST)
.setUrl("Encounter");
return bundle;
}
}
4. CDA 到 FHIR 的轉換
// CDA Document → FHIR Bundle
public Bundle convertCDAToFhir(ClinicalDocument cda) {
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
// CDA Header → Composition
Composition composition = new Composition();
composition.setStatus(Composition.CompositionStatus.FINAL);
composition.setDate(cda.getEffectiveTime().getValue());
composition.setTitle(cda.getTitle().getText());
// CDA recordTarget → Patient
RecordTarget rt = cda.getRecordTargets().get(0);
Patient patient = mapCDAPatient(
rt.getPatientRole());
composition.setSubject(new Reference(
"Patient/" + patient.getId()));
// CDA Sections → Composition sections
for (Section cdaSection : cda.getComponent()
.getStructuredBody().getComponents()) {
Composition.SectionComponent section =
composition.addSection();
section.setTitle(cdaSection.getTitle().getText());
section.getText().setDiv(
cdaSection.getText().toXml());
// Map CDA entries → FHIR resources
for (Entry entry : cdaSection.getEntries()) {
IBaseResource resource = mapCDAEntry(entry);
bundle.addEntry().setResource(resource);
section.addEntry(new Reference(resource));
}
}
bundle.addEntry().setResource(composition);
bundle.addEntry().setResource(patient);
return bundle;
}
5. 使用 ConceptMap 進行程式碼翻譯
{
"resourceType": "ConceptMap",
"url": "http://bv-abc.vn/fhir/ConceptMap/his-gender-to-fhir",
"name": "HISGenderToFHIR",
"title": "HIS Gender Code → FHIR AdministrativeGender",
"status": "active",
"group": [
{
"source": "http://bv-abc.vn/his/gender",
"target": "http://hl7.org/fhir/administrative-gender",
"element": [
{
"code": "0",
"display": "Nữ",
"target": [
{"code": "female", "relationship": "equivalent"}
]
},
{
"code": "1",
"display": "Nam",
"target": [
{"code": "male", "relationship": "equivalent"}
]
},
{
"code": "2",
"display": "Khác",
"target": [
{"code": "other", "relationship": "equivalent"}
]
}
]
}
]
}
# Dùng $translate operation
curl 'http://localhost:8080/fhir/ConceptMap/$translate?\
system=http://bv-abc.vn/his/gender&\
code=1&\
target=http://hl7.org/fhir/administrative-gender'
6. 中介軟體架構
┌──────────┐ ┌──────────┐ ┌──────────┐
│ HIS 1 │ │ HIS 2 │ │ LIS │
│ (Oracle) │ │(SQL Srv) │ │ (HL7 v2) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────┐
│ Integration Engine │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ Adapters │ │ Mappers │ │ Router │ │
│ │ (DB/v2) │ │ (→FHIR) │ │ │ │
│ └──────────┘ └──────────┘ └────────┘ │
└────────────────┬───────────────────────┘
│
▼
┌──────────────┐
│ FHIR Server │
│ (HAPI FHIR) │
└──────┬───────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Portal │ │Mobile │ │ HIE │
│BN │ │App │ │Gateway │
└────────┘ └────────┘ └────────┘
7. 綜合最佳實踐
- 非侵入性 — 不改變現有的HIS,使用Facade或ETL
- 冪等 — PUT 固定 ID,同步無重複
- 增量同步 — 僅同步變更資料(基於時間戳記或 CDC)
- 錯誤處理 — 失敗記錄的死信隊列
- 監控 — 追蹤同步延遲、錯誤率、吞吐量
- 數據品質 — 在推送到 FHIR 伺服器之前進行驗證
- 概念圖 — 用於代碼翻譯,而不是硬編碼映射
- 復原 — 保留原始 HIS、FHIR 作為輔助
八、總結
FHIR 門面 — HIS前面的FHIR API圖層,即時地圖,不編輯HIS
資料同步(ETL) — 從 HIS DB → FHIR 伺服器同步批次/CDC
HL7 v2 → FHIR — 將ADT/ORM/ORU訊息轉換為FHIR資源
CDA → FHIR — 將 CDA 文件轉換為 FHIR Bundle
概念圖 — 將 HIS 內部代碼轉換為標準代碼(LOINC、SNOMED、ICD-10)
中介軟體 — 整合引擎處理多個資料來源