CR #201 » CSP-001_編碼標準.md
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. 程式風格
- 單一職責:一個方法不超過 40 行;超過者必須重構抽取,不得以註解「分段」掩飾。
-
不可變優先:值物件(
Money、CardNumber、LimitAmount)一律final、不可變;禁止setter隨意改寫領域狀態。 -
明確例外:禁止以
catch (Exception e) {}吞掉例外;必須記錄並轉換為領域例外(§CSP-003)。 -
無魔法數字:所有常數集中於
domain/constant/或組態檔;禁止散落if (status == 3)。 -
日期時間:一律使用
java.time(UTC 儲存、顯示時轉時區);禁止java.util.Date/Calendar。 -
貨幣運算:一律以最小貨幣單位(分)之
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 無語意;表名為技術慣用單數。
約束:
-
只適用基礎設施表(
*_outbox、*_idempotency、*_history等無更新語意者);
業務主檔一律不適用——業務表缺欄即應補欄,不得豁免。 -
reason必填;缺理由視同違規,Code Review 應退件(與 §5.2 同)。 - 登錄檔納入版控與 Code Review——「例外可稽核」之精神與 §5.2 一致。
-
新寫的遷移檔仍應優先使用 §5.2 檔內豁免,避免例外散落兩處;
§5.3 僅為凍結檔之唯一可用管道。 -
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 必須全綠:
- 編譯通過;2. 單元測試通過且覆蓋率達標(CSP-005);3. 靜態分析 0 error;
-
禁止模式掃描通過(§8);5. API 契約與
contracts/openapi/*.yaml一致(SDD-006); -
SQL 規則檢查通過(§5.1,
tools/validate_sql.py);7. 架構分層規則通過(ArchUnit,見 CSP-006 §2.9); -
契約 ↔ 實作逐端點核對通過(
tools/verify_contract_dto.py,見 §7.1)。 -
請求層設計規則(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 項 |
⚠️ 實作紀律:
-
不得為通過而放寬判定——若判定為真缺陷,應修契約或實作(依
CR-019 §6之方向),
不得於工具內加白名單掩蓋。 -
不得手改
contracts/openapi/*.yaml——一律改產生器tools/gen_openapi.py後重生
(見CSP-007 §5.3.1);且因該產生器無命令列旗標(執行即重生全部),
影響範圍須以cp -r+diff -rq全目錄比對證明。 -
掃描器之輸出須逐筆鑑別——本工具首輪曾產出 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 之判定邏輯未放寬
|