# CR-024　服務間呼叫改採 `ServiceClientHttpClient`（修除 Feign 繞過出站憑證之缺陷）

| 項目 | 內容 |
|---|---|
| 文件編號 | CR-024 |
| 版本 | **1.0** |
| 提出日 | 2026-09-29 |
| 提出者 | WorkBuddy AI（開發團隊） |
| 緣起 | `GOV-006 §4 #21`（節後開工之第一動，BEAM PMO 指示）。原記載為「`LimitServiceClient`（Feign）未接 M2M outbound token → 一律 401」，**修法方向原建議「Feign 加 `RequestInterceptor`」**；**本次動工前查證後修正該建議**（見 §2） |
| 影響範圍 | `authorization-service`：`LimitServiceClient`／`CardServiceClient`（刪除）、`LimitServiceHttpClient`／`CardServiceHttpClient`（新增）、`RemoteLimitPort`／`RemoteCardPort`／`RemoteLimitUploadAdapter`（改注入）、`AuthorizationServiceApplication`（移除 `@EnableFeignClients`）、`build.gradle.kts`（移除 Feign 依賴） |
| 狀態 | ✅ **已執行（2026-09-29）**——編譯通過（`BUILD SUCCESSFUL`），真機驗證見 §5 |

---

## 1. 問題（`#21` 之實測證據）

**現象**（2026-09-29 真機實測，`AUTH000041`）：

```
Stand-in 降級：limit-service 查詢失敗，改用本地讀模型。
  原因=feign.FeignException$Unauthorized: [401] …
Stand-in 降級：limit-service 佔用失敗，改由本地代佔（須事後上送）。
  原因=feign.FeignException$Unauthorized: [401] …
```

**後果**：
1. 授權之額度佔用**全數降級為本地代佔** → 決策**恆為 `STANDIN`**（無法產生 `APPROVED`）；
2. **`Stand-in` 後送永遠 401** → `standin_upload_at` **恆為空** →
   `#A-98`（`CR-022` (A) 後送）與 `#A-99`（`/auth/stip` 頁）**兩項交付無法真正閉環**；
3. **`card-service` 同型** → 卡片狀態檢核降級讀本地快取，日誌自身警告
   「**注意：鎖卡可能未即時生效**」——此為**資安層級**之降級。

## 2. ★ 查證：原建議之修法方向**不正確**（本 CR 之核心）

`#21` 原建議「**Feign 加 `RequestInterceptor` 接 `OutboundTokenProvider`**」。
動工前查證後**修正**，理由如下：

| 查證項 | 結果 |
|---|---|
| 平台是否已有服務間呼叫之共用客戶端？ | ✅ **有**：`common-spring` 之 `ServiceClientHttpClient`（`CSP-001 §8.5`） |
| 其如何處理憑證？ | 建構子**強制要求非空**之 `OutboundTokenProvider`；每次送出前取 `authorizationHeaderValue()` 並加 `Authorization` 標頭 |
| 其設計意圖？ | 註解明載：「**杜絕『建了客戶端卻沒給憑證來源 → 靜默不帶標頭 → 全部 401』，與 `CR-010` 所欲修除之缺陷同型**」 |
| 其他服務怎麼做？ | ✅ `statement`（×2）／`payment`／`posting` **皆用 `ServiceClientHttpClient`** |
| 誰用 Feign？ | ⚠️ **僅 `authorization-service`（×2）與 `card-service`（×1）** —— **異類** |
| authorization-service 之憑證機制有啟用嗎？ | ✅ **有**——啟動日誌載「服務間呼叫憑證已啟用：**client_credentials（client=cc-authorization-service）**」 |

**結論**：**憑證機制本來就是好的**，問題純粹是 **Feign 客戶端繞過了它**。
若照原建議「Feign 加 interceptor」，將是**重造** `ServiceClientHttpClient` 已實作之行為，
且**失去其「強制非空」之保護**（`Objects.requireNonNull`）——**同型缺陷可能再犯**。

→ **故改採：直接使用 `ServiceClientHttpClient`**（與 4 個既有服務同構）。

## 3. 變更內容

| 檔案 | 動作 |
|---|---|
| `LimitServiceHttpClient` | **新增**（`ServiceClientHttpClient` 之封裝；含 `queryLimits`／`hold`／`release`） |
| `CardServiceHttpClient` | **新增**（`lookup`；**查無此卡改以 HTTP 404 判斷**，原為 `feign.FeignException.NotFound`） |
| `LimitServiceClient` | **刪除**（Feign） |
| `CardServiceClient` | **刪除**（Feign） |
| `RemoteLimitPort` | 改注入 `LimitServiceHttpClient` |
| `RemoteCardPort` | 改注入 `CardServiceHttpClient` |
| `RemoteLimitUploadAdapter` | 改注入 `LimitServiceHttpClient`（**後送路徑**） |
| `AuthorizationServiceApplication` | **移除** `@EnableFeignClients` |
| `build.gradle.kts` | **移除** `spring-cloud-starter-openfeign` 依賴 |

**契約變更**：**無**（端點與 DTO 不變，僅呼叫方式改變）。

**失敗語意不變**：所有遠端失敗仍拋 `InfrastructureException`（`CC-PLT-5001`），
降級決策仍統一由 `StandInLimitPort`／`StandInCardPort` 處理——**本次未動降級邏輯**。

## 4. 範圍說明（為何一併修 `card-service`）

`#21` 僅點名 `LimitServiceClient`，但 `CardServiceClient` **同服務、同型缺陷、同修法**，
且其後果為**資安層級**（鎖卡未即時生效）。若只修一半，authorization-service
仍有一半呼叫 401 → 故**一併修**。

**未動**：`card-service` 之 `CreditReviewClient`（**不同服務**）——
已登記為後續項（同型，應以同一原則處理）。

## 5. 驗證

| 項 | 結果 |
|---|---|
| 編譯 | ✅ `BUILD SUCCESSFUL`（含測試編譯） |
| 單元測試 | 見 `HANDOVER §3.A #A-100` |
| **真機驗證** | 見 `HANDOVER §3.A #A-100`：授權**不再 401**、決策為 `APPROVED`（非 `STANDIN`）、後送**成功**寫入 `standin_upload_at` |

## 6. 未竟

- ⚠️ `card-service` 之 `CreditReviewClient`（Feign）**尚未處理**——同型缺陷，已登記。
- ⚠️ `#21` 原記載之「ACL 白名單須同步」——本次未動 ACL（實測未出現 403，故無須變更）；
  若後續出現 403，再依 `ServiceAcl` 處理。

---

## 7. 修訂記錄

| 版本 | 日期 | 說明 |
|---|---|---|
| 1.0 | 2026-09-29 | 初版：`#21` 實測證據；**★ 查證修正原建議之修法方向**（Feign+interceptor → `ServiceClientHttpClient`）；變更清單；範圍說明（一併修 card）；驗證與未竟 |
