# CR-004　平台層 500-vs-400 錯誤映射缺口（`GlobalExceptionHandler`）

| 項目 | 內容 |
|---|---|
| 文件編號 | CR-004 |
| 版本 | 1.1 |
| 狀態 | **已核定／已執行**（2026-09-19 BEAM PMO 核定；同日執行完成並實測通過） |
| 提出日期 | 2026-09-19 |
| 核定日期 | 2026-09-19 |
| 完成日期 | 2026-09-19 |
| 提出者 | 開發團隊（`SDD-005` 值域稽核後之待辦彙整時發現；`SCS-010 §10.4` #15 曾局部揭露） |
| 影響文件 | CSP-003 §2／§3／§7、SDD-006 §3.3／§3.4、SCS-010 §10.4、SCS-018 |
| 影響服務 | **全部 17 個服務**（`common-spring` 之共用元件） |
| 優先級 | **高（P1）**——客戶端錯誤一律回 **500 並觸發告警**，且**違反全部 17 份契約** |
| 核定結論 | 4 項待決**全數核准**；細節授權開發團隊安排（詳 §6） |
| 驗收結果 | **6/6 全對**（2 服務各自前後對比）；請求層錯誤 **0 筆 ERROR 日誌**（詳 §4.1） |

---

## 1. 摘要

`common-spring` 之 `GlobalExceptionHandler` **僅有 2 個 handler**
（`DomainException`、`Exception`）。Spring MVC 自身拋出之**請求層例外**
（主體不可解析、必要標頭缺失、方法不支援、路徑不存在……）
**皆非 `DomainException` 子類**，因而落入 `Exception` 兜底分支 →
**一律回 `500 CC-PLT-5000`**。

後果有三：

1. **契約違反**：全部 17 份 OpenAPI 契約對這些情境宣告 `400`／`404`／`405`。
2. **告警污染**：`GlobalExceptionHandler` 自身之日誌策略為「5xx 記 ERROR 並附完整堆疊
   （伺服器問題，**需告警**）」。客戶端送錯格式即產生 ERROR 與告警——
   **告警疲勞**將使真正的伺服器故障被淹沒。
3. **實作分歧**：`rewards-service` 為繞開此缺陷，**放棄使用 Bean Validation**、
   改以手寫檢核（`SCS-010 §10.4` #15）。此繞道使各服務之驗證風格不一致。

---

## 2. 現況與實測證據

以下為 **2026-09-19 實測**（`installment-service`，埠 8089，本地 Oracle 26ai `FREEPDB1`），
非推測。實作路由為 `/api/v1/installments`（**非** `/installment-plans`）。

| # | 情境 | 契約要求 | **實測結果** | 判定 |
|---|---|---|---|---|
| ① | `POST` 缺必要標頭 `X-Channel` | `400` | **`500` `CC-PLT-5000`** | ❌ **缺陷** |
| ② | `POST` 請求主體為非法 JSON（`{bad json`） | `400` | **`500` `CC-PLT-5000`** | ❌ **缺陷** |
| ③ | 路徑變數 UUID 格式錯誤 | `400` | `400` `CC-INS-4000` | ✅ 正確（服務自行檢核後拋 `DomainException`） |
| ④ | `DELETE`（不支援之方法） | `405` | **`500` `CC-PLT-5000`** | ❌ **缺陷** |
| ⑤ | 不存在的路徑 | `404` | **`500` `CC-PLT-5000`** | ❌ **缺陷** |
| ⑥ | 查無資源（合法 UUID 但不存在） | `404` | `404` `CC-INS-4040` | ✅ 正確（`DomainException`） |

**6 項中 4 項錯誤。**

**③／⑥ 為何正確**：服務在進入 Spring MVC 之例外處理前**自行檢核**，
拋出已登錄之 `DomainException`（`CC-INS-4000`→400、`CC-INS-4040`→404）。
**這正是繞道**——每個服務都得自己重做一次框架本該做的事。

### 2.1 根因

```java
@ExceptionHandler(DomainException.class)  // ← 業務例外：正確映射
@ExceptionHandler(Exception.class)        // ← 兜底：一律 500
```

