跳轉到

Activity Framework & Sticker System 技術債全面清查:GameDataProvider 模式統一、Migration 衝突修正、nil-bypass 消除(含 AuditHooks 架構決策)

文檔資訊

  • 分類: architecture
  • 難度: advanced
  • 預估閱讀時間: 15 分鐘
  • 標籤: go-wire, dependency-injection, migration, activity-framework, sticker, tech-debt, gamedata-provider, fleet-registry, audit-hooks, nil-bypass, two-round-audit

摘要

針對 Activity Framework v4.0 與 Sticker System 進行嚴格兩輪技術債稽查。第一輪建立清單後,第二輪「確定嗎?」推動發現三個額外 P0(SetFleetRegistry nil-bypass、RegisterAndStartHeartbeat、ValidateBundleAdmission wiring gap),並糾正三個錯誤斷言。最終落地:Migration 044 衝突折回修正、ScheduleConfigLoader 整個刪除改用既有 GameDataProvider 模式、SetFleetRegistry nil-bypass 消除(fail-open → fail-loud)、P0-3 降級並路由到現有 AuditHooks 架構、P1 文檔與 DoD 同步、STICKER_ENABLED env flag 移除、SettlementDispatcher YAGNI interface 刪除。

關鍵學習

  • Go Wire 的 post-init setter 模式只允許用於真正的循環依賴(architecture.md §4.6);Activity 模組誤用此模式,根因是沒有遵循既有 GameDataProvider 模式

  • 正確修法是刪掉 ScheduleConfigLoader interface 整個抽象,直接複用 configManager(已實作 GameDataProvider),不新增任何 adapter 或 loader

  • Migration 版號衝突的根因處理:FK constraint 應直接 fold 回建表 migration(043),而非新增 044;確認 staging/prod 都沒 deploy 是此修法的前提

  • nil-bypass(silent fail-open)是高嚴重性 bug:checkFleetConvergence if fleetRegistry == nil { return nil } 讓 fleet 收斂檢查在未 wire 時靜默跳過,必須改成 fail-loud error

  • RBAC 稽核不應另起 BlockingAuditor 爐灶;Activity admin write RPC 應走現有 AuditHooks 模式(CommandHandler + before/after state + reason/ticket 強制驗證),完整 11 欄位稽核是 EXT-RBAC 範圍

  • 兩輪稽查模式的價值:第一輪遺漏了 SetFleetRegistry、RegisterAndStartHeartbeat、ValidateBundleAdmission 三個 P0 wiring gap,以及 nil-bypass 和三個錯誤斷言;第二輪「確定嗎?」推動發現這些問題

  • SQSDispatcher type 不存在(第一輪錯誤斷言),admin write RPC 實際是 9 個而非 18 個,executeAction 是 RetryAction→FailAction 非 silent fail — 每個斷言都應 grep 驗證

技術細節

GameDataProvider 統一模式

所有模組(player、dungeon、teach、sticker)都透過 Wire constructor injection 取得 config.Manager(實作 GameDataProvider interface):

// main.go 標準流程
configManager = config.NewManager(cfg.ConfigDir, ...)
var gameData player.GameDataProvider = configManager
playerDeps = player.InitializePlayer(..., gameData)
teachDeps  = teach.InitializeTeach(..., gameData, ...)

Activity 模組是唯一的例外,自行發明了 ScheduleConfigLoader interface 並用 setter 暴露。這個 interface 本質上只是讀 gameData.GetGameData().Tables_ActivityScheduleConfig,與 dungeon 的 NewLubanGameDataProvider(configManager) 包裝模式完全一樣。

修法(零新抽象):刪掉 ScheduleConfigLoader interface、SetScheduleConfigLoader setter、configLoader mutable 欄位;InitializeActivitygameData GameDataProvider 參數,直接讓 Activity 定義自己的 GameDataProvider interface(避免 import player),從 GetGameData().Tables_ActivityScheduleConfig 讀排期資料。PD 尚未交表時 tables 為 nil → fail-loud,正確行為。

Migration 衝突處理

044 版號衝突:044_create_sticker_tables044_add_settlement_runs_activity_fk 同時存在,golang-migrate file source driver 以版號前綴為 key,兩個都是 044_ 就爆衝突。

修法:確認未 deploy(staging/prod 都沒跑過 044),直接把 FK constraint 折回 043_create_activity_tables.up.sql,刪除兩個 044 FK migration 檔案,獨立 commit。已提交 e182772

SetFleetRegistry nil-bypass 消除

// 修前(silent fail-open)
func (s *ActivityService) checkFleetConvergence(ctx context.Context, bundleID int64) error {
    if s.fleetRegistry == nil {
        return nil  // 靜默通過,完全繞過 fleet 收斂檢查
    }
    ...
}

// 修後(fail-loud)
func (s *ActivityService) checkFleetConvergence(ctx context.Context, bundleID int64) error {
    if s.fleetRegistry == nil {
        return fmt.Errorf("fleetRegistry not wired: call SetFleetRegistry before use")
    }
    ...
}

同時 InitializeActivity 加入 FleetRegistry 參數,main.go 改為 constructor injection,刪除 types.goFleetRegistry exported field 和 SetFleetRegistry setter。

AuditHooks 架構決策

P0-3 原本設計「BlockingAuditor 擋住所有 admin write RPC,直到 #30 RBAC middleware 到位」。調研現有 admin service 後發現:admin 服務已有完整 CommandHandler 模式 + AuditHooks,走 before/after state、reason/ticket 強制驗證的 11 欄位稽核。

