00 · 給後端的對齊說明

十分鐘讀完。 這份是第一次溝通用的,講清楚「我們要的 FHIR 模式是什麼、不是什麼」, 以及前後端各自負責到哪裡。細節在後面的 01–07 與 Swagger,這裡不重複。

對象:EMR 後端開發。前端(Vue 3)與 Swagger 由我方負責。


1. 一句話

資料庫是你們自建的,API 的形狀照 HL7 FHIR R4。

伺服器不裝 HAPI FHIR,不改變這個結論——FHIR 在本專案是介面契約,不是儲存格式。 你們的表怎麼設計不受影響(就照 G1_物件目錄 那 71 個物件), 要求只有一件事:對外那層 JSON 與 URL 長得像 FHIR。

Vue 前端  ──FHIR R4 JSON──>  你們的 API  ──SQL──>  你們的資料表
                                 │
                                 └──D1 事件──>  HIS

2. 為什麼不用 HAPI 也做得到

因為我們一開始就沒把 HAPI 才划算的功能寫進規格。整份規格要求的,是這幾件可以自己實作的事:

要做 大概是什麼工
資源的 JSON 形狀 從你們的表組裝 JSON,欄位對照表我們已經列好(02)
REST 路徑與互動 GET /Patient/{id}、POST /MedicationRequest… 共 27 個 path、41 支
搜尋參數 四種型別(reference/token/date/string)翻成 SQL WHERE,07 §7.7 有對照
版本與樂觀鎖 ETag + If-Match,一個版本欄位就夠
標準錯誤格式 OperationOutcome,一個中介層做完
Bundle transaction 一個資料庫交易 + 內部引用解析

明確不做:chained search、_filter、GraphQL、Subscription、$everything、XML。 這些是 HAPI 的強項,也正是我們排除掉的部分。


3. 五條規則,違反了就不是 FHIR 模式

前四條是 FHIR 的形狀,第五條是這家醫院的臨床要求。

3.1 資源用名詞,動作用 HTTP 方法

✅ POST /MedicationRequest          ❌ POST /createMedicationOrder
✅ GET  /Observation?patient=…      ❌ GET  /getVitalSignsByPatient
✅ PUT  /Composition/{id}           ❌ POST /updateProgressNote

只有「多個副作用必須原子完成」才用自訂操作,本專案只有五支: $sign、$submit、$receive、$roster、$expand。

3.2 編碼一律帶 system + code

// ✅
"medicationCodeableConcept": {
  "coding": [{ "system": "http://icu.emr.local/CodeSystem/his-medication",
               "code": "50631", "display": "Efferalgan 300 mg (Paracetamol)" }],
  "text": "Efferalgan 300 mg (Paracetamol)"
}

// ❌ 只有字串,下游無法比對,也無法對到 HIS 目錄
"medication": "Efferalgan 300mg"

只給 text 的請求要拒絕(錯誤碼 CODE-REQUIRED)。藥品碼、給藥途徑、服務碼都適用。

3.3 關聯用 Reference,不用裸 id

"subject":   { "reference": "Patient/demo-a" }        // ✅
"patientId": "demo-a"                                  // ❌

而且引用歷史資料時要帶版本:"reference": "MedicationRequest/mr-x/_history/3"。 給藥執行指向的是「開立當下那一版醫囑」,病程引用的是「當時看到的那一筆檢驗值」。 之後醫囑被更正,這些引用也不會跟著改——這是稽核的基礎。

3.4 錯誤一律回 OperationOutcome

{
  "resourceType": "OperationOutcome",
  "issue": [{
    "severity": "error",
    "code": "business-rule",
    "details": { "coding": [{ "system": "http://icu.emr.local/CodeSystem/api-error",
                              "code": "FIVE-RIGHTS-INCOMPLETE" }] },
    "diagnostics": "需逐項人工確認五對,不能由單次掃碼推定。",
    "expression": ["MedicationAdministration.extension('…/verificationChecklist')"]
  }]
}

四個欄位各有用途,缺一不可:

欄位 前端拿來做什麼
details.coding.code 決定畫面行為(例如未確認名單就跳去確認頁)
diagnostics 直接顯示給臨床使用者看,所以要寫人話,不是 stack trace
expression 標紅出錯的欄位
severity / code 分辨錯誤等級

錯誤碼共 61 個,表在 05 §5.2, Swagger 的 ApiErrorCode 是同一份。

3.5 缺值不補、資料不覆寫