Spring MVC 之請求層例外（`MissingRequestHeaderException`、
`HttpMessageNotReadableException`、`HttpRequestMethodNotSupportedException`、
`NoResourceFoundException`……）**均為 `Exception` 之直接或間接子類，
但非 `DomainException`**，故全部落入兜底。

### 2.2 影響範圍（實測統計）

| 項 | 數值 |
|---|---|
| 引用 `common.spring` 之服務 | **17 / 17** |
| 自行加 `@ExceptionHandler` 之服務 | **0** |
| 受影響契約 | **17 份**（全部） |

由於缺陷位於共用元件，**一次修正即覆蓋全部 17 個服務**。

---

## 3. 建議修法

### 3.1 新增錯誤碼（須先擴充 `SDD-006 §3.4`）

依 `CSP-003 §2`「**Coder 不得自創錯誤碼**；新增碼須先擴充 `SDD-006 §3.4`」之程序，
擬新增 5 碼。惟**現行類別位定義無處可放**（見 §3.2），故同時提請擴充類別位語意。

**原提案**（立 CR 時）：

| 擬增碼 | HTTP | 名稱 | 涵蓋之例外 |
|---|---:|---|---|
| `CC-PLT-4000` | 400 | 請求格式錯誤 | `HttpMessageNotReadableException`、`MethodArgumentTypeMismatchException`、`HttpMediaTypeNotSupportedException` |
| `CC-PLT-4001` | 400 | 必要輸入缺失 | `MissingRequestHeaderException`、`MissingServletRequestParameterException`、`MissingPathVariableException` |
| `CC-PLT-4002` | 400 | 欄位驗證失敗 | `MethodArgumentNotValidException`、`HandlerMethodValidationException`、`ConstraintViolationException` |
| `CC-PLT-4003` | 405 | HTTP 方法不支援 | `HttpRequestMethodNotSupportedException` |
| `CC-PLT-4040` | 404 | 路徑或資源不存在 | `NoResourceFoundException`、`NoHandlerFoundException` |

**實作定案**（核定後，授權開發團隊調整細節；已寫入 `SDD-006 §3.4`）：

| 定案碼 | HTTP | 名稱 | 涵蓋之例外 |
|---|---:|---|---|
| `CC-PLT-4001` | 400 | 缺少必要請求標頭或參數 | `MissingRequestHeaderException`、`MissingServletRequestParameterException` |
| `CC-PLT-4002` | 400 | 請求主體格式錯誤 | `HttpMessageNotReadableException` |
| `CC-PLT-4003` | 400 | 請求參數驗證失敗 | `MethodArgumentNotValidException`、`HandlerMethodValidationException`、`ConstraintViolationException`、`MethodArgumentTypeMismatchException` |
| `CC-PLT-4040` | 404 | 資源或路徑不存在 | `NoResourceFoundException` |
| `CC-PLT-4050` | 405 | HTTP 方法不支援 | `HttpRequestMethodNotSupportedException` |

**定案與提案之差異（3 項，均為刻意調整）**：

| # | 差異 | 理由 |
|---|---|---|
| 1 | 提案之 `CC-PLT-4000`（請求格式錯誤）**未採用**；其涵蓋之 `HttpMessageNotReadableException` 改歸 `4002` | 避免同一 HTTP 400 出現兩個語意相近之碼（`4000` 與 `4002`）。`4000` **保留未用**，可作日後「通用 400」之空間 |
| 2 | 方法不支援由 `4003` 移至 **`4050`** | 使**後兩碼對齊 HTTP 狀態碼**（`4001`／`4002`／`4003`→400、`4040`→404、`4050`→405）。此規則與既有服務碼（`CC-INS-4000`→400、`CC-INS-4040`→404）之編號習慣一致，且**可由碼反推狀態**，便於人眼判讀 |
| 3 | 提案之 `MissingPathVariableException`、`HttpMediaTypeNotSupportedException`、`NoHandlerFoundException` **未涵蓋** | 見 §3.5 之逐項理由 |

> ⚠️ **編號差異為文件間須對齊之處**：本 CR §3.1 原提案表**已失效**，
> 權威版本為 `SDD-006 §3.4`（實作定案）。

### 3.2 擴充類別位語意（原「須裁定」，**已裁定：同意**）

`CSP-003 §2` 之**修正前**類別位：

