游戏里的货币扣减通常比发奖更敏感。玩家买商品、挂拍卖、参加竞拍、抽卡、跨服交易时,一旦出现扣了钱没拿到东西,信任会迅速崩塌。很多系统最初只提供一个 deduct 接口,业务方调用成功后再做后续逻辑,失败时尝试 refund。这个模型在单服务、低并发时还能勉强工作,一旦跨服、异步结算、订单超时和人工补偿混在一起,就会出现重复退款、余额短暂为负、订单状态和货币流水对不上等问题。货币预占与托管架构的目标,是让“扣减意图”和“最终归属”分阶段落账。
典型场景
玩家在跨服拍卖行竞拍一件稀有装备,出价 10000 金币。系统不能直接把金币扣给卖家,因为竞拍可能失败;也不能只检查余额不扣,因为玩家可以同时在多个拍卖里出价。正确流程是先把 10000 金币从可用余额转入托管账户,竞拍成功后托管转给卖家,竞拍失败后托管释放回玩家。整个过程需要订单 id、预占 id、托管流水和过期回收任务配合。
架构示意
sequenceDiagram
participant P as Player
participant O as Order Service
participant W as Wallet Service
participant E as Escrow Account
participant S as Settlement
P->>O: Place bid(orderId)
O->>W: Reserve currency(reservationId)
W->>E: Move available to escrow
O->>S: Wait auction result
S->>E: Commit to seller or release to player
E-->>W: Ledger finalized
余额要拆成可用、预占和冻结
单一 balance 字段无法表达复杂交易状态。至少需要 available、reserved、frozen 三个视角。available 用于普通消费,reserved 用于已提交业务意图但未最终结算的订单,frozen 用于风控、客服争议或封禁处理。玩家展示余额时要说明可用余额,内部风控和客服工具则要能看到完整构成。不要让业务方自己维护“临时扣减表”,否则钱包服务失去资产真相。
预占必须有过期时间和业务归因
每一笔预占都要带 reservationId、业务类型、业务单号、金额、币种、创建时间、过期时间和状态。过期时间不是随便写一个小时,而要由业务场景决定。拍卖竞价可能需要持续到拍卖结束,商城订单可能只需要五分钟,抽卡请求通常不应该长时间预占。过期回收任务必须幂等,不能因为重复扫描把已提交的托管又释放。
托管账户不是虚构概念,要进入流水
当货币从玩家可用余额转入托管账户时,应产生清晰流水:玩家 available 减少、玩家 reserved 增加或系统 escrow 增加。最终结算时,再从 escrow 转给卖家或释放给玩家。流水里要能追踪资产从哪里来、到哪里去、为什么变化。这样即便业务订单表损坏,也可以通过钱包流水重建资产路径。
业务状态机要适配预占生命周期
订单不能只有 paid 和 failed。更实际的状态包括 created、reserved、committing、committed、releasing、released、expired、manual_review。每个状态对应允许的幂等操作。比如订单处于 committing 时,重复 commit 应返回当前处理结果或继续查询,不应再发起新的扣款;处于 expired 时,业务不能再把它改成成功,除非走人工修复流程。
跨服交易要把结算点收敛到一个权威服务
跨服交易最怕两个分区同时认为自己能结算。预占可以发生在玩家归属服的钱包服务,最终托管结算应由交易控制面串行处理。控制面不一定是单点机器,但同一订单必须有唯一 owner。使用分区锁、订单路由或一致性哈希都可以,关键是不要让买家服和卖家服各自推进状态。
关键设计取舍
| 维度 | 架构处理 | 重点风险 |
|---|---|---|
| 商城购买 | 短预占,支付成功即提交 | 订单超时释放 |
| 拍卖竞价 | 长预占,结果出来后结算 | 竞价失败释放 |
| 玩家交易 | 双方资产进入托管 | 确认窗口和争议处理 |
| 抽卡 | 尽量同步提交 | 失败补偿和风控审计 |
落地检查清单
- 钱包模型拆出 available、reserved、frozen
- 预占记录带业务单号、过期时间和幂等 id
- 托管转入、提交、释放全部写入资产流水
- 订单状态机覆盖 committing 和 releasing 中间态
- 过期回收任务只处理未提交且未释放的预占
一线排障与复盘建议
这个架构上线后,团队要提前准备几类排障入口。第一是按玩家、业务单号或场景 id 查询完整链路,能看到请求进入、状态变化、关键版本、外部依赖结果和最终响应。第二是按时间窗口查看异常分布,区分是全局配置错误、单分片容量问题,还是少量玩家边界条件触发。第三是保留人工修复入口,但修复入口必须写审计流水,记录修复前状态、修复后状态、操作人、审批单和影响范围。没有审计的手工修复,短期能救火,长期会破坏系统可信度。
容量评估也要贴近玩法节奏,而不是只看平均在线。运营开活动、赛季结算、跨服匹配、周常刷新和主播带队都会让请求集中到很短窗口。压测脚本应模拟重复点击、弱网重试、服务超时、实例重启和消息乱序,不要只跑顺滑路径。对于玩家资产、资格、奖励、处罚这类敏感链路,压测结果里要额外检查幂等流水和最终状态,不只是吞吐量。
上线前可以采用影子模式:生产请求仍走旧逻辑,新架构旁路计算结果并记录差异。差异样本要由服务端、策划和客服一起看,因为有些差异来自旧逻辑 bug,有些来自新规则理解错误。等差异收敛后,再按小区服、低风险玩法或内部账号灰度。灰度期间观察错误码、超时、回滚次数、人工工单和玩家反馈,确认系统在真实噪声下仍然可解释。
账户流水字段设计
钱包流水至少包含 ledgerId、playerId、currencyType、amount、direction、balanceBefore、balanceAfter、reservationId、businessType、businessId、operator 和 createdAt。托管账户流水还要记录 escrowAccountId、counterpartyId 和 settlementId。不要只记录“扣了 100 金币”,而要记录这 100 金币为何从可用余额进入托管,最终又流向哪里。
预占记录建议保持有限状态:reserved、committed、released、expired、manual_hold。状态转换必须单向,committed 后不能 release,released 后不能 commit。若业务确实需要冲正,走一笔新的 reversal 流水,而不是修改旧流水。资产系统最忌讳“改历史”,因为一旦历史被改,后续所有对账都失去基础。
故障案例:退款任务重复执行
某交易系统早期在订单失败时调用 refund 接口,退款任务由消息队列投递。一次队列重试风暴中,同一失败订单触发了两次退款,原因是退款接口只校验订单状态 failed,没有校验 refundId 是否已处理。玩家余额多出了货币,卖家没有损失,短期看似不严重,但经济系统出现了不可解释增发。
改造后,订单失败不再直接 refund,而是把托管预占 release。release 使用 reservationId 幂等,释放成功后预占状态变为 released。重复 release 返回同一结果,不产生新流水。若订单已经 committed,release 会被拒绝并告警。这个模型把“退款”从业务补偿动作变成了预占生命周期的一部分,风险小很多。
对账与风控
每天至少跑三类对账:玩家余额等于历史流水累加结果,预占总额等于托管账户余额,业务订单状态与预占状态一致。对账不一定要实时阻断玩家,但必须能在小时级发现异常。对于高价值货币,可以把对账窗口缩短,并在异常时冻结相关玩家的交易入口,而不是冻结整个账号。
风控还可以利用预占行为识别异常。例如玩家短时间创建大量预占又释放,可能是在探测库存或竞拍系统;多个账号围绕同一卖家反复预占,可能是转移资产;预占长期不提交,可能是业务回调卡住。把预占指标接入风控,比事后看余额变化更早。
业务接入规范
任何业务接入钱包预占前,都要回答四个问题:预占何时创建,何时提交,何时释放,过期后谁负责解释。业务方不能只实现成功路径。接口层可以强制要求提交 reservationPolicy,包括 maxHoldDuration、allowPartialCommit、onExpireAction 和 compensationChannel。没有策略的业务不允许上线。这样钱包服务不是被动提供扣款接口,而是资产安全的守门人。
上线验收指标
货币预占上线必须带对账。灰度区服每小时检查 available、reserved、frozen 与流水汇总是否一致,检查托管账户余额是否等于所有未结算 reservation 总额,检查订单 committed 数是否等于对应托管转出流水。只看购买成功率没有意义,资产系统要证明每一分钱都能解释。
压测脚本要覆盖重复点击、支付服务超时、竞拍失败释放、订单成功但发货失败、过期任务重复扫描、玩家在预占期间被封禁等情况。回滚不能简单关闭预占,因为已有 reservation 仍需提交或释放。正确回滚是停止创建新预占,保留结算和释放 worker,等存量清空后再切回旧路径。这个步骤需要写入发布计划。
团队协作边界
钱包服务应对业务方保持强约束。业务方只能提交预占、提交、释放和查询,不能直接修改余额,也不能要求“临时帮忙加一个退款接口”。商业化、交易、拍卖和抽卡团队接入前都要写清楚失败补偿路径。
客服工具要能展示玩家可用余额、预占余额、冻结余额和相关业务单号。玩家问“钱去哪了”时,客服需要直接看到是哪笔竞拍、订单或交易占用了货币,而不是找研发查日志。资产系统越透明,争议处理越快。
总结
货币预占不是把扣款流程变复杂,而是把复杂性放在可审计的位置。只要涉及“先占住资产,稍后决定归属”的玩法,都应该优先考虑托管模型。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。