CR #232 » CR-031_批次C剩餘項讀模型設計與裁定.md
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
|