| 類別位 | 意義 | HTTP |
|---:|---|---|
| 1 | 業務規則拒絕 | 422 |
| 2 | 併發／冪等衝突 | 409 |
| 3 | 狀態機衝突 | 409 |
| 4 | 資源與重複 | 404 / 409 |
| 5 | 外部與基礎設施 | 500 / 503 |

**問題**：請求格式錯誤（400）與方法不支援（405）**不屬任何現行類別位**
——它們既非業務規則拒絕（422），亦非資源與重複（404/409）。

**裁定**：將**類別位 4** 之語意由「資源與重複」擴充為「**請求與資源**」，
涵蓋 4xx 之請求層錯誤（400／404／405／409）。
此為**最小改動**（不動既有 5 個類別位之編號與其他碼），且與 `CC-INS-4000`（400）、
`CC-INS-4040`（404）等既有服務碼之編號習慣一致。

**已落地**：

| 文件 | 位置 | 內容 |
|---|---|---|
| `CSP-003` **1.3** | §2 類別位表、**新增 §2.1** | 類別位 4 改為「請求與資源」；新增四子語意表與**選碼判準表**（判準＝「**尚未進入業務邏輯**」） |
| `SDD-006` **1.6** | §3.3 類別位表、**新增 §3.3.1** | 同步擴充；載明擴充理由與設計原則 |

> 替代方案（**已否決**）：新增類別位 6 專供請求層錯誤。缺點為同一 HTTP 400
> 可能來自類別位 1（業務規則）或 6（格式），編號語意分裂。

### 3.3 修正 `GlobalExceptionHandler`（`common-spring`）——**已完成**

於 `common-spring` 之 `GlobalExceptionHandler` 新增對應之 `@ExceptionHandler` 方法。
**須遵守既有二原則**：

1. **不得洩漏內部實作**（`CSP-003 §7 E2／E4`）——回應之 `message` 一律為
   **通用文案**，**不得**回傳原始例外訊息（`HttpMessageNotReadableException`
   之訊息含 Jackson 內部路徑與類別名）。診斷細節僅寫入日誌。
2. **日誌分級不變**——4xx 記 `WARN`、5xx 記 `ERROR` 並附堆疊。

**實作結果**：`GlobalExceptionHandler` 由 **2 個 handler 增為 7 個**
（1 領域 ＋ 5 請求層 ＋ 1 兜底），請求層 5 個方法共涵蓋 **9 種例外型別**：

| 方法 | 涵蓋例外 | 對應碼 |
|---|---|---|
| `handleMissingRequestValue` | `MissingRequestHeaderException`、`MissingServletRequestParameterException` | `CC-PLT-4001` |
| `handleNotReadable` | `HttpMessageNotReadableException` | `CC-PLT-4002` |
| `handleParameterValidation` | `MethodArgumentNotValidException`、`HandlerMethodValidationException`、`ConstraintViolationException`、`MethodArgumentTypeMismatchException` | `CC-PLT-4003` |
| `handleNoResource` | `NoResourceFoundException` | `CC-PLT-4040` |
| `handleMethodNotSupported` | `HttpRequestMethodNotSupportedException` | `CC-PLT-4050` |

共用私有輔助 `requestError(...)`：

- **HTTP 狀態以 `ErrorCodes` 為權威**（`ErrorCodes.httpStatusOf(code)`）——避免此處硬編而與登錄表漂移。
- **一律記 `WARN`**，且日誌**僅記例外之類別名**，不記訊息與堆疊（避免請求主體內容入日誌）。
- 回應 `message` 一律為固定通用字串；`details` 僅放**欄位名與規則**，不含輸入值。

**兜底分支 `Exception` 維持不變**（真正的未預期例外仍須 500 並告警）。

#### 3.3.1 實作時發現並補強之缺口

