Project

General

Profile

CR #248 » CR-047_活動回饋上限(cap)用量模型與回饋價值換算參數化.md

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

 

CR-047 活動回饋上限(cap)用量模型與回饋價值換算參數化

項目 內容
文件編號 CR-047
版本 1.0
狀態 施工中(BEAM PMO 2026-10-17 裁定:① 需開 CR 並回訪既有架構;② 點↔元換算之「點」與「元」都要是參數,現防呆為 1 點=1 元)
提出日期 2026-10-17
提出者 開發團隊(WorkBuddy AI)
觸發 CR-046 §7.9 之 cap 兩落差(#A-262)——BEAM PMO 裁定「若需要 CR 才能執行,請開立 CR,並審慎執行,尤其必須回訪既有架構是否需要調整。參數極大化是這次微服務化的宗旨之一,不能用寫死或 workaround 解決」
需求依據 BRD-005 §11(cap 六單位各自可配)、BR-03(回饋計算與上限截斷)、BR-14(預算每戶上限);例 1(每戶每期上限 500 元)、例 3(月上限 50 元)
規格依據 SCS-026 §7.9(提案)→ 本 CR 定案後改為 §7.10;SCS-010 §7(累積)、SCS-002 §7.14(商品參數消費閉環慣例)
影響模組 campaign-service(cap 用量+回填)、rewards-service(活動歸屬+事件欄位)、CC_CPG/CC_RW 遷移

1. 問題陳述(兩落差,實查)

# 落差 實查證據
① campaign_caps 僅有活動級單一 consumed_minor,無任何 scope/period 維度 V1__baseline.sql:(id, campaign_code, cap_unit, cap_value_minor, consumed_minor, …);cap_unit CHECK 含六值(PER_CARD/PER_HOUSEHOLD/PER_STATEMENT/PER_MONTH/PER_YEAR/PER_HOUSEHOLD_MEMBER)
② rewards.points.accrued.v1 payload 無活動歸屬 SCS-010 §7.1 payload=accountId/pointsType/bucket/points/afterBalance——campaign 收到回流無法識別屬哪個活動

兩者皆非加一欄可解:BRD §11 明訂 cap「每卡/每戶/每期/每月/每年 各自可配」——同一活動可並存多組 cap(例:每戶 500 元 且 每月 200 元);且同一 cap 需對多個 scope 實例分別記帳(戶 A/戶 B/2026-10 月/2026 年…)。單表單計數器無法表達。


2. ★ 架構回訪(BEAM PMO 要求;逐項結論)

# 回訪問題 結論 依據
R1 campaign_caps 是否應直接加維度欄? 否——改為「定義/用量」兩表分工:campaign_caps 答「上限是多少」(定義,不變);新表 campaign_cap_usages 答「已用多少」(可變用量,逐 scope 實例一列) 專案既有原則「兩張表分工要能一句話講清」(CR-030 P1 單一真實來源)
R2 用量維度如何參數化(不 hardcode)? 通用 scope_key+period_key:由 cap_unit 決定填入語意——PER_CARD→instrumentId;PER_HOUSEHOLD→householdId;PER_HOUSEHOLD_MEMBER→partyId;PER_STATEMENT→帳單期識別;PER_MONTH→YYYYMM;PER_YEAR→YYYY。新增單位不需改表結構(參數極大化) BEAM PMO 裁定「不能用寫死或 workaround」
R3 上限與用量的單位? 統一以回饋價值(分) 記帳;點數→價值以商品參數換算(見 §3),兩側皆參數 BEAM PMO 裁定 ②
R4 rewards 側是否需調整? 是——AccrualRequest 加可選 campaignCode(來源=campaign 指令)、POINTS_LEDGER 加 campaign_code 可空、points.accrued.v1 payload 加可選 campaignCode(依 SAD-004 §5.3 不升版) 落差 ②;不臆造反查
R5 是否需改 SCS-026 §3 #17? 是——原列五種單位,漏 PER_HOUSEHOLD_MEMBER(DDL 有);本 CR 更正 文件漂移(preflight 紀律)
R6 本期可完整實作之單位? PER_CARD/PER_HOUSEHOLD/PER_MONTH/PER_YEAR(來源齊);PER_STATEMENT(帳單期需 statement 事件)/PER_HOUSEHOLD_MEMBER(附卡人識別需 party 歸屬)→ fail-closed,記 WARN 不臆造 不臆造原則

3. 設計

3.1 用量模型(V6__cap_usages.sql,campaign)

CREATE TABLE campaign_cap_usages (
  id            RAW(16) PRIMARY KEY,
  campaign_code VARCHAR2(40) NOT NULL,
  cap_id        RAW(16) NOT NULL,          -- → campaign_caps.id(定義列)
  scope_key     VARCHAR2(64) NOT NULL,     -- 由 cap_unit 決定之實例鍵(戶/卡/YYYYMM/YYYY)
  period_key    VARCHAR2(16),              -- 期間維度(月/年/帳單期);非期間型為 NULL
  consumed_minor NUMBER(18) DEFAULT 0 NOT NULL,
  created_at / updated_at / version,
  CONSTRAINT uq_cap_usage UNIQUE (cap_id, scope_key, period_key)
);
  • 原子遞增:UPDATE … SET consumed_minor = consumed_minor + ? WHERE cap_id=? AND scope_key=? AND period_key=?(單語句;同 campaign_quotas 慣例)。
  • 事前截斷(指令下發前):以 campaign_caps.cap_value_minor 為界,檢查 consumed_minor + 本次價值 > cap_value_minor → 不發指令(截斷)。
  • 事後記帳(points.accrued 回流):以實際入點價值遞增用量(rewards 實際入點可能因層級倍率與指令推定不同——以回流為準,記 WARN 差異)。

3.2 回饋價值換算(商品參數,兩側皆參數)

參數 命名空間/鍵 語意 預設(防呆)
點值 rewards / point-value-minor 1 點 = N 分(回饋價值) 1(=1 點 1 元,現行防呆)
  • 取值:GET /api/v1/parameters/{ns}/{key}(帶 businessDate;SCS-002 §7.14 已驗證之消費慣例)。
  • 「點」與「元」皆為參數:points 為實際值(資料)、point-value-minor 為換算參數(政策);換算後以分記入用量。改變參數值 → cap 截斷行為隨之改變(可驗收)。
  • 取參失敗 → fail-closed(不截斷亦不臆造換算:記 WARN 並不發指令,沿用 SCS-002 §7.14 A4 降級模式)。

3.3 rewards 側調整(活動歸屬)

變更 內容
AccrualRequest 加可選 campaignCode(campaign 指令已帶;HTTP 契約同步加可選欄位)
POINTS_LEDGER V6__ledger_campaign.sql 加 campaign_code VARCHAR2(40) 可空
points.accrued.v1 payload 加可選 campaignCode(不升版;SDD-006 §4.2 同步)

4. 驗收(真機)

  1. 事前截斷:cap 每戶 500 元(=50,000 分)、點值 1 → 第 3 筆指令使推定價值超限 → 不發;
  2. 事後回填:points.accrued 回流 → 對應 (cap_id, scope_key, period_key) 用量遞增;
  3. 期間邊界:PER_MONTH 跨月重置(10 月用量不計入 11 月);
  4. 換算參數:point-value-minor 由 1 改 2 → 同一筆交易之推定價值加倍 → 截斷點前移(改參數→行為改變,非快取);
  5. fail-closed:取參失敗/PER_STATEMENT/PER_HOUSEHOLD_MEMBER → 不發指令並記 WARN;
  6. 不變式:consumed_minor 不得超過 cap_value_minor(應用層截斷+CHECK 為防線)。

5. 風險與邊界

  1. 推定價值 vs 實際入點:campaign 事前以推定價值截斷、事後以實際回流記帳——兩者可能有差(層級倍率);本期以回流為準並記差異 WARN,不回溯撤銷已入點(撤銷屬 clawback 範圍,BR-15)。
  2. PER_STATEMENT/PER_HOUSEHOLD_MEMBER:fail-closed,待帳單期事件/party 歸屬來源齊備後另立增量。
  3. 疊加(D9):多活動並存之截斷次序(priority/exclusion_group)未實作——本期 cap 僅單活動維度,不得宣稱疊加已正確。

6. 修訂記錄

版本 說明
1.0 2026-10-17:初版。依 BEAM PMO 裁定開立;含架構回訪 R1–R6(定義/用量兩表分工、通用 scope/period 參數化、單位以回饋價值記帳、rewards 側活動歸屬調整、PER_HOUSEHOLD_MEMBER 文件更正、兩種單位 fail-closed);設計、驗收、風險。
1.2 2026-10-17:★ POINTS_LEDGER.CAMPAIGN_CODE 寫入完成(1.1 之待辦結案)——PointsLedgerEntry 加 campaignCode(末尾)+JdbcPointsRepository(INSERT 欄位/綁定、LEDGER_COLUMNS 加欄、mapRow)+AccrualService 帶 request.campaignCode()(到期/兌換分錄為 null);稽核可追溯「此筆點數屬哪個活動」。
1.1 2026-10-17:★ 實作完成(真機通過)。① 遷移:CC_CPG V6__cap_usages.sql(campaign_cap_usages,通用 scope_key/period_key+原子遞增)、CC_RW V6__ledger_campaign.sql(POINTS_LEDGER.campaign_code 可空);② campaign:CapUsageRepository/JdbcCapUsageRepository、CapGuardService(事前截斷/事後回填)、PointAccruedListener(cc.rewards.points.accrued)、TransactionRecognitionService 下發前經守衛;③ rewards:AccrualRequest 加 3 可選欄位、accrued payload 加 4 可選欄位(不升版)、PointValuePort/ProductPointValueHttpClient(點值商品參數;預設 1=既有防呆);④ 契約:gen_openapi.py 同步(106 endpoints/0 errors)。驗收:未達上限可發/回填達上限停發/PER_MONTH 跨月獨立/PER_STATEMENT fail-closed 皆真機通過;campaign 43 例、rewards 158 例 全綠;SQL 30 檔 0 違規;掃描器 0 命中;契約↔DTO 0 不符。★ 過程由關卡攔下兩次真實漂移:verify_contract_dto(DTO 有、契約無之 3 欄)與 rewards 既有「payload 恰 5 欄」斷言(契約擴充之判定力)。仍缺:精確事前截斷(需 rate_schedule)、POINTS_LEDGER.campaign_code 之寫入(欄位已備、寫入列下一增量)、PER_STATEMENT/PER_HOUSEHOLD_MEMBER(來源未備)。
    (1-1/1)