政府 API 版本追逐戰:用 Claude Code 應對 5 次退件的外部 API 適配


外部 API 介接是所有開發者都會遇到的任務。但當對方是政府機關的 API,遊戲規則就不一樣了:

  1. 規格文件經常更新——v5.0.12、v5.0.13、v5.0.15、v5.0.16⋯⋯版號密集迭代
  2. 驗證規則嚴格——欄位格式、必填邏輯、值域範圍全部自動檢核
  3. 退件不告訴你哪裡錯——只說「不符規格」,具體哪個欄位要自己查
  4. 沒有 sandbox——送出去才知道對不對

這篇記錄一個 Legacy PHP 系統介接政府 API 的過程:7 天內 5 個版本 + 1 次 Revert,以及 Claude Code 怎麼加速每次迭代。


背景:政府資料上傳 API

系統需要定期將照護紀錄上傳至政府平台(VendorAPI-A)。上傳內容包括:

  • 基本資料(BaseData)
  • 照護評估(Evaluation)
  • 照護計畫(CarePlan)
  • 照護紀錄(CareRecord)——內含生理徵象(VitalSign)和傷口紀錄(WoundRecords)

每個區塊都有嚴格的欄位規格,由 API 端自動檢核。


前傳:生理徵象上傳的 4 次修正

在主戰場之前,光是「生理徵象上傳」就經歷了 4 次修正:

v1:陣列索引覆蓋

// ❌ 每次迴圈都覆蓋,只留最後一筆
$VitalSign_list = $VitalSign;

// ✅ v2 修正:用計數器索引
$VitalSign_list[$count_VitalSign] = $VitalSign;

v2:PHP 陣列 vs stdClass

API 要求的 JSON 格式是物件,不是陣列:

// ❌ PHP 陣列轉 JSON 會變成 [{"Date":...}]
$VitalSign = ['Date' => '2026-03-01', ...];

// ✅ v3 修正:用 stdClass 轉 JSON 會變成 {"Date":...}
$VitalSign = new stdClass();
$VitalSign->Date = '2026-03-01';

v3:空值預設值

// ❌ 預設 "0.000",API 不接受
$VitalSign->Temperature = "0.000";

// ✅ 修正:體溫 "0.0",其他 "0"
$VitalSign->Temperature = "0.0";
$VitalSign->Pulse = "0";

v4:無資料時不要傳空物件

// ❌ 沒有生理徵象也傳空物件,被 API 拒絕
$VitalSign_list = new stdClass();

// ✅ v4 修正:null 判斷
$VitalSign_list = null;
if (!is_null($VitalSign_list)) {
    $CareRecord->VitalSign = $VitalSign_list;
}

4 次修正,4 天。 每一版都是「送出去被退件」才知道哪裡不對。


API 版本升級:v5.0.15 / v5.0.16

緊接著,API 發布新版本規格,又需要調整:

視力部位欄位格式變更

// ❌ 舊格式:合併在一個字串
$Answer = "左(白內障、青光眼);右(白內障)";

// ✅ v5.0.15 新格式:拆分成獨立項目,改用 MultipleAnswer
$MultipleAnswer = "左眼白內障;左眼青光眼;右眼白內障";
$Answer = "";  // Answer 留空

護家端移除 EvaluationTime

v5.0.16 規格中移除了 EvaluationTime 欄位。舊版本送這個欄位沒事,新版本送了反而報錯。


主戰場:傷口紀錄 + 血糖血氧(7 天 5 版本)

v1:大改 444 行

第一版是最大的改動——9 個檔案、+444 行

資料庫:新增 BloodGlucose(血糖)、BloodOxygen(血氧)欄位

傷口紀錄全新實作

// 查詢傷口紀錄,組裝 API 格式
$WoundRecords = new stdClass();
$WoundRecords->Part = $wound['Part'];
$WoundRecords->Length = ($wound['Length'] == -1) ? "" : $wound['Length'];
$WoundRecords->Width = ($wound['Width'] == -1) ? "" : $wound['Width'];
$WoundRecords->Depth = ($wound['Depth'] == -1) ? "" : $wound['Depth'];

// 分類 mapping
$categoryMap = [
    'pressure' => '壓傷',
    'dermatitis' => '失禁性皮膚炎',
    'other' => '其他'
];

