MatchRPG 貼紙系統 + 活動框架正式設計審計:17 Findings 與 5 工作流執行計劃¶
文檔資訊
- 分類: architecture
- 難度: advanced
- 預估閱讀時間: 12 分鐘
- 標籤:
architecture-audit,golang,postgresql,redis,sticker-system,activity-framework,capacity-planning,schema-design,reward-system,game-server
摘要¶
針對 MatchRPG_Server 的貼紙系統(Sticker)與活動框架(Activity Framework)進行全面設計審計,對照設計文檔與實際後端代碼,識別出 17 個問題(2 個確定 correctness bug、5 個容量風險、6 個交付阻塞、4 個平台技術債),並收斂成 5 個可執行工作流程。最終結論:目前不能簽核容量,也不能簽核上線,需先完成 WF-1(Reward Claim 統一)和 WF-2(Inventory Seasonal Schema)。
關鍵學習¶
-
設計審計應將「確定 bug」與「容量假說」嚴格分開,避免混淆執行優先級
-
結算獎勵不應依賴 mutable aggregate(如 progress.sets_reward_claimed),真相必須收斂到 ledger 或 claims 表
-
inventory schema 缺乏 season_id 和 item_secondary_type 原生欄位,導致 AUTO_CONVERT snapshot 查詢模型完全對不上實作
-
PostgreSQL 可對 JSONB 做 expression index,但 hot predicate(如 season_id)不應放 JSONB,25M snapshot path 不可接受
-
settlement_reward_ledger 語義是結算專用,不能直接泛化為通用 claim registry;正確做法是新建 generic reward_claims 表
-
全局事件 sorted-set key 的 TTL 被持續刷新而舊事件不會 GC,是長期容量炸彈
-
事件投遞契約分裂(proto meta vs HTTP header vs SyncService)會造成 client contract、代理行為與 observability 三套語義並存
技術細節¶
審計範圍¶
對照設計文檔與以下後端實作進行交叉驗證:
internal/modules/player/models.go
internal/modules/player/player_inventory_repository_postgres.go
pkg/reward/distributor.go
pkg/db/postgres/tx.go
pkg/events/redis_store.go
pkg/middleware/pending_events.go
internal/modules/sync/sync_service.go
cmd/twirpserver/main.go
17 個 Findings 分類¶
Correctness Blockers(設計正確性)¶
- C-1:OpenStickerPack 套卡獎勵雙發 —
sets_reward_claimed在 progress aggregate 和 settlement ledger 各有一份真相,correctness bug 已在代碼層確認 - C-2:卡包
season_id缺失、item_secondary_type在 inventory schema 不存在原生欄位,snapshot 查詢模型和實際 schema 完全對不上
Capacity Blockers(容量簽核)¶
- V-1:settlement_snapshot_items / settlement_batches 缺 DDL、index、分區定義
- V-2:OpenStickerPack hot path 估算 11-18 queries,5K RPS → 60K+ QPS(upper-bound sizing estimate,尚未量測)
- V-3:settlement worker read path 尚未 benchmark
- V-4:未做跨 repo 外部驗證
- V-5:OpenStickerPack 缺明確 service/tx deadline,
pkg/db/postgres/tx.go:36的 5 秒 default 不一定生效
Delivery Blockers(上線交付)¶
- D-1 ~ D-4:PD 配置表、RBAC owner 等跨 repo 項目
- D-5:
RewardTypeSticker(6)/RewardTypeStickerPack(7)未接入通用 reward pipeline;需擴展playerRewardGranter.GrantRewards+pkg/reward/resolver.go - D-6:
cmd/twirpserver/main.go未 wire Activity / Sticker 模組
Platform Debt(平台並行債)¶
- T-1:Global event sorted-set 舊 member 無 GC —
publishScript對整個 key 做 EXPIRE,redis_store.go:39,42只刷 TTL,舊事件永不掉出集合 - T-2:事件投遞契約分裂 — proto RequestMeta/ResponseMeta vs HTTP header X-Last-Event-Cursor vs SyncService 走 proto meta,三套語義並存
- T-3, T-4:長期架構/ops 問題
5 個工作流程(WF)¶
| WF | 主題 | 依賴 |
|---|---|---|
| WF-1 | Reward Claim 統一(新建 generic reward_claims 表) | 無 |
| WF-2 | Inventory Seasonal Schema(season_id + pack type 原生欄位) | 無 |
| WF-3 | Settlement Hot Tables DDL + Worker Read Path | WF-1 |
| WF-4 | Reward Pipeline + main.go Integration(D-5、D-6) | Phase 0 契約 PR + WF-½ 實作 |
| WF-5 | Capacity Benchmark + Release Gates | WF-3、WF-4 |
WF-4 是獨立支線,不在 WF-5 之後,需單獨排期。
What Changed¶
審計前狀態¶
貼紙系統與活動框架僅有設計文檔(Plan 狀態),server wiring 未接入 Activity/Sticker 模組,settlement 熱表缺可審計的物理設計,獎勵真相分裂於多處。
審計過程發現¶
透過對照設計文檔與實際代碼,確認了 2 個確定 correctness bug(C-1 套卡獎勵雙發、C-2 schema mismatch),並與純文檔推斷的問題明確區分,建立了「Confirmed in current repo」vs「Capacity hypothesis」vs「Cross-repo validation needed」三層證據分級。
最終輸出¶
產出可直接進執行的正式審計表:Executive Summary + 5 Workflows(附 owner、依賴、輸出物)+ Appendix 17 Findings(作為證據索引)。結論是目前不能簽核容量、不能簽核上線,先開 WF-1 和 WF-2。
So What¶
這份審計建立了一套可複製的方法論:先對照設計文檔與實作代碼交叉驗證,再按「確定 bug / 容量假說 / 交付阻塞 / 平台債」四維分類,最後收斂成有依賴關係的工作流程而非平鋪工單。
這個框架對任何需要在「功能未完全實作」狀態下做容量簽核或上線決策的項目都有參考價值,尤其是如何避免把「設計文檔層面的問題」和「代碼層面的確定 bug」混淆在一起。
Trade-offs¶
- 新建 generic reward_claims 表 vs 泛化 settlement_reward_ledger:前者短期更容易落地,後者長期更統一;選前者是因為 ledger 的 claiming_run_id / applied_run_id 強綁 settlement_runs,語義不可直接泛化
- season_id 放 JSONB metadata vs 原生欄位:JSONB 可補功能但查詢性能不可接受(25M snapshot path),必須原生欄位 + index
- item_secondary_type vs item_id 作為 pack type predicate:取決於 pack type 是否能由 item_id 唯一決定,需先確認 item_id 的語義粒度
- 17 個 findings 直接開票 vs 收斂成 5 個工作流:後者 owner/順序/阻塞關係更清晰,適合實際執行排程
Try It Fast¶
# 驗證 Activity / Sticker 是否已 wire 進 server
grep -n 'activity\|sticker\|ActivityService\|StickerService' \
cmd/twirpserver/main.go
# 確認 inventory schema 是否有 season_id 或 item_secondary_type 原生欄位
grep -n 'season_id\|item_secondary_type' \
internal/modules/player/player_inventory_repository_postgres.go
# 確認 reward pipeline 是否處理 Sticker/StickerPack reward type
grep -n 'RewardTypeSticker\|StickerPack\|case 6\|case 7' \
pkg/reward/distributor.go \
pkg/reward/resolver.go
# 驗證 global event GC 機制是否存在
grep -n 'ZREMRANGEBYSCORE\|ZRem\|GC\|cleanup' \
pkg/events/redis_store.go
# 跑現有事件/同步底座測試(只驗證底座,不代表 sticker/activity ready)
go test ./pkg/events ./internal/modules/sync ./test/... \
-run 'Sync|Event|Pending' -v
Recommendation¶
- 立即開工 WF-1:新建
reward_claims通用表,讓OpenStickerPack和SettlePlayer都寫它;不要直接泛化settlement_reward_ledger - 平行開工 WF-2:在 inventory 加
season_id(原生、可索引)和 pack type predicate;先確認item_id語義是否能唯一決定 pack type,再決定是否需要item_secondary_type - WF-½ 完成後啟動 WF-3:補
settlement_snapshot_items/settlement_batches的 DDL、index、分區,跑 settlement worker read path benchmark - WF-4 排獨立支線:擴展
playerRewardGranter.GrantRewards+pkg/reward/resolver.go接入RewardTypeSticker(6)/RewardTypeStickerPack(7),補 wiring 到main.go - WF-5 最後執行:跑完整 capacity benchmark,達標後才能簽核容量與上線