# 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-` 前綴：<br>`junit-jupiter` → `testcontainers-junit-jupiter`<br>`oracle-free` → `testcontainers-oracle-free`<br>`kafka` → `testcontainers-kafka`<br>舊名於 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 | 同上 | Coder |
| `domain/model|service/` 聚合與領域服務 | 同上 | Coder |
| `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. 產生器用法

```bash
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 平台）** |
