结算系统设计
概述
对局结束后的结算决定了玩家这局拿到什么:胜负、积分、金币、奖励。结算涉及资金与排名,是经济与竞技公平的关键环节。本文设计完整的结算系统:胜负判定、积分/金币计算、对战数据统计、MVP 评选、战报生成,并强调幂等与安全的工程要点。
一、结算触发与流程
1.1 结算触发条件
正常结束:
有人达到胜利条件(出完牌、达到目标分)
局数打完(多局制)
异常结束:
所有对手掉线/判负
房间解散导致对局终止
服务器强制结算(异常兜底)1.2 结算流程
1. 判定胜负(按玩法规则)
2. 计算积分/金币(底分 × 倍数 × 结果)
3. 统计对战数据(每玩家的出牌/得分/时长)
4. 评选 MVP(贡献度)
5. 生成战报(对局摘要)
6. 发奖/扣分(幂等,事务)
7. 广播结算消息 + 记录流水二、胜负判定
2.1 判定方式
| 玩法 | 判定规则 |
|---|---|
| 斗地主 | 某方先出完牌 |
| 德州扑克 | 摊牌比牌型 |
| 桌游 | 达成目标分/淘汰 |
| 休闲竞技 | 时间到按比分 |
设计原则:
判定逻辑集中在 RuleEngine(规则引擎)
配置驱动:不同玩法不同判定实现
服务器权威判定,客户端结果仅供参考2.2 平局与异常
平局处理:
比分相同 → 平分奖励/加赛
特殊平局规则按玩法配置
异常结束的胜负:
掉线判负:按规则(对手胜/托管算分)
强制结算:按当前局面快照判定
处理结果要可审计三、积分/金币计算
3.1 计算模型
通用公式:
结果分 = 基础分 × 倍数 × 权重
基础分:房间配置(底注/基础分)
倍数:玩法倍数(地主翻倍、连胜加成)
权重:玩家表现(MVP、连胜等)
示例(斗地主):
底注 10,地主倍数 2
地主胜:地主 +20,农民各 -103.2 计算实现
java
public class SettlementService {
public SettlementResult settle(Room room, GameResult result) {
SettlementResult sr = new SettlementResult();
for (Member m : room.snapshotMembers()) {
int outcome = result.outcomeOf(m.getPlayerId()); // 胜/负/平
long baseScore = room.getConfig().getBaseScore();
long multiplier = result.multiplierOf(m.getPlayerId());
long delta = baseScore * multiplier * outcome;
// 写积分/金币变动(走经济系统统一入口,含流水)
economyService.changeCoin(m.getPlayerId(), delta);
sr.addDelta(m.getPlayerId(), delta);
}
return sr;
}
}计算要点:
所有数值用 long(避免浮点精度问题)
变动走统一经济入口(含流水日志)
整局计算在事务内(同玩家数据一致性)3.3 幂等与防重复结算
结算必须幂等:
同局只结算一次(结算标记)
掉线重连后客户端重发结算请求 → 不重复发奖
实现:
结算记录表(roomId 唯一)
重复结算 → 直接返回已结算结果
关键:发奖前查记录(防重复加钱)四、对战数据统计
4.1 统计维度
| 数据 | 说明 |
|---|---|
| 胜负 | 胜/负/平 |
| 表现 | 得分、出牌数、淘汰数 |
| 时长 | 对局时长、平均操作耗时 |
| 连击 | 连胜/连败 |
| 里程碑 | 成就触发点 |
4.2 数据用途
实时用途:
排行榜(胜场、积分)
段位更新(ELO 调整,见匹配篇)
成就解锁(见成就系统)
离线用途:
数据分析(玩家行为、平衡性)
战绩查询(玩家历史对局)
客服溯源4.3 统计落库
统计表:
battle_record(对局记录):
recordId, roomId, mode, players, result, createTime
player_battle_stat(玩家累计):
playerId, wins, losses, totalScore, ...
写入时机:
结算时写 record + 更新 stat
统计异步化(MQ),不阻塞结算五、MVP 评选
5.1 评选算法
MVP 候选维度:
胜负(胜方优先)
表现分(综合贡献)
特殊成就(最佳操作、最多击杀)
通用做法:
表现分 = Σ(各维度得分 × 权重)
得分最高(通常胜方)为 MVP
示例(团队休闲游戏):
表现分 = 击杀×1 + 助攻×0.5 + 得分×0.1 - 失误×0.35.2 评选实现
java
public class MvpCalculator {
public long calcMvp(List<BattleStat> stats) {
return stats.stream()
.max(Comparator.comparingLong(this::performanceScore))
.map(BattleStat::getPlayerId)
.orElse(-1L);
}
private long performanceScore(BattleStat s) {
return s.getKills() * 1 + s.getAssists() * 5 / 10
+ s.getScore() / 10 - s.getFaults() * 3 / 10;
}
}MVP 奖励:
额外金币/称号
荣誉展示(战报中高亮)
配置化权重,便于平衡调整六、战报生成
6.1 战报内容
对局摘要:
房间信息:模式、局数、时间
玩家列表:每人的胜负、得分、表现
MVP 高亮
关键节点:翻盘、决胜手
奖励明细:金币/积分变动6.2 生成与推送
生成:
结算后生成战报对象
可持久化(历史战绩查询)
推送:
结算消息直接带战报(客户端展示)
非实时需求(历史)查库
扩展:
战报可分享(社交传播)
战报数据驱动"复盘"功能七、安全与一致性
7.1 结算安全清单
安全要点:
结算由服务器触发,客户端不能请求发奖
结算幂等(防重复)
数值校验(负分不能发奖、防溢出)
流水记录(货币变动可对账)
分布式锁(同玩家结算串行)7.2 结算失败兜底
结算中断(进程崩溃):
结算记录表标记"结算中"
重启后扫描未完成 → 补结算
幂等保证补结算不重复
对账:
流水收支平衡校验
发现差异 → 告警 + 人工处理八、常见问题
| 问题 | 处理 |
|---|---|
| 重复发奖 | 结算记录幂等 |
| 结算中途崩溃 | 补结算机制 |
| 金币出现负数 | 数值校验 + 最低余额兜底 |
| MVP 权重失衡 | 配置化调整 |
| 战报数据缺失 | 统计异步化 + 幂等写入 |
九、小结
结算系统把一局的成果转化为玩家的实际收益:胜负判定由服务端 RuleEngine 权威裁决,覆盖正常与异常结束;积分/金币计算基于"底分 × 倍数 × 结果"模型,全部走统一经济入口并记录流水;对战数据统计同步更新战绩记录与排行榜/段位数据,异步化不阻塞结算;MVP 评选用可配置的表现分权重选出发光玩家;战报汇总整局并支持分享。工程上把"幂等、服务端触发、流水可对账、失败可补结算"四条安全底线贯穿始终,结算才算真正稳得住钱与排名。