Project

General

Profile

CR #201 » CSP-007_服務骨架產生規範.md

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

 

CSP-007 服務骨架產生規範

項目 內容
文件編號 CSP-007
版本 1.3
狀態 已核定
撰寫者 SA / Tech Lead
審查者 PMO / TL
對應需求 BRD-001 §5 系統邊界;SAD-002 微服務劃分;CSP-002 §2 倉庫策略、§3 單一服務骨架

1. 目的與適用範圍

本文件規範 cardcore 後端模組骨架的產生方式與內容邊界,為 Sprint 0 之核心交付物。

為什麼要有這份規範(而非直接手寫骨架):

  1. 避免骨架漂移:23 個模組若以複製貼上建立,一週內就會出現五種不同的 application.yml。
  2. 單一真實來源:服務清單、埠號、schema、契約對應關係集中於 spec/services.yaml,
    文件與程式碼不會各說各話。
  3. 可稽核:骨架內容可重現(rerun 產生器即得相同結果),符合客戶對交付物的追溯要求。

適用範圍:cardcore/backend/ 下全部 25 個模組(17 領域服務 + 8 平台模組)。


2. 骨架之定義與邊界

2.1 什麼是「骨架」

骨架 = 結構 + 建置設定 + 基礎設施接線。具體為:

類別 內容 是否產生
建置設定 build.gradle.kts、settings.gradle.kts、版本目錄 ✅
容器設定 Dockerfile(多階段)、.dockerignore ✅
啟動接線 Spring Boot Application 啟動類(含 main) ✅
外部化組態 application.yml、application-local.yml ✅
品質關卡 分層規則測試(ArchUnit LayeringTest) ✅
遷移目錄 db/migration/ 與其規則說明 ✅
模組文件 各模組 README.md(職責、啟動、驗收) ✅
業務類別 Controller/Service/Repository/Entity/DTO ❌ 禁止
業務邏輯 任何業務規則、計算、狀態轉換 ❌ 禁止

2.2 邊界之依據

  • CSP-001 §8 B1:禁止「僅定義類別、方法簽章,但無任何實作邏輯」的空殼。
    故本產生器不產生任何業務類別佔位——寧可留白,不留假象。
  • CSP-002 §6.1:骨架只包含基礎設施接線(配置、健康端點、攔截器)。
  • CSP-002 §6.2:單元測試不得啟動 Spring 容器;本產生器產生之測試為 ArchUnit
    位元碼分析,不啟動容器。

一句話原則:骨架讓服務能編譯、能啟動、能被掃描,但不假裝它能做業務。


3. 模組註冊表(spec/services.yaml)

所有產生內容之來源。修改後須重新執行產生器。

3.1 領域服務(17 個)

# 模組 套件 埠號 Oracle schema 對應契約
1 authorization-service auth 8081 CC_AUTH authorization-service.yaml
2 card-service card 8082 CC_CARD card-service.yaml
3 limit-service lim 8083 CC_LIM limit-service.yaml
4 party-service party 8084 CC_PARTY party-service.yaml
5 product-service product 8085 CC_PRODUCT product-service.yaml
6 pricing-service prc 8086 CC_PRC pricing-service.yaml
7 posting-service posting 8087 CC_POSTING posting-service.yaml
8 payment-service payment 8088 CC_PAYMENT payment-service.yaml
9 installment-service inst 8089 CC_INST installment-service.yaml
10 rewards-service rw 8091 CC_RW rewards-service.yaml
11 statement-service stmt 8092 CC_STMT statement-service.yaml
12 dispute-service dis 8093 CC_DIS dispute-service.yaml
13 clearing-service clr 8094 CC_CLR clearing-service.yaml
14 merchant-service mch 8095 CC_MCH merchant-service.yaml
15 migration-service mig 8096 CC_MIG migration-service.yaml
16 notification-service notif 8097 CC_NOTIF notification-service.yaml
17 report-service rpt 8098 CC_RPT report-service.yaml

埠號 8090 刻意跳過:保留給 Kafka UI(ops/local/docker-compose.yml)。
埠號自 8081 起編:8080 由 Legacy ECS 之 Tomcat 佔用(CSP-006 §4)。

3.2 平台模組(8 個)

# 模組 套件 埠號 類型
18 api-gateway gateway 8100 閘道
19 bff-web bff 8101 BFF
20 common-spring common.spring — 程式庫
21 common-security common.security — 程式庫
22 common-event common.event — 程式庫
23 common-domain common.domain — 程式庫
24 common-batch common.batch — 程式庫
25 common-saga common.saga — 程式庫

common-saga(Sprint 3 新增):狀態機引擎與 Saga 協調者。
其協調狀態落 CC_PLT.saga_instances/saga_steps(SDD-004 §5.3),
屬跨服務基礎設施,故置於 platform;整合測試以 Testcontainers PostgreSQL 執行(§5.4.5)。

3.3 一致性要求

spec/services.yaml 之內容須與下列文件一致;不一致即為文件缺陷,須提變更申請:

