CR #205 » CR-004_平台層錯誤映射缺口.md
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。
後果有三:
-
契約違反:全部 17 份 OpenAPI 契約對這些情境宣告
400/404/405。 -
告警污染:
GlobalExceptionHandler自身之日誌策略為「5xx 記 ERROR 並附完整堆疊
(伺服器問題,需告警)」。客戶端送錯格式即產生 ERROR 與告警——
告警疲勞將使真正的伺服器故障被淹沒。 -
實作分歧:
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 根因
@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 方法。
須遵守既有二原則:
-
不得洩漏內部實作(
CSP-003 §7 E2/E4)——回應之message一律為
通用文案,不得回傳原始例外訊息(HttpMessageNotReadableException
之訊息含 Jackson 內部路徑與類別名)。診斷細節僅寫入日誌。 -
日誌分級不變——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 命中」作為「不存在」之證據前,必須先用已知存在之樣本驗證搜尋樣式本身有效。
處置:
-
Requests.javajavadoc 之第 3 點(專案慣例)刪除,改列「本服務與其他服務之驗證層次不同」,
並加註「⚠️ 亦已更正之錯誤敘述」小節,載明原敘述有誤及根因。 -
SCS-010 §10.4#15.1 同步更正。 - 新增待裁定項(不擅自變更已通過測試之行為):
| # | 待決 | 選項 | 影響 |
|---|---|---|---|
| 1 |
rewards-service 之驗證層次是否對齊其他 8 服務? |
① 維持應用層(現狀)② 於控制器補 Bean Validation(與其他 8 服務一致) | ① 風格不一致,但保護所有入口、錯誤碼精確 ② 風格一致,但規則重複(控制器+應用層各一份)或須放寬 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,並將「是否對齊」列為待裁定項(不擅自變更已通過測試之行為) |