Project

General

Profile

CR #231 » CR-030_事件流治理十二項待裁定之架構裁定.md

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

 

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

裁定:

  1. 語意重新界定:authorization.authorization.approved.v1 = 「授權已佔用額度」之事件(非「決策為核准」)。
    • 三者皆佔用額度:APPROVED(正常核准)/STANDIN(代授權)/REFERRAL(轉人工)。
    • DECLINED 不佔用 → 不屬本事件(亦不另立,見 #7)。
  2. AuthDecision 新增 holdsLimit()(APPROVED/STANDIN/REFERRAL),發射條件由 isApproved() 改為 holdsLimit()。
  3. payload 補 decision(可選)——下游得以分辨「核准」與「轉人工待覆核」。
  4. 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 → 補發射 + 補消費(重新佔用額度)

裁定:

  1. payment.payment.unmatched.v1 發射:payload 依 SDD-006 §4.2(paymentId/accountId/amount/daysUnmatched)+ currency(可選,依 #10 通則);於 PaymentUnmatchedService 轉換成功後同交易寫 outbox。
  2. payment.payment.reversed.v1 發射:payload 本 CR 定義為 paymentId/accountId/amount/reason/currency(可選);於 PaymentService.reverse 同交易寫 outbox。
  3. 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

裁定:

  1. 不變式入規(SAD-004 §5.4):「同一 aggregateId 之所有事件型別,須共用同一單調序號來源」。
  2. 修正唯一違反者 card-service:cardIssuedEnvelope/tokenBoundEnvelope 原用時間戳基底序號,status-changed 用卡片版本 → 改為全部使用同一來源(CardEventFactory.nextSequenceNo())。
  3. 已知限制保留並升級為待辦: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 項(業務文案/卡組織規範/聯絡資訊/累點規則)
    (1-1/1)