CR #201 » SCS-003_卡片服務程式設計規格.md
SCS-003 卡片服務(card-service)程式設計規格
| 項目 | 內容 |
|---|---|
| 文件編號 | SCS-003 |
| 版本 |
1.14(CR-056 §12:發卡自動建 PHYSICAL 載體——§7.4 新增步驟 ⑥-b(同交易、失敗整筆回滾)+ §7.14 補「發卡即建載體,POST /instruments 降為補登用途」;★ 併揭露既有卡片仍未回填載體。前版 1.13(CR-056 第一步:新增 §7.15 查卡清單——`GET /api/v1/cards?accountId |
| 狀態 | 已核定(回溯補建) |
| 服務埠號 | 8082 |
| 套件根 | com.beam.cardcore.card |
| 權威 schema | CC_CARD |
| 對應需求 |
ISS-2.2(卡片生命週期)、DEB-3.1(簽帳金融卡) |
1. 對應需求
| FR 編號 | 需求名稱 |
|---|---|
ISS-2.2 |
卡片生命週期管理(開卡/鎖卡/掛失/補發/註銷) |
DEB-3.1 |
簽帳金融卡(Debit Card) |
2. 服務職責與邊界
承作:
- 卡片主檔(
cards)維護 - 卡片生命週期狀態轉換(
SDD-005 §2) - 供授權服務查詢卡片狀態(依卡號雜湊)
-
發卡與給號(
card_no_pool/issued_card_numbers)——本期補(HANDOVER #C-10) -
申請案件(
card_applications)、Token 綁定與查詢(tokens)、快發快綁——本期補
不做:
- 授權決策 →
authorization-service - 三要素(身分證字號/生日/核准序號)之實體驗證 → 本期以
ThreeFactorVerifier埠抽象,
預設實作僅驗證「有值」並於 §10 揭露(介接行內系統屬後續)
3. 領域模型
3.1 聚合
| 型別 | 職責 |
|---|---|
Card |
卡片聚合(不可變) |
CardBlockCode |
區塊碼狀態機 |
3.2 狀態機(SDD-005 §2)— 共 14 條轉換 + 終態
| # | 來源 | 事件 | 目標 |
|---|---|---|---|
| 1 | INACTIVE |
ACTIVATE |
ACTIVE |
| 2 | INACTIVE |
CLOSE |
CLOSED |
| 3 | ACTIVE |
LOCK |
LOCKED |
| 4 | LOCKED |
UNLOCK |
ACTIVE |
| 5 | ACTIVE |
REPORT_LOST |
LOST |
| 6 | LOST |
REISSUE |
REISSUED |
| 7 | ACTIVE |
REISSUE |
REISSUED |
| 8 | REISSUED |
ACTIVATE |
ACTIVE |
| 9 | ACTIVE |
EXPIRE |
EXPIRED |
| 10 | EXPIRED |
CLOSE |
CLOSED |
| 11 | ACTIVE |
CLOSE |
CLOSED |
| 12 | LOCKED |
CLOSE |
CLOSED |
| 13 | REISSUED |
CLOSE |
CLOSED |
終態:CLOSED。非法 → CC-CARD-3001;終態再轉換 → CC-CARD-3002。
3.3 兩個狀態維度(關鍵,不可混淆)
| 維度 | 欄位 | 維護者 | 用途 |
|---|---|---|---|
| 生命週期 | block_code |
本服務狀態機 | 可否交易以此判定(§2.3) |
| 啟用/帳務 | status |
由 block_code 衍生(SDD-005 §2.4、CR-020 F4) |
非獨立維度——隨每次轉換重算 |
可否交易:僅 blockCode == ACTIVE。
-
LOCKED/LOST須即時拒絕。
⚠️
CR-020F4 修正後(2026-09-23):status不再是獨立維度——
由block_code單向推導(映射表內建於CardBlockCode#status(),switch 窮盡性檢查)。
舊載「未在本階段轉換、僅供顯示」已過時。若日後需要真正的獨立帳務維度
(如「卡片ACTIVE而帳務SUSPENDED」),須依SDD-005 §2.4鐵律 2
另立獨立狀態機並廢除映射,不得同時維持映射與獨立更新。
3.4 非法轉換(須被拒絕)
| 嘗試 | 結果 |
|---|---|
CLOSED → ACTIVE |
❌ 終態(CC-CARD-3002) |
LOST → ACTIVE |
❌ 須經 REISSUED
|
INACTIVE → LOCKED |
❌ 未開卡不得鎖卡 |
EXPIRED → ACTIVE |
❌ 須重新申請 |
4. 程式結構
| 套件/類別 | 職責 |
|---|---|
domain/model/Card |
卡片聚合 |
domain/model/CardBlockCode |
狀態機(serviceCode CARD) |
domain/model/CardNumberIssuer |
卡號產生:給號、Luhn 檢查碼、三態衍生(本期新增) |
domain/model/CardApplication |
申請案件聚合(本期新增) |
domain/model/TokenBinding |
Token 綁定聚合(本期新增) |
domain/port/CardRepository |
持久化埠 |
domain/port/CardNumberPoolPort |
號碼池埠(本期新增) |
domain/port/CardApplicationRepository |
申請案件持久化埠(本期新增) |
domain/port/TokenRepository |
Token 持久化埠(本期新增) |
domain/port/ThreeFactorVerifier |
三要素驗證埠(本期新增;預設實作僅驗證「有值」,見 §10) |
domain/port/IdempotencyStore |
冪等鍵埠(比照 party-service;本期新增) |
application/CardQueryService |
查詢 + 狀態轉換 |
application/CardIssueService |
發卡(本期新增) |
application/CardApplicationService |
申請案件(本期新增) |
application/CardTokenService |
Token 綁定/查詢(本期新增) |
infrastructure/jdbc/JdbcCardRepository |
JDBC 實作 |
infrastructure/jdbc/JdbcCardNumberPool |
號碼池 JDBC 實作(本期新增) |
infrastructure/jdbc/JdbcIdempotencyStore |
冪等鍵 JDBC 實作(本期新增) |
infrastructure/security/Sha256Hex |
SHA-256 小寫十六進位(本期新增,見 §7.3) |
interfaces/rest/CardController |
REST 端點(含本期新增 5 端點) |
5. 資料模型
| 表 | 關鍵欄位 | 實測約束 |
|---|---|---|
cards |
id、account_id、party_id、card_no_enc(BLOB)、card_no_masked、card_no_hash(CHAR(64))、product_code、bin、expiry_date、block_code、status、instant_issued、version
|
block_code IN ('INACTIVE','ACTIVE','LOCKED','LOST','REISSUED','EXPIRED','CLOSED');status IN ('UNACTIVATED','ACTIVE','SUSPENDED','CLOSED');PARTY_ID/CARD_NO_ENC/BLOCK_CODE NOT NULL;uq_card_hash UNIQUE(card_no_hash)
|
card_no_pool |
bin(CHAR(8))、range_start、range_end、used_count、total_count、status
|
給號來源 |
issued_card_numbers |
card_no_hash(CHAR(64) PK)、card_id、issued_at
|
已發出卡號——PK 即「不重複」之最終防線 |
tokens |
card_id、token_no(UNIQUE)、platform、device_ref、status、bound_at
|
platform IN ('APPLE_PAY','GOOGLE_PAY','SAMSUNG_PAY','GARMIN_PAY','FITBIT_PAY','TAIWAN_PAY','QR_CODE');status IN ('ACTIVE','SUSPENDED','DELETED')
|
card_applications |
apply_no(UNIQUE)、account_id、channel、product_code、status、approval_no、fast_issue_flag
|
channel IN ('WEB','MOBILE','API','BRANCH','MERCHANT');status IN ('RECEIVED','REVIEWING','SUPPLEMENT','APPROVED','REJECTED')
|
card_idempotency |
key(PK)、fingerprint、entity_type、entity_id
|
冪等鍵(V2 遷移,比照 party-service) |
⚠️
X-Channel標頭與card_applications.channel列舉不同(前者WEB/MOBILE/PARTNER/BATCH/SYSTEM、
後者WEB/MOBILE/API/BRANCH/MERCHANT)。本期以請求 body 之channel寫入,
標頭僅供稽核/路由,不可直接寫入channel欄位(會噴ORA-02290)。此不一致已列 §10 #8。
⚠️ 本文件先前版本將
card_no_hash誤記為CHAR(256),實測 DDL 為CHAR(64)(本期更正)。
6. API 設計
| 端點 | 說明 | 錯誤碼 | 狀態 |
|---|---|---|---|
GET /api/v1/cards/lookup/{cardNoHash} |
依卡號雜湊查狀態(供授權) | 404(查無) | 既有 |
GET /api/v1/cards/{cardId} |
查詢卡片 | CC-CARD-3001 |
既有 |
POST /api/v1/cards/{cardId}/lifecycle-transitions |
生命週期狀態轉換 |
CC-CARD-3001/CC-CARD-3002/CC-CARD-1002(三要素不符或查無來源申請案件)/CC-CARD-2001(冪等鍵異指紋)/CC-CARD-4040(查無卡片)
|
既有(CR-020 修正後) |
POST /api/v1/cards |
發卡(亂數給號) |
CC-CARD-1003(號碼池用盡) |
本期新增 |
POST /api/v1/cards/{cardId}/instant-issuance |
快發快綁 |
CC-CARD-1001/CC-CARD-1002
|
本期新增 |
POST /api/v1/cards/{cardId}/tokens |
Token 綁定 |
CC-CARD-1004(重複綁定) |
本期新增 |
GET /api/v1/cards/{cardId}/tokens |
查詢 Token(陣列) | — | 本期新增 |
POST /api/v1/card-applications |
建立申請案件 | 400(欄位/列舉不符) | 本期新增 |
operatorId為稽核所需,契約未定義,已如實記錄待補登錄。
6.1 本期修正之契約瑕疵
產生器原將兩個端點之請求/回應誤用他處 schema(複製貼上),本期修正:
| 端點 | 原(誤) | 修正後 |
|---|---|---|
POST /cards/{cardId}/tokens |
請求 InstantIssuanceRequest
|
TokenBindRequest{platform, deviceRef} |
POST /card-applications |
請求 CreateCardRequest/回應 CardView
|
CardApplicationRequest{accountId, productCode, channel, fastIssueFlag}/CardApplicationView
|
POST /cards/{cardId}/instant-issuance |
請求無三要素欄位 |
InstantIssuanceRequest 新增 threeFactors(必填)——否則 CC-CARD-1002 無從觸發 |
契約由
tools/gen_openapi.py產生,修正一律改產生器,不得手改 YAML。
7. 關鍵邏輯
7.1 狀態轉換
- 合法性由
CardBlockCode狀態機判定,服務不自行判斷。 - ⚠️
CR-020F4 修正後:轉換同時更新block_code與status——
status由blockCode.status()推導(SDD-005 §2.4),
JdbcCardRepository.updateBlockCode(cardId, blockCode)不收status參數
(一律由blockCode推導,呼叫端無從傳錯)——
舊載「轉換後更新block_code(不是status)」已過時且即為CR-020F4 之根因。 - ★ 開卡三要素(
CR-020F1):event=ACTIVATE時必須驗證三要素——
以CardApplicationRepository.findByCardId(cardId)反查本卡之來源申請案件
(CARD_APPLICATIONS.CARD_ID,V7 遷移),經ThreeFactorVerifier比對其核准序號;
查無來源案件/未核准/三要素不符 →CC-CARD-1002(422,fail-closed)。
與快發快綁(§7.5)共用同一ThreeFactorVerifier——同一業務概念不得兩套行為。 - ★ 冪等(
CR-020F2):X-Idempotency-Key確實生效(鑑別值
CARD_LIFECYCLE_TRANSITION;指紋=cardId|event|reason|operatorId);
同鍵同指紋重放(回當前狀態)、同鍵異指紋回CC-CARD-2001;
缺X-Channel/X-Idempotency-Key由介面層回CC-PLT-4001(400)。 - ★ 查無卡片:
transition()與findById()一致回CC-CARD-4040(404)
(CR-020F3;舊為CC-CARD-3001/409)。 - 相關性:卡片一經
LOCK→ 授權服務(讀blockCode)即時拒絕(端到端驗證通過)。
7.2 查詢供授權
- 授權服務以卡號雜湊查詢;
card_no_hash為CHAR(64)→ 以TRIM比對(Oracle 補空白比較僅在同為 CHAR 時生效)。
7.3 卡號三態(密文/遮罩/雜湊)
明文僅存在於發卡請求當下——不落庫、不進日誌、不進回應。
| 衍生形式 | 欄位 | 產生方式 | 用途 |
|---|---|---|---|
| 密文 |
card_no_enc (BLOB) |
FieldEncryptor.encrypt(pan).getBytes(UTF_8);自描述格式 v1:<keyId>:<base64(IV)>:<base64(ct‖tag)>(CSP-007 §5.3.2) |
儲存、取用時解密 |
| 遮罩 | card_no_masked |
PiiMasker.maskCardNumber(pan)(前 6 後 4,如 411111******1111) |
顯示、API 回應 |
| 雜湊 | card_no_hash |
SHA-256 小寫十六進位(64 字元) | 比對、查詢 |
雜湊格式之實測依據(非憑印象):
| 來源 | 值 |
|---|---|
| Oracle 庫內既有 seed |
card_no_hash = 9bbef19476623ca56c17da75fd57734dbf82530686043a6e491c6d71befe8f6e
|
| seed 產生式 | LOWER(RAWTOHEX(STANDARD_HASH('4111111111111111','SHA256'))) |
本機 printf '4111111111111111' | shasum -a 256
|
9bbef19476623ca56c17da75fd57734dbf82530686043a6e491c6d71befe8f6e |
→ 三者一致,確認採小寫十六進位。程式以 Sha256Hex(MessageDigest + String.format("%02x", b))實作,
與 party-service.AccountService.fingerprint 同一寫法。
⚠️ seed 之
card_no_enc是佔位:TO_BLOB(HEXTORAW(RAWTOHEX(STANDARD_HASH(...))))為 32 位元組雜湊,
不是自描述密文,故對 seed 卡片呼叫decrypt會拋CryptoException。
僅本期發卡所產生之卡片可解密(此為 seed 既有註記,非本期引入,見 §10 #7)。⚠️ 遮罩格式與 seed 不一致:seed 為
**** **** **** 1111(僅留末 4),
本期依PiiMasker(SAD-005 §5之既定工具)產出411111******1111(前 6 後 4)。
二者皆符 PCI DSS(可揭露前 6/末 4);seed 僅供本地開發(SAD-006 §2.1禁用於 SIT 以上),
故以PiiMasker為準,差異記 §10 #12。
7.4 發卡與給號(POST /api/v1/cards)
輸入 CreateCardRequest{accountId, partyId, productCode, specifiedCardNo?, ticketType?}。
ticketType(v1.4 起持久化):EASYCARD/IPASS/ICASH,null=非票證卡。
v1.3 及以前為「接受但不持久化」(§10 #14);v1.4 依 BEAM PMO 2026-09-20 核定「補 DDL」落地:
①SDD-001 §3.2CARD 實體補入ticket_type(權威來源)
②V3__card_ticket_type.sql追加欄位 +CHECK約束(不改 V1 基線,SAD-003 §9)
③領域列舉CardTicketType④JDBC 讀寫 ⑤冪等指紋納入ticketType。
列舉不符 →CC-CARD-4001,於建卡前檢核(避免由 DB 約束擋下而誤報 500)。
| 步驟 | 規則 | 失敗 |
|---|---|---|
| ① 冪等 | 查 IdempotencyStore(鍵=X-Idempotency-Key,指紋=請求內容 SHA-256) |
同鍵同指紋 → 回前次結果;同鍵異指紋 → CC-CARD-2001(409) |
| ② 號碼池 | 取 card_no_pool 中 status='ACTIVE' 且 used_count < total_count 之池 |
無可用 → CC-CARD-1003(422) |
| ③ 給號 |
亂數:於 range_start..range_end 隨機取號,以 Luhn 補末位檢查碼,並以 CardNumber.isWellFormed 複核指定: specifiedCardNo(7–8 碼區段)拼入 BIN 後須通過 isWellFormed
|
格式不符 → CC-CARD-4001
|
| ④ 不重複 | 寫入 issued_card_numbers(card_no_hash 為 PK) |
PK 衝突 → 重試,最多 5 次;仍失敗 → CC-CARD-1003
|
| ⑤ 三態衍生 | 依 §7.3 產生 enc/masked/hash | — |
⑤-a 來源申請案件(CR-020 F1)
|
CreateCardRequest.applicationId(可為 null):有值時須為已核准案件(未核准 → CC-CARD-1002;查無 → CC-CARD-4040);null 時仍可發卡 |
查無 → CC-CARD-4040;未核准 → CC-CARD-1002
|
| ⑥ 寫入 |
cards(block_code='INACTIVE'、status='UNACTIVATED'、instant_issued=0)+ issued_card_numbers + card_no_pool.used_count+1(樂觀鎖)+ applicationId 有值時回填 CARD_APPLICATIONS.CARD_ID(updateCardId,與狀態更新分離——不併入樂觀鎖以免「回填關聯」與「狀態推進」互相牽連版本)
|
併發衝突 → CC-CARD-3001(409) |
⑥-b 建立交易載體(CR-056 §12,2026-10-17)
|
同交易建立 PHYSICAL 載體:Instrument.register(UUID, PHYSICAL, cardId, cardNoHash, null, null, accountId)。cardNoHash 取剛寫入之卡片之雜湊(確保 card_id/card_no_hash 同源) |
失敗即整筆回滾(含已寫入之卡片)——不留「有卡無載體」之假成功 |
| ⑦ 效期 |
expiry_date = 發卡日 +5 年(⚠️ 契約未定義,見 §10 #9) |
— |
| ⑧ 回應 | 201 CardView——僅含 cardNoMasked,不含明文/雜湊/密文 |
— |
⚠️ 為何發卡必須自動建載體(
CR-056§12):limit-service之卡片級額度查詢鍵為
instrumentId(GET /api/v1/instruments/{instrumentId}/limits),不是cardId。
若發卡不建載體,新卡在 360 的鏈路會斷在最後一環
(parties ✅ → cards ✅(instrumentId=null)→ limits ✗),且需人工POST /instruments補登。
為何不採事件驅動:載體是發卡的固有部分,非外部副作用;非同步會留下
「發卡成功、載體稍後才到」之視窗,而該視窗內 360 仍缺額度。故與pool.reserve
同為同交易語意。重複註冊防護:instruments對(type, natural_key)有唯一約束,
且InstrumentService.register於寫入前先existsByNaturalKey→ 手動重複註冊得
CC-CARD-1004,不會產生第二列。
⚠️ 不支援「依產品選池」:
card_no_pool之 DDL 無product_code欄位,
故本期不分產品選池,一律取第一個可用池(實測ORDER BY bin)。
卡號之bin由號碼池之區間決定,cards.bin存卡號前 6 碼(見 §10 #16)。⚠️ 為何發卡要帶
applicationId並回填關聯(CR-020F1,BEAM PMO 裁定方案 A):
開卡(ACTIVATE)須驗證三要素,驗證之比對基準為「本卡所屬申請案件之核准序號」;
而cards與card_applications原無任何關聯欄位(實測 16 卡僅 5 張能以
account_id對上,且同帳戶多卡會共用核准序號=語意錯誤)。
由發卡流程一次性回填card_id,是唯一語意正確之權威來源;
不由呼叫端每次傳遞(開卡是卡片自身之狀態轉換,卡片應能追溯來源)。
代價(fail-closed,刻意):無來源案件之卡(含多數既有種子/測試卡)無法開卡,
ACTIVATE回CC-CARD-1002——不得為相容舊資料而放寬驗證。為何需要
issued_card_numbers這層防線:cards.uq_card_hash雖有 UNIQUE,
但卡片CLOSE後未必保留;以獨立表記錄「曾發出」可避免重發已用過之卡號,
且 PK 衝突是原子的(不需先查再寫,無 TOCTOU 競態)。
7.5 快發快綁(POST /cards/{cardId}/instant-issuance)
輸入 InstantIssuanceRequest{applicationId, fastIssueAgreed, bindPlatform?}。
| 步驟 | 規則 | 失敗 |
|---|---|---|
| ① 卡片存在 |
cardId 查無 |
CC-CARD-4040(404) |
| ② 條款同意 |
fastIssueAgreed == true 方得線上發卡 |
否則 CC-CARD-1001(422) |
| ③ 申請案件 | 查 applicationId;須 status='APPROVED'
|
查無 → CC-CARD-4040;非核准 → CC-CARD-1002(422) |
| ④ 三要素 |
idNo/birthDate/approvalNo 三者皆須有值,且 approvalNo 與案件之 approval_no 相符 |
否則 CC-CARD-1002(422) |
| ⑤ 生效 |
instant_issued=1、block_code='ACTIVE'、status='ACTIVE'(樂觀鎖) |
併發衝突 → CC-CARD-3001
|
| ⑥ 綁定 |
bindPlatform 有值 → 依 §7.6 一併建立 Token |
同 §7.6 |
⚠️ 三要素驗證為「形式」而非「實質」:本期
ThreeFactorVerifier之預設實作
僅驗證格式與有值,未介接行內身分驗證/徵信系統(屬外部系統介接,非本期範圍)——見 §10 #10。
7.6 Token 綁定與查詢(POST/GET /cards/{cardId}/tokens)
| 規則 | 失敗 |
|---|---|
cardId 查無 |
CC-CARD-4040(404) |
platform 須在 7 值列舉內(APPLE_PAY…QR_CODE) |
CC-CARD-4001(400) |
同卡同 platform 已有 ACTIVE Token → 重複綁定 |
CC-CARD-1004(422) |
GET 回傳 TokenView[](含全部 status,不篩除 DELETED) |
— |
token_no = tok_ + 32 位隨機十六進位。
⚠️ 本期為服務自行產生;實務應由 Token Service Provider(TSP)簽發(見 §10 #11)。
7.7 申請案件(POST /api/v1/card-applications)
| 步驟 | 規則 | 失敗 |
|---|---|---|
| ① 冪等 | 同 §7.4 ① |
CC-CARD-2001(409) |
| ② 欄位 |
channel 須在 5 值列舉內;accountId/productCode 必填 |
CC-CARD-4001(400) |
| ③ 案件編號 |
apply_no = APP + yyyyMMdd + 8 碼隨機英數字(UNIQUE,撞號重試 3 次) |
— |
| ④ 寫入 |
status='RECEIVED'、fast_issue_flag 依請求、approval_no=null
|
— |
| ⑤ 回應 | 201 CardApplicationView
|
— |
核准(
APPROVED)不屬本期:無審核流程端點,故快發快綁之「案件須已核准」
於本地測試時須以直接更新資料方式準備(見 §9 之E2E-CARD-03)。
7.8 本期新增錯誤碼對照
| 碼 | HTTP | 觸發情境 |
|---|---|---|
CC-CARD-1001 |
422 | 快發條款未同意(fastIssueAgreed != true) |
CC-CARD-1002 |
422 | 三要素不符/案件未核准 |
CC-CARD-1003 |
422 | 號碼池用盡或無相符號碼池 |
CC-CARD-1004 |
422 | 同卡同 platform 重複綁定 Token |
CC-CARD-2001 |
409 | 冪等鍵衝突(同鍵異指紋) |
CC-CARD-4001 |
400 | 欄位/列舉不符 |
CC-CARD-4040 |
404 | 卡片/申請案件/Token 查無 |
7.9 統計端點(GET /cards/stats、GET /card-applications/stats)——2026-10-01 補建(CR-026)
緣由:
CR-026(營運總覽之業務 KPI 統計端點)。/dashboard之「業務 KPI」區塊需各服務提供統計端點
——經查 17 服務皆無,故本節為規格先行之補建。
7.9.1 端點規格
| 方法 | 路徑 | 語意 | 回應 |
|---|---|---|---|
GET |
/api/v1/cards/stats |
卡片統計 | 200 CardStats
|
GET |
/api/v1/card-applications/stats |
進件統計 | 200 CardApplicationStats
|
★
CR-043 §3.1#4/#5:回應型別原名CardStatsView/CardApplicationStatsView,與實作(domain/model/CardStats/CardApplicationStats)不一致,已統一為實作名。
CardStats:
| 欄位 | 型別 | 說明 |
|---|---|---|
total |
integer | 卡片總數 |
byStatus |
object(狀態 → 筆數) |
UNACTIVATED/ACTIVE/SUSPENDED/CLOSED(DDL CHECK 之 4 值) |
byBlockCode |
object(區塊碼 → 筆數) |
INACTIVE/ACTIVE/LOCKED/LOST/REISSUED/EXPIRED/CLOSED(7 值) |
CardApplicationStats:
| 欄位 | 型別 | 說明 |
|---|---|---|
total |
integer | 申請案件總數 |
byStatus |
object(狀態 → 筆數) |
RECEIVED/REVIEWING/SUPPLEMENT/APPROVED/REJECTED(5 值) |
pendingReview |
integer |
待審件數 = REVIEWING + SUPPLEMENT
|
7.9.2 ★ 「待審」之定義(不得自行推導)
pendingReview = status IN ('REVIEWING','SUPPLEMENT')。
| ⚠️ 易錯點 | 說明 |
|---|---|
RECEIVED 不計入 |
已受理但尚未送徵信(SUBMIT_REVIEW 未執行)——不屬「待審」 |
SUPPLEMENT 計入 |
補件中=仍在審查流程(等客戶補件後續審) |
| 不變式 |
pendingReview ≤ total,且 必等於 byStatus['REVIEWING'] + byStatus['SUPPLEMENT']
|
7.9.3 實作要求
-
以
GROUP BY聚合(單次查詢),不得逐狀態各發一次COUNT(N+1)。 -
total由後端計算(COUNT(*)),不得由前端相加分組——若後端有未列於分組之狀態,相加會少算。 -
byStatus/byBlockCode僅含「實際存在」之值(不補 0)——補 0 會使「該狀態無資料」與「該狀態不存在」混淆。
⚠️ 前端顯示時未出現之狀態視為 0(顯示層處理),後端不回傳假 0。 - 不快取——統計須即時。
-
不加查詢參數(無日期區間)——本階段為總量統計;時序/區間統計屬後續(
CR-026 §3)。
7.10 卡片狀態事件輸出(card.card.status-changed.v1)——2026-10-01 新增(GOV-006 4.8 W-2 第二片)
緣由:
SAD-004 §5.2載「card.card.status-changed.v1→ authorization, party」,
惟本服務從未發射任何事件(2026-10-01 實測:事件型別字串僅現於common-event)。
authorization-service之讀模型(card_status_caches)因此恆空(見SCS-001 §7.12)。本節補生產端。
觸發:POST /cards/{cardId}/lifecycle-transitions(狀態轉換成功時)。
機制(Transactional Outbox,SDD-004 §4.3):
| 步驟 | 說明 |
|---|---|
| 1. 寫入 | 於 CardQueryService.transition 之同一交易內寫 outbox(SDD-001 §3.15 形狀) |
| 2. relay |
OutboxRelayScheduler(@Scheduled,預設 2s)呼叫 common-event 之 OutboxPublisher.publishBatch
|
| 3. 發布 |
KafkaEventPublisher(KafkaTemplate<String,String>)送 Kafka;主題=cc.card.card.status-changed
|
信封(SAD-004 §5.1;eventType=card.card.status-changed.v1):
| 欄位 | 值 |
|---|---|
aggregateId/分區鍵 |
cardId(同卡有序) |
sequenceNo |
CardEventFactory.nextSequenceNo()(與同一 cardId 之其他事件共用來源;供消費端亂序防護) |
payload |
instrumentId/cardNoHash/cardId/accountId/cardNoMasked/status/blockCode/expiryDate
|
設計要點:
- ⚠️ 與權威寫入同交易——否則會出現「狀態已改、事件未發」之落差。
-
instrumentId由CC_CARD.INSTRUMENTS依cardId(查無時再以cardNoHash)解析;尚未回填載體之既有卡片保留null,不得臆造鍵值,供事件向後相容。 - ⚠️ Kafka 值序列化須為
StringSerializer——payload 已是信封 JSON 字串;
若用JsonSerializer會多包一層引號並轉義,消費端解析失敗(實測踩過)。 - ⚠️ relay 失敗不降級——單列失敗累加
retry_count(上限 5,達上限即 ERROR 待人工),不改寫本地檔案、不假成功。 -
退避由排程週期決定,不得
Thread.sleep(CSP-003 §7 E5)。
真機驗證(2026-10-01;真實 Kafka + Oracle,與 SCS-001 §7.12 之消費端串測):
LOCK → outbox 落列且 published_at 非空 → 授權讀模型更新為 SUSPENDED|LOCKED|seq=1;
再 UNLOCK → ACTIVE|ACTIVE|seq=2|v=2(seq 單調遞增、版本遞增);outbox 2 列皆已發布。
7.11 發卡事件輸出(card.card.issued.v1)——2026-10-01 新增(GOV-006 4.8 W-2 推廣・CR-027 批次 A-3)
緣由:
SAD-002 §3.3明列本服務發布card.card.issued.v1,SDD-006 §4.2亦已定義 payload,
惟從未有人發射(同§7.10之型)。
主題:cc.card.card.issued(命名規則 SAD-004 §5.4)。
| 項目 | 值 |
|---|---|
eventType |
card.card.issued.v1 |
Outbox aggregate_type
|
card |
Outbox event_type
|
card.issued(不含版本) |
| Kafka 分區鍵 | cardId |
payload(v1;欄位名依 SDD-006 §4.2):cardId/cardNoMasked/productCode/instantIssued。
處理:CardIssueService.issue 於號碼池占用成功之後、同一交易寫 outbox。
⚠️ 刻意置於樂觀鎖之後——若 poolPort.reserve 失敗會整筆回滾,屆時不應留下事件。
設計要點:
- ★ 只帶遮罩卡號(
cardNoMasked,如**** **** **** 1111)——不含明文;
與§7.10(card.card.status-changed.v1)之既有作法一致(測試以assertFalse(contains(明文))釘住)。
7.12 Token 綁定事件輸出(card.token.bound.v1)——2026-10-01 新增(GOV-006 4.8 W-2 推廣・CR-027 批次 A-3)
主題:cc.card.card.token.bound。payload:tokenId/cardId/platform。
處理:CardTokenService.bind 於 tokenRepository.save 後、同一交易寫 outbox。
分區鍵=cardId(Token 屬卡片聚合之一部分,與 §7.10/§7.11 同鍵)。
序號來源已統一(CR-030 §2 #12;詳 §7.13):
| 事件 |
sequenceNo 來源 |
|---|---|
card.card.status-changed.v1(§7.10) |
CardEventFactory.nextSequenceNo() |
card.card.issued.v1/card.token.bound.v1(§7.11/§7.12) |
CardEventFactory.nextSequenceNo() |
三者之 aggregateId 同為 cardId,且共用同一單調序號來源;消費者可依 (aggregateId) 進行亂序防護。惟序號產生器目前僅保證同 JVM,多實例部署仍待後續以持久化序號強化。
真機驗證(2026-10-01;真實 Oracle + Kafka):
POST /api/v1/cards(PLATINUM、號碼池給號)→ outbox 落列(card.issued)→ relay 已發布;
POST /cards/{cardId}/tokens(APPLE_PAY)→ outbox 落列(card.token.bound)→ 已發布;
Kafka 主題 cc.card.card.issued/cc.card.card.token.bound 皆自動建立,aggregateId=cardId;
payload 與 SDD-006 §4.2 逐欄一致:
{"cardId":…,"cardNoMasked":"411111******2640","productCode":"PLATINUM","instantIssued":false}/
{"tokenId":…,"cardId":…,"platform":"APPLE_PAY"}(卡號為遮罩值,非明文)。
7.13 sequenceNo 來源統一——2026-10-01 新增(CR-030 §2 #12)
緣由(★ 靜默型缺陷):本服務原有兩個序號來源——
card.issued/token.bound用
時間戳基底序號(CardEventFactory.nextSequenceNo()),card.status-changed用卡片版本。
惟三者之aggregateId同為cardId→ 同一聚合之事件序號可能交錯,
使消費者之亂序防護(sequenceNo <= MAX(aggregateId))將後發事件誤判為亂序而略過——
靜默資料遺失,且無錯誤訊息。
裁定(CR-030 §2 #12):依 SAD-004 §5.4 之不變式
「同一 aggregateId 之所有事件型別,須共用同一單調序號來源」——
| 事件 | 修正前 | 修正後 |
|---|---|---|
card.card.issued.v1 |
CardEventFactory.nextSequenceNo() |
不變 |
card.card.token.bound.v1 |
CardEventFactory.nextSequenceNo() |
不變 |
card.card.status-changed.v1 |
card.version() + 1 |
CardEventFactory.nextSequenceNo() |
⚠️ 既有消費端之相容性:
authorization-service之card_status_caches.last_sequence_no存有舊值(小);
新序號為時間戳基底(大) → 不會誤判為亂序(方向安全)。
⚠️ 已知限制(保留):序號產生器為同 JVM 內遞增,多實例部署不安全;
正解為「聚合層級持久化序號」——列為後續強化項。
7.14 交易載體(BR-16;CR-046 B2-6)
權威主檔:CC_CARD.INSTRUMENTS(V9__instrument.sql)。交易載體最小識別為 instrumentId;
實體卡、虛擬卡號、Token 分別以互斥的 cardNoHash、virtualNo、tokenRef 識別。
| 端點 | 必填標頭 | 行為 |
|---|---|---|
POST /api/v1/instruments |
X-Channel、X-Idempotency-Key
|
註冊載體;PHYSICAL 若帶 cardId,歸戶與卡號雜湊以卡片權威;不一致回 CC-CARD-4001
|
GET /api/v1/instruments/{instrumentId} |
— | 查詢載體;回應不含原始敏感識別 |
GET /api/v1/accounts/{accountId}/instruments |
— | 依歸戶列舉,含 RETIRED 供稽核 |
POST /api/v1/instruments/{instrumentId}/status-transitions |
X-Channel、X-Idempotency-Key
|
ACTIVE/SUSPENDED/RETIRED 轉換;RETIRED 為終態 |
冪等/併發:註冊與狀態轉換均以請求內容 SHA-256 做冪等指紋;同鍵異指紋回 CC-CARD-2001;
自然鍵重複回 CC-CARD-1004;狀態更新採 version 樂觀鎖,衝突回 CC-CARD-3001。
★ 發卡自動建載體(
CR-056§12,2026-10-17):POST /api/v1/cards於同一交易內
建立 PHYSICAL 載體(見 §7.4 步驟 ⑥-b),故POST /api/v1/instruments對
本期發卡產生之卡片而言是「補登/特殊載體」用途,非發卡之必要後續動作。
既有(V9之前/本期前)已發之卡片仍未回填載體 → 其instrumentId為null,
需以一次性維運程式補建(不得臆造鍵值)。
個資邊界:InstrumentView 不暴露 cardNoHash/virtualNo/tokenRef;實體卡僅可回傳既有遮罩顯示值。
7.15 查卡清單(依帳戶/持卡人)——2026-10-17 新增(CR-056 第一步)
⚠️ 順序如實記錄:本節為「契約先行 → 實作 → SCS 補記」,非嚴格規格先行
(BEAM PMO 要求「先寫能跑的程式、文件墊後」;功能範圍已於CR-056 §3先行界定)。
端點:GET /api/v1/cards?accountId=…/?partyId=…
| 項目 | 規則 |
|---|---|
| 必填條件 |
accountId 與 partyId 至少一項;皆缺 → CC-PLT-4003(400) |
| ★ 為何不以「查全部」為預設 | 無範圍掃描+可能洩漏他人卡片;fail-closed |
| 排序 | created_at DESC |
| 查無 | 空清單(非 404、非 null) |
回應 CardSummaryView[]:cardId/accountId/partyId/cardNoMasked/productCode/
bin/expiryDate/status/blockCode/instantIssued。
守界:
- 不含卡號雜湊與明碼(清單不需要;雜湊可作比對鍵,洩漏風險)。
-
不含額度——權威在
limit-service(CARD維度),由呼叫端另行查詢(避免跨服務直呼與雙頭馬車)。
★ 能力邊界(誠實揭露):本端點不足以完成「以身分證字號查全部卡」——
parties 綁 account_id(同一人跨帳戶為不同 party 記錄),且身分證僅有加密欄與遮罩欄
(遮罩不可作比較鍵)→ 需 id_no_hash(CR-056 §4,資深決策待裁)。
8. 外部依賴
| 依賴 | 用途 |
|---|---|
Oracle CC_CARD
|
權威儲存 |
common-saga(僅狀態機引擎) |
狀態檢核 |
9. 測試案例
| 編號 | 情境 | 結果 |
|---|---|---|
| UT-CARD-01 | LOCK(ACTIVE→LOCKED) | 201,可交易=False |
| UT-CARD-02 | UNLOCK(LOCKED→ACTIVE) | 201,可交易=True |
| UT-CARD-03 | 非法:ACTIVE + ACTIVATE |
409 / CC-CARD-3001
|
| UT-CARD-04 | CLOSE(→CLOSED 終態) | 201 |
| UT-CARD-05 | 對終態 CLOSED + ACTIVATE |
409 / CC-CARD-3002
|
| E2E-CARD-01 | 鎖卡後授權 |
DECLINED(CARD_STATUS) |
| E2E-CARD-02 | 解鎖後授權 | APPROVED |
| UT-CARD-06 | 發卡(亂數給號) | 201;卡號通過 Luhn;card_no_enc 可解密回原號 |
| UT-CARD-07 | 發卡卡號三態一致 | sha256(解密後卡號) == card_no_hash |
| UT-CARD-08 | 號碼池用盡/無相符池 |
422 / CC-CARD-1003
|
| UT-CARD-09 | 指定卡號未通過 Luhn |
400 / CC-CARD-4001
|
| UT-CARD-10 | 發卡冪等:同鍵同指紋 | 回前次結果,卡片數不增加 |
| UT-CARD-11 | 發卡冪等:同鍵異指紋 |
409 / CC-CARD-2001
|
| UT-CARD-12 | 快發快綁:fastIssueAgreed=false
|
422 / CC-CARD-1001
|
| UT-CARD-13 | 快發快綁:三要素不符 |
422 / CC-CARD-1002
|
| UT-CARD-14 | 快發快綁成功 |
instant_issued=1、block_code=ACTIVE、status=ACTIVE
|
| UT-CARD-15 | Token 綁定成功 | 201;GET 可查得 |
| UT-CARD-16 | Token 重複綁定(同 platform) |
422 / CC-CARD-1004
|
| UT-CARD-17 | Token platform 不在列舉 |
400 / CC-CARD-4001
|
| UT-CARD-18 | 建立申請案件 | 201;status=RECEIVED、apply_no 唯一 |
| UT-CARD-19 | 申請案件 channel 不在列舉 |
400 / CC-CARD-4001
|
| UT-CARD-20 | 卡片/申請案件查無 |
404 / CC-CARD-4040
|
| E2E-CARD-03 | 真實 Oracle:發卡 → 查卡片 | 發卡後以 card_no_hash 查得到 |
| E2E-CARD-04 | 真實 Oracle:發卡後解密還原 |
FieldEncryptor.decrypt(card_no_enc) == 原卡號 |
10. 已知限制
-
發卡(亂數給號)未實作→ 已於 v1.2 實作並實測(見 §9.1) | -
快發快綁未實作→ 已實作並實測 | -
Token 綁定未實作→ 已實作並實測 | -
申請案件未實作→ 已實作並實測 | -
status維度未做轉換(僅攜帶) | -
監控未接 |
-
seed 之
card_no_enc為佔位密文(STANDARD_HASH32 位元組),非自描述密文 →
對 seed 卡片呼叫decrypt會拋CryptoException;僅本期發卡產生之卡片可解密
(seed 既有註記,非本期引入) | -
X-Channel標頭列舉與card_applications.channel欄位列舉不一致
(WEB/MOBILE/PARTNER/BATCH/SYSTEMvsWEB/MOBILE/API/BRANCH/MERCHANT)。
本期以請求 body 為準,標頭僅供稽核。是否須對齊,待定奪 | -
expiry_date之年期(5 年)為本期假設——契約未定義效期規則 | -
三要素驗證僅為形式:
ThreeFactorVerifier預設實作只驗證「有值」,
未介接行內身分驗證/徵信系統 | -
token_no由服務自行產生,非由 TSP(Token Service Provider)簽發 | -
card_no_masked格式與 seed 不一致:本期依PiiMasker產411111******1111,
seed 為**** **** **** 1111;seed 僅供本地(SAD-006 §2.1),故以PiiMasker為準 | -
冪等僅實作於「發卡」與「申請案件」二個建立型端點;既有的→ ✅ 已於
lifecycle-transitions仍無冪等(既有缺口,非本期引入)CR-020解除
(2026-09-23):lifecycle-transitions現確實綁定X-Channel/X-Idempotency-Key
並落實冪等(同鍵同指紋重放、同鍵異指紋CC-CARD-2001,鑑別值CARD_LIFECYCLE_TRANSITION)。
⚠️ 併請注意本服務仍有一處未解之冪等缺口(非本 CR 範圍,登記供後續):
POST /api/v1/cards/{cardId}/instant-issuance僅綁定X-Channel、
未綁定X-Idempotency-Key(契約宣告為必要) | -
→ 已於 v1.4 解除(BEAM PMO 2026-09-20 核定「補 DDL」):ticketType無對應欄位
SDD-001 §3.2補ticket_type→V3__card_ticket_type.sql追加欄位(VARCHAR2(24 CHAR),
CHECK (ticket_type IN ('EASYCARD','IPASS','ICASH')))→ 領域列舉CardTicketType+ JDBC 讀寫 +
冪等指紋納入。實測見 §9.1 #15–#18。
✅ 殘留缺口亦已於 v1.5 解除(BEAM PMO 2026-09-20 核定「CardView揭露ticketType」):
契約(gen_openapi.py→--write)與 DTO(CardController.CardView)同步補ticketType
(nullable,值域同CreateCardRequest.ticketType,非票證卡為null)。
⚠️ 實作時發現「只改CardView仍是半套」——查詢路徑回傳的是另一個 DTO
CardStatusView,故會變成「發卡當下看得到、事後GET /cards/{cardId}卻查不到票種」。
已一併揭露於CardStatusView(契約與 DTO 同步)。實測見 §9.1 #19–#21。
⚠️ 順帶修正反向不一致:契約CardStatusView原缺transactable/expired
(實物有、契約無),已一併補齊,使契約與實物一致 | -
specifiedCardNo之契約描述與卡號結構不符:契約註記為「7-8 碼區段」,
但 16 碼卡號=15 碼主體+1 碼檢查碼;本期以「15 碼數字主體」為準,
長度不符即CC-CARD-4001| -
card_no_pool無product_code欄位 → 不支援「依產品選號碼池」 -
快發快綁無→ 已於 v1.3 接上並實測:bindPlatform之實際綁定
CardIssueService注入CardTokenService,生效更新後於同一交易內建立 Token;
綁定失敗(列舉不符CC-CARD-4001/重複綁定CC-CARD-1004)連帶回滾生效更新
(實測證明見 §9.1 #13–#14) | -
契約
CardView.createdAt無來源(本次作業中發現,未解):契約列有createdAt,
但CardViewDTO 與IssueResult皆無此欄,Card聚合亦無建立時間欄位
(CC_CARD.CARDS無對應欄)。故該欄契約有、實物無——
屬「文件與實物不一致」,須補 DDL(類 #14 當初之作法)或自契約移除。
⚠️ 本期不擅改:補 DDL 屬結構變更,須 BEAM PMO 核定 -
card.card.issued.v1/card.token.bound.v1之sequenceNo與status-changed不同源(§7.11/§7.12,#A-126):
前者用同 JVM 內遞增之時間戳基底序號,後者用卡片版本;惟二者aggregateId同為cardId
→ 同一聚合之不同事件型別可能序號交錯。故消費者之亂序判準應為 (aggregateId, eventType)。
已列CR-027 §5 #12待裁定(統一序號來源)。 -
本二事件目前無消費者(
§7.11/§7.12,#A-126):SAD-002 §3.3列之訂閱者(如party-service)
尚未實作消費端 → 現階段為「有人發、沒人收」(如實揭露)。 |
9.1 真實 Oracle 實測結果(2026-09-20)
| # | 情境 | 結果 |
|---|---|---|
| 1 |
POST /api/v1/cards(亂數給號) |
201;blockCode=INACTIVE、status=UNACTIVATED、instantIssued=false
|
| 2 | 冪等重放(同鍵同內容) |
201 且回同一 cardId,卡片數不增加 |
| 3 | 冪等衝突(同鍵異內容) |
409 CC-CARD-2001
|
| 4 | POST /api/v1/card-applications |
201;status=RECEIVED、applyNo=APP2026092015289764
|
| 5 | 申請案件 channel=PARTNER
|
400 CC-CARD-4001
|
| 6 |
POST /cards/{cardId}/tokens(APPLE_PAY) |
201;tokenNo=tok_…
|
| 7 | 同平台重複綁定 |
422 CC-CARD-1004
|
| 8 | GET /cards/{cardId}/tokens |
200 陣列 1 筆 |
| 9 | 快發快綁 fastIssueAgreed=false
|
422 CC-CARD-1001
|
| 10 | 快發快綁三要素不符 |
422 CC-CARD-1002
|
| 11 | 快發快綁成功 |
201;blockCode=ACTIVE、status=ACTIVE、instantIssued=1
|
| 12 | 依卡號雜湊查卡片(供授權) |
200;transactable=true
|
| 13 | 快發快綁 bindPlatform=GOOGLE_PAY
|
201;回應 tokens[] 含該 Token,庫內 tokens 有 GOOGLE_PAY/ACTIVE 列 |
| 14 | 快發快綁 bindPlatform=NO_SUCH_PAY
|
400 CC-CARD-4001;且卡片仍為 INACTIVE/UNACTIVATED/instant=0、tokens 0 列——證明綁定失敗連帶回滾生效更新(原子性) |
| 15 | 發卡 ticketType=IPASS
|
201;cards.ticket_type = IPASS(真實 Oracle 讀回確認) |
| 16 | 發卡 ticketType=NO_SUCH_TICKET
|
400 CC-CARD-4001(訊息含允許值);卡片未建立 |
| 17 | 同冪等鍵、僅 ticketType 不同(EASYCARD → ICASH) |
409 CC-CARD-2001——證明 ticketType 已納入冪等指紋;若漏納會被誤判為重放而回前次結果 |
| 18 |
繞過應用層直接 INSERT ... ticket_type='BITCOIN'
|
ORA-02290 check constraint violated,BAD_ROWS=0——證明 DB CHECK 為第二道防線(應用層被繞過時仍然有效) |
| 19 | 發卡 ticketType=ICASH → 回應是否帶出票種(SCS-003 §10 #14) |
是:ticketType='ICASH'(v1.5 前 無此欄) |
| 20 | 事後 GET /cards/{cardId} 是否查得到票種(v1.5 一併修 CardStatusView) |
是:ticketType='ICASH'(修前查無,屬「發卡看得到、事後查不到」之半套) |
| 21 | 一般卡(未帶 ticketType) |
發卡與查詢皆為 null(契約 nullable) |
#14 之原子性為何須以真實資料庫證明:單元測試直接
new服務時沒有 Spring 交易代理,
對假物件之寫入不會因例外還原——在此斷言「未生效」會是假驗證(與實際行為無關)。
故該性質僅由本節真實 Oracle 實測佐證,單元測試只驗證錯誤碼與「未留下 Token」。
卡號三態一致性之證明(非推論,實測值):
| 項目 | 實測值 |
|---|---|
| 遮罩(回應) | 411111******9682 |
| 由遮罩反推之卡號 |
4111113994379682(16 碼,Luhn 通過) |
sha256(卡號) |
83a168f234c0130300846c447988c985c34e70c5a770ed7f5781192f6800c675 |
庫內 card_no_hash
|
83a168f234c0130300846c447988c985c34e70c5a770ed7f5781192f6800c675 ✅ |
庫內 card_no_enc
|
v1:dev-2026:YkiKAQNHdT7vuva/:oCeXW2w0uvQ(73 bytes,自描述密文) |
密文前綴
v1:dev-2026:證明是真實 AES-GCM 加密,
與 seed 之 32 位元組STANDARD_HASH佔位截然不同(後者無法解密)。
號碼池與樂觀鎖:發卡 2 張後 used_count=2、version=2——證明樂觀鎖每次占用皆生效。
本節 #1–#4 已解除(見 §11 v1.2)。
11. 修訂記錄
| 版本 | 說明 |
|---|---|
| 1.14 | 2026-10-17 |
| 1.13 | 2026-10-17 |
| 1.12 | 2026-10-16 |
| 1.11 | 2026-10-01 |
| 1.10 | 2026-10-01 |
| 1.9 |
GOV-006 4.8 W-2 第二片:§7.10 卡片狀態事件輸出(2026-10-01)——新增 card.card.status-changed.v1 之生產端。緣由:SAD-004 §5.2 載本事件之消費者,惟本服務從未發射(實測:事件型別字串僅現於 common-event)→ 授權讀模型恆空。實作:CardQueryService.transition 於同一交易寫 outbox(V8__outbox.sql);OutboxRelayScheduler(@Scheduled)+ KafkaEventPublisher + OutboxRelayConfig(組裝 common-event 之 OutboxPublisher);⚠️ Kafka 值序列化改 StringSerializer(payload 已為信封 JSON 字串)。真機:LOCK → outbox 已發布 → 授權讀模型 SUSPENDED|LOCKED|seq=1;UNLOCK → ACTIVE|ACTIVE|seq=2|v=2
|
| 1.0 | 初版:回溯補建,與既有程式一致 |
| 1.3 | 2026-09-20(HANDOVER #C-10 未竟事項):§10 #17 解除——bindPlatform 由「僅記錄於日誌」改為實際建立 Token(CardIssueService 注入 CardTokenService,同交易內綁定,失敗回滾);§9.1 補 #13–#14 實測(含原子性證明,並註明為何不能由單元測試佐證);回應 CardView.tokens 由恆空改為帶入綁定結果。測試 36 → 38
|
| 1.4 | 2026-09-20 |
| 1.5 | 2026-09-20 |
| 1.2 | 2026-09-20(HANDOVER #C-10):實作完成並以真實 Oracle 實測——新增 §9.1 實測結果(12 情境全數符合預期,含卡號三態一致性之數學證明);解除 §10 #1–#4;補 #14–#17 揭露(ticketType 無欄位、specifiedCardNo 契約描述不符、號碼池無 product_code、快發快綁之 bindPlatform 未實際建立 Token);§7.4 ② 更正為「不依產品選池」(原寫法與 DDL 不符);配套修正:既有 GET /cards/{cardId} 查無由 CC-CARD-3001(409) 改為 CC-CARD-4040(404)(經查無任何呼叫端依賴);新增 V2__card_idempotency.sql;補 04-dev-card-seed.sql 之號碼池與已發出卡號 |
| 1.1 | 2026-09-20(HANDOVER #C-10):SPEC 先行——新增 §7.3 卡號三態、§7.4 發卡給號、§7.5 快發快綁、§7.6 Token、§7.7 申請案件、§7.8 錯誤碼對照;§4 補類別、§5 補 4 張表並更正 card_no_hash 為 CHAR(64)(原誤記 CHAR(256));§9 補 15 個測試案例;§10 補 #7–#13 揭露。配套: SDD-006 §3.4 補登 CC-CARD-1004/2001/4001/4040(119 碼);契約瑕疵修正見 §6.1 |