把 SOAP 改成 REST,XML 換成 JSON,看起來是乾淨多了。不過我會再往下看一層:呼叫端拿到 JSON 之後,是不是還要自己知道 ERP 哪個欄位代表什麼、哪些值有特殊意義?如果還要,那 ERP 的複雜度只是換了個格式,沒有真的被擋在 API 後面。
2026-09-10T08:30:00+08:00。常見的情況是這樣:欄位名稱照抄 ERP,日期還是 20260909 這種八碼字串,成功和失敗都回 HTTP 200,要看某個欄位的值才知道有沒有成功。呼叫端雖然不用再處理 XML,卻還是得把舊介面的這些慣例學一遍。
因為要衡量的是呼叫端的負擔,所以我也用呼叫端來檢查:只拿平台的契約,不看 ERP 文件,串接做得完嗎?如果做不完,還得去找 ERP 欄位對照表,就回頭看哪些東西可以在 Adapter 裡先整理好。
像 ERP 的 Y/N、空白與特殊日期,我會先確認它們在業務上代表什麼,再決定怎麼對到 boolean、null 或 ISO 8601。為了讓各個介面不會各自解讀,規則要記下來。

圖 Day 15-1:SOAP 與 REST 轉換。
有些值代表未填,有些日期代表沒有期限,看起來都能正常轉換,但意思一旦弄錯,後面的判斷就會跟著錯。所以這四項裡,我會多花一點時間確認資料語意。
空字串、空白字元和 null,在 ERP 裡可能有不同意思,所以空值也值得單獨測試。哪些保留、哪些轉成 null,要先決定好,也把原因留下來。
第四項要單獨說一下。舊的 ERP 端點不會動,變的只有平台這一層的契約,所以相容的責任全在 Adapter,不能要求所有呼叫端一起改。我的原則很簡單:加欄位可以,舊呼叫端不認識會直接忽略;要改既有欄位的意思或型別,就開新版本的路徑,讓舊呼叫端繼續用舊契約。
拿工單查詢走一遍。一開始回應只有工單號、狀態、到期日三個欄位,A 系統照這份契約串好上線。三個月後 B 系統來串,要多一個「工單類型」,這個值 ERP 本來就有,直接加進回應,A 系統不認識這個欄位、會忽略,不受影響。但如果 B 系統同時希望到期日改成 ISO 8601,A 系統已經在解析 20260909 這種八碼字串,一改就出問題了,這種就開 v2 路徑,A 繼續用 v1。整個過程 ERP 的 SOAP 端點一行都沒改。
接手的人要能照契約處理,不需要猜 ERP 原始訊息的意思。所以 HTTP 狀態碼先讓呼叫端知道是哪一類問題,詳細原因再用穩定的業務錯誤碼說明:
在 Adapter 裡,這個分工就是一個集中處理例外的類別。以下是簡化後的節錄:
@RestControllerAdvice
public class ErpExceptionHandler {
// ERP 回 <Status code="-1"> 或 ReturnCode 不為 0:請求合法,但不符合業務規則
@ExceptionHandler(ErpBusinessException.class)
public ResponseEntity<ErrorResponse> onBusiness(ErpBusinessException ex) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
.body(ErrorResponse.of("ERP-BIZ-" + ex.erpCode(), ex.getMessage()));
}
// 非逾時的連線層錯誤:連線被拒、HTTP 錯誤或 SOAP Fault 都歸這一類
@ExceptionHandler(ErpConnectionException.class)
public ResponseEntity<ErrorResponse> onConnection(ErpConnectionException ex) {
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(ErrorResponse.of("ERP-CONN-001", "ERP 連線異常,請稍後再試"));
}
// 逾時:ERP 可能已經執行完,只是結果沒回來
@ExceptionHandler(ErpTimeoutException.class)
public ResponseEntity<ErrorResponse> onTimeout(ErpTimeoutException ex) {
return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
.body(ErrorResponse.of("ERP-TIMEOUT-001", "ERP 未在時限內回應"));
}
}
回頭看節錄,三個 handler 是兩類寫法。onBusiness 回 422,錯誤碼是 ERP-BIZ- 接 ERP 原始代碼,訊息也直接帶 ERP 的,因為呼叫端要靠那個代碼決定下一步;onConnection 與 onTimeout 則是寫死的 ERP-CONN-001、ERP-TIMEOUT-001 加一句固定文字,因為 ERP 回了什麼、內部路徑長什麼樣,都不該讓外面看到。節錄沒放的還有三種 400:值被拒、什麼都沒帶、取不到身分(JWT 合法但裡面沒有可用的呼叫者工號),三種處置不同,所以也是三個代碼。
這幾個字串一旦對外,就是契約的一部分。之後可以加新的代碼,但 ERP-CONN-001 不能下一版變成代表別的事。旁邊那句「ERP 連線異常,請稍後再試」是給人看的,可以改;代碼是給程式判斷的,不能改。
onConnection 與 onTimeout 上面的兩行註解,就是 502 與 504 的差別。502 是逾時以外的連線層錯誤,像連線被拒、HTTP 錯誤或 SOAP Fault,請求多半沒有真正被 ERP 執行,這種重送通常沒有副作用;504 是等不到回應,但 ERP 可能已經執行完,只是結果沒回來,這時重送就可能做兩次。所以要不要重送,得先確認這個操作是否冪等、交易狀態走到哪裡。
| 取捨 | 這樣選的理由 | 何時要重新評估 |
|---|---|---|
| 平台契約不沿用 ERP 欄位名稱 | 呼叫端不需理解舊系統慣例 | 介面很少用到、另做欄位對照划不來時 |
| 錯誤類別用 HTTP、原因用業務錯誤碼 | 類別穩定可依賴,原因可擴充 | 需要更細的類別語意時 |
| 轉換規則集中留檔 | 避免各介面各自解讀 | 規則數量成長到需要工具化管理時 |
| 舊端點維持不變 | 既有呼叫端不受影響 | 舊端點退場條件成立時 |
每加一項轉換規則,後面都需要相容性檢查與測試。新增時先確認是不是平台共用的需要;只屬於某個呼叫端的特殊呈現,就再看看由哪一層負責比較合適。
request/response 應有 schema、契約測試及 golden sample,並涵蓋以下邊界情境:
| 邊界情境 | 為什麼要測 |
|---|---|
| 日期格式與特殊日期值 | 舊系統常以特定值代表無期限或未設定 |
| 中文與多位元組字元 | 編碼設定錯誤時只在特定資料上出錯 |
| decimal 精度與捨入 | 金額與數量的差異會直接造成對帳問題 |
| null 與空字串 | 兩者語意不同,轉換規則必須明確 |
| 重複欄位 | XML 允許重複節點,JSON 對應方式需固定 |
| 大量資料 | 筆數上限與分頁行為要與 ERP 限制一致 |
因為之後調整轉換規則時,要能確認原本正確的結果沒有被一起改掉,所以我會保留一組 golden sample,拿同一組資料比一比。
把格式、資料意思、錯誤與版本一起整理好,呼叫端才比較容易安心使用平台契約。下一篇,接著談查詢條件:想保留彈性,又要怎麼把範圍控制好?