決策:Activity admin write RPC 應掛到現有 admin 稽核路線,而非另起 BlockingAuditor。#8 降級,成為 #30 EXT-RBAC 的一部分。PD 透過 admin service 管理活動,走既有的 CommandHandler + AuditHooks 覆蓋。

What Changed

P0 修正(全部落地)

P0-1(e182772):Migration 044 衝突,FK constraint 折回 043,刪除 044 FK 檔案,獨立 commit。

P0-2:整個 ScheduleConfigLoader 抽象被刪除,Activity 改用與其他模組一致的 GameDataProvider constructor injection。SetFleetRegistry nil-bypass 同步消除,改為 fail-loud error。RegisterAndStartHeartbeatValidateBundleAdmission wiring gap 補齊。

P0-3:BlockingAuditor 方案廢棄,Activity admin write RPC 路由到現有 AuditHooks 架構,#8 降級為 #30 的子項目。

第一輪稽查的錯誤斷言修正

三個錯誤被第二輪「確定嗎?」糾出:SQSDispatcher type 不存在(#21 降 P3)、admin write RPC 數量從 18 改為 9、executeAction 不是 silent fail 而是 RetryAction→FailAction(#9 刪除)。

P1 及後續清理

  • 11~#15:EtherHourglass 枚舉值 8→9 修正(3 份 sticker docs)、Activity DoD 7 項錯誤斷言修正、sticker DoD coverage 62.8→65.9、activity 資料表數量 6/7→8、§2.4 false CLOSED 撤銷

  • 37:STICKER_ENABLED env flag 完全移除,sticker 模組直接註冊

  • 21:SettlementDispatcher YAGNI interface 刪除,直接使用 DBBatchDispatcher concrete type

  • 26:getSeasonProgress 無 active season 錯誤路徑補生產級別 test case

So What

這段對話最有記錄價值的不是個別 bug fix,而是三個系統性發現:

1. 模式偏離識別:Activity 是整個 repo 唯一沒有走 GameDataProvider constructor injection 的模組。這種「孤兒模式」如果沒有在 code review 或稽查時被抓出來,會持續成為 main.go 的負擔。刪掉整個 interface 而非「修正注入方式」是更高層次的正確解。

2. nil-bypass 的系統性危害if x == nil { return nil } 在未 wire 的依賴上是靜默的、沒有測試覆蓋的 fail-open。Fleet 收斂檢查被跳過在 staging 不會崩潰,但在 prod 會讓 bundle 在 fleet 還沒收斂時就放行,是高嚴重性的正確性 bug。

3. 兩輪稽查的制度性價值:第一輪給出清單後主動問「確定嗎?」,推動發現 3 個額外 P0 和 3 個錯誤斷言。這個習慣應成為所有稽查流程的標準步驟。

Trade-offs

  • 刪掉 ScheduleConfigLoader vs 改建構期注入:刪掉更乾淨(零新抽象、零新 adapter),但需要等 PD 交 Luban 表(#29)才能真正生效;期間 GetGameData().Tables_ActivityScheduleConfig 會是 nil,必須 fail-loud 而非 stub
  • BlockingAuditor vs 走 AuditHooks:BlockingAuditor 快速可落地,但是另起爐灶,日後要遷移;走 AuditHooks 是正確路線但需要 #30 RBAC middleware 先完成,短期內 Activity admin write RPC 無稽核覆蓋(可接受,因為功能尚未對外)
  • P0-2 折回 043 的前提:此修法只能在 migration 尚未 deploy 到任何環境時使用;已 deploy 的情境必須用純 additive migration

Try It Fast

# 驗證 migration 衝突是否修正(沒有重複版號前綴)
ls migrations/postgres/ | grep '^04[34]' | sort
# 應只看到:043_create_activity_tables.{up,down}.sql 和 044_create_sticker_tables.{up,down}.sql

# 驗證 ScheduleConfigLoader 已完全移除
grep -r 'ScheduleConfigLoader\|SetScheduleConfigLoader\|configLoader' internal/modules/activity/
# 應 0 結果

# 驗證 GameDataProvider 已注入 Activity
grep 'InitializeActivity' internal/modules/activity/wire.go
# 應包含 GameDataProvider 參數

# 驗證 nil-bypass 已消除
grep -n 'fleetRegistry == nil' internal/modules/activity/activity_fleet_service.go
# 應返回 fail-loud error 而非 return nil

# 驗證 STICKER_ENABLED 已完全移除
grep -r 'STICKER_ENABLED' .
# 應 0 結果

# 驗證 SettlementDispatcher interface 已刪除
grep -r 'type SettlementDispatcher interface' internal/modules/activity/
# 應 0 結果

Recommendation

  1. #29(PD Luban 表)完成後,立即驗證 GetGameData().Tables_ActivityScheduleConfig 可正常讀取,並補上 TestSyncActivitiesFromConfig 的 config round-trip test
  2. **所有新模組**在 Wire 設計階段就必須把 GameDataProvider 列為 constructor 參數,不允許 setter pattern(除非有可以 code review 佐證的循環依賴)
  3. nil-bypass code review checklist:每個 if x == nil { return nil } 在依賴欄位上都應該觸發審查,確認是刻意 optional 還是未完成 wiring
  4. 兩輪稽查習慣:稽查清單建立後應主動問「這些 fix 有沒有 workaround 或技術債?」,讓第二輪自我審查成為標準流程
  5. #30 EXT-RBAC 完成後,重新評估 Activity admin write RPC 的 AuditHooks 覆蓋狀態,確認 before/after state 與 reason/ticket 強制驗證均到位

本文檔由 Semi-Brain 自動生成

Session ID: a7f313c0-2db2-4e32-8dc4-43117b1a66b8

分析信心度: 97%