虚拟货币系统
概述
货币是游戏经济的血液:充值得钻石(硬通货)、对战得金币(软通货)、玩法耗体力(资源货币)。货币系统的核心是三件事:类型与流通规则(哪些可交易、哪些只能充值)、数量安全(并发扣减不能变负、不能溢出)、流水审计(每一分钱从哪来到哪去)。本文设计完整的货币系统。
一、货币类型
1.1 分类设计
| 货币 | 获得方式 | 用途 | 特点 |
|---|---|---|---|
| 钻石(硬通货) | 充值、活动 | 抽卡、购买限定 | 不产出游戏内,防通胀 |
| 金币(软通货) | 对战、任务 | 日常消费 | 游戏内循环产出 |
| 体力/精力 | 时间回复 | 玩法入场 | 调节游戏节奏 |
| 公会贡献 | 公会活动 | 公会商店 | 绑定公会 |
| 竞技积分 | 竞技场 | 专属商店 | 荣誉货币 |
货币设计原则:
硬通货严格管控产出(只进不出 vs 反向打折)
软通货可回收(消耗项),维持经济循环
货币间兑换单向(钻石→金币,不可逆)
每类货币独立存储与计数1.2 货币配置
货币类型配置化定义,含上限、流通范围、充值比例等:
CurrencyDef:
currencyId、名称
maxLimit(上限,防刷)
rechargeable(是否可充值)
tradeable(是否可玩家间转移)
rate(兑换/充值比例)二、数量安全
2.1 数据类型选择
整数货币:long(推荐)
游戏货币基本是整数,用 long 足够(9.2×10^18)
避免浮点误差
Java 侧加减法注意溢出
小数货币:long 存最小单位
如金币存"分"而非"元",展示层换算
避免 BigDecimal 的复杂性与性能损耗// 不用 double/float 计货币
// 1 金币 + 0.1 金币 在浮点下有精度问题
// 一律 long 整数运算,除法向下取整
long coin = 100;
long cost = 30;
long remain = coin - cost; // 702.2 并发扣减
货币变动高频且并发,必须保证不超扣、不变负:
方案一:数据库原子更新(强一致)
UPDATE player_currency
SET amount = amount - ?
WHERE player_id = ? AND currency_id = ?
AND amount >= ?
方案二:Redis 原子扣减 + 落库
Redis Lua 脚本:检查余额 → 扣减(原子)
异步落库兜底
方案三:分布式锁 + 内存扣减
同玩家串行(游戏服常用),单线程扣减游戏服务器通常同玩家串行处理请求(见消息分发章节),单玩家内货币操作天然原子,配合乐观锁/条件更新兜底即可。
2.3 统一入口
所有货币变动走统一接口,禁止业务代码直接改余额:
统一变更接口:
changeCurrency(playerId, currencyId, delta, reason, bizId)
校验上限/下限
原子扣减或累加
写流水
推送客户端余额变化校验规则:
加:不超过上限(超过截断并记录)
减:余额不足直接失败
同业务幂等:bizId 去重,防重复发放三、流水日志与审计
3.1 流水记录
每一笔货币变动都写流水,是经济审计与客服追溯的依据:
player_currency_log:
logId、playerId、currencyId
delta(正负)、balanceAfter(变动后余额)
reason(对战/充值/任务/邮件/GM…)
bizId(业务单号,幂等键)
createTime
索引:(playerId, currencyId, createTime)流水写入策略:
与余额更新同事务(强一致)
或异步写入(余额已持久化,流水可后置)
高频小面额可聚合(如战斗多次扣费合并一条)3.2 对账审计
审计手段:
流水汇总 = 余额(按玩家对账)
充值流水与支付渠道对账(渠道回调 → 发钻石)
异常检测:余额突增、负余额、循环套现
定查工具:按玩家/原因/时间范围查流水常见审计场景:
活动奖励重复领取 → 查同 bizId 多条流水
刷币嫌疑 → 流水原因分布异常
客服补发 → 流水留痕,玩家可查四、体力与时间回复
体力是节奏型货币,按时间恢复,涉及离线累计:
体力模型:
上限(如 100),每 N 分钟回 1 点
在线时定时回、离线时按离线时长回
回复加速(好友赠送、道具)
离线恢复:
登录时计算 最后恢复时间 → 当前时间 的差值
一次性补发(封顶上限)
更新最后恢复时间// 离线体力计算:避免长时间未登录攒出无限体力
long now = System.currentTimeMillis();
long elapsed = now - lastRecoverTime;
int recover = (int) Math.min(elapsed / RECOVER_INTERVAL, MAX_BONUS);
int energy = Math.min(energy + recover, ENERGY_LIMIT);五、实现要点
核心接口:
changeCurrency(playerId, currencyId, delta, reason, bizId)
getBalance(playerId, currencyId)
recharge(playerId, currencyId, amount, channelOrderId) // 充值专用,幂等
getFlowList(playerId, currencyId, page)工程注意:
充值到账幂等(渠道订单号唯一)
负数余额绝对禁止(校验 + 告警)
大额变动二次确认(GM/异常防护)
汇率/上限配置化,热更新常见坑:
并发扣成负数 → 条件更新 + 事务
浮点金额精度 → long 最小单位
流水缺失 → 变更与流水同事务/异步补偿
充值重复到账 → 渠道单号唯一索引与其他系统衔接:
货币 → 商城购买(商城章节)
货币 → 抽卡消耗(抽奖章节)
货币 → 任务奖励(任务章节)
发放入口 → 邮件/成就/活动统一调用