Project

General

Profile

CR #201 » CSP-001_編碼標準.md

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

 

CSP-001 編碼標準(Coding Standard)

項目 內容
文件編號 CSP-001
版本 1.11(#A-214:G-9/G-10 之掃描器已實作——tools/scan_request_layer.py,含 --selftest 6/6;★ 全庫 0 命中)
(1.10:CR-041 §7 增列 G-9/G-10;★ 掃描器當時待實作)
(前版 1.9:CR-019 §7 增列 G-8 契約↔實作逐端點核對——tools/verify_contract_dto.py,含實作紀律三條)
狀態 已核定
撰寫者 SA / Tech Lead
審查者 PMO / Security Architect
對應需求 NFR-SEC 群組、NFR-QUAL 群組;BRD-001 §7 品質準則

1. 目的與適用範圍

本文件規範 cardcore 全系統的程式撰寫標準,適用於後端微服務、前端 WebUI、共用平台與
建置腳本。所有 Coder 提交之程式碼,合併(merge)前必須通過本標準之自動化檢查(§7)。

環境決定權:依專案約定,正式開發環境(語言、框架、Build 工具)由客戶於程式撰寫階段拍板。
客戶已於 2026-09-18 拍板,技術棧基線改由 CSP-006 技術棧與開發環境基線定義——
後端為 Java 21 LTS + Spring Boot 4.1.1 + Spring Cloud 2025.1.3 + Gradle 9.7.1,
前端為 React 18 + Vite + TypeScript。

本文件之架構、命名、禁止事項與品質門檻全部繼續適用;若日後客戶指定其他技術棧,
除語法與工具鏈章節外,本文件其餘內容亦全部適用。


2. 分層與套件結構(強制)

每一個微服務內部嚴格遵循 DDD 四層(見 SDD-000 §2),套件結構如下:

<service>/
├── interfaces/      接收請求、格式驗證、命令轉換、事件消費
│   ├── rest/        Controller / Request-Response DTO
│   ├── event/       Message Consumer(Kafka / RabbitMQ)
│   └── mapper/      DTO ↔ 領域物件映射
├── application/     用例編排、交易邊界、埠介面
│   ├── command/     Command + CommandHandler(CQRS 寫側)
│   ├── query/       Query + QueryHandler(CQRS 讀側)
│   └── port/        對外埠介面(RepositoryPort / GatewayPort)
├── domain/          業務規則、不變式、狀態轉換(無框架依賴)
│   ├── model/       Aggregate / Entity / Value Object
│   ├── service/     Domain Service
│   └── event/       Domain Event
└── infrastructure/  持久化、事件發布、外部適配
    ├── persistence/ Repository 實作、Outbox
    └── gateway/     外部系統適配器

禁止事項(§8 詳述):domain/ 層不得 import 任何框架類別(Spring、JPA、HTTP Client 等);
不得出現 new HashMap<>() 之外、卻完全未被呼叫的「預留」類別。


3. 命名規範

元素 規範 正確 錯誤
類別 PascalCase,業務語意 Authorization, LimitCheckService Auth1, SvcUtil
方法 camelCase,動詞開頭 approve(), calculateAvailable() do(), process2()
常數 UPPER_SNAKE MAX_RETRY_COUNT maxRetry
變數 camelCase,具名 pendingTransactions list, tmp
錯誤碼 CC-<SVC>-<NNNN> CC-AUTH-1001 ERR_1
測試類 <受測類>Test AuthorizeHandlerTest TestAuth

4. 程式風格

  1. 單一職責:一個方法不超過 40 行;超過者必須重構抽取,不得以註解「分段」掩飾。
  2. 不可變優先:值物件(Money、CardNumber、LimitAmount)一律 final、不可變;禁止 setter 隨意改寫領域狀態。
  3. 明確例外:禁止以 catch (Exception e) {} 吞掉例外;必須記錄並轉換為領域例外(§CSP-003)。
  4. 無魔法數字:所有常數集中於 domain/constant/ 或組態檔;禁止散落 if (status == 3)。
  5. 日期時間:一律使用 java.time(UTC 儲存、顯示時轉時區);禁止 java.util.Date / Calendar。
  6. 貨幣運算:一律以最小貨幣單位(分)之 long 或 BigDecimal 計算;禁止 double 做金額加減。

5. 靜態分析與格式化

語言 工具 門檻
Java Checkstyle + SpotBugs + PMD 0 error、0 critical warning
TypeScript ESLint(Airbnb 規則子集)+ Prettier 0 error
YAML yamllint 0 error
SQL 自製 tools/validate_sql.py(見 §5.1) 0 error

格式化在 pre-commit hook 自動執行;CI 以 ./gradlew check 複驗,未通過不得合併。

5.1 SQL 檢查規則(tools/validate_sql.py)

以 SAD-003 之資料架構原則(DP-01~DP-08)與 GOV-001 §4.3 命名規範為依據,
對 Flyway 遷移腳本逐檔檢查。同時支援 Oracle 與 PostgreSQL 方言。

規則 檢查內容 依據
SQL-01 金額欄位(*_amount、*_balance、*_limit)須為整數型別,禁止浮點(FLOAT/DOUBLE PRECISION/BINARY_DOUBLE/NUMBER(x,y>0)) DP-03
SQL-02 時間欄位(*_at)須含時區(TIMESTAMP WITH TIME ZONE/timestamptz);禁止無時區之 TIMESTAMP/DATE DP-07
SQL-03 禁止破壞性語句:DROP TABLE、TRUNCATE、無 WHERE 之 DELETE/UPDATE DP-04、CSP-002 §5
SQL-04 命名:表名複數 snake_case、欄位 snake_case、主鍵 id、外鍵 <entity>_id、布林 is_*/has_* GOV-001 §4.3
SQL-05 每張表須具備 id、created_at、updated_at、version 四欄 SAD-003 §9
SQL-06 交易類表不得有 deleted_at(僅主檔可軟刪除) SAD-003 §9
SQL-07 禁止跨服務 schema 直連(腳本內出現非本服務 schema 之 DML/DDL) DP-01、DP-02
SQL-08 個資欄位(卡號、身分證字號)須標記 COMMENT ... 'PII' DP-06、SAD-005 §5
SQL-09 表名/欄位名不得為 Oracle 保留字或半保留字(如 resource、level、date、user、session、size) 26ai 實測:ORA-03050;清單取自 v$reserved_words(reserved='Y' OR res_semi='Y',共 108 字)

規則可於必要時增修,但只能加嚴、不得放寬;放寬須經 Tech Lead 與 Security Architect 同意。

5.2 明示豁免機制

規則不因個案而放寬;確有正當例外時,須在 CREATE TABLE 前 5 行內明示豁免並寫出理由:

-- lint:skip=SQL-04,SQL-05  理由:append-only 平台表,無更新語意(SDD-001 §3.15)
CREATE TABLE CC_PLT.outbox ( ... );
  • 一個豁免只對「緊接其後的那張表」生效(比對行號距離 ≤ 5)。
  • 理由為必填——無理由之豁免視同違規,Code Review 時應退件。
  • 目的:讓例外可稽核,而非靜默略過或全面放寬規則。
  • ⚠️ 已由 Flyway 套用之遷移檔不得使用本機制:任何內容變更(含新增註解)
    皆會使 checksum 失配,導致服務啟動失敗(HANDOVER #C-36)。此類檔案改用 §5.3。

實例:SDD-001 §3.15 之 outbox 為 append-only 表,無 updated_at/version 語意,
且表名為技術慣用單數。此類情況應以豁免指令處理,而非修改 SQL-04/SQL-05 之判定。

5.3 外部豁免登錄檔(tools/sql_exemptions.yaml)

適用時機:遷移檔已由 Flyway 套用而內容凍結時——§5.2 之檔內豁免不可用
(見 §5.2 末項)。此時以登錄檔宣告豁免:規則本身不放寬(§5.1 判定邏輯不變),
僅將例外自凍結檔移出,使其仍可稽核。

# 僅供「已套用而凍結」之遷移檔使用;新寫之遷移檔一律優先使用 §5.2 檔內豁免。
- table: pty_idempotency
  file: backend/services/party-service/src/main/resources/db/migration/V2__pty_idempotency.sql
  rules: [SQL-04, SQL-05]
  reason: >
    冪等鍵表:以 idempotency key 為自然主鍵,記錄一經寫入即不更新,
    故 id/updated_at/version 無語意;表名為技術慣用單數。

約束:

  1. 只適用基礎設施表(*_outbox、*_idempotency、*_history 等無更新語意者);
    業務主檔一律不適用——業務表缺欄即應補欄,不得豁免。
  2. reason 必填;缺理由視同違規,Code Review 應退件(與 §5.2 同)。
  3. 登錄檔納入版控與 Code Review——「例外可稽核」之精神與 §5.2 一致。
  4. 新寫的遷移檔仍應優先使用 §5.2 檔內豁免,避免例外散落兩處;
    §5.3 僅為凍結檔之唯一可用管道。
  5. file 欄須填實際檔案路徑;登錄檔中出現不存在之檔案或
    檔案中不存在之表名時,掃描器應報警(防止登錄檔腐敗為萬能赦免)。

判準(避免假綠):不得以「補上 id/created_at/updated_at/version 欄位」
之方式規避豁免——對無更新語意之表,補欄即無意義欄位,
形同以假資料通過檢查,違反 §8 誠實性要求。


6. 版本控管與提交規範

  • 分支策略:main(可發布)/develop(整合)/feature/<fr-id>-<brief>(如 feature/FR-04-01-limit)。
  • 提交訊息:<type>(<scope>): <subject>,type ∈ {feat, fix, refactor, test, docs, chore}。
  • 關聯需求:提交必須帶 Refs: FR-XX-NN 或 Refs: NFR-XX-NN,供追溯矩陣(GOV-004)自動彙整。
gitGraph
    commit id: "init" tag: "v0.1"
    branch develop
    checkout develop
    branch feature/FR-04-01-limit
    commit id: "impl limit VO"
    commit id: "impl check svc"
    checkout develop
    merge feature/FR-04-01-limit
    checkout main
    merge develop tag: "v0.2"

7. 自動化檢查(Quality Gate)

合併前 CI 必須全綠:

  1. 編譯通過;2. 單元測試通過且覆蓋率達標(CSP-005);3. 靜態分析 0 error;
  2. 禁止模式掃描通過(§8);5. API 契約與 contracts/openapi/*.yaml 一致(SDD-006);
  3. SQL 規則檢查通過(§5.1,tools/validate_sql.py);7. 架構分層規則通過(ArchUnit,見 CSP-006 §2.9);
  4. 契約 ↔ 實作逐端點核對通過(tools/verify_contract_dto.py,見 §7.1)。
  5. 請求層設計規則(G-9 不得用 ResponseStatusException/G-10 選填數值用 wrapper,見 §7.2)
    ——★ 規則已立;掃描器待實作(列後續)。

7.1 契約 ↔ 實作逐端點核對(G-8,tools/verify_contract_dto.py)

緣由(CR-019):CR-016 §8 與 CR-018 於同一日、同一次動工前查證中先後被發現,
兩者皆為「契約與實作脫鉤」而非程式缺陷。此類缺陷有四項特性使其極難被既有關卡攔下:
編譯不會錯(契約是 YAML、DTO 是 Java,無編譯期關聯)、啟動不會錯(契約不參與執行期繫結)、
單測不會錯(單測驗實作行為,不驗「契約是否描述實作」)、只有人逐欄看才會發現。
→ 故必須把它變成可重複執行的關卡,不能依賴人的記憶。

項 內容
檢查項 A. 回應型別:契約該端點成功回應之 data(陣列?元素 schema)vs Controller 回傳型別
B. 欄位集合:契約 components.schemas.<Name>.properties vs Java record <Name>(…) 元件(雙向)
門檻 --service <name> 為 0 不符(hits);warns(型別名不符但欄位一致)須於 PR 說明處理方式
呼叫範圍 全庫 python3 tools/verify_contract_dto.py;改動某服務之端點或回應 DTO 時,至少須跑該服務
零相依 本環境無 PyYAML(實測)且 CI 未必有 → 自製最小 YAML 子集解析器;解析失敗一律拋錯,不靜默略過
判定力回測 python3 tools/verify_contract_dto.py --selftest——改動本工具後必跑(4 案例:陣列性不符/DTO 有契約無/契約有 DTO 無/對齊無缺陷)
與第 5 項之關係 第 5 項(契約與 SDD-006 一致)為人工複核;本項為其可執行之子集(僅涵蓋回應側與 schema 欄位),不取代第 5 項

⚠️ 實作紀律:

  1. 不得為通過而放寬判定——若判定為真缺陷,應修契約或實作(依 CR-019 §6 之方向),
    不得於工具內加白名單掩蓋。
  2. 不得手改 contracts/openapi/*.yaml——一律改產生器 tools/gen_openapi.py 後重生
    (見 CSP-007 §5.3.1);且因該產生器無命令列旗標(執行即重生全部),
    影響範圍須以 cp -r + diff -rq 全目錄比對證明。
  3. 掃描器之輸出須逐筆鑑別——本工具首輪曾產出 12 處偽陽性(自身解析缺陷,見 CR-019 §2.1);
    「0 命中」與「命中很多」兩者都不可直接採信。

7.2 請求層之二條設計規則(CR-041,G-9/G-10)

緣由(CR-041/#A-184/#A-185):B2 批次為 16 服務建 @WebMvcTest 切片測試時實測揭露。
兩者皆為「編譯不會錯、啟動不會錯、單測不會錯」之型別——只有真機/切片打過該端點才會現形。

G-9 控制器不得以 ResponseStatusException 取代已登錄之錯誤碼

項 內容
規則 控制器拋錯一律用已登錄之 DomainException 子類(BusinessRuleException + ErrorCodes 之碼);不得使用 Spring 之 org.springframework.web.server.ResponseStatusException
為什麼 ★ common-spring 之 GlobalExceptionHandler 不涵蓋 ResponseStatusException(CR-004 之 handler 清單未含)→ 落兜底 Exception → 回 500 CC-PLT-5000(契約要求 404 時亦同)。且 ResponseStatusException 繞過錯誤碼登錄表(SDD-006 §3.4),使回應碼不受治理
實測 GET /api/v1/cards/lookup/{cardNoHash} 查無 → 應 404、實回 500(#A-184;@WebMvcTest 實測 expected 404 but was 500)
正解 拋 BusinessRuleException(ErrorCodes.CC_<域>_4040, …)(該碼已登錄為 404)——與各服務既有慣例一致
把關(★ 已實作,#A-214) ✅ tools/scan_request_layer.py:interfaces/rest/ 下之程式碼(★ 先去註解)不得出現 ResponseStatusException。
⚠️ 必須去註解:card-service 之 CardController Javadoc 中提及該型別(記錄 CR-041 之歷史修法)——★ 程式碼合規,直接 grep 會偽陽性。
★ 實測:全庫 0 命中(--selftest 6/6 通過)

G-10 請求 DTO 之數值欄位型別依「必填/選填」決定

項 內容
規則 契約 required → 可用 primitive(long/int/boolean);
契約選填 → 必須用 wrapper(Long/Integer/Boolean)(null = 未提供)
為什麼 ★ Jackson 3(CR-038 升版)預設 FAIL_ON_NULL_FOR_PRIMITIVES=true(Jackson 2 為 false;Spring Boot 4 之 JacksonProperties 另有 useJackson2Defaults 旗標,實查)→ primitive 欄位一省略即 400 CC-PLT-4002。對必填欄位而言正確(等同把契約 required 落到實作);但選填欄位若誤用 primitive,選填將變成必填
實測 MerchantSettlementRequest(long grossAmount/feeAmount/taxAmount,契約 required)省略金額 → MismatchedInputException: Cannot map null into type long → 400。★ 經契約查證後判定為正確行為(#A-185 原判「缺陷」,已更正)
正例/反例 ✅ MerchantSettlementRequest.batchCount 為 Integer(選填、預設 0)——正確
❌ 若誤寫為 int,編譯與既有測試都不會報,選填將靜默變成必填
把關(★ 已實作,#A-214) ✅ tools/scan_request_layer.py:請求 DTO 中契約未列 required 之數值欄位,型別不得為 primitive。
★★ 不可只做同名比對——本專案存在命名不符之配對(Java SettlementRequest ↔ 契約 ClearingSettlementRequest;Java AdjustmentRequest ↔ 契約 LimitAdjustmentRequest);★ 若只比同名,該等 DTO 整批漏檢(偽陰性)。故採同名優先、否則後綴唯一匹配(多個後綴命中 → 不臆測,列 WARN)。
★ 實測:全庫 0 命中(--selftest 6/6 通過)
⚠️ 範圍限制(如實揭露):僅檢查頂層 *Request record;★ 巢狀值物件(如 card-service 之 ThreeFactorRequest,為 TransitionRequest.threeFactors 之元素)不在範圍內 → 列後續

8. 絕對禁止事項(客戶硬性要求)

以下行為一經掃描命中,CI 直接紅燈、程式碼退件,不列入「待改善」。

編號 禁止事項 偵測方式 違例範例
B1 塞框架 / 空殼類:僅定義類別、方法簽章,但無任何實作邏輯(placeholder) scan_scaffold.py:方法本體行數 ≤2 且無呼叫 public void approve() { }
B2 Mock data 當成品:以硬編資料(如 return List.of(new Card("TEST")))取代真實查詢/計算 scan_mockdata.py:比對 return new/List.of 與真實 Port 呼叫 return Arrays.asList("A","B"); 偽裝查詢結果
B3 吞例外:catch 後無日誌、無重拋、無補償 PMD 規則 EmptyCatchBlock catch(Exception e){}
B4 洩漏內部實作:將堆疊、SQL、框架錯誤直接回傳前端 例外處理規範(CSP-003) throw new RuntimeException(e.getMessage()) 原樣外拋
B5 TODO / FIXME 留待上線:正式分支不得殘留未解決標記 grep -r "TODO|FIXME" 於 main // TODO 之後接真 DB
B6 硬編連線資訊:帳密、URL 寫死於程式 秘密掃描(gitleaks) jdbc:...password=xxx

B2 之判定原則:單元測試內的 given 資料、與 stub(測試雙)屬合法;
但生產程式碼路徑(interfaces/ application/ domain/ infrastructure/ 中非測試檔)
若出現「為了讓編譯過而回傳假資料」即違規。掃描器以 src/test 白名單豁免測試目錄。

8.1 scan_scaffold.py 之判定精確化(v1.3)

實作 common-* 程式庫時發現掃描器有兩處誤判,已修正——判定力不變,僅去除誤判:

# 誤判 成因 修正
F-1 含日誌佔位符 {} 之方法被誤判為空殼 EMPTY_METHOD 以 \{\s*\} 比對方法本體,而 SLF4J 佔位符即 {}。任何人只要寫一句 log.warn("code={}", x) 就會被 CI 擋下 分析前先以 strip_literals() 將字串/字元常值內容替換為空白(保留位置與行號),使字串內之 {} 不參與結構比對
F-2 工具類別之 private 無參建構子被誤判為空殼方法 該建構子本體確實為空,但目的在阻止實例化,屬標準慣用法,非佔位 比對出方法名等於所屬類別名時視為建構子,不納入方法空殼判定(B1 規範對象為「方法」)

附帶修正:EMPTY_METHOD 原以 \{\s*(...) 開頭,但比對對象為「已剝除大括號之方法本體」,
該分支恆不成立(死分支)——即 public void x() { return; } 從未被檢出。已改為
^(?:return\s*;|pass|throw new UnsupportedOperationException...;)?$。

驗證:修正後對 backend/ 掃描為 0 命中;對人工空殼樣本(空方法、return; 方法)
仍全數檢出,且正確忽略 private 建構子與含 {} 之日誌方法。


8.2 scan_mockdata.py 之判定精確化(v1.4)

實作 common-saga 時發現:InMemorySagaStore.loadSteps() 於查無資料時
return List.of();,被判定為「以硬編資料冒充查詢結果」(B2)。
此為誤判——B2 規範的是「捏造元素冒充真實查詢」,
而「查無資料回傳空集合」是正確且必要之語意。

# 誤判 成因 修正
M-1 return List.of()(空集合)被判為 mock data HARDCODE_RET 僅比對構造型式(`List.of Arrays.asList

修正後之判定基準

寫法 判定 理由
return List.of(new Card("TEST")); ❌ 違規(B2) 捏造元素冒充查詢結果
return Arrays.asList("A","B","C"); ❌ 違規(B2) 同上
return new String[]{"x","y"}; ❌ 違規(B2) 同上(陣列帶初始值)
return List.of(); ✅ 合法 查無資料
return new String[0]; ✅ 合法 空陣列

驗證:修正後對 backend/ 掃描 0 命中;
人工假資料樣本(含元素之 List.of/Arrays.asList/new String[]{…})3 處全數檢出,
判定力未因此打折。


8.3 scan_scaffold.py 之判定精確化(v1.5,第二輪)

實作 common-spring 之 SecurityAutoConfiguration 時再發現三處判定缺陷,
並建立可回歸之人工樣本機制——避免日後修正掃描器時「只追求真實程式碼 0 命中」
而把判定力改到失效。

# 缺陷 成因 修正
F-3 巢狀類別之 private 建構子被誤判為空殼 F-2 之修正僅比對 top-level 類別名(class_name_of);巢狀類別(如 SecurityAutoConfiguration.EnvelopeWriter)之建構子與 top-level 不同名,仍被判為空殼 改以「所在型別」比對——取方法位置之前所有型別宣告名稱(enclosing_type_names),同名即視為建構子。enum 私有建構子亦適用
F-4 方法本體僅有註解時未被檢出(漏判) 本體比對未剝除註解,{ // TODO } 剝除字面常值後仍為非空字串,故繞過 B1 本體比對前先 strip_comments()(保留換行與行號);僅有註解之本體仍判為空殼
F-5 測試中「僅取簽章」之載體方法被判為空殼 B1 之規範對象為交付之生產程式;測試中 MethodParameter 載體等方法本體空白為正當手法 方法空殼規則對測試目錄豁免。空的 @Test 方法另由 scan_faketest.py T5(測試方法本體無斷言)把關,兩支掃描器職責不重疊

--selftest 回測機制

新增人工樣本目錄 tools/fixtures/scan_scaffold/,以行尾註解標記預期:

標記 意義
EXPECT-HIT 該行應被判定為空殼(正樣本,測漏判)
EXPECT-CLEAN 該行不應被判定為空殼(負樣本,測誤判)

執行:python3 tools/scan_scaffold.py --selftest(納入 CI)。
正樣本涵蓋:空方法、return;、throw new UnsupportedOperationException()、空殼類別、// TODO;
負樣本涵蓋:top-level/巢狀/enum 私有建構子、SLF4J 佔位符、record 緊湊建構子、匿名類別方法、
測試檔載體方法。

驗證:--selftest 正樣本 5 全數檢出(漏判 0)、負樣本 12 全數放行(誤判 0);
backend/ 與 frontend/ 全量掃描皆 0 命中。

8.4 scan_faketest.py 之判定精確化(v1.6)

實作 posting-service 計息服務時,測試以記憶體假倉儲取代外部依賴,
因而暴露 T5(空測試方法)之三處缺陷:

# 缺陷 成因 修正
F-6 非測試方法被判為空測試(updateStatus) 以方法名稱是否含 test/should/verify… 識別,且為子串比對——updateStatus 含 teSt 即中獎 改以測試註解(@Test/@RepeatedTest/@ParameterizedTest…)判定,名稱一概不看
F-7 僅用 assertThrows 之測試被判為無斷言(放寬名稱條件後一度爆出 56 處誤判) ASSERT_CALL 列舉式樣式漏了 JUnit 5 的 assertThrows/assertDoesNotThrow/assertNotNull 等 改為斷言函式前綴比對:assert\w*/expect\w*/should\w*/verify
F-8 方法本體抓取錯誤,判定形同隨機 text.find("{", m.end()) 從方法左括號之後開始找,抓到的是「下一個」區塊 改為 m.end() - 1(本方法之左括號)再做大括號配對

連帶修正:回看註解之視窗須含 match 本身——
規則之 (?:@Test\s*)? 會把註解吃進 group(0),
若只看到 m.start() 為止,反而漏掉緊鄰簽章的那個 @Test(修正後一度假測試全部抓不到)。

回測(人工樣本):①全量 backend/ 0 誤判;
②植入 @Test void emptyProbe() {} → 確實檢出;③移除後回復 0。
——正負樣本同時測,避免為了消除誤判而讓掃描器失去判定力。


8.5 服務間 HTTP 呼叫:逾時與重試(v1.7)

背景(實測,非推測):4 支服務間 *Client.java 原一律硬編
connectTimeout(800ms)/timeout(800ms)。實機執行帳單週期時,
posting → pricing 首次呼叫即逾時 → 計息失敗 → 該帳戶被跳過、未產生帳單;
預熱後同一呼叫僅 110ms。
——批次語意下(單一帳戶失敗即失去該期帳單),800ms 不足以吸收冷啟動與 GC 抖動。

⚠️ 屬「靜默不做」型缺陷的變體:它不在呼叫端報錯,而是以業務失敗
(帳戶被跳過)呈現,極難由日誌反推為逾時。

規範

項目 規則
逾時值 不得硬編於客戶端;一律經 cardcore.service-client.* 組態(ServiceClientProperties)
預設值 連線 2s/請求 3s(原 800ms 對批次不足;互動式呼叫可由服務自行調降)
重試 預設 1 次(共 2 次嘗試),退避 200ms;僅對 IOException(連線/逾時)重試
不得重試者 HTTP 層錯誤(4xx/5xx)一律不重試——那是下游的業務決定(如 CC-PLT-5005),重試只會放大流量並掩蓋問題
冪等 會被重試之寫入呼叫必須帶冪等鍵(計息已帶 X-Idempotency-Key)
落點 機制集中於 common-spring 之 ServiceClientHttpClient,不得逐服務重工

為何「不重試 5xx」:pricing 掛掉時 posting 回 503 CC-PLT-5005 且不入帳
(無 fail-open,見 CR-006 實測)。若對 5xx 重試,該性質雖仍在,
卻會在下游已明確拒絕時繼續施壓並拉長批次時間,故重試僅限網路層。


9. 參考

  • SDD-000 分層職責與命名、SDD-006 API Contract、CSP-003 錯誤處理、CSP-005 測試規範。
  • CSP-006 技術棧與開發環境基線(技術棧版本之單一真實來源)。
  • 禁止模式掃描器:cardcore/tools/scan_scaffold.py、cardcore/tools/scan_mockdata.py、cardcore/tools/scan_faketest.py。
  • SQL 規則檢查器:cardcore/tools/validate_sql.py。

10. 修訂記錄

版本 日期 修訂者 說明
1.0 2026-09-17 SA / Tech Lead 初版
1.1 2026-09-18 SA / Tech Lead ①§1 技術棧建議基線由「Java 21 + Spring Boot 3.2 + Spring Cloud」改為由 CSP-006 定義之「Java 21 LTS + Spring Boot 4.1.1 + Spring Cloud 2025.1.3 + Gradle 9.7.1」——原 3.2 已於 2024-12-31 EOL,全 3.x 線於 2026-06-30 終止 OSS 支援;②§5 補列 SQL 檢查規則(原僅一行提及 validate_sql.py,未定義檢查內容,且該檔當時尚未交付);③§7 品質關卡增列 SQL 檢查與 ArchUnit 分層檢查
1.2 2026-09-18 SA / Tech Lead 依 validate_sql.py 實作與 Oracle 26ai 實測結果補強:①§5.1 新增 SQL-09(禁止 Oracle 保留字/半保留字作為識別碼)——清單取自 v$reserved_words(reserved='Y' OR res_semi='Y',共 108 字),成因是 2026-09-18 實測發現 resource 與 level 欄位在 26ai 建表失敗(ORA-03050);②新增 §5.2 明示豁免機制,要求例外逐表明示並附理由,不得為個案放寬規則
1.3 2026-09-18 SA / Tech Lead 新增 §8.1 scan_scaffold.py 判定精確化:修正兩處誤判(F-1 字串內之 SLF4J 佔位符 {} 致方法被誤判為空殼、F-2 工具類別之 private 無參建構子被誤判),並修正 EMPTY_METHOD 之死分支。判定力不變、僅去除誤判;修正後 backend/ 掃描 0 命中,人工空殼樣本仍全數檢出
1.4 2026-09-18 SA / Tech Lead 新增 §8.2 scan_mockdata.py 判定精確化:修正 M-1 誤判——原掃描器將「查無資料」之 return List.of()(空集合)判為 B2 違規;改為僅當集合含實際元素時才算違規,並補回 new X[]{…} 帶初始值陣列之偵測(改版時一度漏失)。B2 之精神為「禁止捏造資料冒充真實查詢」,非禁止空結果。修正後 backend/ 掃描 0 命中,人工假資料樣本 3 處全數檢出
1.5 2026-09-19 SA / Tech Lead 新增 §8.3 scan_scaffold.py 判定精確化(第二輪):修正 F-3(巢狀類別 private 建構子被誤判——原修正只比對 top-level 類別名)、F-4(方法本體僅有註解時未被檢出之漏判)、F-5(測試中僅取簽章之載體方法被誤判,改為豁免並由 scan_faketest.py T5 把關);並新增 --selftest 人工樣本回測機制(tools/fixtures/scan_scaffold/,正負樣本同時回測漏判率與誤判率),避免日後修正掃描器時只追求「真實程式碼 0 命中」而失去判定力。驗證:正樣本 5 全數檢出、負樣本 12 全數放行,backend/、frontend/ 全量掃描 0 命中
1.6 2026-09-20 新增 §8.4 scan_faketest.py 判定精確化:修正 F-6(以名稱子串識別測試方法,致 updateStatus 等非測試方法被誤判)、F-7(斷言樣式漏 JUnit 5 之 assertThrows 等,一度爆出 56 處誤判)、F-8(方法本體抓取錯位,判定形同隨機),以及回看註解視窗之連帶修正。回測:全量 backend/ 0 誤判,且植入之空 @Test 仍能檢出
1.7 2026-09-20 WorkBuddy AI 新增 §8.5「服務間 HTTP 呼叫:逾時與重試」:原先 4 支 *Client.java 一律硬編 800ms,實機致 posting→pricing 首呼逾時、帳戶被跳過未產生帳單(HANDOVER #C-40)。規範逾時不得硬編(改由 cardcore.service-client.* 組態,預設連線 2s/請求 3s)、重試僅限 IOException(HTTP 4xx/5xx 屬下游業務決定,不重試)、機制集中於 common-spring,不得逐服務重工
1.8 2026-09-20 WorkBuddy AI 新增 §5.3 外部豁免登錄檔(tools/sql_exemptions.yaml),並於 §5.2 補註「已套用之遷移檔不得使用檔內豁免」(HANDOVER #C-36)。成因:實測盤點發現 validate_sql 之 7 處違規全數位於已套用凍結之遷移檔(V2__pty_idempotency、V4__card_application_outbox)——依 §5.2 補 -- lint:skip 看似正解,實則不可用:註解亦屬內容變更,會使 Flyway checksum 失配、服務無法啟動;而對無更新語意之基礎設施表硬補 id/updated_at/version 屬無意義欄位(假綠),亦違反 §8 誠實性要求。故增設「規則不放寬、例外移出凍結檔」之登錄檔機制,並明定五項約束(僅適用基礎設施表/reason 必填/納入版控/新寫檔案仍優先 §5.2/登錄檔指向不存在之檔或表須報警)。SQL-01~SQL-09 之判定邏輯未放寬
(23-23/67)