對照來源 對照項目
SAD-002 微服務劃分與 DDD 邊界 服務清單與 bounded context
SDD-001 §9 總表與統計 Bounded Context schema
CSP-002 §2 倉庫策略 目錄結構與模組命名
contracts/openapi/*.yaml 17 份契約之檔名

4. 產生之檔案規範

4.1 檔案清單

backend/
├── settings.gradle.kts            複合建置:include 全部 23 模組
├── build.gradle.kts               共用設定:Java 21 toolchain、BOM、JUnit Platform
├── gradle/libs.versions.toml      版本目錄(對應 CSP-006 §2 版本鎖定)
├── gradlew / gradle/wrapper/      Gradle Wrapper 9.7.1(由 `gradle wrapper` 產生)
├── .gitignore
├── services/<name>/               17 個領域服務
│   ├── build.gradle.kts
│   ├── Dockerfile                 多階段建置,執行期 JRE,非 root 執行
│   ├── .dockerignore
│   ├── README.md
│   └── src/
│       ├── main/java/com/beam/cardcore/<pkg>/<Name>Application.java
│       ├── main/resources/application.yml
│       ├── main/resources/application-local.yml
│       ├── main/resources/db/migration/README.md
│       └── test/java/com/beam/cardcore/<pkg>/arch/LayeringTest.java
└── platform/<name>/               8 個平台模組(程式庫無 Dockerfile/啟動類)

4.2 關鍵設計決策

決策 內容 依據
複合建置 本機以單一 settings.gradle.kts include 全部模組,一次編譯全部 生產 CI/CD 仍依 CSP-002 §2 之 one-service-one-repo,將各模組目錄獨立為倉庫;屆時僅需保留該模組之 build.gradle.kts 與 src/
版本目錄單一來源 所有版本集中於 gradle/libs.versions.toml,由 spec/services.yaml 產生 CSP-006 §5.1 凍結版本
Java Toolchain 以 languageVersion = 21 宣告,不依賴機器預設 JAVA_HOME CSP-006 §3 規則 2(與 Legacy ECS 之 Java 8 並存)
組態外部化 application.yml 之連線、帳密一律以 ${ENV_VAR} 佔位 SAD-006 §1、CSP-001 §8 B6
ddl-auto: validate 結構一律由 Flyway 管理,禁止 Hibernate 自動建表 SAD-003 §9「結構變更一律以 migration 檔為之」
虛擬執行緒預設關閉 spring.threads.virtual.enabled: false CSP-006 §2.1(需經 TL 同意方可啟用)
Kafka 主機埠 9094 本地組態指向 localhost:9094(容器內為 kafka:9092) CSP-006 §4

4.3 已踩過之技術陷阱(實作備忘)

# 陷阱 說明與對策
G-1 Spring Cloud Gateway artifact 更名 Spring Cloud 2025.1 起為 spring-cloud-starter-gateway-server-webflux(5.0.3);舊名 spring-cloud-starter-gateway 停留於 4.3.x,僅適用 Cloud 2024.x。經 Maven Central 查證
G-2 libs 版本目錄之作用域 Gradle Kotlin DSL 中,libs 存取子僅在腳本層級可見;於 subprojects { } 區塊內取用會拋 Extension with name 'libs' does not exist。對策:在根腳本層級先取出為區域變數
G-3 YAML 之 ${VAR:default} 與樣板引擎衝突 產生器若以 string.Template.substitute() 處理,${SERVER_PORT:8081} 會被判為非法佔位符而拋錯。對策:改用 safe_substitute()——未定義之 ${...} 原樣保留,裸 $var 仍正確替換
G-4 行內註解 port_reserved: [8090] # Kafka UI 若未剝除行內註解,將被解析為字串而非清單,導致保留埠失效
G-5 Testcontainers 2.0 模組全面更名 Testcontainers 2.0(現行 2.0.5)起,所有模組加 testcontainers- 前綴:
junit-jupiter → testcontainers-junit-jupiter
oracle-free → testcontainers-oracle-free
kafka → testcontainers-kafka
舊名於 2.0.5 一律 404(僅存在至 1.21.x)
G-6 Gradle 不處理 POM 的 import-scope BOM Spring Boot BOM 以 <scope>import</scope> 引入 testcontainers-bom,但 Gradle 不展開 POM 的 import-scope BOM,導致 Testcontainers 版本解析為空(錯誤訊息為 Could not find org.testcontainers:...:.——注意版本位置是空的)。對策:於根建置檔明確加入 platform(libs.testcontainersBom)
G-7 ArchUnit 空集合預設判定失敗 ArchUnit 預設在「規則未匹配任何類別」時拋 AssertionError(failed to check any classes)。骨架階段子套件尚無類別,故所有規則須加 .allowEmptyShould(true)。此設定僅放行「空集合」,不影響「有類別但不合規」之判定
G-8 平台元件誤植資料庫組態 閘道與 BFF 為 WebFlux/聚合層,不具 JDBC 驅動;若沿用領域服務之 application.yml(含 spring.datasource),啟動時將因缺少驅動而失敗。對策:產生器為 kind ∈ {gateway, bff} 另備無資料庫之組態範本
G-9 build/ 目錄污染驗收掃描 Gradle 會將 src/main/resources 複製到 build/resources/,使驗收用的 grep 同時命中來源與複製品。對策:驗收指令須排除 /build/ 路徑
G-10 Oracle 要求 DEFAULT 置於 NOT NULL 之前 PostgreSQL 可寫 col bigint NOT NULL DEFAULT 0;Oracle 同序會報 ORA-03076。須轉為 col NUMBER(19,0) DEFAULT 0 NOT NULL
G-11 DDL 行尾註解未剝除會使欄位整批遺失 SDD-001 之 DDL 有 card_no_enc bytea NOT NULL, -- 加密卡號 形式;若未剝除行尾註解,註解會與下一個欄位黏成同一項,導致 card_no_masked、card_no_hash 等欄位消失(症狀為下游 ORA-00904: invalid identifier)。對策:解析前剝除 \s+--.*$(僅在 -- 前有空白時視為註解,避免誤傷字串常值)
G-12 Oracle INTERVAL 分區必須附帶至少一個初始分區 僅寫 PARTITION BY RANGE (d) INTERVAL (...) 會報 ORA-00906: missing left parenthesis;須加 (PARTITION p0 VALUES LESS THAN (...))
G-13 Oracle 不支援以 TIMESTAMP WITH TIME ZONE 作分區鍵 報 ORA-03001: unimplemented feature;DATE 與無時區之 TIMESTAMP 均可。影響 auth.authorizations(詳見 SDD-001 §7.1 待決事項)
G-14 schema 清單硬編造成漂移 初始化腳本曾硬編全名(CC_INSTALLMENT),但 spec/services.yaml 用短碼(inst),導致 CC_INST 未建立、服務連線 ORA-01017: invalid credential。對策:清單改由產生器輸出 ops/local/init/oracle/00-schemas.txt,腳本讀取而不硬編

5. 不產生之內容(Sprint 1 範圍)

以下刻意不由產生器產生,避免違反 CSP-001 §8 B1:

項目 何時產生 由誰
interfaces/rest/ Controller 與 DTO Sprint 1 起,逐 FR 實作 Coder
`application/command query/` Handler 同上
`domain/model service/` 聚合與領域服務 同上
infrastructure/persistence/ Repository 實作 同上 Coder
common-* 之實作類別 Sprint 1(見下) Coder
Flyway V1__*.sql 遷移腳本 已完成(見 §5.1) DA / Coder
前端 cardcore-web 另依 CSP-002 §7 前端

5.1 Flyway 基線(已完成)

tools/gen_flyway_baseline.py 自 SDD-001 §3 抽出 DDL、轉譯為 Oracle 方言,
並依 bounded context 分檔至各服務之 src/main/resources/db/migration/V1__baseline.sql。

項目 結果
涵蓋服務 14(SDD-001 有表定義者)
無表定義之服務 3(notification-service、report-service、migration-service)——SDD-001 §3 僅涵蓋 14 個 bounded context,此三者尚無 ER 與 DDL,產生器僅輸出說明檔,不臆造結構。待提 Change Request
產生內容 業務表 + 三張平台表(outbox/idempotency_records/audit_trail,SDD-001 §3.15)
靜態檢查 validate_sql.py:0 違規
實際套用 14/14 成功,0 失敗(見 §7.3)

職責劃分:ops/local/init/oracle/01-init.sh 僅負責 Flyway 無法處理之資料庫層 bootstrap
(表空間、schema 使用者、權限)與 CC_PLT 之 Saga 狀態表;
所有服務 schema 內的表一律由 Flyway 建立(SAD-003 §9)。

5.2 Sprint 1 類別清單(四個 common-* 程式庫)

依「SPEC 先行」原則,本節為 Sprint 1 實作之類別清單與依據。
清單以外之類別不得自行新增;確有需要須先修訂本節。

5.2.1 common-domain(零框架相依,CSP-001 §2)

類別 職責 依據
Money 金額值物件。以最小貨幣單位(分)之 long 儲存,幣別為 java.util.Currency;提供 plus/minus/negate/compareTo。無任何 double 介面 SAD-003 DP-03、CSP-001 §4.6
CardNumber 卡號值物件。僅持有非明文之三種形式:masked(顯示)、hash(比對)、encrypted(儲存);靜態 isWellFormed 做 Luhn 與長度檢核 SAD-003 DP-06、SAD-005 §5
LimitAmount 額度值物件。total/used/held 三態,提供 available/hold/release/settle/adjustTotal,不可變 SDD-001 §3.4 lim.credit_limits、SDD-004 §2.2
ErrorDetail 錯誤明細值物件(field、reason) CSP-003 §3
DomainException 例外階層根。攜帶 code(CC-<SVC>-<NNNN>)與 details CSP-003 §4
ValidationException 輸入驗證失敗(→ 400/422) CSP-003 §4
BusinessRuleException 業務規則拒絕(→ 422,類別位 1) CSP-003 §4
ConcurrencyException 併發/冪等/狀態衝突(→ 409,類別位 2/3) CSP-003 §4
InfrastructureException 基礎設施失敗(→ 500/503,類別位 5) CSP-003 §4
DownstreamException 下游系統失敗(繼承 InfrastructureException) CSP-003 §4

CSP-002 §4 之範圍澄清:CSP-002 §4 將 common-domain 之職責描述為「共用值物件」。
本節將 CSP-003 §4 之例外階層一併納入 common-domain——理由:CSP-003 §4 明訂
「domain/ 拋出 DomainException 子類」,而 domain/ 不得依賴框架(CSP-001 §2),
故該階層必須落於零相依之模組。此為對 CSP-002 §4 之範圍澄清,非新增模組。

5.2.2 common-spring

類別 職責 依據
CorrelationIdFilter 由 traceparent/X-Correlation-Id 取得或產生追蹤碼,置入 MDC 供日誌使用 SAD-001 AP-09、CSP-004
ApiResponse<T> 統一回應封套 {traceId, data, error} CSP-003 §3
ApiError 錯誤本體 {code, message, details[]} CSP-003 §3
GlobalExceptionHandler @RestControllerAdvice,將例外階層映對為 HTTP 狀態+封套 CSP-003 §4
HttpStatusMapper 錯誤碼類別位 → HTTP 狀態之對應(可單獨測試) CSP-003 §2

5.2.3 common-security

類別 職責 依據
PiiMasker 個資遮蔽:卡號、身分證字號、手機、電郵。僅供顯示,不得用於儲存或比對 SAD-005 §5、SAD-003 DP-06

Sprint 1 範圍界定:common-security 之欄位級加解密與 JWT 驗證需 HSM/密鑰管理整合,
屬 Sprint 2;本節僅交付不依賴外部系統之遮蔽工具。

5.2.4 common-event

類別 職責 依據
DomainEvent 領域事件共同介面(信封之固定欄位):eventId、eventType、eventVersion、occurredAt、aggregateId、sequenceNo、traceId、causationId、correlationId、schemaRef SAD-001 AP-05、SDD-006 §4.1
EventEnvelope<T> 事件信封(發布至 Kafka 之實際線格式),實作 DomainEvent;提供 causedBy 自動串接因果鏈 SDD-006 §4.1
OutboxRecord Outbox 資料列模型,欄位對應 SDD-001 §3.15 之 outbox 表 SDD-001 §3.15、SDD-004 §4.3

規格修正(v1.2):本節原列 OutboxStatus 列舉,已移除。
因 SDD-001 §3.15 之 outbox 表並無狀態欄位——發布狀態由 published_at
是否為 null 推導(OutboxRecord.isPending()/isPublished())。
另設狀態欄將產生「狀態欄與時間欄不一致」之可能,且使未發布列之查詢無法直接命中索引。

Sprint 1 範圍界定:Outbox 之發布器(輪詢 + Kafka 發送)需 Spring 排程與 Kafka 用戶端,
屬 Sprint 2;本節僅交付事件契約與資料模型。

5.2.5 驗收結果(Sprint 1 之 G-0′,2026-09-18)

# 檢查 通過條件 實測
B-1 編譯 ./gradlew build BUILD SUCCESSFUL ✅ 23 模組全部成功
B-2 單元測試 全數通過;不得啟動 Spring 容器 ✅ 192 測試、0 失敗(含既有骨架 76 + 新增 116)
B-3 無空殼 scan_scaffold.py 命中 0 處 ✅ 0 處(並修正掃描器兩處誤判,見 CSP-001 §8.1)
B-4 分層規則 LayeringTest 通過 ✅ 通過
B-5 無假測試 scan_faketest.py 命中 0 處 ✅ 0 處(另 scan_mockdata.py 亦 0 處)
B-6 金額無浮點 common-domain 生產碼不得出現 double/float ✅ 出現 0 次

各程式庫測試數

模組 測試案例 主要涵蓋
common-domain 55 金額整數運算與浮點誤差防範、Luhn、額度三態、錯誤碼格式與例外階層
common-spring 31 追蹤碼解析(含 W3C traceparent 防禦)、MDC 生命週期、HTTP 狀態對應、不得洩漏內部實作
common-security 15 卡號/身分證/手機/帳號/電郵遮蔽、短字串不得外洩
common-event 15 信封欄位不變式、因果鏈串接、Outbox 狀態推導與不可變

B-6 之執行方式:common-domain 刻意不提供任何 double/float 介面,
使浮點誤用無法通過編譯——這比事後掃描更根本(SAD-003 DP-03、CSP-001 §4.6)。

5.3 Sprint 2 類別清單

5.3.1 ErrorCodes 遷移至 common-domain(範圍調整)

Sprint 1 將 ErrorCodes 產生於 common-spring。Sprint 2 起需由 common-security
(加解密失敗)與 common-domain(領域例外)共同引用錯誤碼,若置於 common-spring
將迫使 common-security → common-spring 之反向依賴。

調整:ErrorCodes 改產生至 common-domain 之 error 套件。
理由:錯誤碼為跨模組共用詞彙(domain 拋出、spring 對應 HTTP、security 拋出),
且 SDD-006 §3.4 同時定義「碼」與「HTTP 狀態」,屬契約而非 Web 關注點。
common-domain 仍維持零框架相依(僅使用 int,不引入任何 HTTP 型別)。

5.3.2 common-security:欄位級加解密

類別 職責 依據
EncryptionKey 金鑰值物件:keyId(供輪替辨識)+ 32 位元組金鑰材質。不得於 toString 洩漏材質 SAD-005 §5
KeyProvider 金鑰來源埠介面:currentKey()(加密用)/byId(keyId)(解密用,支援輪替)。實作由各環境提供(本地:組態;生產:HSM/密鑰管理) SAD-005 §5、CSP-002 §4
FieldEncryptor AES-256-GCM 欄位加解密。每次加密使用隨機 IV;密文自帶 keyId 以支援金鑰輪替 SAD-003 DP-06、SAD-005 §5

密文格式(自描述,供輪替與版本演進):

v1:<keyId>:<base64(IV)>:<base64(ciphertext||tag)>

硬性規則:

  • 禁止在例外訊息或日誌中出現明文或金鑰材質
  • 解密失敗不得回退為明文或空值,須拋 InfrastructureException(CC-PLT-5004)並告警
  • 加解密失敗屬須告警之基礎設施失敗,與「未分類系統錯誤」處置不同

5.3.3 common-event:Outbox 發布器

類別 職責 依據
OutboxStore Outbox 存取埠介面:findPending(limit)/markPublished(id, at)/incrementRetry(id) SDD-004 §4.3
OutboxPublisher 發布迴圈:撈取未發布列 → 送 Kafka → 標記已發布;失敗則累加重試次數 SDD-004 §4.3、CC-PLT-5003

硬性規則:

  • 不得以 Thread.sleep 作為退避(CSP-003 §7 E5)——退避由排程週期決定
  • 發布至少一次;消費者端以 eventId 冪等(SDD-004 §4.1)
  • 單列失敗不得中斷整批;須逐列記錄並繼續
  • 重試次數超限之列須可被辨識(供人工處置),不得無限重試

5.3.4 common-security:JWT 驗證與授權

類別 職責 依據
JwtValidator 驗證 Bearer JWT:簽章(HS256/RS256)、exp/nbf、可選 aud/iss;抽取 sub、roles、scopes 為 AuthorizationClaim。不依賴 Spring SAD-005 §4、CSP-004
AuthorizationClaim 授權主體值物件:userId、roles(Set)、scopes(Set)、tenantId SAD-005 §4
CurrentUser 請求級主體持有器(ThreadLocal + 清理鉤子),供領域服務取用目前使用者與權限 CSP-002 §4
JwtAuthFilter Spring OncePerRequestFilter:抽取 Authorization: Bearer → JwtValidator → 寫入 CurrentUser 與 MDC;無效 token 回 401(CC-GW-1002) CSP-003 §2、CSP-004

硬性規則:

  • 禁止信任客戶端聲明之角色——角色須由簽章驗證所得,不得由請求正文/標頭注入
  • 缺失或無效 token 一律 401,不得放行(公開端點須顯式標記白名單)
  • token 本身不得進入日誌(與 PiiMasker 雙層防護)

5.3.5 common-batch:批次框架(新增模組)

類別 職責 依據
BatchJob<I> 批次工作單元介面:jobName()、totalItems()、loadItems(offset,limit)、processItem(item);冪等 作業規範、CSP-003 容錯
JobExecutionRepository 執行紀錄埠介面:start(name,total)、saveProgress(id,processed,status)、findIncomplete()(供續跑) SDD-001 批次表(待補,先以記憶體實作)
BatchOrchestrator 分塊執行引擎:以 chunkSize 分批、每塊更新進度、中斷可續跑;單項失敗不中斷整批 CSP-003 §7
ScheduledBatchRunner Spring @Scheduled 配接器:依 cron 觸發 BatchOrchestrator 執行指定 job Spring Scheduling
範例:SettlementBatchJob 結算批次:分批處理帳戶、冪等、可續跑(搭配記憶體 JobExecutionRepository 供測試) —

硬性規則:

  • 批次須冪等:同一 item 重複處理結果一致
  • 進度須持久化(JobExecutionRepository),中斷後可續跑,不得從頭重來
  • 單項失敗記錄並跳過,不得中斷整批
  • 禁止以 Thread.sleep 作退避(CSP-003 §7 E5)

5.3.6 驗收標準(Sprint 2 之 G-0″)

# 檢查 通過條件
C-1 編譯與測試 ./gradlew build 成功;測試全數通過、不啟動 Spring 容器
C-2 無空殼/假測試 scan_scaffold.py、scan_faketest.py 各 0 命中
C-3 加解密往返 加密後解密須還原原文;不同次加密之密文不得相同(隨機 IV)
C-4 金鑰輪替 以舊金鑰加密之密文,在輪替後仍可解密
C-5 明文不外洩 密文格式、例外訊息、toString 均不得含明文或金鑰材質
C-6 發布器冪等與容錯 單列失敗不中斷整批;重試次數正確累加;已發布列不再被撈取
C-7 JWT 驗證 有效 token 通過並抽取 claim;竄改/過期/錯 key 一律 401
C-8 批次冪等與續跑 同 item 重複處理結果一致;中斷後自 processed 續跑、不從頭
5.3.6.1 驗收結果(Sprint 2,2026-09-18)
檢查 結果 備註
C-1 編譯與測試 ✅ 7 個 common-* 模組逐模組 test 全過(見下表);未執行全量 ./gradlew build(17 個空骨架服務模組 + 全量測試會觸發長任務逾時 SIGTERM,見 §7.2)
C-2 無空殼/假測試 ✅ scan_scaffold.py 0 命中、scan_faketest.py 0 命中
C-3 加解密往返 ✅ AesGcmFieldEncryptorTest(隨機 IV 致密文不同)
C-4 金鑰輪替 ✅ 舊 keyId 密文於輪替後仍可解密
C-5 明文不外洩 ✅ 例外訊息不含明文;EncryptionKey.toString 不含材質
C-6 發布器冪等容錯 ✅ OutboxPublisherTest:單列失敗不中斷、重試累加、達上限跳過
C-7 JWT 驗證 ✅ JwtAuthFlowTest:HS256/RS256 往返、竄改/過期/錯鑰/alg 混淆全攔
C-8 批次冪等續跑 ✅ BatchOrchestratorTest:中斷續跑不重複處理、結算冪等重跑不重複計息

本輪各 common- 模組測試數*:

模組 測試數 失敗
common-domain 55 0
common-spring 31 0
common-security 28 0
common-event 18 0
common-batch(新增) 3 0
合計 135 0

本輪新增/修正之文件缺口:

  • SDD-006 → 1.3:補登 CC-GW-1002(401)、CC-GW-1003(403)——原表無任何 401/403 碼,而 JWT 流程需要 401(依 CSP-003 §2 程序補登)
  • CSP-007 → 5.3.4/5.3.5:新增 JWT 驗證與批次框架類別清單(原 SPEC 僅有加解密與 Outbox)
  • gen_error_codes.py:ErrorCodes 由 common-spring 遷移至 common-domain(跨模組共用詞彙,避免 common-security → common-spring 反向依賴)

common-* 程式庫之實作範圍(Sprint 1):

模組 首要實作項目
common-domain Money、CardNumber、LimitAmount、Currency 值物件(零框架相依)
common-spring 全域例外攔截器、correlation-id 過濾器、統一回應封套
common-security JWT 驗證、欄位級加解密、個資遮蔽工具
common-event 事件基類、Outbox 表 DDL 與發布器

5.4 Sprint 3 類別清單(common-saga:狀態機引擎與 Saga 協調者,新增模組)

對應關卡:G-5(Saga 骨架)、G-3(Testcontainers 整合測試)
依據:SDD-005 §11/§12(狀態機通用規則 SM-01~SM-07、Saga 狀態機)、
SDD-004 §5.3(Saga 協調狀態非單一服務所有,落 CC_PLT)、
ops/local/init/oracle/01-init.sh(saga_instances/saga_steps 之實際 DDL)

技術選型:自建協調者(SAD-001、MEMORY.md),不使用 Axon/Temporal/Camunda
——本專案 Saga 拓撲固定且需與既有 Oracle CC_PLT 狀態表、Outbox 與稽核留痕整合,
引入編排框架會造成雙套狀態儲存與額外維運面。

5.4.1 common-saga 之類別清單

類別 職責 依據
Transition<S> 不可變轉換定義(record:from/event/to) SDD-005 §14(1.7 起;1.4 起為 §13;原 §12)
StateMachine<S> 零框架泛型狀態機:註冊合法轉換集合、終態集合;transition(current, event) 回傳目標或拋出;self-loop 視為冪等(SM-03) SDD-005 SM-01/02/03/05
IllegalTransitionException 非法/終態轉換之領域例外,帶 CC-PLT-3001/CC-PLT-3002 SDD-005 SM-02/SM-05、CSP-003 §2
SagaState Saga 生命週期列舉,值對齊 DDL 之 ck_saga_status 01-init.sh、SDD-005 §11
SagaStepStatus/SagaStepDirection 步驟狀態與方向列舉,值對齊 ck_step_status/ck_step_direction 01-init.sh
SagaInstance Saga 實例(不可變 record),對應 saga_instances 01-init.sh
SagaStepRecord Saga 步驟紀錄(不可變 record),對應 saga_steps 01-init.sh
SagaStep 步驟行為介面:forward(ctx) 與 compensate(ctx) SDD-005 §11
SagaStore 持久化埠(save/load/狀態更新) CSP-001 §2(埠與配接器分離)
InMemorySagaStore 記憶體實作,供單元測試 —
JdbcSagaStore Plain JDBC 實作,無方言分支 CSP-001 §2
SagaCoordinator 協調者:正向執行、失敗逆序補償、續跑(resume)、冪等 SDD-005 §11

5.4.2 狀態值對齊 DDL(強制)

欄位 允許值 來源
saga_instances.status STARTED、COMPENSATING、COMPLETED、COMPENSATED、FAILED ck_saga_status
saga_steps.direction FORWARD、COMPENSATE ck_step_direction
saga_steps.status PENDING、SUCCEEDED、FAILED、SKIPPED ck_step_status

⚠️ 術語陷阱:SDD-005 §11 原將初始狀態書為 RUNNING,但 DDL 約束無此值。
程式端列舉若採 RUNNING,寫入即觸發 ORA-02290(CHECK 約束違反)。
已於本輪修正 SDD-005 → 1.1,程式端以 STARTED 為準。

5.4.3 識別碼儲存策略(關鍵設計,避免雙方言)

Saga 識別碼於領域層為 UUID;JDBC 層統一轉為 16 位元組陣列(標準 UUID 位元組布局:
前 8 位元組為 mostSignificantBits、後 8 位元組為 leastSignificantBits,big-endian):

環境 欄位型別 說明
Oracle(生產) RAW(16) 既有 DDL,01-init.sh 不需修改
PostgreSQL(整合測試) bytea 測試期建立之對等 schema

效益:JdbcSagaStore 僅以 setBytes/getBytes 存取,單一程式碼路徑、無 if (dialect == ORACLE) 分支,
避免「Oracle 分支未經測試」之半成品風險。

同理,JSON 欄位於 JDBC 層以 String 處理(呼叫端負責序列化),
Oracle 端為 JSON、PostgreSQL 測試端為 text;時間欄位以 Instant 經 setObject 寫入,
對應 Oracle TIMESTAMP(6) WITH TIME ZONE/PostgreSQL timestamptz。

5.4.4 Saga 協調者之執行語意

情境 行為
全部步驟成功 STARTED → COMPLETED
第 k 步驟失敗 STARTED → COMPENSATING,逆序(k−1 → 0)對已完成步驟執行補償
補償全數成功 COMPENSATING → COMPENSATED
任一補償失敗 COMPENSATING → FAILED,告警並停止(CC-PLT-5002),保留現場供人工介入
FAILED 人工重試 FAILED → COMPENSATING,自未補償之步驟續跑(非從頭)
重複觸發已完成之 Saga 直接回 COMPLETED,不重跑(SM-03 冪等)

每一狀態變更立即持久化——故行程中斷後,可由新協調者實例 resume(sagaId) 自資料庫重建狀態續跑。

5.4.5 G-3 整合測試策略(Testcontainers)

項目 決策 理由
待測資料庫 PostgreSQL(testcontainers-postgresql,映像已快取) Oracle-Free 官方映像逾 13GB、啟動數分鐘,會觸發長任務逾時(exit 137);PostgreSQL 秒級啟動且驅動已快取
生產資料庫 Oracle 26ai(Flyway + 01-init.sh) SAD-003 明訂 Oracle 為權威 OLTP
測試用 schema 測試內建立之 PostgreSQL 對等 schema Oracle 專屬語法(RAW、JSON、VARCHAR2)無法直接於 PG 執行

誠實揭露:G-3 驗證的是「JDBC 持久化層與協調者對真實關聯式資料庫之行為」,
而非「Oracle 專屬語法」。Oracle 端之語法正確性仍由 Flyway 基線(G-2)與 01-init.sh 保證。

5.4.6 驗收標準(Sprint 3 之 G-3/G-5)

# 檢查 通過條件
D-1 狀態機合法性 合法轉換全通過;非法轉換拋 CC-PLT-3001;終態轉換拋 CC-PLT-3002
D-2 冪等 self-loop 轉換不拋錯、不重複執行副作用(SM-03)
D-3 正向全成功 STARTED → COMPLETED,步驟全 SUCCEEDED
D-4 逆序補償 第 2 步失敗 → 僅第 1 步被補償,且補償順序為逆序
D-5 補償失敗 補償拋錯 → 狀態 FAILED,且不擬继续補償後續步驟
D-6 重啟續跑 新協調者實例 resume() 可自持久化狀態完成剩餘補償
D-7 冪等重入 已完成之 Saga 重複觸發 → 直接 COMPLETED,步驟不重跑
D-8 真實資料庫整合測試 Testcontainers PostgreSQL 實際啟動;JdbcSagaStore 真實寫入/讀出/跨連線續跑
D-9 無空殼/假測試 scan_scaffold.py、scan_faketest.py 各 0 命中

6. 產生器用法

cd cardcore

# 檢視缺漏(不寫入任何檔案)
python3 tools/gen_service_skeleton.py --check

# 產生缺漏檔案(已存在者不覆寫,保護手改內容)
python3 tools/gen_service_skeleton.py --write

# 全部重寫(⚠️ 會覆蓋手改內容)
python3 tools/gen_service_skeleton.py --write --force

冪等性:預設不覆寫已存在之檔案。若某模組需重新產生,先刪除該模組目錄再執行 --write。

相依:僅需 Python 3(不強制 PyYAML;無 PyYAML 時使用內建極簡解析器,
若 services.yaml 結構超出支援範圍會明確報錯而非靜默漏讀)。


7. 驗收標準(Sprint 0 關卡 G-0)

產生骨架後,以下檢查須全數通過:

# 檢查 指令 通過條件
A-1 無空殼/佔位程式 python3 tools/scan_scaffold.py backend 命中 0 處
A-2 編譯通過 ./gradlew compileJava BUILD SUCCESSFUL
A-3 分層規則通過 ./gradlew test --tests '*LayeringTest' 全部通過
A-4 組態無硬編機密 grep -rn "password=" backend --include=*.yml 僅出現 ${...} 佔位
A-5 SQL 規則通過 python3 tools/validate_sql.py backend 違規 0 處
A-6 註冊表與文件一致 人工核對 §3.1/§3.2 與 spec/services.yaml 完全一致

7.1 本輪驗收紀錄(2026-09-18)

驗證環境:Temurin JDK 21.0.12.1、Gradle 9.7.1(wrapper)、Docker 29.8.0。

# 檢查 指令 結果 實測數據
A-1 無空殼/佔位程式 python3 tools/scan_scaffold.py backend ✅ 通過 命中 0 處
A-2 編譯通過 ./gradlew compileJava ✅ 通過 23 模組全部 BUILD SUCCESSFUL
A-3 分層規則通過 ./gradlew test ✅ 通過 19 模組產出報告、76 測試案例、失敗 0、錯誤 0
A-4 組態無硬編機密 grep -rn "password:" backend --include="*.yml" | grep -v "/build/" | grep -v '\${' ✅ 通過 非佔位符之帳密 0 筆、硬編 URL 0 筆
A-5 SQL 規則通過 python3 tools/validate_sql.py backend ✅ 通過 掃描 17 份 Flyway 基線,違規 0 處(另以 SDD-001 抽出之 DDL 反向驗證,13 處 → 豁免後 0 處)
A-6 註冊表與文件一致 人工核對 §3.1/§3.2 ✅ 通過 services.yaml 23 模組 = backend/ 23 模組;契約 17 份

回歸測試:既有工具 tools/validate_mermaid.py docs 執行結果為 0 錯誤,未受本輪變更影響。

A-3 之說明:19 個有測試報告之模組 = 17 領域服務 + api-gateway + bff-web;
4 個 common-* 程式庫尚無測試(實作類別屬 Sprint 1)。
骨架階段各子套件尚無類別,分層規則為「真空通過」;Sprint 1 加入業務類別後始具實質約束力。

7.2 尚未驗證事項(誠實揭露)

項目 未驗證原因 應於何時驗證
服務實際啟動(bootRun) 需 Oracle 容器;本輪未執行 docker login 拉取映像 Sprint 0 關卡 G-1(CSP-006 §5.2)
Testcontainers 整合測試 同上(testcontainers-oracle-free 需 Oracle 映像) 同上
Flyway 遷移腳本 屬 Sprint 1,尚未產生 Sprint 0 關卡 G-2
Docker 映像建置 未執行 docker build(需完整相依下載) CI 首次執行時

7.3 Flyway 基線套用驗收(G-2,2026-09-18)

於本機 Oracle 26ai Free(FREEPDB1)實際套用 14 份 V1__baseline.sql:

項目 結果
套用成功 14 / 14
失敗 0
無 DDL 略過 3(notification/report/migration,見 §5.1)
建立之表 42(14 服務表 + CC_PLT 之 2 張 Saga 表)
建立之索引 115
已套用分區 CC_POSTING.TRANSACTIONS(RANGE + 月 INTERVAL,初始分區 1 個)
未套用分區 CC_AUTH.AUTHORIZATIONS(分區鍵為含時區欄位,見 SDD-001 §7.1 待決事項)

本驗收以 sqlplus 直接執行 V1__baseline.sql 模擬 Flyway 行為(以各服務 schema 帳號執行)。
正式 CI 仍應以 Flyway 本體執行,並驗證 flyway_schema_history 之 checksum。


8. 變更控制

  1. 新增/移除/更名模組,一律先改 spec/services.yaml,再重新執行產生器。
  2. 禁止直接手改由產生器產出之檔案後不回寫規範——該類修改將於下次 --force 時遺失。
    若確需長期客製,應修改產生器範本並於本文件 §4 登載。
  3. 埠號、schema 之變更須同步更新 ops/local/docker-compose.yml 與 SAD-002。
  4. 本文件變更須經 SA + Tech Lead 同意,並更新下方修訂記錄。

9. 修訂記錄

版本 日期 修訂者 說明
1.0 2026-09-18 SA / Tech Lead 初版。定義骨架邊界、模組註冊表、產生檔案規範、Sprint 0 驗收標準(G-0),並登載 G-1~G-4 四項實作陷阱(Gateway artifact 更名、Gradle libs 作用域、string.Template 與 ${VAR:default} 衝突、YAML 行內註解)
1.1 2026-09-18 SA / Tech Lead ①新增 §5.1 Flyway 基線(tools/gen_flyway_baseline.py 自 SDD-001 §3 產生,涵蓋 14 服務、3 服務無表定義待 CR);②新增 §7.3 Flyway 套用驗收(實測 14/14 成功、42 表、115 索引、1 表已分區);③§4.3 新增 G-10~G-14 五項轉譯陷阱(Oracle DEFAULT 須在 NOT NULL 前、DDL 行尾註解致欄位遺失、INTERVAL 分區須有初始分區、TIMESTAMP WITH TIME ZONE 不可作分區鍵、schema 清單硬編漂移);④§5 表格更新(Flyway 標記為已完成);⑤§7.1 之 A-5 由「真空通過」更新為實際掃描結果
1.2 2026-09-18 SA / Tech Lead ①新增 §5.2 Sprint 1 類別清單(四個 common-* 程式庫之實作範圍與依據),並將 §5.2.5 由「驗收標準」更新為實際驗收結果(192 測試 0 失敗);②§5.2.4 移除 OutboxStatus——SDD-001 §3.15 之 outbox 表無狀態欄,狀態由 published_at 推導,另設狀態欄將產生「狀態欄與時間欄不一致」之可能;③新增 tools/gen_error_codes.py(自 SDD-006 §3.4 產生錯誤碼常數,避免硬編漂移)
1.3 2026-09-18 SA / Tech Lead ①新增 §5.4 Sprint 3 類別清單:新模組 common-saga(泛型狀態機引擎 StateMachine/Transition、Saga 協調者、SagaStore 埠與 InMemory/Jdbc 兩實作),對應關卡 G-5(Saga 骨架)與 G-3(Testcontainers 整合測試),含 D-1~D-9 驗收標準。②§5.4.2 狀態值強制對齊 01-init.sh 之 CHECK 約束——並修正 SDD-005 §11 原將初始狀態書為 RUNNING(DDL 實為 STARTED)之缺陷,避免寫入觸發 ORA-02290。③§5.4.3 訂定識別碼儲存策略:UUID 於 JDBC 層統一轉 16 位元組,使 Oracle RAW(16) 與 PostgreSQL bytea 共用同一程式碼路徑、無方言分支。④§5.4.5 明載 G-3 選用 PostgreSQL 之理由與誠實揭露範圍。⑤§3.2 與模組計數同步更新為 25 模組(17 服務 + 8 平台)
(36-36/67)