CR #231 » CR-030_事件流治理十二項待裁定之架構裁定.md
CR-030 事件流治理:CR-027 §5 十二項待裁定之架構裁定(Best Practice)
| 項目 | 內容 |
|---|---|
| 文件編號 | CR-030 |
| 版本 | 1.0 |
| 提出日 | 2026-10-01 |
| 提出者 | WorkBuddy AI(開發團隊) |
| 裁定者 | 架構師(本專案)——BEAM PMO 2026-10-01 指示:「以 Best Practice(Payment Industry 20 年金融科技/架構師)直接將這些問題解決」 |
| 緣起 |
CR-027 §5 累積 12 項待裁定。BEAM PMO 指出「多為 Day 1 立規格時未深思熟慮所致」——本 CR 不再逐項回問,改以業界實務直接裁定,並載明理由、業界依據、影響面與可逆性,供日後覆核。 |
| 範圍 |
CR-027 §5 之 #5~#16(#1~#4 已由 CR-028 化解) |
| 影響範圍 |
SAD-002 §3、SAD-003 §4、SAD-004 §5.2/§5.4、SDD-006 §2.5/§4.2;SCS-001/SCS-002/SCS-008/SCS-011;CR-027;程式:authorization/payment/limit/card/product/clearing/dispute/posting
|
| 狀態 | 12 項全數裁定;文件面已執行;程式面分 A/B 兩批執行(見 §3) |
0. 裁定原則(本 CR 之方法論)
作為支付產業架構師,本案的裁定一律依循以下四條業界通則;後續每一項裁定皆註明所依據之原則:
| 原則 | 內容 | 出處/理由 |
|---|---|---|
| P1 單一真實來源 | 同一事實只能有一份權威記載;重複記載必然漂移 | 本專案鐵律;已實證漂移(card.card.issued 在 SAD-002 有、SAD-004 無) |
| P2 事件=已發生的狀態變更 | 事件流只承載狀態變更之事實,不承載「請你做某事」(命令)與「統計指標」 | Event-Driven Architecture 基本原則;命令走 API、指標走 metrics |
| P3 同步 vs 非同步之分界 | 需即時回應者走同步;解耦的通知/投影/分析走事件。同一寫入不得兩條路徑都做 | 支付授權有 <100ms 之硬性延遲要求;重複路徑必然重複套用 |
| P4 契約演進向後相容 | 新增可選欄位不升版;移除/改型別/新增必填才升版 |
SAD-004 §5.3(與業界 schema-evolution 慣例一致) |
兩類事項本 CR 不代為決定(須外部輸入,不得由開發臆造):
(a) 客戶業務文案(通知模板文字);(b) 卡組織規範值(各組織拒付時限、回應碼對映)。
此二類僅能「先立骨架、留待接入」。
1. 十二項裁定總表
| # | 爭點 | 裁定 | 依據 | 執行 |
|---|---|---|---|---|
| 5 | 事件清單以哪份為權威 |
(c) SAD-004 §5.2 為唯一權威清單;SAD-002 §3.x 之「事件(發布/訂閱)」欄降為指標並指向 §5.2 |
P1 | 文件(已執行) |
| 6 |
SAD-003 §4.1 之 7 個 q_* 語意 |
廢除全部 7 個自投影;改立「跨服務投影清單」,由實際需求驅動 | P1/P2 | 文件(已執行) |
| 7 |
declined.v1 是否納入 |
不納入事件流;改以 metrics(Micrometer counter) 涵蓋 | P2 | 文件 + 程式(A 批) |
| 8 |
REFERRAL 事件缺漏 |
approved.v1 之語意重新界定為「授權佔用額度之決策事件」,並補 decision(可選);發射條件由 isApproved() 改為 holdsLimit()(APPROVED/STANDIN/REFERRAL) |
P2 | 文件 + 程式(A 批,真缺陷) |
| 9 |
UNMATCHED/REVERSED 未實作 |
補發射 + 補消費(limit 重新佔用)——此為超額風險之真缺陷
|
P3 | 文件 + 程式(A 批,真缺陷) |
| 10 |
posted.v1 缺 currency
|
確認為「可選」欄位(已執行);並提升為通則:凡涉金額之事件 payload 一律帶 currency
|
P4 | 文件(已執行)+ 通則入規 |
| 11 |
approved.v1 擴充 2 可選欄位 |
追認(cardNoMasked 為遮罩非明文,符合 PCI 原則) |
P4 | 文件(追認) |
| 12 |
sequenceNo 跨事件型別交錯 |
明訂不變式:「同一 aggregateId 之所有事件型別,須共用同一單調序號來源」;並修正 card-service(唯一違反者:改用單一序號來源) |
P2 | 文件 + 程式(A 批) |
| 13 |
SDD-006 §4.2 之 card.status-changed payload 更正 |
追認以實作為準(實作已真機驗證且有消費端;原契約表為唯一錯誤記載) | P1 | 文件(追認) |
| 14 |
limit.hold.created.v1 補 currency
|
追認(可選、不升版) | P4 | 文件(追認) |
| 15 | Kafka 主題命名不一致 |
(a) 對齊文件慣例(cc.<domain>.<event-short>);修正 product/clearing/dispute/posting 之 event_type 常數 |
P1 | 文件 + 程式(B 批) |
| 16 | 事件 vs 同步呼叫之重複 | (b)「同步為主、事件為輔」——事件僅供跨服務投影與通知,不重做同步已做之寫入 | P3 | 文件(已執行)+ SAD-004 §5.2 用途欄改寫 |
2. 逐項裁定理由(含業界依據)
#5 事件清單權威 → (c) 合併為單一清單
裁定:SAD-004 §5.2 為唯一權威(含生產者/消費者/用途)。SAD-002 §3.x 之「事件(發布)」/「事件(訂閱)」降為服務視角之指標,並在表頭明示「權威清單見 SAD-004 §5.2」。
理由(P1):兩份清單並存已經漂移——card.card.issued.v1/card.token.bound.v1 見於 SAD-002 §3.3 而不在 SAD-004 §5.2(見該節之揭露註記)。支付系統的事件契約若無單一權威,下游訂閱將依「看哪份文件」而異,屬治理缺陷。
業界依據:金融機構之 event catalog 通常以中央 registry(Schema Registry/Event Catalog)為單一權威;服務文件僅引用。
#6 q_* 讀模型 → 廢除 7 個自投影,改立「跨服務投影清單」
裁定:SAD-003 §4.1 之 7 列全數刪除(已逐列比對確認 7/7 為自投影,來源事件與存放服務同一)。改立跨服務投影清單,內容為實際已實作或已裁定要做之投影(如 card_status_caches/account_limit_caches/limit_holds),並由 CR-029 之盤點決定後續。
理由(P1/P2):CQRS 讀模型之存在價值在「跨服務查詢」——避免同步呼叫扇出(fan-out)與跨庫 join。自投影只是把權威表複製一份到讀側,帶來雙寫、延遲、一致性三重成本而零收益(違反 CQRS 初衷),且與權威表同名同義易誤用(q_transaction_history vs posting.transactions)。
業界依據:CQRS 之 read model 是「為查詢而生的另一種視圖」;同構複製不構成 read model。支付核心常見之投影為「跨域客戶視圖」(卡+額度+點數+帳單)而非自我複製。
#7 declined.v1 → 不納入事件流;改以 metrics 涵蓋
裁定:authorization.authorization.declined.v1 不發射。拒絕率/拒絕原因分佈改以 Micrometer counter(如 cardcore_authorization_declined_total{reason=…})涵蓋,供 Prometheus/Grafana。
理由(P2):拒絕不改變任何下游狀態——不佔額度、不產生待請款、不需通知(不應對每筆拒絕都發簡訊)。依 P2,事件流只承載狀態變更之事實;「拒絕率統計」是觀測性需求,屬 metrics 之職責。
業界依據:卡組織與發卡行之實務——decline 是授權主機的即時回應碼(ISO 8583 DE39),其統計走 APM/BI;domain event 通常僅於「需要跨域副作用」時發射。將高頻拒絕灌入事件流會造成 topic 爆量與無謂消費者負擔。
⚠️ 可逆性:SDD-006 §4.2 之 payload 保留並標註「刻意未發射」;若日後客戶要求「拒絕率即時看板」,僅需新增生產端發射 + 看板消費者,無契約破壞。
#8 REFERRAL → 重新界定 approved.v1 為「佔用額度之決策事件」+ 補 decision
裁定:
-
語意重新界定:
authorization.authorization.approved.v1= 「授權已佔用額度」之事件(非「決策為核准」)。- 三者皆佔用額度:
APPROVED(正常核准)/STANDIN(代授權)/REFERRAL(轉人工)。 -
DECLINED不佔用 → 不屬本事件(亦不另立,見 #7)。
- 三者皆佔用額度:
-
AuthDecision新增holdsLimit()(APPROVED/STANDIN/REFERRAL),發射條件由isApproved()改為holdsLimit()。 -
payload 補
decision(可選)——下游得以分辨「核准」與「轉人工待覆核」。 -
notification端:decision = REFERRAL→ 不建立通知(無對應模板,不臆造文案),記 INFO。
理由(P2 + 實證缺陷):實測 AuthorizeUseCase:110 之 willApprove = approved || referralOnly,:113 據此呼叫 limitPort.hold(...)——即 REFERRAL 確實凍結額度;惟 :148 之發射條件 authorization.isApproved() 排除 REFERRAL → 額度被佔用而下游完全不知。此為真缺陷,非「要不要做」之選擇。
⚠️ 併更正程式註解之不實:
:145-147原註「REFERRAL 亦已佔用額度,屬核准路徑(與Authorization.isApproved()之定義一致)」——前半正確、後半與程式碼矛盾(isApproved()明確排除 REFERRAL)。已於本 CR 一併更正。
業界依據:Card authorization 之 hold 生命週期——REFER(轉人工)在 VISA/Mastercard 實務上仍為 authorization hold,商戶已取得授權碼之占位;發卡行必須讓額度、待請款、通知三側同步得知,否則會出現「客戶額度被凍結但客服查不到原因」。
#9 UNMATCHED/REVERSED → 補發射 + 補消費(重新佔用額度)
裁定:
-
payment.payment.unmatched.v1發射:payload 依SDD-006 §4.2(paymentId/accountId/amount/daysUnmatched)+currency(可選,依 #10 通則);於PaymentUnmatchedService轉換成功後同交易寫 outbox。 -
payment.payment.reversed.v1發射:payload 本 CR 定義為paymentId/accountId/amount/reason/currency(可選);於PaymentService.reverse同交易寫 outbox。 -
limit-service補消費:訂閱兩者,重新佔用額度(used_amount += amount)——與 #A-124 之「恢復」反向同構。
理由(P3 + 實證缺陷):SCS-008 §8.2 明載 POSTED → UNMATCHED 會把 LIMIT_RESTORED 設回 false(撤銷額度恢復=重新佔額)、§7 沖正亦然;惟 §7.1 只發射 posted.v1 →
「繳款入帳 → 額度已恢復 → 三日後轉未匹配」時,額度不會被重新佔用 → 可用額度永久高估。
以支付核心之標準,這屬 P0 級帳務/風控缺陷(超額放行風險),必須補。
⚠️ 與 #16 之區別(重要,避免誤解):
limit之「重新佔用」不是重做同步已做之寫入——
該行為目前無任何路徑涵蓋(markUnmatched為批次、reverse為 API,皆未通知limit)。
#16 禁止的是「重複做同步已做的事」;不禁止「做沒人做的事」。
業界依據:Payment reconciliation 之「沖銷/退回必回沖額度」為鐵律;發卡行若在未匹配期間仍讓客戶可用額度含該筆繳款,即形成信用暴露。
#10/#11/#13/#14 → 追認 + 提升為通則
裁定:
-
#10/#14 追認:
currency為可選欄位(依 P4 不升版)。 -
#11 追認:
cardNoMasked/merchantId為可選;★cardNoMasked為遮罩值(先例見card.card.status-changed.v1),不含 PAN,符合 PCI DSS 對「不得儲存/傳遞明文 PAN」之要求。 - #13 追認:以實作為準(真機驗證過且有消費端)。
-
★ 提升為通則(新增):凡事件 payload 涉及金額者,一律帶
currency(可選)——理由:金額無幣別即無語意(多幣別帳戶下不可解讀);列為SDD-006 §2.5之設計通則。
業界依據(P4):ISO 20022 之 Amount 一律為「Currency + Amount」複合型別;金融訊息中裸金額被視為建模錯誤。
#12 sequenceNo → 明訂不變式 + 修正 card-service
裁定:
-
不變式入規(
SAD-004 §5.4):「同一aggregateId之所有事件型別,須共用同一單調序號來源」。 -
修正唯一違反者
card-service:cardIssuedEnvelope/tokenBoundEnvelope原用時間戳基底序號,status-changed用卡片版本 → 改為全部使用同一來源(CardEventFactory.nextSequenceNo())。 -
已知限制保留並升級為待辦:JVM 內計數器多實例部署不安全;正解為「聚合層級持久化序號」(每
aggregateId一計數列,同交易遞增)——記為後續強化項(非本批)。
理由(P2):消費者之亂序防護以「sequenceNo <= MAX(aggregateId)」判定——若同一聚合之兩個事件型別序號不同源,後發之事件可能因序號較小而被誤判為亂序而略過(資料遺失,且無錯誤訊息)。此為靜默型缺陷,最難察覺。
為何選 (b) 精神而非 (c):(c)「明訂序號僅在 (aggregateId, eventType) 內有意義」是把問題下推給消費者——每個消費者都要記得按型別分開判斷;而 (b) 讓生產端保證不變式,消費者之現行邏輯即正確。生產端修一處 < 消費者各修一處,且不變式更強。
業界依據:Event Sourcing 之 sequence_number 慣例為per-aggregate 嚴格遞增(Axon/EventStoreDB 皆然),不因事件型別而分段。
#15 主題命名 → (a) 對齊文件慣例
裁定:主題一律為 cc.<aggregate_type>.<event_type>,其中 event_type = 事件全名去 .vN 後,再移除一層與 aggregate_type 相同之首段。
修正 product/clearing/dispute/posting 四個服務之常數:
| 服務 | 現值 | 改為 | 主題(前 → 後) |
|---|---|---|---|
product |
product.parameter.changed |
parameter.changed |
cc.product.product.parameter.changed → cc.product.parameter.changed
|
clearing |
clearing.batch.matched |
batch.matched |
cc.clearing.clearing.batch.matched → cc.clearing.batch.matched
|
dispute |
dispute.case.opened |
case.opened |
cc.dispute.dispute.case.opened → cc.dispute.case.opened
|
posting |
posting.transaction.posted/.reversed
|
transaction.posted/.reversed |
cc.posting.posting.transaction.* → cc.posting.transaction.*
|
理由(P1):① 文件(SAD-004 §5.4 示例 + SCS-002 §7.5/SCS-003 §7.11)為多數且較早之慣例;② 方案 (b) 會使主題退化為三層(cc.card.card.card.issued),明顯非設計意圖;③ 現階段零消費者(CR-029 盤點確認),變更成本 = 每服務 1 個常數。
⚠️ 正式環境之遷移注意(本批為本地開發,無消費者故直接改):若已有消費者,需雙軌發布(同時寫新舊 topic)≥1 release。已記入
SAD-004 §5.4。
#16 事件 vs 同步呼叫 → (b)「同步為主、事件為輔」
裁定:事件僅供跨服務投影與通知;不得重做同步呼叫已完成之寫入。
SAD-004 §5.2 之「用途」欄改寫為「投影/通知」語意,並移除「佔用額度」「繳款入帳」等命令式描述。
理由(P3):三條重複路徑(auth→limit 佔用/payment→posting 入帳/statement→posting 查詢)若同時走同步與事件,必然重複套用(重複佔用=額度被扣兩次;重複入帳=帳務翻倍)。且授權有 <100ms 延遲要求,同步不可去。
業界依據:CQRS/EDA 之標準分工——write model 之間以同步(強一致)或 saga 協作;read model 與通知以事件(最終一致)餵養。「同一事實兩條寫入路徑」在支付系統中屬明確反模式。
連帶效果(CR-029 之批次範圍收斂):
CR-029 條目 |
原判定 | 裁定後 |
|---|---|---|
#2 approved.v1 → limit |
需裁定 | 不做(同步已佔用) |
#11 payment.posted.v1 → posting |
需裁定 | 不做(同步已入帳) |
#5/#9 posting.posted/reversed → statement |
需裁定 | 不做(結帳時同步查詢) |
#1 approved.v1 → posting |
需裁定 | 不做(無「待請款」模型;且屬命令) |
#8 posting.posted → limit |
payload 缺口 | 降級為不做(同步已佔用) |
#6/#10 posting.* → rewards |
payload 缺口 | 改為「以事件驅動累點」之獨立議題(見 §4) |
#7 posting.posted → clearing |
語意未定 | 不做(清算以批次讀取,非事件) |
3. 執行分批
| 批 | 內容 | 性質 | 真機驗證 |
|---|---|---|---|
| 文件批 |
SAD-002 §3/SAD-003 §4/SAD-004 §5.2/§5.4/SDD-006 §2.5/§4.2/SCS-001/CR-027(12 項標記結案) |
治理 | — |
| A 批(真缺陷) |
#8 REFERRAL(auth)/#9 unmatched + reversed(payment + limit)/#12 card 序號統一/#7 decline metrics(auth) |
程式 | 是(auth/payment/limit/card) |
| B 批(命名) | #15 四服務常數 + 主題 | 程式 | 是(product/clearing/dispute/posting) |
4. 明確不做與留待外部輸入之事項
4.1 本 CR 裁定「不做」(並載明理由,避免日後重提)
| 事項 | 理由 |
|---|---|
approved.v1 → limit/posting 之消費端 |
#16:同步已做 |
payment.posted.v1 → posting 之消費端 |
#16:同步已做 |
posting.posted/reversed → statement 之消費端 |
#16:結帳時同步查詢即為正解(批次一致性優於事件最終一致) |
posting.posted → clearing |
清算為批次對帳作業,以批次讀取為正解;事件驅動反而引入順序與完整性風險 |
declined.v1 之發射 |
#7:改以 metrics 涵蓋 |
4.2 留待外部輸入(不得由開發臆造)
| 事項 | 需何輸入 |
|---|---|
rewards.points.*/dispute.case.opened 之通知 |
業務文案(模板文字)——由業務單位提供 |
posting.* → rewards 之自動累點
|
累點規則與 MCC 對映(且需 posting.posted.v1 補 mcc/merchantId)——屬新功能,非事件推廣 |
| 卡組織拒付時限與回應碼對映 | 卡組織規範文件(真阻塞) |
notification 之收件人(手機/Email) |
party-service 之聯絡資訊端點(目前不存在)——需產品決策 |
原則:「沒人做的事」由架構師裁定;「需要業務定義的事」留給業務。 兩者不得混淆。
5. 修訂記錄
| 版本 | 日期 | 說明 |
|---|---|---|
| 1.0 | 2026-10-01 | 初版:依 BEAM PMO 指示「以 Best Practice 直接解決」,裁定 CR-027 §5 全部 12 項(#5~#16)——先立 4 條業界通則(P1~P4)作為方法論,再逐項裁定並載明「理由/業界依據/影響面/可逆性」;★ 其中 #8/#9 為實證之真缺陷(REFERRAL 佔額不發事件、未匹配/沖正不回沖額度);★ #16 之裁定連帶收斂 CR-029 之批次範圍(4 條改判「不做」);★ 明列留待外部輸入之 4 項(業務文案/卡組織規範/聯絡資訊/累點規則) |