iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

摘要
Day 12 已完成第六條核心交換契約規則 LAB-UNIT-002,六條規則都已能獨立測試。Day 13 接著把這六條規則接回 Bundle 解析流程與首頁結果頁,讓使用者貼上或上傳 Bundle 後,可以在同一個畫面看到 JSON、FHIR R4、TW Core、交換契約四層結果,以及最小 Quality Gate 判定。

為什麼今天處理首頁結果頁?

Day 12 結束時,系統內部已經有六條核心交換規則:

Observation.subject → Patient
DiagnosticReport.result → Observation
DiagnosticReport.subject 與 Observation.subject → 同一 Patient
Observation.code → 契約允許 LOINC
Observation.valueQuantity.unit → 可讀單位存在
Observation.valueQuantity.system/code → 契約允許 UCUM 單位集合

開發者可以透過單元測試確認規則行為,但使用者在瀏覽器貼上 Bundle 時,還看不到:

哪一條交換契約規則通過?
哪一條交換契約規則失敗?
失敗的 JSON path 是哪裡?
實際值是什麼?
系統期待的條件是什麼?
最後 Quality Gate 是 Passed、Warning 還是 Blocked?

所以 Day 13 先簡單包裝前端,確認「使用者可操作」。

今天的實作範圍

今天新增或修改的範圍有:

  • GateOutcome
  • ValidationResult
  • BundleParseService
  • 六條 ContractRule 的 Spring component 註冊
  • index.html
  • BundleParseServiceTests
  • ParseControllerTests

完成後,首頁可以展示:

Quality Gate
JSON parse
FHIR R4 parse
Resource Type Gate
FHIR R4 validation
TW Core validation
Exchange contract rule results

讓四層驗證結果第一次在同一個可操作畫面裡串起來。

為什麼要新增 GateOutcome?

原本每一條交換規則會回傳 RuleResult

PASS
FAIL
NOT_APPLICABLE
NOT_EVALUATED

但使用者不只需要知道單條規則結果。
使用者還需要一個整體判斷:

這份 Bundle 最後能不能通過資料品質閘門?

所以 Day 13 新增:

public enum GateOutcome {
  PASSED,
  PASS_WITH_WARNINGS,
  BLOCKED
}

這不是新的醫療規則,它是把 JSON、FHIR R4、TW Core 與交換契約規則整合後的最小總結。

Quality Gate 的分類規則

Day 13 的 Quality Gate 只做最小分類:

PASSED
PASS_WITH_WARNINGS
BLOCKED

分類條件如下:

JSON parse 失敗 → BLOCKED
FHIR R4 parse 失敗 → BLOCKED
Resource type 不是 Bundle → BLOCKED
FHIR R4 validation 有 error/fatal → BLOCKED
TW Core validation 明確 FAILED → BLOCKED
任一交換契約規則 FAIL → BLOCKED
沒有阻擋,但 TW Core validation 是 NOT_EVALUATED → PASS_WITH_WARNINGS
沒有阻擋,但任一交換契約規則是 NOT_EVALUATED → PASS_WITH_WARNINGS
其餘情況 → PASSED

這裡有一個重要邊界,TW Core NOT_EVALUATED 不等於通過。
但它也不直接等於阻擋,原因是本專案的 90 小時停止線已經明確定義:

TW Core package 若無法穩定載入,TW Core 層必須安全降級為 NOT_EVALUATED。

所以 Day 13 的處理方式是:

TW Core NOT_EVALUATED → PASS_WITH_WARNINGS
TW Core FAILED        → BLOCKED

這樣可以避免兩種錯誤:

把尚未評估冒充成通過
把真正 Profile error 放行

BundleParseService 現在做什麼?

Day 13 以前,BundleParseService 的主要責任是:

讀入字串
        ↓
檢查 JSON parse
        ↓
檢查 FHIR R4 parse
        ↓
確認是否為 Bundle
        ↓
執行 FHIR R4 validation
        ↓
執行 TW Core validation 或安全降級

Day 13 之後,多了交換契約規則聚合:

讀入字串
        ↓
檢查 JSON parse
        ↓
檢查 FHIR R4 parse
        ↓
確認是否為 Bundle
        ↓
執行 FHIR R4 validation
        ↓
執行 TW Core validation 或安全降級
        ↓
依固定順序執行六條 ContractRule
        ↓
計算 Quality Gate

六條規則固定依這個順序執行:

LAB-REF-001
LAB-REF-002
LAB-REF-003
LAB-CODE-001
LAB-UNIT-001
LAB-UNIT-002

固定順序很重要,如果順序每次由 Spring 注入順序決定,頁面顯示與測試就可能不穩定。
所以今天在 BundleParseService 中明確排序:

private Map<String, Integer> ruleOrder() {
  return Map.of(
      LabRef001ObservationSubjectRule.RULE_CODE, 1,
      LabRef002DiagnosticReportResultRule.RULE_CODE, 2,
      LabRef003ReportObservationPatientRule.RULE_CODE, 3,
      LabCode001ObservationLoincRule.RULE_CODE, 4,
      LabUnit001ObservationQuantityUnitRule.RULE_CODE, 5,
      LabUnit002ObservationUcumCodeRule.RULE_CODE, 6
  );
}

這讓畫面、測試與文章說明可以使用同一個順序。

為何刻意特定情況下不執行交換契約規則?

今天刻意保留一個安全邊界:

JSON parse 失敗
FHIR R4 parse 失敗
輸入不是 Bundle

這三種情況不執行交換契約規則,原因是交換契約規則的輸入前提是:

已經取得一個 FHIR R4 Bundle

如果 JSON 本身不能解析,或 FHIR parser 無法把輸入轉成 Bundle,規則沒有可靠的資料結構可檢查。
這時 contractRuleResults 應該是空集合,不能顯示成 PASS,也不能顯示成 NOT_APPLICABLE
因為這不是規則不適用,而是規則根本還沒有被安全執行。

ValidationResult 的變化

Day 13 擴充 ValidationResult,新增兩個欄位:

List<RuleResult> contractRuleResults
GateOutcome gateOutcome

contractRuleResults 負責保存六條交換契約規則的詳細結果。
gateOutcome 負責保存整體 Quality Gate 判斷。
這樣 controller 與 Thymeleaf template 不需要重新計算規則。
畫面只需要讀取 ValidationResult

result.gateOutcome
result.contractRuleResults

這也讓後續要做 API、JSON 匯出或 Demo 劇本時,有同一份結果模型可以使用。

頁面現在能展示什麼?

Day 13 的頁面仍然是單頁流程。

使用者可以:

上傳 Bundle JSON
或直接貼上 JSON

送出後,頁面會先顯示整體結果:

Quality Gate
JSON parse
FHIR R4 parse
Resource Type Gate
FHIR R4 validation
TW Core validation
Resource type
Resource count
Error message

接著顯示 Resource summary、OperationOutcome issues、TW Core Profile issues。

https://ithelp.ithome.com.tw/upload/images/20260814/20177913Z8T4Sq4Ft6.png

https://ithelp.ithome.com.tw/upload/images/20260814/20177913JyWW28oGXb.png

最後新增交換契約規則表格。
表格欄位如下:

欄位 用途
Rule code 規則編號,例如 LAB-REF-001
Outcome PASSFAILNOT_APPLICABLENOT_EVALUATED
Severity informationwarningerror
Path 規則檢查的 FHIR path
Actual 實際資料值
Expected 預期條件
Evidence 判斷原因
Suggestion 中文修正方向

https://ithelp.ithome.com.tw/upload/images/20260814/20177913Pic39eG3BK.png

這個表格是 Day 13 最重要的畫面成果。
它讓使用者可以直接看到:

格式驗證通過,不代表交換契約一定通過。

四種測試案例

Day 13 能用四種資料測試 Quality Gate。

第一,四層都通過的 Bundle。
使用:

valid-twcore-contract-bundle.json

這份資料補齊目前 TW Core Profile validation 會要求的最小欄位,並使用合法的 urn:uuid fullUrl reference。
預期畫面會顯示:

Quality Gate → PASSED
JSON parse → PASSED
FHIR R4 parse → PASSED
FHIR R4 validation → PASSED
TW Core validation → PASSED
六條 Exchange contract rules → PASS

https://ithelp.ithome.com.tw/upload/images/20260814/20177913IyBFSU9cA3.png

這個案例用來證明 Day 13 的最小 Quality Gate 可以真正到達 PASSED,而不是只能展示被阻擋的案例。

第二,交換契約合法、但 TW Core 不完整的最小 lab Bundle。
使用:

valid-minimal-lab-bundle.json

這份資料是交換契約規則的正例,但不是完整 TW Core Profile 正例。
預期:

Quality Gate → BLOCKED
FHIR R4 parse / validation → PASSED
TW Core validation → FAILED
六條 Exchange contract rules → PASS

https://ithelp.ithome.com.tw/upload/images/20260814/20177913RAErkvJpdm.png

https://ithelp.ithome.com.tw/upload/images/20260814/20177913eSwGPOzuR0.png

這個案例用來說明:

交換契約規則可以全部通過,但只要 TW Core 層明確失敗,整體 Gate 仍然必須阻擋。

第三,Reference 錯誤。
使用:

missing-internal-reference.json