規則 具體要求
缺值不補 沒有值就省略該元素。不要送 0、不要預設正常、不要推定未執行。0 在臨床是一個真實數值
不覆寫 沒有 DELETE。誤建改標 entered-in-error;病歷更正是新版本;每次寫入都留舊版

4. 誰能寫什麼:三層要先分清楚

這張表決定你們要寫多少程式,建議第一次會議就對完:

層 物件 API
HIS 唯讀鏡像(13 個) patient、patient_identifier、encounter、encounter_location、coverage、diagnostic_report、specimen、microbiology_isolate、antimicrobial_susceptibility、medication_dispense、practitioner、organization、location 只開 GET。臨床端寫入回 422 READ-ONLY-SOURCE
EMR 自有 medication_request、service_request、medication_administration、observation、composition、task、signature… 完整 CRUD(無 delete)+ 版本控制 + 業務規則
回寫 HIS 只有 D1 的 5 支事件 由整合層負責,臨床端不直接碰

唯讀鏡像不需要版本控制、If-Match、業務規則驗證——工作量差很多。

另外有一件事想先講在前面:醫囑有三個獨立的狀態軸,不要合併成一個 status。

軸 放哪 例
臨床狀態 status draft → active → stopped
本地簽署 Provenance.signature 是否存在 已簽/未簽
HIS 傳輸 extension[hisTransmissionStatus] notSent/sent/accepted/rejected/unknown

HIS 的 MedicationChangeStatus 1–5(新開立/審核/匯總領取/發放/急救車補充)沒有一個代表已給藥。 已給藥只有一個來源:護理師建立的 MedicationAdministration。


5. 五個具體例子

完整的 41 支在 Swagger(openapi/swagger.html,雙擊就能開,每支都附請求與回應範例)。 這裡先看最常用的五種形狀。

5.1 讀:搜尋回 Bundle,不是陣列

GET /fhir/r4/Observation?patient=Patient/demo-a&category=vital-signs
    &date=ge2026-08-30T19:00:00+07:00&date=le2026-08-31T19:00:00+07:00&_sort=-date
Accept: application/fhir+json
{
  "resourceType": "Bundle",
  "type": "searchset",
  "total": 2,
  "link": [{ "relation": "self", "url": "…" },
           { "relation": "next", "url": "…" }],     // 分頁跟著 next 走,前端不自己拼 offset
  "entry": [
    { "fullUrl": "…/Observation/obs-bp-0831-1800",
      "resource": { "resourceType": "Observation", "…": "…" },
      "search": { "mode": "match" } }
  ]
}

同一個參數出現兩次= AND(上面的時間窗就是)。

5.2 血壓是一筆兩個 component,不是兩筆

{
  "resourceType": "Observation",
  "code": { "coding": [{ "system": "http://loinc.org", "code": "85354-9" }] },
  "effectiveDateTime": "2026-08-31T18:00:00+07:00",
  "component": [
    { "code": { "coding": [{ "system": "http://loinc.org", "code": "8480-6" }] },
      "valueQuantity": { "value": 92, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]" } },
    { "code": { "coding": [{ "system": "http://loinc.org", "code": "8462-4" }] },
      "valueQuantity": { "value": 54, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]" } }
  ]
}

拆成兩筆的話,收縮壓與舒張壓在時間軸上會失去配對,趨勢圖就錯了。

5.3 寫:一定要帶 If-Match

GET /fhir/r4/Composition/comp-progress-0831
→ 200 OK
  ETag: W/"4"
PUT /fhir/r4/Composition/comp-progress-0831
Content-Type: application/fhir+json
If-Match: W/"4"
→ 200 OK, ETag: W/"5"
狀況 回應
版本相符 200 + 新 ETag
版本落後 409 + OperationOutcome(必須帶 currentVersionId,前端才能合併)
沒帶 If-Match 412。不接受盲目覆寫

5.4 同一劑藥不能給兩次:交給資料庫判斷

POST /fhir/r4/MedicationAdministration
If-None-Exist: identifier=http://icu.emr.local/occurrence-id|M3-V1-D1&status=completed

已有同劑次的完成紀錄 → 回 200 OK + 既有資源(不是 201、不是 409)。 實作用唯一索引,不要「先查再寫」——兩個護理師同時操作時,先查再寫會兩個都通過。

5.5 多個副作用要原子:用 transaction Bundle

簽署一份病歷 = 文件進版 + 建立簽章 Provenance,兩件事必須同生同死:

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    { "fullUrl": "urn:uuid:…0001",
      "resource": { "resourceType": "Composition", "status": "final", "…": "…" },
      "request": { "method": "PUT", "url": "Composition/comp-progress-0831" } },
    { "fullUrl": "urn:uuid:…0002",
      "resource": { "resourceType": "Provenance",
                    "target": [{ "reference": "urn:uuid:…0001" }] },
      "request": { "method": "POST", "url": "Provenance" } }
  ]
}