// 等級 mapping
$levelMap = [
    '1' => '1級', '2' => '2級', '3' => '3級', '4' => '4級',
    'nonLevel' => '不可分級'
];

表單 UI:生理徵象新增血糖/血氧欄位,傷口紀錄表單完全重寫

合併進 release branch 後——被 Revert

被 Revert 的原因

v1 的傷口等級 mapping 使用 1級/2級/3級/4級/不可分級,但 API v5.0.16 的規格是:

v1 寫的API 要求的
1級第一期
2級第二期
3級第三期
4級第四期
不可分級無法分期

而且失禁性皮膚炎的等級也完全不同:

v1 寫的API 要求的
1級/2級/3級1A/1B/2A/2B

等級從 3 級變成 4 級,命名方式完全不同。

v2:修正分級 mapping

// ✅ 依 API v5.0.16 規格修正
$pressureLevelMap = [
    '1' => '第一期', '2' => '第二期',
    '3' => '第三期', '4' => '第四期',
    'nonLevel' => '無法分期',
    'deepTissue' => '深層組織壓力性損傷'  // v1 漏掉的選項
];

$dermatitisLevelMap = [
    '1' => '1A', '2' => '1B',
    '3' => '2A', '4' => '2B'
];

// Level 依 Category 查不同的表
$level = $arrWoundLevel[$wound['Category']][$wound['Level']];

送出——又被退件。這次是傷口尺寸超出欄位限制。

v3:新增傷口尺寸格式驗證

API 規格要求傷口長寬深:最多 3 位整數 + 1 位小數(例如 12.5999.9)。

v2 沒有前端驗證,使用者可以輸入 12345.67,直接被 API 拒絕。

// ✅ 新增自訂驗證規則
"threeIntOneDecimal": {
    "regex": /^([0-9]{1,3})(\.[0-9]{1})?$/,
    "alertText": "* 請輸入最多3位整數及1位小數"
}
// ✅ 後端也要修:0 不應被 empty() 當成空值
// ❌ 原本
if (!empty($wound['Length'])) { ... }

// ✅ 修正
if (is_numeric($wound['Length']) && $wound['Length'] >= 0) { ... }

送出——再被退件。這次 API 說生理徵象的數值欄位也不符規格。

v4:生理徵象格式驗證(含 typo)

// ✅ 體溫:3 位整數 + 1 位小數
// ❌ 但有 typo:句號不是逗號
className: "validate[required. custom[threeIntOneDecimal]]"
//                          ^ 這是句號!

// ❌ 脈搏/呼吸/血壓:用了小數驗證,但 API 要求整數
className: "validate[required, custom[threeIntOneDecimal]]"

兩個問題:

  1. 句號 typo 導致體溫驗證完全失效
  2. 脈搏/呼吸/血壓應該是整數,不應接受小數

送出——又被退件

v5:修正所有驗證規則

// ✅ 修正句號為逗號
className: "validate[required, custom[threeIntOneDecimal]]"

// ✅ 脈搏/呼吸/血壓:改為整數驗證
className: "validate[required, custom[integer], max[999], min[0]]"

// ✅ 血糖/血氧:非必填整數
className: "validate[custom[integer], max[999], min[0]]"

這版終於通過了。


完整時間線

12/05  v1 — +444 行大改(血糖血氧 + 傷口紀錄)
12/09  ❌ Revert v1 — 傷口分級名稱不符 v5.0.16
12/09  v2 — 修正分級 mapping(壓傷用「期」、皮膚炎用 1A/1B/2A/2B)
12/11  v3 — 新增傷口尺寸格式驗證(3位整數+1位小數)
12/12 AM  v4 — 補生理徵象格式驗證(含句號 typo + 整數/小數混用)
12/12 PM  v5 — 修正 typo + 區分整數/小數欄位

7 天、5 個版本、1 次 Revert。 每一版都是「送出 → 被退 → 修 → 再送」的循環。


後續:持續的 API 合規修正

傷口紀錄搞定後,API 合規修正還在繼續:

照護計畫:漏掉停止欄位

// ❌ 照護措施上傳時漏了三個欄位
$Measures->StartDate = $row['StartDate'];
// 漏了 MeasureStopDate、MeasureStopContent、MeasureStopNurseID