| # | 缺口 | 處置 |
|---|---|---|
| 1 | `collectFieldDetails` 原未處理 `HandlerMethodValidationException` → 該類驗證失敗會回**空 `details`** | **已補**：改由 `getParameterValidationResults()` 取參數名（取不到時退回 `參數[i]`）與 `getResolvableErrors()` 之預設訊息 |
| 2 | `NoResourceFoundException` 不在 `spring-web`，而在 **`spring-webmvc`** | `common-spring/build.gradle.kts` 補 `compileOnly("org.springframework:spring-webmvc")`（比照既有 `jakarta.servlet` 之 `compileOnly` 慣例）；另補 `compileOnly("jakarta.validation:jakarta.validation-api")` 供 `ConstraintViolationException` |
| 3 | `NoResourceFoundException` 之建構子為 `(HttpMethod httpMethod, String requestUri, String resourcePath)`——**`getResourcePath()` 取第 3 個參數**，第 2 個僅用於例外訊息 | 已依官方原始碼確認；單元測試依此建構（此為**測試正確性**之關鍵，用錯參數會使斷言失效） |
| 4 | `MethodValidationResult.emptyResult()` 為 Spring 之 stub，其 `getParameterValidationResults()` 會拋 `UnsupportedOperationException` | **不可作為測試 fixture**；改用 `MethodValidationResult.create(...)` ＋ `ParameterErrors` 建構真實結果。已記入測試註解 |

### 3.5 未涵蓋之例外（逐項理由）

| 例外 | 原提案之碼 | 未涵蓋理由 |
|---|---|---|
| `MissingPathVariableException` | `CC-PLT-4001` | 僅在**程式路徑模板與方法簽章不符**時拋出——屬**程式錯誤**而非客戶端錯誤，回 500 並告警為正確行為（可及早發現缺陷） |
| `HttpMediaTypeNotSupportedException` | `CC-PLT-4000` | 需 `Content-Type` 不符（415）才會觸發。本專案 17 份契約之寫入端點**一律 `application/json`**，且 Spring 之 `@RequestBody` 對缺 `Content-Type` 之處理先落 `HttpMessageNotReadableException`。**未實測到觸發路徑**，故不先立碼（避免「有碼無情境」） |
| `NoHandlerFoundException` | `CC-PLT-4040` | 需 `spring.mvc.throw-exception-if-no-handler-found=true`。Spring Boot 3.2+ 之預設路徑已改走 `NoResourceFoundException`（**已實測命中**），故 `NoHandlerFoundException` 為不可達 |

> **原則**：不為「可能用到」而預立錯誤碼。若日後實測到觸發路徑，再依 `CSP-003 §2` 程序補碼。

### 3.4 修正後須一併處理——**執行狀態**

| # | 項目 | 說明 | 狀態 |
|---|---|---|---|
| 1 | 移除 `rewards-service` 之繞道 | `SCS-010 §10.4` #15 載明其「不用 Bean Validation」；修正後應改回標準作法，並更新該節 | ✅ **已完成**——`RewardController`／`RedemptionController` 之標頭改回預設 `required = true`，移除手動 `requireChannel` 檢核；`Requests.requireChannel()` 刪除；`SCS-010` 升 **1.4** |
| 2 | 新增測試 | 於 `common-spring` 建立對 5 類例外之單元測試；並於各服務補整合測試（`TRP-001`） | ✅ **已完成（單元）**——`GlobalExceptionHandlerTest` 由 8 增至 **18** 測試（新增 `@Nested` 請求層 10 項，含「不得落回 500」之回歸守門）；服務層整合測試見 §4.1 之端到端實測 |
| 3 | 契約對齊 | 確認 17 份契約之 `400`／`404`／`405` 回應已宣告（多數已宣告，須逐一核對） | 🟡 **部分**——`rewards-service.yaml` 未列 `CC-RWD-4000`，故本次改動**不影響契約**；17 份之逐一核對列為後續（見 §5 註） |
| 4 | 錯誤碼重生 | 改 `SDD-006 §3.4` 後執行 `tools/gen_error_codes.py --write`（**禁止手改 `ErrorCodes.java`**） | ✅ **已完成**——`ErrorCodes.java` 由 **107 增至 112 碼**；`gen_error_codes.py --check` 回「**112 個｜內容一致**」 |

---

## 4. 驗收條件（修正後重測 §2 之 6 項）——**已通過**