任一筆失敗就整包回滾,不能留半套。


6. 分工

事項 誰
介面規格與 Swagger(本資料夾) 我方
Vue 前端 我方
資料表設計與實作 後端
API 實作(投影、搜尋、版本、錯誤、稽核) 後端
HIS 整合層(D1 五支事件) 後端 + FPT 對接
測試資料(示範病人) 我方提供範例 JSON,後端建 fixture

規格有疑義時:以 Swagger 的請求/回應範例為準,它是與規格文件同一份來源產生的。

建議的交付順序

階段 後端 前端這時能做什麼
M1 唯讀鏡像的 GET + 搜尋、/metadata 病人總覽、生命徵象、檢驗結果接真資料
M2 自有資源的建立/更新、版本、OperationOutcome 病程撰寫、醫囑開立
M3 Bundle transaction、$sign/$receive、條件式建立 簽署、交班、eMAR
M4 $submit 與 HIS 整合 醫囑送出與狀態顯示

M1 做完前端就能脫離假資料,建議優先。


7. 需要後端回覆的事

# 問題 為什麼要先問
B-1 資料庫與語言是什麼(PostgreSQL/SQL Server/MySQL?Java/.NET/Node?) 07 的 DDL 與索引寫法要改成對應方言
B-2 時間一律存 UTC、回應時轉 +07:00,可以嗎? 現在決定最省事;之後再改要動所有既有資料
B-3 版本快照表(每次寫入存一份 FHIR JSON)接受嗎? 這是 vread/帶版本引用/病歷不覆寫的共同基礎,不做就三件都做不出來
B-4 稽核表能否設成只允許 INSERT(DB 權限層面)? 「不覆寫」要能辯護,靠程式自律不夠
B-5 檢驗指標字典(WBC/Na/K…)院內有沒有既有代碼表? 對應規格的 Q-02。先用院內碼也可以,但要有一份清單
B-6 認證走 SMART on FHIR 還是院內既有的 SSO? 影響 scope 檢查與病人範圍的實作位置

8. 先跑起來看(比讀文件快)

規格旁邊有一份可執行的參考實作(../fhir-server,Express + SQLite,只有 express 一個相依):

cd fhir-server && npm install && npm start
# 打開 http://127.0.0.1:8787/console.html

演練台左欄是一日流程 12 步(確認名單 → 寫病程 → 開醫囑 → 簽署 → 送 HIS → 給藥 → 判讀 → 交班), 右欄是 8 個「故意犯錯」按鈕(忘了帶 If-Match、五對沒勾、用 0 代表沒量到、護理師開醫囑…), 下方看得到每一次請求的完整 request/response。

它不是要你們照抄,是讓「規格說要擋」變成看得見的行為。三件事值得先看:

看什麼 在哪
資料表長怎樣、FHIR 從哪投影出來 src/db/schema.sql、src/resources/*.js
搜尋參數怎麼變成 SQL、樂觀鎖怎麼鎖 src/core/search.js、src/core/store.js
規則擋下來時回什麼 npm test(51 個測試,名稱就是 BR-* 編號)

那 51 個測試全部走 HTTP,把 base URL 換掉就能對著你們自己的實作跑。


9. 這個資料夾怎麼讀

你想知道 看哪一份
有哪些端點、各自的請求回應長怎樣 openapi/swagger.html(雙擊開啟,41 支都有範例)
為什麼這樣設計、誰能寫什麼 01 設計原則與資料權威
我的表對到哪個資源、哪個欄位 02 資源對照 + 07 附錄 A
搜尋參數、版本控制、Bundle 的規則 03 CRUD 與搜尋規格
什麼情況要擋、擋了要回什麼碼 04 業務規則 + 05 錯誤、安全與稽核
HIS 事件怎麼對映 06 HIS 整合對映
實際要寫的 SQL 與程式 07 後端實作指南 ← 從這裡開始動手
想直接看它跑起來 ../fhir-server(參考實作+演練台,見上面第 8 節)

規格版本 0.1.0(草案,未經院方核定)。範例中的病人、數值、代碼皆為虛構。