全球休闲手游的排行榜,不是一张表

不是一张表
休闲手游的排行榜很少只有“全服前一百名”。真正上线后,产品通常同时需要全球榜展示最高目标,国家或地区榜提供本地认同,小分组榜让普通玩家感到可追赶,好友榜建立社交比较,赛季榜承载周期奖励,低活跃阶段还可能需要虚拟挑战者维持节奏。
本文讨论的是自建业务后端;平台排行榜只同步符合其规则的真实玩家成绩,不负责国家归属、虚拟账号或完整赛季状态机。核心模型是:服务端确认一份得分事实,再生成多个排名视图。
一份得分事实
排行榜入口不接受客户端声称的“最终分数”,只接受可验证的结果凭证,例如关卡编号、服务端签发的 run_id、开始与结束时间、关键事件摘要和完整性信号。权威服务按关卡规则核对分数,通过后写入不可变的 score_event。
榜单由一个明确的键定位:
board_key = game + mode + level + metric + season + scope_type + scope_id + ruleset
entry = player_id + score + achieved_at + run_id + country_code
键必须规范序列化,不能直接拼接字符串。metric 决定最高、最近或累计分;最近分按权威事件序列更新,累计分按事件去重;scope_id 存国家或桶编号。并列规则也要预先确定,例如同分时较早达成者优先,最后用玩家编号稳定打破平局。
全球与国家
全球榜适合展示顶尖目标,却不适合承担所有玩家的日常动力。绝大多数人看不到前一百名,也无法判断自己下一步该追谁。因此全球榜至少要支持 Top N、我的名次和我附近的玩家,而不是让客户端从榜首翻页到自己。
国家榜使用业务后端维护的 country_code,平台榜通常不会提供可靠国家字段。归属来源、纠错与申诉规则必须公开,并在赛季开始时固定,不能随每次登录 IP 改变。系统只保存国家或地区代码;无法可靠归属的玩家进入国际分组,改区下赛季生效。
全球写入可以就近接收,再异步汇总;界面应允许短暂显示“排名更新中”。涉及奖励时,必须以冻结后的权威快照结算。
分组与好友
分组榜把同一赛季玩家放进较小的稳定桶,例如按进入时间、活跃层级和历史能力带依次填充。桶一旦分配,本赛季内不再变化。这样玩家面对的是几十名可理解的对手,而不是数百万人的长尾。分桶规则需要版本号;调整桶容量时创建新赛季或新榜,不能悄悄改变正在进行的竞争集合。
好友榜通常不单独维护有序集合。查询时只读取已授权且仍有效的好友关系,再批量取得同赛季成绩并排序;授权撤销后立即失效。这样把写放大控制在固定范围,只有访问量证明是瓶颈时才缓存。
虚拟榜边界
虚拟榜可以解决冷启动和小国家玩家不足,但不能把合成对手伪装成真人。它应使用独立的自建“挑战榜”命名空间,条目标记 is_virtual,使用独立头像,并关闭资料、关注和私信。
虚拟分数不能随意追着玩家上涨。每个赛季根据难度带生成一条确定性曲线,由赛季、桶和虚拟角色编号共同作为种子;同一玩家重复进入看到的结果一致。曲线应遵守该指标的更新策略和理论上限,保留可追赶区间,也允许玩家真正登顶。若每名玩家看到完全不同的虚拟对手,就不能称为共享排行榜,只能称为个人挑战进度。
虚拟条目不参与真实名次编号、百分位、奖励门槛,也不提交到平台排行榜。昵称来自本地化词库,不复制真人资料;运营统计必须分开,否则业务指标会被合成数据污染。
赛季状态机
赛季不是定时清空一个有序集合,而是一条状态机:scheduled 允许预热;open 接受成绩;frozen 停止新写入并等待合法的在途事件;settling 固化名次并幂等发奖;archived 只读保存历史。
所有边界时间用 UTC 存储,再按本地时区显示。赛季归属以服务端确认的结束时间判断;冻结要等待每个接入区域的高水位,并明确迟到事件的截止与重算策略。结算可重入、可人工挂起,奖励用 season_id + player_id + reward_id 幂等;快照保存不可变的版本与哈希。旧赛季只读归档。
写入与查询
下面是服务端写入路径的 Go 风格伪代码。它表达的是事务边界,不是某个现成项目的源码:
func SubmitScore(ctx context.Context, cmd ScoreCommand) error {
result := verifyRun(ctx, cmd.RunID, cmd.Proof)
event := buildScoreEvent(result)
return transaction(ctx, func(tx Tx) error {
if !tx.InsertEventUnique(event.RunID, event) { return nil }
tx.EnqueueOutbox(event.ID, expandBoards(event))
return nil
})
}
数据库必须对 run_id 建唯一约束。Outbox 至少投递一次,消费者还要按 event_id 幂等并维护同榜事件顺序。实时排名可用有序集合,但同分时默认按成员字典序,不会读取 achieved_at;需要复合排序时,应使用多字段索引或经精度证明安全的整数编码,不能随意塞进浮点分数。
读接口一次返回榜首、玩家附近区间、玩家自身条目、榜单版本和更新时间。客户端据此渲染不同模块,不自行计算权威名次。
防作弊与结算
排行榜会把作弊收益公开放大,因此客户端提交、设备完整性结果和异常检测都只能作为证据,最终写入权必须留在服务端。除了 run_id 幂等,还要检查不可能分数、异常完成时间、重复结果、规则版本和封禁状态。高风险成绩先进入隔离榜,复核后再合入公开榜,避免一次误判直接影响奖励。
全球化还包括删除与隐私:玩家删除账号后,榜单成员和展示资料必须可清除;未成年人或匿名玩家可以显示系统昵称;国家榜只暴露国家级范围,不返回归属来源。榜单条目、奖励快照和分析事件也要有各自的保留期限。
所有边界必须可审计。一套成熟的排行榜并不是“选 Redis 还是数据库”这么简单。先定义得分事实,再定义范围、生命周期、并列规则和奖励资格,存储才有明确目标。全球榜负责目标,国家榜负责归属,分组榜负责可追赶,好友榜负责关系,虚拟榜负责明确标识的节奏补位;它们共享事实,但绝不能共享含糊的规则。