| # | 情境 | 期望（原） | 實測（修正後） | 判定 |
|---|---|---|---|---|
| ① | 缺必要標頭 | `400` `CC-PLT-4001` | `400` **`CC-PLT-4001`** | ✅ |
| ② | 非法 JSON | `400` `CC-PLT-4000` → **定案為 `CC-PLT-4002`** | `400` **`CC-PLT-4002`** | ✅ |
| ③ | 路徑 UUID 格式錯誤 | `400`（服務自檢，維持不變） | `400` `CC-INS-4000`／`CC-RWD-4000` | ✅ |
| ④ | 不支援之方法 | `405` `CC-PLT-4003` → **定案為 `CC-PLT-4050`** | `405` **`CC-PLT-4050`** | ✅ |
| ⑤ | 不存在之路徑 | `404` `CC-PLT-4040` | `404` **`CC-PLT-4040`** | ✅ |
| ⑥ | 查無資源 | `404`（服務自檢，維持不變） | `404` `CC-INS-4040`／`CC-RWD-4040` | ✅ |

**且**：①／②／④／⑤ **不得**再產生 `ERROR` 級日誌（應為 `WARN`）。→ ✅ **實測 0 筆 ERROR**。

### 4.1 實測證據（2026-09-19）

**方法**：兩個服務各自做**同服務前後對比**（同一組路徑、同一份腳本），
以排除「換服務導致差異」之可能。舊版為修正前仍在執行之程序；新版為重新編譯啟動。

#### A. `installment-service`（埠 8089；`§2` 原基準量測處）

| # | 情境 | 修正前 | 修正後 |
|---|---|---|---|
| ① | 缺 `X-Channel` | **`500` `CC-PLT-5000`** ❌ | `400` `CC-PLT-4001` ✅ |
| ② | 非法 JSON | **`500` `CC-PLT-5000`** ❌ | `400` `CC-PLT-4002` ✅ |
| ③ | 路徑非 UUID | `400` `CC-INS-4000` ✅ | `400` `CC-INS-4000` ✅ |
| ④ | `DELETE` 不支援 | **`500` `CC-PLT-5000`** ❌ | `405` `CC-PLT-4050` ✅ |
| ⑤ | 路徑不存在 | **`500` `CC-PLT-5000`** ❌ | `404` `CC-PLT-4040` ✅ |
| ⑥ | 查無資源 | `404` `CC-INS-4040` ✅ | `404` `CC-INS-4040` ✅ |

**修正前 6 項中 4 項錯誤**（與 §2 完全吻合，證明基準可重現）；**修正後 6/6 全對**。

#### B. `rewards-service`（埠 8091；**繞道移除處**）

| # | 情境 | 修正前（含繞道） | 修正後（繞道已移除） |
|---|---|---|---|
| ① | 缺 `X-Channel` | `400` **`CC-RWD-4000`** ⚠️ 繞道 | `400` **`CC-PLT-4001`** ✅ 平台統一 |
| ② | 非法 JSON | **`500` `CC-PLT-5000`** ❌ | `400` `CC-PLT-4002` ✅ |
| ③ | 路徑非 UUID | `400` `CC-RWD-4000` ✅ | `400` `CC-RWD-4000` ✅ |
| ④ | `DELETE` 不支援 | **`500` `CC-PLT-5000`** ❌ | `405` `CC-PLT-4050` ✅ |
| ⑤ | 路徑不存在 | **`500` `CC-PLT-5000`** ❌ | `404` `CC-PLT-4040` ✅ |
| ⑥ | 查無資源 | `404` `CC-RWD-4040` ✅ | `404` `CC-RWD-4040` ✅ |

> ① 修正前為 `400` 但**碼是 `CC-RWD-4000`**（服務自檢）——**這正是繞道之指紋**：
> 結果對，但由服務自行重做框架該做的事。修正後改由平台統一產生 `CC-PLT-4001`，
> 且服務程式碼更短（少一次手動檢核）。

#### C. 日誌分級

| 服務 | 6 項情境產生之 `ERROR` | `WARN` |
|---|---:|---:|
| `installment-service` | **0** | 6 |
| `rewards-service` | **0** | 6 |

日誌格式（實錄）：

```
WARN ... c.b.c.c.s.error.GlobalExceptionHandler : 請求層例外：code=CC-PLT-4001,
     type=MissingRequestHeaderException, traceId=b9f2a4b854c04e8a8c9efd2ee997e7b8
```