// ✅ 補上
$Measures->MeasureStopDate = $row['MeasureStopDate'] ?? '';
$Measures->MeasureStopContent = $row['MeasureStopContent'] ?? '';
$Measures->MeasureStopNurseID = $row['MeasureStopNurseID'] ?? '';

身分證字號大小寫

// ❌ 使用者輸入小寫身分證字號,API 拒絕
"NurseID": "a123456789"

// ✅ 遞迴轉大寫函式,套用至所有 9 個上傳區塊
function uppercase_id_fields(&$data) {
    $fields = ['NurseID', 'CreateID', 'MeasureStopNurseID'];
    foreach ($fields as $field) {
        if (isset($data->$field)) {
            $data->$field = strtoupper($data->$field);
        }
    }
    // 遞迴處理子物件和陣列
    foreach ($data as &$value) {
        if (is_object($value) || is_array($value)) {
            uppercase_id_fields($value);
        }
    }
}

退件原因分析

回頭看所有退件,可以歸類為五種模式:

退件類型範例頻率
值域不符「1級」vs「第一期」、小寫身分證最常見
格式超限傷口尺寸超過 3+1 位數常見
欄位缺漏停止日期/原因/人員沒送常見
結構錯誤陣列 vs 物件、空物件 vs null偶爾
版本差異舊版本有的欄位新版本不能送偶爾

Claude Code 怎麼加速 API 適配

1. 解析 API 規格文件

> 這是 VendorAPI-A v5.0.16 的規格文件。
  跟我們目前的上傳邏輯比對,列出所有不符合的欄位。

Claude Code 可以同時讀 API 規格和現有程式碼,一次列出所有不符的地方。不用像 v1→v5 那樣每次只修一個被退的點。

2. 自動產生 mapping 表

> 根據 API 規格,產生傷口分類和等級的 PHP mapping 陣列。
  注意不同分類(壓傷、皮膚炎、其他)的等級選項不同。

v1 的 mapping 錯誤(用了「級」而不是「期」)就是人工對照規格時看錯了。Claude Code 不會看錯。

3. 批次產生驗證規則

> 這些欄位的格式限制如下:
  - 體溫:3位整數1位小數
  - 脈搏/呼吸/血壓:0-999 整數
  - 血糖/血氧:0-999 整數,非必填
  幫我產生前端 validationEngine 規則和後端 PHP 驗證。

v4 的句號 typo 就是手動寫驗證規則時的人為錯誤。讓 Claude Code 產生,至少不會有 typo。

4. 遞迴處理所有上傳區塊

> 這個 uppercase 邏輯需要套用到所有 9 個上傳區塊的
  NurseID、CreateID、MeasureStopNurseID 欄位。
  幫我寫一個遞迴函式統一處理。

身分證字號大小寫的修正就是這個模式——Claude Code 寫了遞迴函式,一次套用到所有區塊,不用逐個區塊手動改。

5. 退件後的差異分析

> API 回傳「不符規格」。
  幫我比對我們送出的 JSON 和 API 規格文件,
  找出哪些欄位的值不在允許的值域內。

最耗時的不是修改程式碼,而是找出哪裡不對。Claude Code 可以比對 JSON 輸出和規格文件,精準定位問題欄位。


外部 API 適配的檢查清單

項目說明
欄位值域每個欄位的允許值是否完全符合規格
資料型別字串 vs 數字 vs 物件,JSON 結構是否正確
必填邏輯什麼條件下哪些欄位必填
空值處理null vs 空字串 vs 不送,API 各自的要求不同
格式限制位數限制、大小寫、日期格式
版本差異新舊版本的欄位增刪
前後端一致前端驗證規則和 API 規格完全一致

結語

外部 API 適配最痛苦的不是寫程式,而是資訊不對稱。你不知道 API 端的驗證規則有多嚴格、值域定義有多精確,直到你送出去被退件。

7 天 5 個版本,每一版都是「猜哪裡不對 → 修 → 再送」的循環。如果一開始就讓 Claude Code 完整比對規格文件和現有程式碼,很多問題在第一版就能抓到,不用等到被退件才發現。

不過有些坑是 Claude Code 也無法預測的——例如「舊版本送 EvaluationTime 沒事,新版本送了會報錯」。這種「規格文件沒寫、要踩了才知道」的暗規則,只能靠經驗累積。