CR #201 » SAD-004_整合架構與API治理.md
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 |
實作注意(實測取得):
- Spring Cloud 2025.1 之屬性命名空間為
spring.cloud.gateway.server.webflux.routes
(由spring-cloud-gateway-server-webflux 5.0.3之 configuration-metadata 查證)——
舊寫法spring.cloud.gateway.routes會被靜默忽略(不報錯、不路由)。 - 各路由以
RewritePath去前置(/{domain}-api/(?<segment>.*) → /api/$\{segment}),
還原服務內部路徑/api/**。 - 下游 URI 以環境變數覆寫(
CARD_CORE_*_URI),本機預設localhost。 -
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 可重綁) |
⚠️ 實作陷阱(實測取得,勿重蹈):
- 分流過濾器執行於
RewritePath之後(order 10010 > route filter 之 1),
exchange 之 request path 已被 mutate 為內部路徑(/api/...)——
前綴判斷須取GATEWAY_ORIGINAL_REQUEST_URL_ATTR(原始對外路徑)。 - 該屬性之實際型別為
LinkedHashSet(非List)——以Collection<URI>接收
取首筆;直接轉List拋ClassCastException。 - 上述二者皆以**暫時探針(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(命令),非事件。程式碼零影響。 |