外部 API 介接是所有開發者都會遇到的任務。但當對方是政府機關的 API,遊戲規則就不一樣了:
- 規格文件經常更新——v5.0.12、v5.0.13、v5.0.15、v5.0.16⋯⋯版號密集迭代
- 驗證規則嚴格——欄位格式、必填邏輯、值域範圍全部自動檢核
- 退件不告訴你哪裡錯——只說「不符規格」,具體哪個欄位要自己查
- 沒有 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.5、999.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]]"
兩個問題:
- 句號 typo 導致體溫驗證完全失效
- 脈搏/呼吸/血壓應該是整數,不應接受小數
送出——又被退件。
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 沒事,新版本送了會報錯」。這種「規格文件沒寫、要踩了才知道」的暗規則,只能靠經驗累積。