跳轉到

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-5RewardTypeSticker(6) / RewardTypeStickerPack(7) 未接入通用 reward pipeline;需擴展 playerRewardGranter.GrantRewards + pkg/reward/resolver.go
  • D-6cmd/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

  1. 立即開工 WF-1:新建 reward_claims 通用表,讓 OpenStickerPackSettlePlayer 都寫它;不要直接泛化 settlement_reward_ledger
  2. 平行開工 WF-2:在 inventory 加 season_id(原生、可索引)和 pack type predicate;先確認 item_id 語義是否能唯一決定 pack type,再決定是否需要 item_secondary_type
  3. WF-½ 完成後啟動 WF-3:補 settlement_snapshot_items / settlement_batches 的 DDL、index、分區,跑 settlement worker read path benchmark
  4. WF-4 排獨立支線:擴展 playerRewardGranter.GrantRewards + pkg/reward/resolver.go 接入 RewardTypeSticker(6) / RewardTypeStickerPack(7),補 wiring 到 main.go
  5. WF-5 最後執行:跑完整 capacity benchmark,達標後才能簽核容量與上線

本文檔由 Semi-Brain 自動生成

Session ID: c903e4ed-15e4-4a39-b7b8-30b428e38075

分析信心度: 92%