Project

General

Profile

CR #232 » CR-031_批次C剩餘項讀模型設計與裁定.md

內容更新:內部稱謂去識別化(2026-10-18) - Redmine Admin, 10/18/2026 05:32 AM

 

CR-031 批次 C 剩餘項:讀模型設計與裁定(party 卡片狀態投影/參數快取)

項目 內容
文件編號 CR-031
版本 1.2
提出日 2026-10-01
最後修訂 2026-10-01(1.2:bff-web 之 BffAccountOverview 新增 cards 區塊,見 §4.5;★ 併更正 v1.0 對下游之誤記。1.1:C-5 實作完成並通過真機驗證;★ 併揭露並修正一項跨服務種子不一致,見 §4.4)
提出者/裁定者 WorkBuddy AI(開發團隊)——架構師(依 CR-030 之 P1~P4 通則)
緣起 BEAM PMO 2026-10-01 指示:「批次 C 剩下的(party 卡片狀態投影、各服務參數快取)都需先立讀模型規格,我可以直接依 CR-030 的通則自行設計並推進」
依據 CR-030(4 條通則)/CR-029 §4(批次 C 盤點之 #3/#4)/SAD-003 §4.1(已廢除自投影,改立跨服務投影)
範圍 CR-029 §5 之 C-5(party 卡片狀態投影) 與 C-7(參數快取)

1. 兩項之裁定(先講結論)

項 裁定 依據
C-5 party 卡片狀態投影 ✅ 實作 SAD-002 §3.4 明載 party 訂閱 card.card.status-changed.v1;且架構上為唯一可行途徑(見 §2.1)
C-7 各服務參數快取 ⏸ 暫緩實作,僅立設計 ★ 實測:9 個可呼叫 product 之服務,全部沒有參數快取;且參數化本身仍為「後續項」(見 §3.1)→ 建立「無快取可失效」的消費者=違反 CR-027 §7(不為「打通」而建無語意來源之物)

2. C-5:party 之卡片狀態投影

2.1 為何是投影,而非同步查詢(架構上的唯一解)

實測 cardcore-service-acl.yml:

card-service:
  - cc-authorization-service          ← 僅此一個

card-service 只接受 cc-authorization-service 之服務間呼叫。
故 party(以及任何非授權場景:客服、風控、報表)在架構上「無法」同步取得卡片狀態——
事件投影是唯一途徑。這不是「要不要做」,而是「別無他法」。

對照 CR-030 §2 #16(同步為主、事件為輔):該通則禁止「重做同步已做之寫入」。
本項不存在可重做的同步路徑(ACL 即擋死)→ 屬「做沒人做的事」→ 應做。

2.2 讀模型設計

項目 值
存放地 CC_PARTY(party 之 schema)
表名 card_status_caches(與 authorization 同名但語意不同——見下註)
餵養事件 card.card.status-changed.v1(生產者=card-service ≠ 存放地 ✅ 符合「跨服務投影」之必要條件)
主鍵 id RAW(16)(surrogate)
業務鍵 card_no_hash(UNIQUE)——與 authorization 之同名表同一慣例
欄位 card_id/account_id/card_no_masked/status/block_code/expiry_date/last_sequence_no/created_at/updated_at/version
更新語意 upsert by card_no_hash;last_sequence_no 供亂序防護
讀取 GET /api/v1/accounts/{accountId}/cards——依 account_id 列出該歸戶之全部卡片狀態

⚠️ 與 authorization-service 之 card_status_caches 之關係(誠實揭露):
二者同形狀、不同語意——authorization 之投影用於授權熱路徑(免同步查卡況,<100ms);
party 之投影用於歸戶視圖(客服/前端查「此客戶有哪些卡、狀態如何」)。
是否應共用一份? 否——跨服務共用讀模型=跨庫耦合(違反「一服務一 schema」SAD-003 §1 DP-01)。
兩份各自投影是 CQRS 之標準取捨(以儲存換解耦)。

2.3 消費者(避免「有人寫、沒人讀」)

消費者 說明
GET /api/v1/accounts/{accountId}/cards 本 CR 新增之端點——客服作業台/前端帳戶頁之直接查詢
(後續)bff-web 之 BffAccountOverview 可加 cards 區塊;本次未做(見 §4.3 揭露)

為何要有端點:僅有投影而無讀取路徑=寫入即丟(CR-027 §7 所禁止之假綠)。
端點即為「語意來源」——它把投影變成可驗證之讀模型。


3. C-7:參數快取——設計先行,暫緩實作

3.1 暫緩之證據(實測)

事實 證據
9 個可呼叫 product 之服務,皆無參數快取 逐服務掃描 ParameterPort/ParameterCache/ProductClient → 相關檔案 0
參數化本身仍為「後續項」 authorization-service 之 ManualReviewPolicy javadoc:「參數服務(product-service /parameters)供給,已登記為後續項」
product 之 ACL 只允許 9 個服務(非「全體 16」) cardcore-service-acl.yml:authorization/card/installment/limit/merchant/posting/pricing/rewards/statement

裁定:暫緩實作。理由(依 P2)——
目前沒有「快取」這個東西可以失效;事件之價值在使快取失效,
而各服務現在每次同步取用即為最新(正確性上事件零增益)。
在尚無快取時建立失效消費者 = 建一個永遠不會有事的監聽器 = 假綠。

⚠️ 但設計先立(本節)——參數化一旦落地,快取+事件失效即為 drop-in。
⚠️ 另一發現:SAD-004 §5.2 載 product.parameter.changed.v1 之消費者為「全體」,
惟 ACL 只允許 9 個服務呼叫 product → 「全體」實為「9 個」。
已在 SAD-004 §5.2 更正為「可呼叫 product 之 9 服務」。

3.2 設計(落地時依此實作)

項目 設計
放置 平台層(platform/common-spring 或新 common-parameter)——9 服務共用,非逐服務重工
型別 ParameterCachePort(Optional<Object> get(String namespace, String key))
實作 InMemoryParameterCache(ConcurrentHashMap;不引第三方快取框架——CSP-001 之「不塞框架」)
載入 lazy:miss 時經 ServiceClientHttpClient 呼叫 product 之 GET /api/v1/parameters/{ns}/{key}?businessDate=(CR-010 自動帶出站憑證)
失效 ParameterChangedListener(共用,置於平台層)訂閱 cc.product.parameter.changed → invalidate(namespace, key)
鍵 namespace + "/" + paramKey(與事件之 aggregateId 同構)
降級 product 不可用 → 回退為「無快取」(回傳 empty,由呼叫端走既有同步路徑)——fail-soft 且不靜默(記 WARN)
啟用 各服務以 cardcore.parameter-cache.enabled(預設 false)逐服務開啟——未開啟者行為不變
前置條件 ① 至少一個服務實際以參數驅動業務(目前無);② 該服務有可量測之同步呼叫延遲問題

⚠️ P3 之界限:本快取不改變「誰是權威」——product 仍為唯一權威;
快取僅為效能最佳化(移除授權熱路徑上之同步依賴)。
失效為「下次取用時重載」(lazy),非推播數值——避免事件承載業務值(P2)。


4. 執行與驗證

4.1 C-5(本次實作)

項 內容
規格先行 SCS-004 §7.5(新增;v1.1 誤植為 §7.1,v1.2 更正——與既有「§7.1 個資處理」衝突)+ SCS-003 §7.14(消費者註記)
遷移 V3__card_status_projection.sql(CC_PARTY:card_status_caches + consumed_events)
程式 domain/model/CardStatusSnapshot/domain/port/CardStatusProjectionPort(讀+寫)/application/CardStatusProjectionService/infrastructure/jdbc/JdbcCardStatusProjectionRepository/infrastructure/messaging/CardStatusChangedListener/PartyQueryService.listCards/AccountController 新端點
契約 tools/gen_openapi.py 加 GET /api/v1/accounts/{accountId}/cards + CardStatusView schema(重生後 93 端點、0 錯誤)
單測 CardStatusProjectionServiceTest(4 支)+ 既有 → party-service 23 測試 0 失敗;掃描器 3 支 0 命中;validate_sql 全庫 65 檔 0 違規
真機 見 §4.1.1

4.1.1 真機驗證結果(2026-10-01;真實 Oracle 23.26 + Kafka 4.3.1)

環境:party-service 啟於 8084(⚠️ 不是 8096——8096 是 migration-service),Flyway 自 v2 → v3 套用 V3__card_status_projection.sql。

# 樣本 操作 實測結果
0 歷史回填 服務首度啟動(消費群組 party-service 為新群組,auto-offset-reset=earliest) 自動回填主題上既有事件 → 投影落 2 列、consumed_events 3 列
1 正樣本①(INSERT) POST /cards/4e3ad10c…/lifecycle-transitions {"event":"LOCK"} → 201 投影新增列:411111******9682/SUSPENDED/LOCKED/seq=1790868053865/version=0
2 正樣本②(UPDATE) 同上,卡 018f2a3b…0011(已有列,version=1) 該列由 ACTIVE/ACTIVE → SUSPENDED/LOCKED,seq 2 → 1790868093060、version 1 → 2
3 查端點 GET /api/v1/accounts/018f2a3b…0001/cards 200,回 2 卡(依 card_no_masked 排序),status 與庫內一致
4 負樣本①(重複 eventId) 以 kafka-console-producer 重投完全相同之信封(eventId=72232012…) 略過:version 仍為 2、consumed_events 未增列;日誌 卡片狀態事件未套用(重複或亂序,已略過)
5 負樣本②(較舊 sequenceNo) 投遞新 eventId 但 sequenceNo=100、payload 為相反狀態(ACTIVE/ACTIVE) 略過:列仍為 SUSPENDED/LOCKED、version 仍為 2。★ 若無亂序防護,此列會被錯誤覆寫為 ACTIVE——防護得證
6 邊界(404) GET /accounts/99999999…/cards 404 CC-PTY-4040(歸戶不存在)
7 邊界(空陣列) GET /accounts/018f2a3b…0002/cards(歸戶存在、尚無投影) 200、data: [](非 404)
8 還原 UNLOCK 兩張卡 投影回 ACTIVE/ACTIVE;version 2 → 3、0 → 1

★ 第 5 項為本驗證之核心:它證明「事件亂序不得覆寫較新狀態」(SAD-004 §5.4)不只是文件上的話。

4.1.2 真機驗證之副產物:sequenceNo 之新舊不同源(已由 CR-030 §2 #12 修復)

歷史事件之 sequenceNo 為 1/2(舊制「卡片版本 + 1」),新事件為 1790868053865(時間戳基底)。
因時間戳恆大於小整數,新舊交替不會造成誤判——但這正是 CR-030 §2 #12 所述「同一聚合兩型別序號不同源 → 靜默資料遺失」之實物佐證,該缺陷已於同批修復。

4.2 C-7(本次僅立設計)

規格:本 CR §3 + SCS-005 §7.2(消費契約)。不實作(見 §3.1 之證據)。

4.3 ⚠️ 本次未做(誠實揭露)

項 原因
bff-web 之 BffAccountOverview 加 cards 區塊 ✅ 已於 2026-10-01 完成(見 §4.5)。⚠️ 併更正 v1.0 之記述:原記「bff relay 使用者權杖可直接呼叫 card-service」不成立——card-service 無「依歸戶列卡片」端點,且其 ACL 只允許 cc-authorization-service;正解為呼叫 party-service 之讀模型(見 §4.5)
C-7 之實作 見 §3.1(無快取可失效)

4.5 bff-web 之 BffAccountOverview 新增 cards 區塊(2026-10-01 完成)

指令:BEAM PMO「先執行(a)」——即 CR-031 §4.3 所列之「bff 加 cards 區塊」。

項 內容
規格先行 SAD-004 §2.2.1(聚合列改四區塊)+新增 §2.2.1.1(逐項定義);SAD-004 升 1.7
下游選擇 party-service GET /api/v1/accounts/{accountId}/cards——不是 card-service。三項理由:(i) card 無依歸戶列卡片之端點(僅 lookup/{cardNoHash}//{cardId});(ii) card 之 ACL 只允許 cc-authorization-service;(iii) 「歸戶 → 卡片清單」之讀模型由 party 持有(§2)——BFF 應呼叫持有讀模型者,不由 BFF 自行拼裝
契約 tools/gen_openapi.py 之 BffAccountOverview 加 cards($ref BffSection)→ 重生 0 錯誤
程式 AccountOverviewPort.fetchCards/AccountOverviewClientImpl.fetchCards/BffAccountOverview 加 cards 欄位/AccountOverviewService.overview 加第四個 section(...)
降級 沿用既有通則:party 不可用或查無歸戶 → cards 區塊 UNAVAILABLE(保留下游錯誤碼),其餘三區塊不受影響
真機 bff(8101)→ party(8084)→ 真實 Oracle 投影:四區塊實測;★ 負樣本:party 不可用 → cards 與 party 兩區塊 UNAVAILABLE、limits/rewards 仍 OK(不拖垮)
⚠️ 誠實揭露 本端點仍無前端頁面消費者(SDD-007 未立對應 §5.4.x;前端 api/bff.ts 未呼叫)。本區塊之價值在完成「歸戶 360」之聚合能力;接頁面前須先補 SDD-007 §5.4.x 逐鈕定義(GOV-003 條件 9)→ 列為後續項

4.4 ★ 真機驗證時發現並修正之跨服務種子不一致(誠實揭露)

症狀:GET /api/v1/accounts/{accountId}/cards 對所有真實卡片回 404 CC-PTY-4040。

根因(不是程式錯,是種子缺列):

服務之種子 使用之歸戶
04-dev-card-seed.sql(card) …0001/…0002/…0003
05-dev-limit-seed.sql(limit) …0001/…0002/…0003
02-dev-setup.sql(authorization 讀模型) …0001/…0002/…0003
06-dev-party-seed.sql(party) …0010(ACC0000001)——孤島,且該戶無卡

→ party 之歸戶主檔與其他服務對不上;本端點之「歸戶存在」守門(§2.3)因此一律擋下。

修正:於 06-dev-party-seed.sql 補入 …0001/…0002/…0003 三戶(對照表見該檔註解)。
未動 …0010(ACC0000001)——它被 statement 週期、installment 種子、前端四支 Console 頁面與 OpenAPI 範例引用。

⚠️ 守門是否應放寬? 否。歸戶之權威在 party(P1 單一真實來源);
「歸戶不存在 → 404」與「歸戶存在但無卡 → 空陣列」是兩種不同的合法狀態,混為一談才是缺陷。
種子對不上是開發環境資料問題,不該以放寬語意來遷就。


5. 修訂記錄

版本 日期 說明
1.0 2026-10-01 初版:依 BEAM PMO 指示與 CR-030 通則,裁定 C-5 實作(架構上唯一解——card-service 之 ACL 只允許 authorization)、C-7 暫緩(實測 9 服務皆無參數快取,且參數化仍為後續項 → 建失效消費者=假綠);★ 併更正 SAD-004 §5.2:「全體」實為「可呼叫 product 之 9 服務」(ACL 所限)
1.2 2026-10-01 bff-web 之 BffAccountOverview 新增 cards 區塊(BEAM PMO 指示「先執行(a)」)——新增 §4.5:規格先行(SAD-004 §2.2.1 + 新增 §2.2.1.1)、契約重生、程式四檔、單測、真機驗證(含負樣本)。★ 關鍵更正:下游為 party-service 而非 card-service(card 無依歸戶列卡片之端點/其 ACL 僅允許 authorization/歸戶讀模型由 party 持有)。★ 併誠實揭露:該端點仍無前端頁面消費者 → 接頁面前須先補 SDD-007 §5.4.x。SAD-004 升 1.7
1.1 2026-10-01 C-5 實作完成並通過真機驗證:新增 §4.1.1 驗證結果(8 項:歷史回填/INSERT/UPDATE/端點/兩項負樣本/404/空陣列/還原)、§4.1.2 sequenceNo 新舊不同源之實物佐證、§4.4 跨服務種子不一致之發現與修正(party 種子補入 …0001/…0002/…0003)。SAD-004 升 1.6(§5.2 party 消費端 🟡→✅、6 條→7 條全鏈路);SCS-004 升 1.2
    (1-1/1)