僅記**例外類別名**（`type=...`），**不含例外訊息**——避免請求主體內容（可能含個資）入日誌。

#### D. 回應主體（不得洩漏內部實作）

| # | 回應 |
|---|---|
| ① | `{"error":{"code":"CC-PLT-4001","message":"缺少必要請求標頭或參數","details":[{"field":null,"reason":"缺少必要標頭：X-Channel"}]}}` |
| ② | `{"error":{"code":"CC-PLT-4002","message":"請求主體格式錯誤","details":[]}}` |
| ④ | `{"error":{"code":"CC-PLT-4050","message":"HTTP 方法不支援","details":[{"field":null,"reason":"該路徑支援的方法：GET"}]}}` |
| ⑤ | `{"error":{"code":"CC-PLT-4040","message":"資源或路徑不存在","details":[{"field":null,"reason":"不存在的路徑：api/v1/no-such-endpoint"}]}}` |

② 之 `details` 為**空**——Jackson 之解析訊息（含 `[Source: ...]`、內部類別名）**未外洩** ✅。

#### E. 自動化驗證

| 項 | 指令 | 結果 |
|---|---|---|
| 編譯 | `:platform:common-spring:compileJava` | BUILD SUCCESSFUL |
| 單元測試 | `:platform:common-spring:test` | **41 測試／0 失敗**（`GlobalExceptionHandlerTest` 18） |
| 服務測試 | `:services:rewards-service:test`、`:services:installment-service:test` | BUILD SUCCESSFUL |
| 錯誤碼一致性 | `gen_error_codes.py --check` | **112 個｜內容一致** |
| 空殼掃描 | `scan_scaffold.py` | 命中 **0** 處 |
| 假資料掃描 | `scan_mockdata.py` | 命中 **0** 處 |
| 假測試掃描 | `scan_faketest.py` | 命中 **0** 處 |

---

## 5. 影響範圍與回改清單——**執行狀態**

| 文件／元件 | 變更 | 狀態 |
|---|---|---|
| `SDD-006` | §3.3 類別位 4 擴充 ＋ **新增 §3.3.1**；§3.4 新增 5 碼；§8 修訂記錄 | ✅ 升 **1.6** |
| `CSP-003` | §2 類別位 4 擴充 ＋ **新增 §2.1**；HTTP 狀態對應表補 2 列；§8 修訂記錄 | ✅ 升 **1.3** |
| `CSP-003 §7` | 補述請求層例外之訊息不得洩漏內部實作 | ✅ 已含於 §2.1 之實作歸屬說明 |
| `common-spring` | `GlobalExceptionHandler` 新增 5 個 handler（＋補 `collectFieldDetails` 之 `HandlerMethodValidationException` 分支）；`build.gradle.kts` 補 2 個 `compileOnly` | ✅ 已完成 |
| `common-spring` 測試 | `GlobalExceptionHandlerTest` 新增 `@Nested` 請求層 10 項 | ✅ 8 → **18** |
| `ErrorCodes.java` | 由 `gen_error_codes.py` 重生（**不手改**） | ✅ 107 → **112** 碼 |
| `rewards-service` | `RewardController`／`RedemptionController` 標頭回歸 `required = true`；`Requests.requireChannel()` 刪除；`AccrualRequest` javadoc 更正 | ✅ 已完成 |
| `SCS-010 §10.4` #15 | 移除繞道敘述，改記「平台缺口已由 `CR-004` 修正」 | ✅ 升 **1.4** |
| `SCS-018`（WebUI） | 前端錯誤處理：確認 400／404／405 之文案與重試策略 | ⚪ **未做**——見下方註 |
| `TRP-001` | 補對應測試案例 | 🟡 單元測試已補；`TRP-001` 之登錄待補 |

> **§5 註（誠實揭露）**：
> ① `SCS-018`（WebUI）之前端錯誤文案與重試策略**尚未處理**——本輪聚焦後端平台層；
> 前端目前對 400／404／405 之顯示沿用既有通用錯誤處理，**未逐一驗證文案**。
> ② 17 份契約之 `400`／`404`／`405` 宣告**未逐一核對**；已確認 `rewards-service.yaml`
> 不列 `CC-RWD-4000`，故本次改動不影響該契約。
> ③ `TRP-001` 之測試案例登錄尚未補；本次證據以 `common-spring` 單元測試（18 項）
> 與兩服務端到端實測（各 6 項）為準。