這份資料中的 Observation subject 指向 Bundle 內不存在的 Patient。
預期:

Quality Gate → BLOCKED
LAB-REF-001 → FAIL
LAB-REF-003 → FAIL

LAB-REF-001 失敗,是因為 Observation.subject 找不到 Bundle 內對應的 Patient。
LAB-REF-003 也會失敗,因為 DiagnosticReport 指向的 Observation subject 和 DiagnosticReport subject 不是同一個 Patient。

https://ithelp.ithome.com.tw/upload/images/20260814/20177913G2iF34OOn8.png

https://ithelp.ithome.com.tw/upload/images/20260814/20177913oeQLPgSLNw.png

第四,UCUM system/code 錯誤。
使用:

observation-quantity-wrong-ucum-system.json

這份資料有可讀的 unit,但 valueQuantity.system 不是 http://unitsofmeasure.org
預期:

Quality Gate → BLOCKED
LAB-UNIT-001 → PASS
LAB-UNIT-002 → FAIL

https://ithelp.ithome.com.tw/upload/images/20260814/20177913C1RZdeXjsn.png

https://ithelp.ithome.com.tw/upload/images/20260814/201779135VGDUnbuFx.png

這個案例很適合用來說明 Day 11 與 Day 12 的差別:

有 unit,不代表 UCUM system/code 合約檢查會通過。

整合測試

Day 13 修改:

src/test/java/com/twlab/qualitygate/validation/BundleParseServiceTests.java

新增或調整的測試重點有四個。

測試一:合法 Bundle 會回六條規則結果

assertThat(result.contractRuleResults())
    .extracting(RuleResult::ruleCode)
    .containsExactly(
        LabRef001ObservationSubjectRule.RULE_CODE,
        LabRef002DiagnosticReportResultRule.RULE_CODE,
        LabRef003ReportObservationPatientRule.RULE_CODE,
        LabCode001ObservationLoincRule.RULE_CODE,
        LabUnit001ObservationQuantityUnitRule.RULE_CODE,
        LabUnit002ObservationUcumCodeRule.RULE_CODE
    );

這個測試確認規則不只是各自存在,而是真的被 BundleParseService 串起來。

測試二:Reference 規則失敗會阻擋 Gate

assertThat(result.gateOutcome()).isEqualTo(GateOutcome.BLOCKED);
assertThat(result.contractRuleResults())
    .anySatisfy(ruleResult -> {
      assertThat(ruleResult.ruleCode()).isEqualTo(LabRef001ObservationSubjectRule.RULE_CODE);
      assertThat(ruleResult.outcome()).isEqualTo(RuleOutcome.FAIL);
    });

測試三:UCUM 規則失敗會阻擋 Gate

assertThat(result.gateOutcome()).isEqualTo(GateOutcome.BLOCKED);
assertThat(result.contractRuleResults())
    .anySatisfy(ruleResult -> {
      assertThat(ruleResult.ruleCode()).isEqualTo(LabUnit002ObservationUcumCodeRule.RULE_CODE);
      assertThat(ruleResult.outcome()).isEqualTo(RuleOutcome.FAIL);
    });

測試四:invalid JSON 不執行交換規則

assertThat(result.contractRuleResults()).isEmpty();
assertThat(result.gateOutcome()).isEqualTo(GateOutcome.BLOCKED);

這個測試確認早退流程不會誤顯示規則通過。

頁面測試

確認送出 Bundle 後會顯示:

Quality Gate
Exchange contract rule results
LAB-REF-001
LAB-UNIT-002

這代表瀏覽器流程已經能看到交換契約規則結果,而不是只有 service 層能取得。

自動化驗證

指令:

./mvnw test

測試結果:

Tests run: 52, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

https://ithelp.ithome.com.tw/upload/images/20260814/201779136dagxfc1Hg.png

Day 13 從 Day 12 的 50 個測試增加到 52 個測試。
新增的 2 個主要測試集中在整合層:

  • Reference rule failure 會讓 Quality Gate 變成 BLOCKED
  • UCUM rule failure 會讓 Quality Gate 變成 BLOCKED

同時既有 controller 測試也更新成 Day 13 畫面。

常見錯誤 & 排查

  1. NOT_EVALUATED 當成 PASS

NOT_EVALUATED 的意思是尚未評估,它不等於通過。
所以 TW Core 無法穩定執行時,不能顯示成:

PASSED

Day 13 的做法是讓整體 Gate 回:

PASS_WITH_WARNINGS

這比較符合降級處理原則。

  1. 把 TW Core FAILED 當成 warning

TW Core NOT_EVALUATED 可以是 warning。
但 TW Core 明確 FAILED 表示 Profile validation 已經執行,且存在 error/fatal issue。

這時 Quality Gate 必須是:

BLOCKED
  1. 在 JSON parse 失敗時仍然跑交換規則

交換契約規則需要 Bundle 物件。
如果 JSON 不能解析,或 FHIR parser 不能產生 Bundle,規則就沒有可靠輸入。
這時應該讓:

contractRuleResults = []
Quality Gate = BLOCKED
  1. 讓規則顯示順序不穩定

如果直接使用 Spring 注入的 List<ContractRule>,不同環境可能讓顯示順序變得不明確。
Day 13 明確用 rule code 排序,確保畫面與測試都固定。

  1. 只在 service 層做出結果,但頁面沒有顯示

Day 13 的目標是「可操作核心畫面」。
所以只讓 BundleParseService 回傳 RuleResult 還不夠。
一定要在 index.html 顯示:

Quality Gate
Exchange contract rule results

這和「能不能交換」有什麼關係?

Day 12 以前,我們已經能用規則回答:

Reference 鏈是否接得起來?
Observation.code 是否在合作方允許 LOINC 集合?
Quantity 是否具有可讀 unit?
Quantity 是否具有合作方允許的 UCUM system/code?

但這些答案都還停留在單元測試。

Day 13 把這些答案放回使用者流程。

現在使用者可以貼上一份 Bundle (UCUM system 錯誤案例),直接看到:

JSON: PASSED
FHIR R4: PASSED
TW Core: NOT_EVALUATED
Exchange Contract:
  LAB-REF-001: PASS
  LAB-REF-002: PASS
  LAB-REF-003: PASS
  LAB-CODE-001: PASS
  LAB-UNIT-001: PASS
  LAB-UNIT-002: FAIL
Quality Gate: BLOCKED

這正是本系列要展示的核心:

資料通過格式驗證,不代表一定能交換。

FHIR R4 與 TW Core 比較接近標準結構與 Profile 層。

交換契約規則則處理合作方實際接收條件。

Quality Gate 把這些層次整合成使用者可以理解的總結。

今天完成了什麼

  • 新增 GateOutcome
  • ValidationResult 新增 contractRuleResults
  • ValidationResult 新增 gateOutcome
  • 六條 ContractRule 註冊為 Spring component。
  • BundleParseService 在 Bundle parse 成功後執行六條交換契約規則。
  • BundleParseService 固定規則執行與顯示順序。
  • JSON parse 失敗、FHIR parse 失敗、non-Bundle 時不執行交換契約規則。
  • 新增最小 Quality Gate:PASSEDPASS_WITH_WARNINGSBLOCKED
  • TW Core NOT_EVALUATED 進入 warning,不冒充通過。
  • TW Core FAILED 會阻擋 Gate。
  • 首頁新增 Quality Gate 顯示。
  • 首頁新增 Exchange contract rule results 表格。
  • Controller 測試確認頁面會顯示 Quality GateLAB-REF-001LAB-UNIT-002
  • 新增 valid-twcore-contract-bundle.json 作為四層皆通過的展示案例。
  • Service 測試確認合法 Bundle 會回六條規則。
  • Service 測試確認 Reference failure 會 BLOCKED
  • Service 測試確認 UCUM failure 會 BLOCKED
  • ./mvnw test 通過,測試數從 50 增加到 52。

Day 13 尚未處理:

  • 契約 v1.0/v1.1 比較。
  • 契約 YAML/JSON 載入。
  • 契約啟用/停用規則。
  • Change Manifest。
  • Docker Compose。
  • GitHub Actions。
  • README 大整理。
  • Demo 劇本文件。
  • UI 美化、History、Diff、Dashboard。

目前的 MVP 進度:

validation-flow
├─ JSON parse                                  完成
├─ FHIR R4 parse                               完成
├─ FHIR R4 validation                          完成
├─ TW Core validation / safe NOT_EVALUATED      完成
├─ Exchange contract rules
│  ├─ LAB-REF-001                              完成並接回畫面
│  ├─ LAB-REF-002                              完成並接回畫面
│  ├─ LAB-REF-003                              完成並接回畫面
│  ├─ LAB-CODE-001                             完成並接回畫面
│  ├─ LAB-UNIT-001                             完成並接回畫面
│  └─ LAB-UNIT-002                             完成並接回畫面
└─ Quality Gate                                完成最小版

下一步預計處理:

契約 v1.0 / v1.1 基本比較
Docker Compose
GitHub Actions

Repository:twcore-data-quality-gate


上一篇
Day12 - 完成 UCUM system/code 契約允許集合規則
下一篇
Day14 - 契約 v1.0 / v1.1 基本比較骨架
系列文
醫療資料通過標準驗證,就真的能交換嗎?——30 天打造 TW Core 資料品質閘門16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言