Chuyển đến nội dung chính

第 21 課:實踐 — 將 FHIR 與實際 EMR/HIS 集成

FHIR Facade 模式、從傳統 EMR/HIS 的資料映射、HL7 v2 到 FHIR 轉換、CDA 到 FHIR 轉換、醫療資料的 ETL 管道、整合模式、中介軟體架構、現實世界的挑戰。

🏗️ 建築 — 第 21 課 第 21 課:實作 — 將 FHIR 與 實用 EMR/HIS

HL7 FHIR - 基礎到進階醫療資料標準

第 6 部分:實踐 - 建構 FHIR 系統

亞洲開發網

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. 綜合最佳實踐

  1. 非侵入性 — 不改變現有的HIS,使用Facade或ETL
  2. 冪等 — PUT 固定 ID,同步無重複
  3. 增量同步 — 僅同步變更資料(基於時間戳記或 CDC)
  4. 錯誤處理 — 失敗記錄的死信隊列
  5. 監控 — 追蹤同步延遲、錯誤率、吞吐量
  6. 數據品質 — 在推送到 FHIR 伺服器之前進行驗證
  7. 概念圖 — 用於代碼翻譯,而不是硬編碼映射
  8. 復原 — 保留原始 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)

  • 中介軟體 — 整合引擎處理多個資料來源