CR #221 » CR-020_卡片生命週期端點之四項缺陷.md
CR-020 卡片生命週期端點(POST /cards/{cardId}/lifecycle-transitions)之四項缺陷
| 項目 | 內容 |
|---|---|
| 文件編號 | CR-020 |
| 版本 | 1.1(2026-09-23:四項已全數裁定並執行完畢——BEAM PMO 裁定「不得以 workaround 心態處理」,故 F1 採 A 補實作、F2 採 B 補實作、F3 改 404、F4 採 B 完整映射表;真機實測 9 項+單測 10 例+負樣本驗證。見 §7 執行結果) |
| 提出日 | 2026-09-23 |
| 提出者 | WorkBuddy AI(開發團隊) |
| 緣起 |
C-14 批次四(WebUI 頁面 /card/lifecycle 卡片生命週期管理)之動工前查證——依 CSP-001 §7.1 G-8 精神逐端點核對時,以真機實測(非靜態比對)發現 4 項缺陷 |
| 影響範圍 |
card-service(8082)之生命週期轉換端點;contracts/openapi/card-service.yaml(經 tools/gen_openapi.py 重生);SDD-005 §2 之落地完整性;SDD-007 §5.4.16(本批新增之逐鈕定義) |
| 關聯前例 |
CR-019 批 2 之 C-1(AuthorizeRequest 之 merchantName/entryMode/emvData 實作未接收→靜默丟棄)——本 CR 之 F1/F2 屬同一型;#A-89 ③(C-4 判定更正)——同為「執行前查證攔下錯誤判定」之案例
|
1. 為什麼這些缺陷靜態比對抓不到
tools/verify_contract_dto.py(CSP-001 §7.1 G-8)比對的是「回應 schema 之欄位集合」與「Java DTO record 元件名」。
本 CR 之四項缺陷皆落在該工具之檢查面之外:
| 缺陷 | 為何工具測不到 |
|---|---|
| F1 請求欄位被靜默丟棄 | 工具只比回應;TransitionRequest 之 threeFactors 在契約有、實作 record 也有(型別 Map<String,String>)→ 欄位集合相符,但未被使用
|
| F2 契約宣告之必要標頭未實作 | 工具不檢查 parameters/headers
|
| F3 查無資源回 409 而非 404 | 屬語意(同一錯誤碼用於兩種情境),非欄位集合 |
| F4 兩條寫入路徑結果不一致 | 屬跨路徑一致性,須比對兩支 SQL 與 DB 實況 |
★ 結論:G-8 為必要但不充分。本 CR 之四項皆須真機實測 + 讀實作 + 查 DB 方能發現。
與#A-87 ⑨(merchantId同名語意不一致)之教訓一致——工具不可取代端到端實測。
2. 實測證據(2026-09-23,真機;CARDCORE_SECURITY_ENABLED=true)
取 token 方式:cardcore-web(public client,未開 DAG)→ 以 Authorization Code + PKCE 取得 demo.csr 之 token
(azp=cardcore-web、role cardcore-csr;與 tools/verify_sso_pkce.py 同一協定)。
測試卡取自 CC_CARD.CARDS(實測 16 列)。
2.1 F1 threeFactors 契約宣告但實作未接收 → 三要素驗證完全缺失
契約原文(card-service.yaml,TransitionRequest):
threeFactors:
type: object
nullable: true
description: 開卡三要素(身分證字號、生日、核准序號)
端點描述原文:"狀態轉換經狀態機(SDD-005);快發須 threeFactors;非法轉換回 CC-CARD-3001/3002;三要素不符回 CC-CARD-1002。"
實作:CardController.TransitionRequest 有 Map<String,String> threeFactors 元件,但
CardQueryService.transition(cardId, event, operatorId, reason) 完全不接收該參數
→ 介面層收到後未傳入,屬靜默丟棄。
實測(A2):對 INACTIVE 卡送出明顯錯誤之三要素 → 201 成功
POST /api/v1/cards/0ec94427-9980-44ed-9077-0ba6b4484cb1/lifecycle-transitions
X-Channel: WEB ; X-Idempotency-Key: c14-b4-a2-0002
{"event":"ACTIVATE","reason":"實測 A2(三要素錯誤)","operatorId":"CSR001",
"threeFactors":{"idNo":"Z999999999","birthDate":"1900-01-01","approvalNo":"NOT-EXIST"}}
→ HTTP 201
{"data":{"cardId":"0ec94427-…","cardNoMasked":"411111******2599",
"blockCode":"ACTIVE","status":"UNACTIVATED","transactable":true,…},
"success":true}
判定:idNo=Z999999999(非本國身分證格式)、birthDate=1900-01-01、approvalNo=NOT-EXIST
皆不可能通過驗證,卻仍開卡成功。→
CC-CARD-1002 於此路徑不可達;SDD-005 §2.1 轉換 1 之前置條件「三要素驗證通過」未落地。
⚠️ 對照:同一服務之 快發快綁(
POST /cards/{cardId}/instant-issuance)有接收threeFactors
並轉為ThreeFactors值物件(CardControllerL194-207)→ 同一業務概念在兩端點行為不一致。
2.2 F2 X-Channel/X-Idempotency-Key 契約宣告為必要,實作未綁定
契約原文(該端點 parameters):
- $ref: "#/components/parameters/ChannelHeader"
- $ref: "#/components/parameters/IdempotencyHeader"
實作(CardController.transition)僅有 @PathVariable cardId + @RequestBody,無任何 @RequestHeader。
實測(A1):完全不帶兩個標頭 → 201 成功
POST /api/v1/cards/2519efc2-0d15-4d71-97bb-f66dea114d84/lifecycle-transitions
(無 X-Channel、無 X-Idempotency-Key)
{"event":"ACTIVATE","reason":"C-14 批次四 實測 A1(不帶標頭、不帶三要素)"}
→ HTTP 201(開卡成功)
判定:(i) 契約宣告之必要標頭實為選用——契約過度宣告(consumer 照契約送無害,但契約失真);
(ii) 更實質之影響——冪等鍵無效:consumer 依契約帶 X-Idempotency-Key 期待「重送不重複」,
實則完全被忽略;重送之結果僅靠狀態機擋下(ACTIVE → ACTIVATE 為非法轉換 → 409),
屬巧合而非設計(見 §2.3 之 F3:查無卡片亦回 409,兩者無法區分)。
對照:同服務之
POST /cards(發卡)、POST /card-applications皆@RequestHeader綁定兩者;
POST /cards/{cardId}/instant-issuance、POST /cards/{cardId}/tokens綁定X-Channel但未綁定X-Idempotency-Key。
2.3 F3 查無卡片回 CC-CARD-3001(409) 而非 404——同服務內語意不一致
實測(B):對不存在之 UUID 呼叫 →
POST /api/v1/cards/00000000-0000-0000-0000-0000000000FF/lifecycle-transitions
→ HTTP 409
{"error":{"code":"CC-CARD-3001","message":"查無此卡片:00000000-0000-0000-0000-0000000000ff",…}}
判定:
-
CC-CARD-3001之語意為「狀態機衝突」;以之表示「查無資源」語意不符;
契約亦明列404: 資源不存在、409: 衝突(冪等鍵衝突/樂觀鎖衝突/狀態衝突)→ 實作回 409 與契約語意相悖。 - ★ 同服務內已有正確前例:
CardQueryService.findById於HANDOVER #C-10已將「查無」由
CC-CARD-3001修正為CC-CARD-4040(404),其 javadoc 明載理由「查無資源應為 404」。
惟transition()未同步修正 → 同一服務內兩套真相。 -
併發後果:consumer 無法區分「卡片不存在」與「狀態轉換非法」(皆 409、皆
CC-CARD-3001)
——對照 §2.2 之「重送靠狀態機擋下」,此區分能力正是冪等語意所依賴者。
2.4 F4 ACTIVATE 未同步 cards.status → 產生 block_code=ACTIVE 卻 status=UNACTIVATED 之組合
DDL(card-service/V1__baseline.sql,兩者為不同維度):
block_code VARCHAR2(16) DEFAULT 'INACTIVE' NOT NULL CHECK (block_code IN ('INACTIVE','ACTIVE','LOCKED','LOST','REISSUED','EXPIRED','CLOSED')),
status VARCHAR2(16) DEFAULT 'UNACTIVATED' NOT NULL CHECK (status IN ('UNACTIVATED','ACTIVE','SUSPENDED','CLOSED')),
兩條寫入路徑(JdbcCardRepository):
| 路徑 | SQL | 更新欄位 |
|---|---|---|
生命週期轉換(updateBlockCode) |
SET block_code = ?, updated_at = …, version = version + 1 |
僅 block_code |
快發快綁(UPDATE_INSTANT_SQL) |
SET block_code = ?, status = ?, instant_issued = 1, … |
block_code + status |
實測(DB 複核,CC_CARD.CARDS 全表分群):
BLOCK_CODE|STATUS|筆數 ← 實測後
INACTIVE |UNACTIVATED|9
ACTIVE |ACTIVE |4 ← 快發快綁路徑產生(含種子)
LOCKED |SUSPENDED |1
ACTIVE |UNACTIVATED|2 ← ★ 本次 A1/A2 之 ACTIVATE 產生;**測試前此組合為 0 列**
判定:同一業務事件(「開卡」)因路徑不同而結果不同——
- 經生命週期轉換:
ACTIVE / UNACTIVATED - 經快發快綁:
ACTIVE / ACTIVE
SDD-005 §2.1 轉換 1 之副作用為「寫入開卡時間、發 card.card.status-changed.v1」
——事件名即為 status-changed,而 status 竟未變。
→ 建議採 (a) 轉換 1 同步 status=ACTIVE(與快發快綁對齊、與事件名一致)。
⚠️ 對 WebUI 之直接影響:
CardStatusView同時回傳blockCode與status;
若畫面兩欄並列,使用者將看到「ACTIVE / 未開卡」之矛盾組合。
本批之SDD-007 §5.4.16已明訂呈現規則(見 §3.4)。
3. 建議修法(逐項,供一次裁定)
| # | 缺陷 | 建議 | 理由 | 風險 |
|---|---|---|---|---|
| F1 |
threeFactors 未接收 |
(a) 轉換 1(ACTIVATE)補實作三要素驗證,不符回 CC-CARD-1002;(b) 契約加註「本期未實作」 |
(a) 較正確——SDD-005 §2.1 已明列前置條件,且快發快綁已有可用之 ThreeFactors 與驗證邏輯,非從零開發;(b) 僅為契約誠實化,仍留「開卡不需驗證」之實質漏洞 |
(a) 需確認 threeFactors 之權威來源(card_applications.approval_no + 持卡人 id_no/birth_date)——屬跨服務(party)查詢,須確認呼叫路徑 |
| F2 | 標頭未綁定 |
(a) 契約移除該端點之 IdempotencyHeader(承認不支援冪等);(b) 實作補上 @RequestHeader 並落實冪等;(c) 維持現狀 |
(a) 為最低限度之誠實化(成本最低、立即消除契約失真);(b) 最完整但工作量最大(需 card_idempotency 表之既有機制,該表已存在);(c) 不建議——留「契約說有、實際沒有」之靜默陷阱 |
(a) 為契約縮減,須確認無 consumer 依賴;(b) 須處理「同鍵重送之回應重播」語意 |
| F3 | 查無卡片回 409 |
改回 CC-CARD-4040(404),與 findById 對齊 |
findById 已有同一修正之前例(#C-10),理由與作法皆現成;且為 F2(b) 冪等語意之前提 |
極低——但須先查證無 consumer 依賴原 409(#C-10 之查證方式可直接沿用) |
| F4 |
status 未同步 |
(a) 轉換 1 同步 status=ACTIVE;(b) 廢除 status 欄位(單一真實來源);(c) 維持,但契約/畫面明訂「兩維度、各自獨立」 |
(a) 建議——與快發快綁一致、與 status-changed 事件名一致;(b) 牽涉 LOCKED↔SUSPENDED 之對應(見下)不建議本期做 |
(a) 須確認 status 之其他轉換是否亦需同步(LOCK → SUSPENDED?CLOSE → CLOSED?)——若只修 ACTIVATE,將再產生新的不一致
|
⚠️ F4 之關鍵細節(不可只修一半):實測種子資料顯示
LOCKED|SUSPENDED成對存在,
故status並非「僅開卡時同步」——LOCK/CLOSE亦應有對應。
若採 (a),須一次定義 7 個事件各自之status映射(或明確宣告「僅ACTIVATE同步,其餘不變」並說明理由),
否則即為局部修正造成新不一致。
4. 與既有項目之關係
| 既有項 | 關係 |
|---|---|
CR-019 批 2(C-1)
|
同型(契約宣告之請求欄位、實作未接收→靜默丟棄)。C-1 為 authorization-service、本 CR F1 為 card-service;建議一併裁定(同一原則) |
CR-019 C-3(TransitionRequest.operatorId 契約補欄) |
互補——C-3 已使 operatorId 納入契約(實作確有接收並用於日誌);本 CR F1 則為「實作未接收」之反向案例。兩者並存顯示:同一個 record 內,有的欄位有接、有的沒接
|
GOV-006 §4 #17(AMOUNT 設計意圖) |
同屬「不可由工具或機械推論決定」之帳務/語意類裁定 |
#A-87 ⑨(merchantId 同名語意不一致) |
同屬「工具測不到、須實測」之類型 |
5. 誠實揭露(我方施行中之副作用與限制)
-
⚠️ 本次實測改變了測試資料狀態(可回退):為驗證 F1/F2,對 2 張
INACTIVE卡執行ACTIVATE:-
2519efc2-0d15-4d71-97bb-f66dea114d84(411111******0148)→block_code=ACTIVE(status仍UNACTIVATED) -
0ec94427-9980-44ed-9077-0ba6b4484cb1(411111******2599)→ 同上
→ 此為 F4 之實證來源,同時是本次實測之副作用。回退方式(若需):
UPDATE cards SET block_code='INACTIVE' WHERE id IN (…)(本機 dev 資料,未執行)。
教訓:以「寫入型端點」作查證時,須先記下變更前狀態方可回退。
-
-
未驗證之範圍:
REISSUE/EXPIRE等其餘 6 個事件未逐一實測(僅驗ACTIVATE/LOCK/查無);
F4 之「其餘事件之status映射」亦未查證(已列為 §3 之待確認項)。 -
未查證 consumer 依賴:F3 之「改 404」是否影響既有呼叫端,尚未盤點(
#C-10之查證方式可沿用)。 - 本 CR 尚未修改任何程式或契約——僅登記與建議,待 BEAM PMO 裁定。
6. 待裁定(彙總)
| # | 議題 | 選項 |
|---|---|---|
| F1 | 三要素驗證 | (a) 補實作 / (b) 契約加註未實作 |
| F2 | 標頭與冪等 | (a) 契約移除冪等宣告 / (b) 補實作 / (c) 維持 |
| F3 | 查無卡片之錯誤碼 | 改 CC-CARD-4040(404)/維持 CC-CARD-3001(409) |
| F4 |
status 同步 |
(a) 補同步(須定義全事件映射)/ (b) 廢欄位 / (c) 明訂兩維度獨立 |
我方建議之最小可行組合:F3 → 改 404(成本最低、有前例、且為 F2(b) 之前提);
F1 → 先 (b) 契約加註(避免繼續誤導)+ 另立後續項做 (a);
F2 → (a) 契約移除冪等宣告(消除靜默陷阱)+ 後續項評估 (b);
F4 → (a) 但須先定義全事件之status映射(不可只修ACTIVATE)。
7. 執行結果(2026-09-23,BEAM PMO 裁定後執行)
BEAM PMO:「不能用 walk around 的心態處理,以免造成未來接手的開發團隊又有別的方法
然後推翻現在的解決方案又重來一遍。」
→ 故四項全部修根因,不採「契約加註本期未實作」之縮手方案。
7.1 裁定與落地
| # | 裁定 | 落地方式 |
|---|---|---|
| F1 |
A 加 card_id 關聯(最嚴謹) |
V7__card_application_card_id.sql 加 CARD_APPLICATIONS.CARD_ID+索引;CreateCardRequest 新增 applicationId(發卡時回填關聯);CardQueryService.transition 於 ACTIVATE 時以 findByCardId 反查核准序號並確實驗證三要素,不符/查無案件回 CC-CARD-1002
|
| F2 | B 補實作(確實綁定+落實冪等) |
CardController 加 @RequestHeader X-Channel/X-Idempotency-Key;transition() 複用既有 IdempotencyStore(指紋=cardId|event|reason|operatorId),同鍵同指紋重放、同鍵異指紋回 CC-CARD-2001(鑑別值 CARD_LIFECYCLE_TRANSITION) |
| F3 | 改 CC-CARD-4040(404) |
全庫搜尋無任何服務端呼叫端(僅前端),故無破壞風險;與 findById 一致 |
| F4 | B 完整映射表 | 新增 CardStatus 列舉;映射內建於 CardBlockCode#status()(switch 運算式之窮盡性檢查=新增區塊碼未定義對應 status 時編譯即失敗);Card#transition 重算 status;JdbcCardRepository.updateBlockCode 不收 status 參數(一律由 blockCode 推導,呼叫端無從傳錯)→ 兩條寫入路徑現皆同步 |
7.2 真機實測(9 項,全部符合預期)
| # | 情境 | 結果 |
|---|---|---|
| 1 | F3 查無卡片 |
CC-CARD-4040/404(修正前 CC-CARD-3001/409) |
| 2 | F2-a 完全不帶 X-Channel/X-Idempotency-Key
|
CC-PLT-4001/400「缺少必要標頭:X-Channel」(修正前 201) |
| 3 | F1 該卡無來源申請案件 |
CC-CARD-1002/422(修正前 201 開卡成功) |
| 4 | 發卡帶 applicationId
|
201,blockCode=INACTIVE/status=UNACTIVATED
|
| 5 | F1 開卡但核准序號錯誤 |
CC-CARD-1002「開卡三要素驗證未通過」(修正前 201) |
| 6 | F1+F4 開卡三要素正確 | 201,blockCode=ACTIVE/status=ACTIVE
|
| 7 | F2-b 同鍵同指紋重放 | 回當前狀態(ACTIVE/ACTIVE),不重複轉換 |
| 8 | F2-c 同鍵異指紋 | CC-CARD-2001 |
| 9 | F4 LOCK
|
blockCode=LOCKED/status=SUSPENDED,transactable=false
|
DB 複核:CARDS 為 LOCKED|SUSPENDED;CARD_APPLICATIONS.CARD_ID 已回填
(LINKED 由 0 → 1);CARD_IDEMPOTENCY 出現 CARD_LIFECYCLE_TRANSITION 2 筆。
7.3 單測與負樣本
新增 CardLifecycleTransitionTest(10 例,涵蓋 F1-a~f/F2-a,b/F3/F4)。
★ 負樣本驗證:暫時回退 F4(next.status() → status)後重跑 →
F1-a 與 F4 兩例精準轉紅、其餘 8 例仍綠 → 證明測試確實鎖住該行為,非附帶通過。
7.4 ⚠️ 施行後之連帶影響(誠實揭露)
-
既有種子/測試卡多數無來源申請案件 →
ACTIVATE將回CC-CARD-1002(fail-closed,刻意)。
實測 16 張卡中僅 5 張能對上;V7 後僅於「發卡時帶applicationId」之新卡才會建立關聯。 -
前端規格須同步更新(否則為過時記載):
SDD-007 §5.4.16原載「不提供三要素輸入」
——修正後即應提供,否則變成「功能已存在而畫面不給用」。已於SDD-0071.13 更正。 -
未驗證範圍:其餘 6 個生命週期事件(
UNLOCK/REPORT_LOST/REISSUE/EXPIRE/CLOSE
之組合)未逐一實測其status映射(單測僅涵蓋ACTIVATE/LOCK/CLOSE)。
8. 修訂記錄
| 版本 | 日期 | 修訂內容 |
|---|---|---|
| 1.0 | 2026-09-23 | 初版:C-14 批次四動工前查證所發現之四項缺陷(F1 三要素未接收/F2 標頭未綁定/F3 查無卡片回 409/F4 status 未同步),含真機實測證據、DB 複核、逐項建議修法與待裁定彙總 |
| 1.1 | 2026-09-23 |
BEAM PMO 裁定「不得以 workaround 心態處理」→ 四項全部修根因並執行完畢:F1 採 A(V7 加 CARD_APPLICATIONS.CARD_ID)/F2 採 B(確實綁定+冪等生效)/F3 改 CC-CARD-4040(404)/F4 採 B(完整映射表,內建於 CardBlockCode#status())。新增 §7 執行結果(真機實測 9 項+DB 複核+單測 10 例+負樣本)與 §7.4 連帶影響。SDD-005 → 1.8(新增 §2.4 衍生關係)/SDD-007 → 1.13/SDD-001 → 1.14/SCS-003 → 1.5/GOV-006 → 1.21/HANDOVER → 1.70(#A-92) |