Project

General

Profile

CR #201 » SAD-004_整合架構與API治理.md

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

 

SAD-004 整合架構與 API 治理

項目 內容
文件編號 SAD-004
版本 1.9(CR-049:§5.4 新增「線格式」契約列——生產端一律以 KafkaTemplate<String,String> 發送已序列化之信封 JSON 字串,producer value-serializer 必須為 StringSerializer;JsonSerializer 會二次編碼使消費端 eventType 恆 null → 靜默略過。併納入架構關卡 A9 與跨服務真機 E2E 之教訓。前版 1.8(CR-033:§5.2 加註「C-5 已結案」——CR-029 §5 之 C-5(8 條「語意/讀模型未定義」)經逐條查證後實作 1/不做 7/待實作 0;★ 併揭露根因:該 8 條係依 CR-030 之前之本表盤點,CR-030 §2 #5 重寫本表時已移除/改判,惟未回寫 CR-029 → 幽靈待辦(教訓:重寫權威清單後須回寫以舊清單為基礎之盤點)。前版 1.7(CR-031 §4.3:§2.2.1 新增「cards 區塊」——BffAccountOverview 由三區塊擴為四區塊,新增 §2.2.1.1 說明「為何下游是 party 而非 card」(card 無依歸戶列卡片端點、ACL 僅允許 authorization、且歸戶讀模型由 party 持有)與「尚無前端消費者」之誠實揭露。前版 1.6(CR-031 §2:§5.2 card.card.status-changed.v1 之 party 消費端完成——消費端狀態由「🟡(auth ✅/party ⬜)」升為 ✅,並補註「兩份投影刻意不共用」之理由;§5.2 實作進度 6 條 → 7 條全鏈路。前版 1.5(CR-028 hold 事件群四項矛盾之化解(2026-10-01):§5.2 刪除 authorization.hold.created.v1/authorization.hold.released.v1 兩列、補登 limit.hold.created.v1——化解「同一事實兩個生產者」;程式碼零影響(該二事件全庫從未被實作)。前版 1.4(GOV-006 4.8 W-2 推廣・事件 #2(#A-119):§5.2 加註實作進度(✅ 2/16 事件全鏈路)+ §5.4 補 limit.limit.adjusted.v1 之推廣紀錄,併揭露兩項規格偏差(eventId 實作採 UUIDv4 而非 ULID;sequenceNo 為 JVM 內遞增之替代序號)。前版 1.3(GOV-006 4.8 W-2 第一切片:新增 §5.4 事件消費契約——主題命名(cc. + 型別去 .v<major>)/分區鍵/順序/冪等(eventId 去重)/版本容忍/亂序略過/不無限重試。前版 1.2(C-13 bff 首增量 §2.2.1;C-12 §2.3/§2.4 已於 1.1)
狀態 已核定
撰寫者 SA
審查者 SEC / TL
對應需求 SYS-2.4、SYS-2.5、SYS-4、SYS-7、API 設計與管理

1. 整合樣式選用原則

樣式 適用情境 保證 不適用
同步 REST 查詢、即時必要之檢核 即時回應 長時間處理、狀態變更
非同步事件 跨服務狀態傳播 至少一次、分區內有序 需立即取得結果
檔案交換 批次、卡組織清算 批次完整性 即時性要求
ISO 8583 卡組織授權與清算 交易完整性 非支付類資料

選用決策樹

flowchart TD
  A["需要整合"] --> B{"需要立即結果"}
  B -->|"是"| C{"是否為狀態變更"}
  C -->|"是"| D["同步命令 + 事件通知<br/>(雙軌)"]
  C -->|"否"| E["同步查詢 REST"]
  B -->|"否"| F{"資料量與時效"}
  F -->|"即時事件"| G["非同步事件 Kafka"]
  F -->|"批次檔案"| H["檔案交換 + 批次校驗"]
  F -->|"卡組織"| I["ISO 8583"]

2. 邊緣層架構

flowchart LR
  WEB["營運管理平台"] --> GW["API 閘道"]
  MOB["行動銀行 / 網銀"] --> GW
  PARTNER["合作夥伴 / TSP"] --> GW
  GW --> AUTHN["認證<br/>JWT / mTLS / API Key"]
  AUTHN --> AUTHZ["授權初篩<br/>角色 / 範圍"]
  AUTHZ --> RATE["速率限制<br/>依用戶與端點"]
  RATE --> TRACE["追蹤注入<br/>trace_id"]
  TRACE --> ROUTE["路由"]
  ROUTE --> BFF["BFF"]
  ROUTE --> SVC["領域服務"]

2.1 閘道職責

職責 說明 不負責
認證 JWT 驗證、mTLS 雙向驗證、API Key 業務角色判斷
授權初篩 依端點與角色粗篩 資料層級權限
速率限制 依用戶、端點、來源 IP 業務流量控管
追蹤注入 產生並傳遞 trace_id 業務日誌內容
路由與版本 依路徑與標頭路由至服務版本 業務轉換
請求驗證 依 OpenAPI 驗證結構 業務規則驗證
回應碼正規化 統一錯誤格式 業務錯誤語意

2.2 BFF 職責

職責 說明
聚合 將多服務資料組合成前端所需之單一回應
裁切 僅回傳畫面需要之欄位(減少傳輸)
快取 低變動資料(產品、參數)之短期快取

2.2.1 bff-web 首增量(實作,C-13,2026-09-22;四區塊,2026-10-01)

項目 內容
端點 GET /api/v1/bff/accounts/{accountId}/overview(契約 bff-web.yaml 已納入 gen_openapi.py,第 18 份契約)
聚合 四個區塊,鍵=歸戶 accountId:
① party(party-service GET /api/v1/accounts/{id})
② cards(party-service GET /api/v1/accounts/{id}/cards)——2026-10-01 新增
③ limits(limit-service GET /api/v1/accounts/{id}/limits)
④ rewards(rewards-service GET /api/v1/rewards/accounts/{id})
降級 任一下游失敗 → 該區塊 UNAVAILABLE 並保留下游原錯誤碼(不轉碼、不拖垮整體);對照帳務寫入(CR-006 失敗即 503)——讀寫降級語意刻意不同
轉傳 下游呼叫由 ServiceClientHttpClient 自動附加 token-relay 之使用者權杖(SAD-005 §3.4);使用者 token 之 azp=cardcore-web 不受下游 ACL 管制(CR-011)
邊緣認證 bff 啟用 cardcore.security.enabled(fail-closed guard 實測攔截缺依賴之啟動),補 oauth2-resource-server
實測(C-13) 使用者 token(demo.csr,ROPC 本地開啟)→ 200:party OK(王小明)/rewards OK(100,103 點)/limits UNAVAILABLE(CC-LIM-1001 原樣保留)——⚠️ 併揭露「跨域 ID 命名空間未統一」(limit 種子另用 …0001–0003),屬遷移/客戶主檔議題

2.2.1.1 ★ cards 區塊(2026-10-01 新增;CR-031 §4.3)

項目 內容
下游 party-service(非 card-service)——GET /api/v1/accounts/{accountId}/cards
為何不是 card-service (i) card-service 無「依歸戶列卡片」端點(僅 lookup/{cardNoHash} 與 /{cardId});(ii) 其 ACL 只允許 cc-authorization-service(cardcore-service-acl.yml);(iii) 卡片狀態之權威在 card,但「歸戶 → 卡片清單」之讀模型由 party 持有(CR-031 §2、SAD-003 §4.1 跨服務投影清單)——BFF 聚合應呼叫持有讀模型者,不由 BFF 自行拼裝
回傳形狀 BffSection(與其餘三區塊一致)——data 為 CardStatusView[](cardId/cardNoMasked/status/blockCode/expiryDate)
降級 同 §2.2.1 通則:party 不可用或該歸戶查無 → cards 區塊 UNAVAILABLE,其餘三區塊不受影響
不含 PAN cardNoMasked 為遮罩值(SCS-004 §7.5);PCI DSS 要求下不得回明文卡號
⚠️ 誠實揭露 本端點目前尚無前端頁面消費者(SDD-007 未立對應 §5.4.x;前端 api/bff.ts 未呼叫)。本區塊之價值在完成「歸戶 360」之聚合能力;接頁面前須先補 SDD-007 §5.4.x 逐鈕定義(GOV-003 條件 9)。列為後續項

2.3 閘道路由表(實作,C-12,2026-09-22)

實作於 api-gateway(Spring Cloud Gateway WebFlux;埠 8100)。對外路徑與前端
Vite 開發代理同前綴(正式環境僅需將代理目標指向本閘道)。埠號之權威來源為
各服務 application.yml(與 CSP-006 §4 一致)。

對外前綴 下游 埠 對應 FR
/auth-api/** authorization 8081 SYS-4
/card-api/** card 8082 SYS-4
/limit-api/** limit 8083 SYS-4
/party-api/** party 8084 SYS-4
/product-api/** product 8085 SYS-4
/pricing-api/** pricing 8086 SYS-4
/posting-api/** posting 8087 SYS-4
/payment-api/** payment 8088 SYS-4
/installment-api/** installment 8089 SYS-4
/rewards-api/** rewards 8091 SYS-4
/statement-api/** statement 8092 SYS-4
/dispute-api/** dispute 8093 SYS-4
/clearing-api/** clearing 8094 SYS-4
/merchant-api/** merchant 8095 SYS-4
/migration-api/** migration 8096 SYS-4
/notification-api/** notification 8097 SYS-4
/report-api/** report 8098 SYS-4

實作注意(實測取得):

  1. Spring Cloud 2025.1 之屬性命名空間為 spring.cloud.gateway.server.webflux.routes
    (由 spring-cloud-gateway-server-webflux 5.0.3 之 configuration-metadata 查證)——
    舊寫法 spring.cloud.gateway.routes 會被靜默忽略(不報錯、不路由)。
  2. 各路由以 RewritePath 去前置(/{domain}-api/(?<segment>.*) → /api/$\{segment}),
    還原服務內部路徑 /api/**。
  3. 下游 URI 以環境變數覆寫(CARD_CORE_*_URI),本機預設 localhost。
  4. edge 認證:閘道以 spring-boot-starter-oauth2-resource-server(reactive)驗證
    JWT(issuer-uri 與 17 服務共用同一 realm),健康探測端點放行;下游再做業務級授權。
    ⚠️ WebFlux 模組不得套用 common-spring 之 SecurityAutoConfiguration
    (Servlet 版,HttpSecurity)——該配置已補
    @ConditionalOnWebApplication(type = SERVLET)(HANDOVER #A-73:
    未加前 WebFlux 閘道啟動即撞 NoClassDefFoundError: jakarta/servlet/Filter)。
    CorrelationIdAutoConfiguration(註冊 Servlet 過濾器)同樣補上此條件。

2.4 分流(Cycle Split)閘道機制(SYS-8/SAD-002 §6;POC-13,C-12)

項目 設計
分流維度 商店代號(收單;X-Merchant-Id 標頭)——SAD-002 §6.2
判定 商店代號 MD5 雜湊桶(0–99)對照分流比例:桶值 < 比例 → 新系統,否則 → 舊系統(Legacy ECS,legacy-uri 組態,預設 http://localhost:8080)。同一商店代號判定恆定(鐵律:同一歸戶不得跨波次)
邊界 比例 ≥100 → 全量新系統(含未帶標頭);比例 ≤0 或未帶標頭 → 舊系統(未納入灰度,保守可回退)
熱更新 組態檔 config/cycle-split.yml(spring.config.import 掛載)+ POST /actuator/refresh——@ConfigurationProperties 原位重綁,無需重啟(POC-13-5:≤ 1 分鐘)
回退 比例調回 0 即回退(POC-13-6),同樣線上即時生效
稽核 每筆決策記 LOG:merchantId/decision(NEW/LEGACY)/target/當下比例(POC-13 驗收:「每一筆轉接均須有可追溯之 LOG」)
影子比對(W0 之並行驗證) 未實作——屬 migration-service/比對服務之職責,列入後續待辦
實作 CycleSplitDecision(純函式,7 例單測)+ CycleSplitGlobalFilter(GlobalFilter,order = RouteToRequestUrlFilter + 10)+ CycleSplitProperties(refresh 可重綁)

⚠️ 實作陷阱(實測取得,勿重蹈):

  1. 分流過濾器執行於 RewritePath 之後(order 10010 > route filter 之 1),
    exchange 之 request path 已被 mutate 為內部路徑(/api/...)——
    前綴判斷須取 GATEWAY_ORIGINAL_REQUEST_URL_ATTR(原始對外路徑)。
  2. 該屬性之實際型別為 LinkedHashSet(非 List)——以 Collection<URI> 接收
    取首筆;直接轉 List 拋 ClassCastException。
  3. 上述二者皆以**暫時探針(WARN log)**實測定案後修正——「推論」在分散式管線
    的屬性傳遞上不可靠。
    | 限流保護 | 保護後端免受前端高頻輪詢 |

禁止:BFF 不得包含業務規則;業務規則一律在領域服務。


3. API 治理

3.1 契約優先流程

flowchart LR
  A["① 撰寫 OpenAPI 3.1"] --> B["② Schema 驗證"]
  B --> C["③ 產生型別與骨架"]
  C --> D["④ 契約測試(Consumer / Provider)"]
  D --> E["⑤ 破壞性變更檢查"]
  E -->|"有破壞性變更"| F["升版並雙軌"]
  E -->|"無"| G["合併並實作"]
  F --> G

3.2 版本策略

變更類型 是否升版 範例
新增可選欄位 否 回應新增 cardBrand
新增端點 否 GET /api/v1/cards/{id}/tokens
新增必填請求欄位 是 請求新增必填 channelCode
移除欄位 是 移除回應之 legacyId
變更欄位型別 是 string → integer
變更欄位語意 是 amount 由元改為分
變更錯誤碼語意 是 CC-AUTH-1001 由額度不足改為卡片停用

雙軌規則:升版後舊版至少維持一個 release 週期,並於回應標頭回報棄用:
Deprecation: true、Sunset: <date>。

3.3 統一請求與回應格式

請求標頭(必備)

標頭 說明
Authorization Bearer <token> 或 ApiKey <key>
X-Trace-Id 追蹤識別碼(未帶則由閘道產生)
X-Idempotency-Key 寫入操作之冪等鍵(寫入端點必備)
X-Channel 來源通路(WEB/MOBILE/PARTNER/BATCH)
Accept-Language 語系(zh-TW/en-US)

回應信封(統一)

欄位 型別 說明
traceId string 追蹤識別碼
data object | null 成功時之資料
error object | null 失敗時之錯誤
error.code string 錯誤碼(CC-XXX-NNNN)
error.message string 使用者可讀訊息(依 Accept-Language)
error.details array 欄位級錯誤(驗證失敗時)

HTTP 狀態碼對應

狀態碼 語意 使用時機
200 成功 查詢、更新成功
201 已建立 資源建立成功
202 已接受 非同步作業受理
400 請求錯誤 參數驗證失敗
401 未認證 權杖無效或過期
403 無權限 角色不足
404 不存在 資源不存在
409 衝突 冪等鍵重複、狀態衝突、樂觀鎖失敗
422 業務規則不允許 業務規則拒絕(如額度不足)
429 超過速率 限流
500 系統錯誤 未預期錯誤
503 服務不可用 斷路器開啟、維護中

3.4 速率限制

對象 限制 視窗
前端使用者 100 次 分鐘
合作夥伴 API 1,000 次 分鐘
批次匯入 10 次 分鐘
授權端點 依容量規劃(≥ 2,000 TPS) 秒

4. 外部系統介接

外部系統 協定 模式 逾時 重試 斷路器
卡組織(授權) ISO 8583 over TCP 同步 3 秒 0(授權不重試) 是
卡組織(清算) SFTP 檔案 批次 — 3 次 否
徵審系統 REST 同步 5 秒 2 次 是
銀行核心(CIF) REST 同步 3 秒 2 次 是
總帳系統 SFTP + REST 批次 + 同步 10 秒 3 次 是
HSM TCP 專用 同步 1 秒 1 次 是
票證公司 REST / 專用 同步 3 秒 1 次 是
通知通道 REST 非同步 5 秒 3 次 是

4.1 逾時與重試原則

原則 說明
逾時必設 所有外部呼叫須設逾時,不得無限等待
重試須冪等 僅冪等操作可重試;授權類不重試
指數退避 重試間隔 100 ms → 200 ms → 400 ms(含抖動)
斷路器 連續失敗達門檻開啟,半開後試探
降級 外部不可用時之替代行為須明確定義(如 Stand-in)

4.2 Stand-in 代授權(降級)

flowchart TD
  A["授權請求"] --> B{"發卡主機可用"}
  B -->|"是"| C["正常授權流程"]
  B -->|"否"| D["Stand-in 判斷"]
  D --> E{"金額與次數在<br/>代授權限額內"}
  E -->|"是"| F["代授權核准<br/>標記 STAND_IN"]
  E -->|"否"| G["拒絕<br/>主機無回應"]
  F --> H["補正佇列"]
  H --> I["主機恢復後補正"]
  I --> J{"補正結果"}
  J -->|"同意"| K["維持核准"]
  J -->|"拒絕"| L["產生追償紀錄"]

5. 事件治理

5.1 事件信封(統一格式)

欄位 型別 必填 說明
eventId string(ULID) 是 事件唯一識別碼(冪等用)
eventType string 是 <context>.<aggregate>.<verb>.v<major>
eventVersion integer 是 主版本
occurredAt string(ISO 8601) 是 事件發生時間(UTC)
aggregateId string 是 聚合識別碼(Kafka 分區鍵)
sequenceNo integer 是 聚合內序號(順序判定)
traceId string 是 追蹤識別碼
causationId string 否 觸發此事件之命令或事件
correlationId string 否 業務關聯識別碼
payload object 是 事件內容
schemaRef string 是 Schema 版本參照

5.2 事件清單(唯一權威)

★ 本表為全案事件之唯一真實來源(CR-030 §2 #5 裁定 (c))。
SAD-002 §3.x 之「事件(發布)/(訂閱)」欄降為服務視角之指標,衝突時一律以本表為準。

★ 「用途」欄之語意(CR-030 §2 #16 裁定 (b)):本表所列用途一律為「跨服務投影/通知」。
事件不得重做同步呼叫已完成之寫入(SAD-002 §3.x 之「同步呼叫」欄為現行實作)——
否則同一事實將有兩條寫入路徑,必然重複套用(重複佔用/重複入帳)。

事件 生產者 消費者 用途(投影/通知) 生產端 消費端
authorization.authorization.approved.v1 authorization notification 消費通知(AUTH_APPROVED)。⚠️ 本事件之語意為「授權已佔用額度」(decision ∈ {APPROVED, STANDIN, REFERRAL})——見 CR-030 §2 #8 ✅ ✅
limit.limit.adjusted.v1 limit authorization 額度快取投影(account_limit_caches) ✅ ✅
limit.hold.created.v1 limit authorization 佔用投影(limit_holds) ✅ ✅
limit.hold.released.v1 limit authorization 佔用釋放投影 ✅ ✅
card.card.status-changed.v1 card authorization, party 卡片狀態投影(card_status_caches)。★ 兩份投影刻意不共用(CR-031 §2.2):auth 之投影供授權熱路徑、party 之投影供歸戶視圖(GET /accounts/{id}/cards)——跨服務共用讀模型=跨庫耦合(違反 SAD-003 §1 DP-01) ✅ ✅
card.card.issued.v1 card — 卡片生命週期(2026-10-01 補登,CR-030 §2 #5——原僅見於 SAD-002 §3.3) ✅ ⬜
card.card.token.bound.v1 card — Token 綁定(同上補登) ✅ ⬜
product.parameter.changed.v1 product 全體 參數熱更新(各服務參數快取失效)——⚠️ 屬獨立議題(範圍 16 服務),見 CR-029 §5 C-7 ✅ ⬜
posting.transaction.posted.v1 posting campaign, rewards 活動交易認列(CR-046 §7.5,campaign-service 消費已實作 ✅)+ 消費累點(RWD-5.1)——payload 已補 mcc/merchantId/instrumentId(可選;CR-046 AC 11 資料鏈) ✅ 🟡 campaign ✅/rewards ⬜
campaign.reward.commanded.v1 campaign rewards 活動回饋指令派發(CR-046 B4;SCS-026 §7.6)——REWARD_CMDS(PENDING) 經 relay 派發;rewards 以 campcmd:<commandId> 冪等入點(點數計算屬 rewards 權威) ✅ ✅
posting.transaction.reversed.v1 posting rewards 累點回沖(同上) ✅ ⬜
payment.payment.posted.v1 payment notification 繳款通知(PAYMENT_RECEIVED) ✅ ✅
payment.payment.unmatched.v1 payment limit 重新佔用額度(CR-030 §2 #9——真缺陷:逾三日未匹配須回沖已恢復之額度) 🟡 A 批 🟡 A 批
payment.payment.reversed.v1 payment limit 重新佔用額度(沖正同未匹配) 🟡 A 批 🟡 A 批
statement.statement.issued.v1 statement notification + limit 帳單通知(STATEMENT_ISSUED)+ ★ 8-6 繳款截止日之校正(CR-036:以帳單之權威 paymentDueDate 覆寫臨時額度之迄日) ✅ ✅
installment.plan.created.v1 installment — 分期佔用(生產端未實作) ⬜ ⬜
rewards.points.accrued.v1 rewards notification 點數入帳通知——⚠️ 無模板(CR-030 §4.2 待業務文案) ✅ ⬜
rewards.points.redeemed.v1 rewards notification 點數兌換通知——⚠️ 同上 ✅ ⬜
clearing.batch.matched.v1 clearing — 清算比對完成——⚠️ 不做消費端(清算為批次對帳,見 CR-030 §4.1) ✅ —
dispute.case.opened.v1 dispute notification 爭議受理通知——⚠️ 無模板(CR-030 §4.2) ✅ ⬜
merchant.settlement.submitted.v1 merchant — 商戶請款(2026-10-01 補登,CR-030 §2 #5) ✅ ⬜
notification.sent.v1 notification party 通知已遞送(同上補登) ✅ ⬜
authorization.authorization.declined.v1 authorization — ⛔ 刻意未發射(CR-030 §2 #7):拒絕不改變下游狀態,其統計走 metrics 而非事件流。payload 保留於 SDD-006 §4.2,供日後「拒絕率即時看板」需求時啟用 ⛔ —
party.account.created.v1/party.block-code.changed.v1/party.flag.applied.v1 party card(account.created) 歸戶建立——⚠️ 生產端未實作、payload 未定義 ⬜ ⬜

圖例:✅ 已完成|🟡 進行中(本批)|⬜ 未實作|— 不適用/刻意不做

⚠️ 已移除之列:authorization.hold.created.v1/authorization.hold.released.v1
(CR-028 裁定廢除——hold 之權威在 CC_LIM,事件一律由 limit 發布)。

實作進度(2026-10-01):7 條全鏈路(card.status-changed → auth/card.status-changed → party/
limit.adjusted/limit.hold.*/payment.posted → limit、payment.posted → notification、
statement.issued → notification)。
生產端 18 個 cc.* 主題就緒。⚠️ 動工前必讀 CR-027(v1.7)+CR-029(v1.2)+CR-030+CR-031+CR-033。

★ C-5 已結案(CR-033,2026-10-02):CR-029 §5 之 C-5(8 條「語意/讀模型未定義」)
實作 1 條(card.status-changed → party,#A-143)、裁定不做 7 條、待實作 0 條。
★★ 根因:該 8 條係依 CR-030 之前之本表盤點而得;CR-030 §2 #5 將本表重寫為唯一權威時
已移除/改判這些消費邊,惟未回寫 CR-029 → 形成幽靈待辦(詳 CR-033 §3.1)。
教訓:重寫權威清單後,必須回寫「以舊清單為基礎之盤點」。

歷史註記(已由 CR-030 裁定取代,保留供追溯):本節原載「authorization.authorization.approved.v1
僅涵蓋 decision ∈ {APPROVED, STANDIN}——REFERRAL 雖已佔用額度卻不發射任何事件(待裁定)」。
→ CR-030 §2 #8 已裁定:本事件之語意為「授權已佔用額度」,REFERRAL 應發射,並補 decision 欄位。
原載「card.card.issued.v1/token.bound.v1 不在本節(兩份清單不一致,待裁定)」
→ CR-030 §2 #5 已裁定:本表為唯一權威,兩事件已補登於上表。

5.3 事件版本策略

變更 處置
新增可選欄位 不升版
新增必填欄位 升版,雙軌發布 ≥ 1 個 release
移除或改型別 升版,雙軌
消費者 須同時容忍 v1 與 v2(至少一個 release)

5.4 事件消費契約(2026-10-01 補建;GOV-006 4.8 W-2 之第一切片)

緣起:common-event 之生產端(OutboxStore/OutboxPublisher/EventPublisher/EventEnvelope)
早已就位,惟尚無任何服務實際發射或消費事件(2026-10-01 實測:事件型別字串僅出現於 common-event 自身、
全庫無 KafkaListener)。本節先立消費端契約,作為 W-2 之第一切片。

項目 規格
主題命名 cc.<aggregateType>.<eventType>(OutboxPublisher.topicFor)。
★ eventType 之推導(CR-030 §2 #15 裁定 (a)):事件全名去 .vN 後,再移除一層與 aggregateType 相同之首段。
例:card.card.issued.v1 → card.issued → 主題 cc.card.card.issued;dispute.case.opened.v1 → case.opened → cc.dispute.case.opened。
⚠️ 遷移注意:若該主題已有消費者,改名前須雙軌發布(同時寫新舊 topic)≥ 1 release;本專案現階段零消費者,故直接改(CR-030 §3 B 批)
分區鍵 EventEnvelope.aggregateId——同一聚合之事件落同一分區,保證聚合內有序
順序 消費者不得假設全域有序;僅可假設同一聚合內依 sequenceNo 遞增
★ sequenceNo 不變式(CR-030 §2 #12 裁定) 同一 aggregateId 之所有事件型別,須共用同一單調序號來源。
理由:消費者之亂序防護以「sequenceNo <= MAX(aggregateId)」判定——若同一聚合之兩型別序號不同源,後發事件可能因序號較小而被誤判為亂序而略過(靜默資料遺失)。
⚠️ 反例(已修正):card-service 原 issued/token.bound 用時間戳基底序號、status-changed 用卡片版本 → 已統一為單一來源。
⚠️ 已知限制:現行為同 JVM 內遞增之計數器,多實例部署不安全;正解為「聚合層級持久化序號」(每 aggregateId 一計數列、同交易遞增)——列為後續強化項
冪等 至少一次投遞 → 消費者必須以 eventId 去重(持久化於消費端自己的 consumed_events 表,且與業務寫入同交易——否則「已處理」與「已寫入」會分岔)
consumed_events 之共用 該表形狀(event_id/event_type/aggregate_id/sequence_no)與事件型別無關 → 同一服務之所有消費者共用一份,不應為每事件各建一表/一埠(CR-029 批次 C 之實作經驗)
版本 消費者須同時容忍 v1/v2(至少一個 release,§5.3);未知 eventType 一律略過並記 WARN(不拋錯——否則新增事件會毒化消費者)
亂序 sequenceNo 落後於已套用者 → 略過並告警(不覆寫較新狀態)
失敗 不無限重試;以 WARN 記錄,靠 auto-offset-reset=earliest 與人工重放
★ 線格式(CR-049) 生產端一律以 KafkaTemplate<String,String> 發送已序列化之信封 JSON 字串 → producer 之 value-serializer 必須為 StringSerializer。
⚠️ 用 JsonSerializer 會二次編碼(整段信封 JSON 變成一個「字串字面值」)→ 消費端 readTree() 得 TextNode、eventType 恆為 null → 靜默略過、零處理(無異常、無重試、無告警)。
★ 已納入架構關卡 A9(tools/verify_architecture.py):量測全部 value-serializer,非 StringSerializer 即失敗;零量測亦失敗(fail-closed)。
⚠️ 消費端若需巢狀讀取(如 payload.recognition),欄位層級須與生產端一致——CR-049 即為「生產端包在 recognition、消費端讀 payload」導致回沖旗標恆 false 之實例

✅ 生產端(relay)已實作(2026-10-01,#A-118):card-service 於卡片狀態變更時,
於同一交易寫入 outbox(SDD-001 §3.15 形狀),由 OutboxRelayScheduler 週期性
呼叫 common-event 之 OutboxPublisher(KafkaEventPublisher 送 Kafka)。
真機驗證:LOCK → outbox 落列 → relay 發布 → 授權讀模型更新(見 SCS-001 §7.12)。

⚠️ 未竟(誠實揭露):死信佇列(DLQ)、消費延遲指標、事件重放工具尚未實作;
本切片涵蓋「冪等 + 依 sequenceNo 防亂序 + 未知事件略過 + 生產端 relay」。

✅ 推廣至事件 #2(2026-10-01,#A-119):limit.limit.adjusted.v1——
limit-service(同交易寫 outbox + relay)→ Kafka(cc.limit.limit.adjusted)→
authorization-service(冪等 + 亂序防護 → account_limit_caches)。
複用第一切片之 consumed_events 冪等表;account_limit_caches 新增 last_sequence_no。

⚠️ 規格偏差(如實揭露):§5.1 載 eventId 為 ULID,惟本專案實作一律採 UUIDv4
(#A-118/#A-119 同);兩者皆為唯一識別碼、冪等語意不受影響,惟格式與本節文字不一致,
列為待統一項(不擅自改文字以免掩蓋實作現況)。

⚠️ sequenceNo 之實務替代:目前為「同 JVM 內嚴格遞增」之時間戳基底序號
(max(now, last+1));多實例部署仍可能重複,正式應改為聚合層級之持久化序號(見 SCS-002 §7.5)。

★ 真機反駁(2026-10-17,CR-049):campaign → rewards 之全鏈路 E2E 首次揭露——
單元測試、整合測試、契約比對全綠,實機卻「2 筆 SENT、0 筆入點」。根因即上表「線格式」:
campaign 之 producer 仍為 JsonSerializer → 二次編碼 → 消費端 eventType=null 靜默略過。
教訓:此類缺陷只存在於「線上編碼」與「跨服務巢狀層級」,任何以 mock/手工字串為基礎的測試
都結構性看不到。故本專案將「線格式」升為架構關卡 A9,並以
ops/local/e2e/campaign_chain_e2e.sh(32 項斷言)作為跨服務真機驗收之固定手段。


6. 檔案交換

檔案類型 方向 格式 頻率 校驗
卡組織清算檔 雙向 固定長度 / CSV 每日 筆數 + 金額總計 + 檔頭檔尾
請款檔 入 固定長度 即時 / 每日 筆數 + 金額 + 特店合計
自扣媒體檔 出 固定長度 每月 筆數 + 金額
票證加值檔 雙向 專用 每日 筆數 + 金額
報表檔 出 CSV / XLSX 依需求 筆數

6.1 檔案交換保證

保證 機制
完整性 檔頭檔尾含筆數與金額總計,收檔時比對
不重複 檔名含日期與序號;已處理檔案記錄於 file_registry
可重跑 處理具冪等性,重跑不產生重複入帳
可追溯 每次收發留存檔名、筆數、金額、時間、雜湊值
加密 傳輸以 SFTP;敏感檔案內容加密

7. API 清單(彙總)

完整契約見 SDD-006。以下為服務與端點對照。

服務 端點數 代表端點
authorization-service 5 POST /api/v1/authorizations
limit-service 6 POST /api/v1/limit-adjustments
card-service 7 POST /api/v1/cards/{cardId}/lifecycle-transitions
party-service 4 GET /api/v1/accounts/{accountId}
product-service 5 PUT /api/v1/parameters/{ns}/{key}
posting-service 6 POST /api/v1/postings
payment-service 4 POST /api/v1/payments
statement-service 3 POST /api/v1/statement-cycles/{id}/runs
pricing-service 3 POST /api/v1/pricing/interest-calculations
installment-service 3 POST /api/v1/installments
rewards-service 4 POST /api/v1/rewards/redemptions
clearing-service 3 POST /api/v1/clearing/runs
dispute-service 3 POST /api/v1/disputes
merchant-service 3 POST /api/v1/merchants
notification-service 2 POST /api/v1/notifications
report-service 2 POST /api/v1/report-jobs
migration-service 2 POST /api/v1/migration-jobs
合計 65 —

修訂記錄

版本 日期 修訂內容
1.0 2026-09-18 初版:整合樣式、邊緣層架構、API 治理(契約優先/版本策略/統一格式/速率限制)、外部系統介接、事件治理、檔案交換、API 清單彙總
1.1 2026-09-22 依 C-12(api-gateway 實作) 新增 §2.3 閘道路由表(17 服務前綴↔埠對照、RewritePath、屬性命名空間 spring.cloud.gateway.server.webflux.* 之查證註記、edge 認證之 WebFlux/Servlet 分離)與 §2.4 分流(Cycle Split)閘道機制(雜湊桶判定、熱更新與回退、稽核 LOG、影子比對未實作之揭露),含三項實作陷阱實錄。速率限制(§3.4)以 Redis RedisRateLimiter 落地(本機驗證值 2/s/突發 20)
1.2 2026-09-22 依 C-13(bff-web 首增量) 新增 §2.2.1 bff-web 首增量。(★ 本列為 2026-10-01 補記——原版本列已述及 1.2,惟修訂記錄漏列,屬「已做卻漏記」型缺陷。)
1.3 2026-10-01 GOV-006 4.8 W-2 第一切片:新增 §5.4 事件消費契約——主題命名(cc. + 型別去 .v<major>)/分區鍵/順序/冪等(eventId 去重)/版本容忍/亂序略過/不無限重試。(★ 本列為 2026-10-01 補記——同上,修訂記錄漏列。)
1.4 2026-10-01 GOV-006 4.8 W-2 推廣・事件 #2(#A-119):§5.2 加註實作進度(✅ 2/16:card.card.status-changed.v1、limit.limit.adjusted.v1;⬜ 14 待補);§5.4 補事件 #2 之推廣紀錄(limit-service 生產 → authorization-service 消費)。併如實揭露兩項規格偏差:(i) §5.1 載 eventId 為 ULID,實作採 UUIDv4;(ii) sequenceNo 為 JVM 內遞增之時間戳基底序號(多實例可能重複)。
1.8 2026-10-02 CR-033:§5.2 加註「C-5 已結案」——CR-029 §5 之 C-5(#1/#3/#7/#9/#13/#18/#20/#21)逐條查證後:實作 1 條(#3=card.status-changed → party,#A-143)、裁定不做 7 條、待實作 0 條。★★ 揭露根因(幽靈待辦):該 8 條係依 CR-030 之前之本表盤點而得;CR-030 §2 #5 將本表重寫為唯一權威時已移除/改判這些消費邊(authorization.approved.v1 之 posting/payment.posted.v1 之 posting/posting.posted.v1 之 clearing/posting.reversed.v1 之 statement/statement.issued.v1 之 payment/rewards.points.redeemed.v1 之 posting/clearing.batch.matched.v1 之 posting/dispute.case.opened.v1 之 posting),惟未回寫 CR-029。教訓:重寫權威清單後,必須回寫「以舊清單為基礎之盤點」。
1.7 2026-10-01 CR-031 §4.3:BffAccountOverview 新增 cards 區塊(三區塊 → 四區塊)。§2.2.1 之「聚合」列改為四區塊;新增 §2.2.1.1 逐項說明:下游為 party-service 而非 card-service(三項理由:card 無依歸戶列卡片之端點/其 ACL 僅允許 cc-authorization-service/歸戶→卡片清單之讀模型由 party 持有,BFF 應呼叫持有讀模型者);回傳形狀 BffSection(data = CardStatusView[]);降級同通則;不含 PAN(遮罩值)。★ 併誠實揭露:本端點尚無前端頁面消費者(SDD-007 未立對應 §5.4.x)——本區塊之價值在完成「歸戶 360」聚合能力,接頁面前須先補逐鈕定義。真機驗證:bff 8101 → party 8084 → 真實 Oracle 投影,四區塊實測。
1.6 2026-10-01 CR-031 §2(批次 C C-5):card.card.status-changed.v1 之 party 消費端完成。§5.2 該列之消費端狀態 🟡 → ✅;併補註「兩份投影刻意不共用」(auth 供授權熱路徑、party 供歸戶視圖;共用=跨庫耦合,違反 SAD-003 §1 DP-01)。§5.2 實作進度 6 條 → 7 條全鏈路。★ 真機驗證:鎖卡 → 投影落列(INSERT/UPDATE 兩路徑)/端點 200/負樣本(重複 eventId、較舊 sequenceNo)皆正確略過;另 404(歸戶不存在)與空陣列(歸戶存在無投影)語意亦實測。
1.9 2026-10-17 CR-049:§5.4 新增「線格式」契約列——生產端一律以 KafkaTemplate<String,String> 發送已序列化之信封 JSON 字串,producer value-serializer 必須為 StringSerializer;用 JsonSerializer 會二次編碼(整段 JSON 變成字串字面值)→ 消費端 readTree() 得 TextNode、eventType 恆 null → 靜默略過、零處理(無異常、無重試、無告警)。併載明「消費端巢狀層級須與生產端一致」。納入架構關卡 A9(tools/verify_architecture.py;零量測亦失敗)。★ 真機證據:campaign 2 筆 SENT/rewards 0 筆入點;修正後 E2E 32/32。
1.5 2026-10-01 CR-028 hold 事件群四項矛盾之化解:§5.2 刪除 authorization.hold.created.v1/authorization.hold.released.v1 兩列、補登 limit.hold.created.v1(原從未被登錄)。理由(三項客觀證據):(i) 該二事件全庫程式碼零引用(僅文件提及);(ii) authorization.hold.created.v1 之 payload 與 limit.hold.created.v1 逐欄相同——係同一件事的兩個名字;(iii) hold 之權威在 CC_LIM.LIMIT_HOLDS,authorization 對 limit 之佔用請求為同步 API(命令),非事件。程式碼零影響。
(29-29/67)