---

## 6. 核定結果（原「待核定事項」）

| # | 待決 | 核定 | 處置 |
|---|---|---|---|
| 1 | 是否同意新增 §3.1 之 5 個錯誤碼？ | ✅ **同意**，細節授權開發團隊安排 | 已新增 5 碼；編號依 §3.1「實作定案」調整（`4050` 取代提案之 `4003`、`4000` 保留未用） |
| 2 | 是否同意 §3.2 之**類別位 4 語意擴充**？ | ✅ **同意** | `CSP-003` §2／§2.1、`SDD-006` §3.3／§3.3.1 已同步擴充為「請求與資源」 |
| 3 | 修正後是否一併移除 `rewards-service` 之 Bean Validation 繞道？ | ✅ **移除** | 標頭回歸 `required = true`；`Requests.requireChannel()` 刪除。**服務層檢核保留**（理由見 §6.1） |
| 4 | 是否於本輪（Sprint 4）執行，或排入下一輪？ | ✅ **本輪執行** | 已完成並實測通過 |

> 核定前之原則「在核定前不進行任何變更」已解除；本 CR 之變更自 2026-09-19 起生效。

### 6.1 關於第 3 項「移除繞道」之**範圍界定**（實作時查證後之更正）

原 CR 之敘述「改回標準 Bean Validation 作法」在實作前經查證後**須限縮**，理由三點：

| # | 查證發現 | 影響 |
|---|---|---|
| 1 | **（已於 §6.2 更正）** 原記「Bean Validation 於全專案 9/9 服務皆 0 使用」——**該敘述有誤**。實為 **8/17 服務採控制器層 Bean Validation**，`rewards-service` 為**唯一**採應用層檢核者 | 「改回 Bean Validation」不再是「回歸慣例」，而是**與其他 8 服務對齊**；性質改變，故**不擅自變更**，改列待裁定（見 §6.2） |
| 2 | `AccrualServiceTest` **直接呼叫應用服務**（不經 HTTP），並斷言非法輸入須拋 `CC-RWD-4000` | 控制器層之 Bean Validation **攔不到**非 HTTP 入口；若移除服務層檢核，該測試會失敗且**非 HTTP 入口失去防護** |
| 3 | `Requests.requireIdempotencyKey` 之**長度規則（8–120）**屬業務規則，非格式檢核 | 業務規則本就應在應用層，不屬「框架該做的事」 |

**因此本輪移除的是「真繞道」**——即 `@RequestHeader(required = false)` ＋ 手動 `requireChannel`：
框架本可處理存在性檢核，服務卻自行重做。**服務層之檢核保留**，其存在理由已改寫為
經證據支持之兩點（保護非 HTTP 入口／錯誤碼精確），並於 `Requests` 之 javadoc
標明「**⚠️ 歷史（已失效之理由）**」以免後人誤解。

> 此為**對原 CR 敘述之更正**，依「發現矛盾要講」原則明列於此，不掩飾。

### 6.2 更正：`rewards-service` 之 Bean Validation 敘述（**我方的判斷錯誤**）

**錯誤內容**：`CR-004` 1.0 之 §3.4 #1 與 §6.1 原記
「Bean Validation 於**全專案 9/9 服務皆 0 使用**，屬**專案慣例**而非 `rewards-service` 獨有繞道」。
並依此寫入 `Requests.java` 之 javadoc（列為定案理由之第 3 點）與 `SCS-010 §10.4` #15.1。

**複查結果（2026-09-19，全專案逐服務掃描）**：

| 服務 | `jakarta.validation.constraints` | `@Valid` | 驗證層次 |
|---|---:|---:|---|
| `authorization-service` | 1 | 1 | 控制器層 |
| `card-service` | 1 | 1 | 控制器層 |
| `installment-service` | 2 | 1 | 控制器層 |
| `limit-service` | 2 | 2 | 控制器層 |
| `party-service` | 2 | 2 | 控制器層 |
| `payment-service` | 3 | 2 | 控制器層 |
| `posting-service` | 2 | 1 | 控制器層 |
| `statement-service` | 1 | 1 | 控制器層 |
| **`rewards-service`** | **0** | **0** | **應用層**（`Requests`） |
| 其餘 8 服務 | 0 | 0 | 尚未實作 |

