CR #201 » CSP-007_服務骨架產生規範.md
CSP-007 服務骨架產生規範
| 項目 | 內容 |
|---|---|
| 文件編號 | CSP-007 |
| 版本 | 1.3 |
| 狀態 | 已核定 |
| 撰寫者 | SA / Tech Lead |
| 審查者 | PMO / TL |
| 對應需求 | BRD-001 §5 系統邊界;SAD-002 微服務劃分;CSP-002 §2 倉庫策略、§3 單一服務骨架 |
1. 目的與適用範圍
本文件規範 cardcore 後端模組骨架的產生方式與內容邊界,為 Sprint 0 之核心交付物。
為什麼要有這份規範(而非直接手寫骨架):
-
避免骨架漂移:23 個模組若以複製貼上建立,一週內就會出現五種不同的
application.yml。 -
單一真實來源:服務清單、埠號、schema、契約對應關係集中於
spec/services.yaml,
文件與程式碼不會各說各話。 - 可稽核:骨架內容可重現(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-jupiteroracle-free → testcontainers-oracle-freekafka → 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 |
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 拓撲固定且需與既有 OracleCC_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. 變更控制
- 新增/移除/更名模組,一律先改
spec/services.yaml,再重新執行產生器。 - 禁止直接手改由產生器產出之檔案後不回寫規範——該類修改將於下次
--force時遺失。
若確需長期客製,應修改產生器範本並於本文件 §4 登載。 - 埠號、schema 之變更須同步更新
ops/local/docker-compose.yml與SAD-002。 - 本文件變更須經 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 平台)
|