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(草案,未經院方核定)。範例中的病人、數值、代碼皆為虛構。