**正確結論**：**8 個服務採控制器層 Bean Validation**（`@Valid @RequestBody` ＋ DTO 註解，
且應用層**無**對應檢核——實查 `party-service`／`limit-service` 確認）；
**`rewards-service` 為唯一採應用層檢核者**。原敘述「與事實相反」。

**根因**：以**失效之搜尋樣式**盤點（macOS BSD `grep` 之 `\|` 交替不生效，
見 `CSP-001` 之工具陷阱紀錄），致命中 0 筆而誤判為「全專案皆無使用」。
**教訓**：以「0 命中」作為「不存在」之證據前，必須先用**已知存在之樣本**驗證搜尋樣式本身有效。

**處置**：
1. `Requests.java` javadoc 之第 3 點（專案慣例）**刪除**，改列「**本服務與其他服務之驗證層次不同**」，
   並加註「⚠️ 亦已更正之錯誤敘述」小節，載明原敘述有誤及根因。
2. `SCS-010 §10.4` #15.1 同步更正。
3. **新增待裁定項**（不擅自變更已通過測試之行為）：

| # | 待決 | 選項 | 影響 |
|---|---|---|---|
| 1 | `rewards-service` 之驗證層次是否對齊其他 8 服務？ | ① 維持應用層（現狀）② 於控制器補 Bean Validation（與其他 8 服務一致） | ① 風格不一致，但保護所有入口、錯誤碼精確<br>② 風格一致，但**規則重複**（控制器＋應用層各一份）或須放寬 `AccrualServiceTest` 斷言 |

> **本 CR 不代為決定**——此屬設計取捨，依「設計決策由 BEAM PMO 拍板」原則待裁定。
> 已記入 `HANDOVER §📊 §3.C`。

---

## 7. 修訂記錄

| 版本 | 日期 | 修訂者 | 說明 |
|---|---|---|---|
| 1.0 | 2026-09-19 | 開發團隊 | 初版。立 CR（BEAM PMO 指示），載明實測證據、根因、建議修法與待核定事項 |
| 1.1 | 2026-09-19 | 開發團隊 | **核定與執行完成**。① 狀態改「已核定／已執行」；4 項待決全數核准（§6）。② §3.1 新增「**實作定案**」對照表，載明與原提案之 3 項差異及理由（**後兩碼對齊 HTTP 狀態碼**；`4000` 保留未用）。③ §3.2 記裁定結果與落地位置（`CSP-003` 1.3／`SDD-006` 1.6）。④ §3.3 記實作結果（2→**7** handler，涵蓋 **9** 種例外型別）；**新增 §3.3.1** 載明實作時發現並補強之 4 項缺口（含 `HandlerMethodValidationException` 明細缺失、`NoResourceFoundException` 之 jar 歸屬與建構子語意、`emptyResult()` 不可作 fixture）。⑤ **新增 §3.5** 逐項說明 3 個未涵蓋例外之理由（不為「可能用到」預立碼）。⑥ §3.4／§5 改為「執行狀態」表並加註 3 項誠實揭露（`SCS-018` 前端文案、17 契約逐一核對、`TRP-001` 登錄尚未補）。⑦ **新增 §4.1 實測證據**——兩服務同服務前後對比（各 6 情境）、日誌分級（0 ERROR）、回應主體不洩漏、5 項自動化驗證。⑧ **新增 §6.1** 更正原「改回 Bean Validation」之敘述（範圍限縮為「移除真繞道」），理由三點。⑨ **新增 §6.2 更正我方之判斷錯誤**——原記「Bean Validation 全專案 9/9 服務皆 0 使用」**與事實相反**（實為 **8/17 服務採控制器層 Bean Validation**，`rewards-service` 為唯一採應用層檢核者）；根因為以失效之搜尋樣式（BSD `grep` 之 `\|`）盤點致 0 命中而誤判。已同步更正 `Requests.java` javadoc 與 `SCS-010 §10.4` #15.1，並將「是否對齊」列為**待裁定項**（不擅自變更已通過測試之行為） |
