资讯详情

游戏付费系统设计:货币分层、订单链路与幂等发货

发布时间:2026/9/30 3:50:02

500+
企业客户服务经验
120+
行业领域内容覆盖
3000+
原创页面设计沉淀
98%
客户满意度

游戏付费系统设计:货币分层、订单链路与幂等发货

1. 先想清楚钱怎么变成战力三层货币与数值闭环做游戏付费系统代码其实是最不重要的部分。我在项目里踩过的第一个大坑不是回调验签失败也不是订单重复发货而是数值策划和支付模块根本没说清楚钱进来之后到底变成什么。我们这套系统是照着原神那套付费模型搭的——创世结晶、原石、抽卡券、月卡、纪行通行证——但真正落到服务端得先把货币分层定死不然写到一半就会发现到处都是特殊商品的分支判断。1.1 法币、代币、消耗品三层货币模型我把整套体系切成三层每一层的职责和数据来源都完全不同。层级典型名称获取途径是否可交易服务端归属第一层真实货币人民币/美元支付渠道否支付系统不落游戏库第二层创世结晶付费代币充值获得1元≈10结晶否充值模块直接发货第三层原石、摩拉、抽卡券结晶兑换、任务、活动否游戏内资源模块关键点在于支付系统只负责把第一层变成第二层绝不碰第三层。玩家充值648元服务端发的永远是8080个创世结晶含首充双倍则是16160至于玩家把这批结晶换成原石还是买月卡那是游戏内兑换模块的事。这条边界一旦模糊后面退款、扣货、补偿全部会乱套——你根本说不清退他648该扣多少原石。我见过有项目为了省事充值直接发原石。结果遇到一个活动原石兑换比例临时调了退款的时候按哪个比例扣两边都算不平最后只能人工赔钱。1.2 为什么不把原石直接当商品卖最直接的理由是退款可控性。付费代币是单向闸门结晶一旦换成原石就进了消耗池而原石一旦抽掉就变成了角色、武器。整条链路越往后回收成本越高。第二层货币还有一个隐藏用途跨渠道统一计价。App Store、各家安卓渠道、PC 端各自抽成不同、结算币种不同但玩家看到的是648 元 8080 结晶这个统一锚点。渠道差异被挡在支付系统内部游戏逻辑完全无感。还有一点容易被忽略首充双倍、月卡、礼包这些都是围绕结晶定价的。如果商品直接是原石那么首充双倍就得做成原石×2一旦后续需要调整兑换比率历史订单的追溯会变成一场灾难。1.3 档位定价与锚点设计付费系统里最值钱的东西不是代码是那六个价格档位。常见的结构是 6 / 30 / 98 / 198 / 328 / 648每一档的单位价值递增制造买大档更划算的错觉。我在配置表里把它做成了这样{ product_id: gem_648, price_fen: 64800, currency: CNY, base_gem: 6480, bonus_gem: 1600, first_charge_bonus: 8080, group: gem_pack, sort: 60 }base_gem bonus_gem才是常规到账量first_charge_bonus是首充额外那一份。注意first_charge_bonus只在group维度首次触发——这里有个设计取舍首充双倍是按整个充值组发一次还是按每个档位各发一次我们最终选了整组一次理由是数值上更可控玩家也不会为了薅首充去挨个买小档。提示所有涉及发多少的数值都必须落在配置表里不能硬编码在代码里。版本更新时策划要能直接改代码只负责读取和执行。这一层设计定下来之后后面所有的订单、发货、退货逻辑才有统一的语言。我个人的体会是付费系统 70% 的线上事故追根溯源都是货币层级没分清楚。2. 一笔充值从点击到到账订单链路的完整拆解玩家点击购买 648到屏幕上跳出 8080 结晶中间大概有 6 个环节。这条链路上任何一环断掉玩家看到的都是我付了钱但没到账而这类工单的优先级在运营那边永远是最高级。我把整条链路拆开讲一遍顺便说说每一环最容易翻车的地方。2.1 预下单客户端只负责发起不负责决策客户端点了按钮之后做的唯一一件事就是请求服务端创建订单POST /api/pay/create { product_id: gem_648, channel: wxpay, client_version: 1.4.2, device_id: xxxx }服务端这时候要做四件事校验product_id是否存在且上架、校验玩家账号状态封禁中不允许下单、从配置表读取金额、生成全局唯一的order_id落库状态置为待支付。金额绝对不能让客户端传。我接手过一个老项目客户端把price一起传上来服务端直接用了。这个洞被人发现之后测试环境被人用 0.01 元买了一堆大档。服务端必须自己查表算钱客户端传来的任何价格字段都只做交叉校验不一致直接拒绝下单。order_id的生成也有讲究。我们用业务前缀 时间戳 分片ID 自增序列比如G64820240518153012000042。不要用 UUID因为对账的时候需要能肉眼看出时间也不要纯自增因为要防遍历。2.2 回调验签三种最常见的翻车姿势支付渠道的回调是整个系统里唯一一个外部世界主动打进来的入口所有安全压力都集中在这。翻车通常翻在这三处第一没验签或者验签写错。有的实现只检查了sign字段存在没真正做摘要比对。正确的做法是先按渠道文档规定的字段顺序拼接待签串用渠道给的密钥做一次签名再和回调里的sign比较比较时用恒定时间比较避免时序侧信道。第二金额没做二次校验。回调里带了total_fee必须和订单表里的amount_fen对齐。我见过回调只验签不验金额的实现攻击者用一笔 1 元订单的合法签名去伪造 648 元订单的发货请求。虽然渠道签名理论上绑定了订单号但多一道本地校验成本几乎为零。第三把回调当同步接口用。渠道回调的超时窗口通常很短如果在这个请求里做了大量数据库操作、发了邮件、调了别的服务很容易超时渠道就会重推于是就变成了重推 慢处理的双重压力。正确做法是回调接口只做验签、落库、投递消息实际发货交给异步消费者。func HandleCallback(w http.ResponseWriter, r *http.Request) { body, _ : io.ReadAll(r.Body) if !verifySign(body, r.Header.Get(X-Sign)) { w.WriteHeader(401) return } var cb CallbackBody json.Unmarshal(body, cb) // 幂等同一笔渠道单号只落一次 if err : orderSvc.MarkPaid(cb.OutTradeNo, cb.ChannelOrderNo, cb.TotalFee); err ! nil { // 已处理过也要返回成功否则渠道会一直重推 w.Write([]byte({code:SUCCESS})) return } mq.Publish(order.paid, cb.OutTradeNo) w.Write([]byte({code:SUCCESS})) }注意最后那个注释即使内部处理失败只要不是这笔订单不存在这种硬错误回调也必须回成功否则渠道会按照退避策略反复重推严重的时候能把你的入口打崩。2.3 幂等发货用状态机锁死一份订单一次货发货环节的核心只有一句话用一条带条件的 UPDATE 语句来保证只发一次。UPDATE t_order SET status 2, deliver_time NOW(), version version 1 WHERE order_id ? AND status 1;如果affected_rows 1说明这次是第一个把订单从已支付推到已发货的调用者放行发货如果等于 0说明有人抢先了直接返回成功即可。这套写法比先查询状态再判断再更新可靠得多因为查询和更新之间存在时间窗口。哪怕中间隔了 200 毫秒两个并发的消费者都可能同时读到status 1。发货本身也要落到流水表里并且order_id上加唯一索引CREATE TABLE t_deliver_log ( id BIGINT NOT NULL AUTO_INCREMENT, order_id VARCHAR(32) NOT NULL, uid BIGINT NOT NULL, item_type VARCHAR(32) NOT NULL COMMENT gem/card/pass, item_count INT NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_item (order_id, item_type) ) ENGINEInnoDB;流水表的唯一索引是最后的保险丝。哪怕前面状态机的逻辑被人改坏了数据库这一层还会拦一次。2.4 掉单与补单定时对账任务怎么写再严密的实时链路也会掉单。渠道回调丢包、服务重启、消息队列积压都会导致钱到了但货没发。所以必须有一个对账任务我一般做成两级分钟级扫描status 1且pay_time超过 2 分钟仍未发货的订单重新投递发货消息。处理的是短时抖动。小时级拉取渠道的账单文件和本地订单表做双向比对。处理的是彻底丢失的回调。双向比对的双向很关键。只做渠道有、本地没有会漏掉退款只做本地有、渠道没有会漏掉伪造订单。我一般在差集出来之后只对金额大于阈值的差异自动补单小额的走人工避免对账任务本身被异常数据带偏。3. 原石消耗端的设计抽卡、保底与付费深度测算付费系统的上半段是钱变成结晶下半段是结晶变成原石再变成角色。下半段虽然不直接碰支付渠道但它决定了玩家愿不愿意继续付钱所以在系统设计上同等重要。3.1 保底计数器存在哪、怎么存抽卡池的状态比想象中复杂。以一个典型的限定角色池为例需要记录的东西至少包括字段含义说明pity_5距离上次五星的抽数硬保底 90 时归零pity_4距离上次四星的抽数硬保底 10 时归零guarantee_5下次五星是否为大保底歪了之后置 1pool_id卡池标识不同池独立计数CREATE TABLE t_gacha_counter ( uid BIGINT NOT NULL, pool_id INT NOT NULL, pity_5 INT NOT NULL DEFAULT 0, pity_4 INT NOT NULL DEFAULT 0, guarantee_5 TINYINT NOT NULL DEFAULT 0, total_draw INT NOT NULL DEFAULT 0, update_time DATETIME NOT NULL, PRIMARY KEY (uid, pool_id) ) ENGINEInnoDB;这里的设计取舍是计数器按池独立还是全局共享独立计数对玩家更友好但会让数值策划的付费深度测算变复杂因为玩家可以在多个池之间分仓消耗免费抽数。共享计数则相反。我参与过的项目大多选择独立因为玩家体验优先付费意愿其实更高。3.2 随机数可信性与概率公示抽卡的随机数必须走服务端而且要用密码学安全的随机源不能用普通伪随机。原因不是技术洁癖而是可申诉性玩家怀疑概率有问题的时候你需要能拿出这次抽卡的完整记录。CREATE TABLE t_gacha_log ( id BIGINT NOT NULL AUTO_INCREMENT, uid BIGINT NOT NULL, pool_id INT NOT NULL, item_id INT NOT NULL, rarity TINYINT NOT NULL, pity_at INT NOT NULL COMMENT 本次抽取时的累计计数, rand_value INT NOT NULL COMMENT 本次判定用的随机数原始值, create_time DATETIME NOT NULL, PRIMARY KEY (id), KEY idx_uid_time (uid, create_time) ) ENGINEInnoDB;把rand_value存下来是个被低估的好习惯。有玩家申诉我 89 抽没出五星的时候把这一串记录拉出来能清清楚楚看到每一次判定的原始值落在哪个区间。概率公示在合规上是硬要求。常见的公示值是五星基础概率 0.6%第 74 抽起概率逐步提升第 90 抽必出综合概率约 1.6%四星基础 5.1%每 10 抽必出综合约 13%。这些数字一旦公示就不能随便改动所以概率表必须是配置且带版本号每次调整都要留档否则很容易说不清历史数据。3.3 付费深度的粗算方法数值侧最关心的一个问题是一个大 R 抽满一个限定池大概要花多少钱。粗略算法是这样的一抽消耗 160 原石648 档位常规到账 8080 结晶1 结晶 1 原石也就是约 50.5 抽。硬保底 90 抽出一金出金期望大约是 62 抽左右。如果限定角色需要歪一次再保底那期望落在 120 抽上下也就是 19200 原石约合 2.4 个 648 档位。这个测算决定了礼包怎么设计。如果大 R 两三单就能拿满那礼包的溢价空间就很小如果期望值到了五六单就可以穿插一些抽卡券礼包来降低单次决策成本。我在项目里做这块的时候会专门做一个消耗-付费漏斗看板看每一档的转化率用来反推是价格问题还是概率问题。4. 数据表与并发控制别让同一笔钱发两次货前面聊了链路的业务流程这一节讲底层。付费系统的并发压力其实不大——峰值也就每秒几十单——但它对数据一致性的要求极高。少发一次货玩家会投诉多发一次货是直接的资金损失而且很难追回。4.1 订单表设计的几个关键约束CREATE TABLE t_order ( order_id VARCHAR(32) NOT NULL COMMENT 自研订单号, uid BIGINT NOT NULL, zone_id INT NOT NULL, product_id VARCHAR(64) NOT NULL, amount_fen INT NOT NULL COMMENT 实付金额单位分, channel VARCHAR(16) NOT NULL COMMENT 渠道标识, channel_order VARCHAR(64) DEFAULT NULL COMMENT 渠道单号, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已退款 4已关闭, create_time DATETIME NOT NULL, pay_time DATETIME DEFAULT NULL, deliver_time DATETIME DEFAULT NULL, version INT NOT NULL DEFAULT 0, PRIMARY KEY (order_id), UNIQUE KEY uk_channel_order (channel, channel_order), KEY idx_status_time (status, create_time), KEY idx_uid_time (uid, create_time) ) ENGINEInnoDB;三个索引各有用途uk_channel_order防止同一个渠道单号落成两笔订单渠道重推时最有用idx_status_time给对账和补单任务扫表用idx_uid_time给客服查玩家充值记录用。金额统一用分存整数这是老规矩了用浮点数存钱迟早出事。version字段是给乐观锁留的虽然发货主路径用的是条件更新但在一些管理后台的修改场景里还是会用到。4.2 事务边界划在哪发货涉及三个写操作更新订单状态、写发货流水、给玩家加结晶。这三个必须在一个事务里否则会出现订单标记已发货但结晶没加上这种状态。但有个例外发邮件通知、打点上报、写日志这类操作不能放在事务里。我见过一个项目因为事务里调了邮件服务邮件服务一挂整个发货事务全部回滚玩家充了几百单全部卡住。异步的东西一律扔消息队列事务里只留最核心的三个写操作。4.3 什么时候真的需要分布式锁很多人的第一反应是发货要加分布式锁。我的经验是先用数据库的条件更新和唯一索引只有在确实需要跨表、跨服务协调的时候才上锁。场景推荐方案原因单订单发货条件 UPDATE数据库原子性足够同账号并发下单唯一索引 限流避免用锁做业务约束跨服发货分布式锁Redis需要跨进程互斥抽卡计数器更新行锁 重试热点行注意死锁Reds 锁这块有个坑值得说加了锁之后一定要设置过期时间并且要在 finally 里释放。更稳妥的做法是用带唯一值的锁释放前校验是不是自己持有的避免 A 的锁超时后被 B 拿走、A 又把它删了。另外抽卡这种热点操作如果同一个玩家疯狂连点计数器那一行会成为热点。我们的做法是在网关层就对uid做按抽数粒度的限流同时在数据库层设置一个短暂的重试退避实测能把死锁概率压到几乎为零。5. 月卡、首充双倍、纪行那些有状态的付费权益一次性购买、一次性发货的商品好做。真正麻烦的是有状态的权益——月卡要连续 30 天发首充要一生一次纪行要按周期结算。这些东西的共同特点是一次付费多次发奖跨天跨版本。5.1 月卡跨天重置与漏领补发月卡的本质是一张 30 天的订阅每天领一次。表结构我一般这么设计CREATE TABLE t_month_card ( uid BIGINT NOT NULL, card_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, last_claim_day DATE DEFAULT NULL COMMENT 最后领取的自然日, total_days INT NOT NULL DEFAULT 30, remain_days INT NOT NULL DEFAULT 30, PRIMARY KEY (uid, card_id) ) ENGINEInnoDB;关键点有三个。第一用自然日而不是 24 小时周期否则玩家每天领奖时间会漂移越领越晚。第二跨天的时间点要用服务器统一时区不能各服务器各自为政否则合服的时候会撞车。第三last_claim_day用来做幂等同一天重复请求只发一次。漏领补发是个产品问题而非技术问题。我们的策略是月卡有效期内不补但过期时可以把未领取的剩余天数折算成一次性奖励邮件发出去。这个折算比例是策划定的但技术上要保证折算这个操作本身也是幂等的不能玩家点两次就发两次。5.2 首充双倍一次性标记的正确存法首充双倍看起来最简单实际上最容易出问题。千万别用查询该玩家历史订单里有没有成功的充值来判断首充因为订单表会分表、会归档、会被清理而且退款的订单算不算首充说不清楚。正确做法是单独存一个标记表CREATE TABLE t_first_charge ( uid BIGINT NOT NULL, group VARCHAR(32) NOT NULL, used_time DATETIME NOT NULL, PRIMARY KEY (uid, group) ) ENGINEInnoDB;发货流程变成发货前先尝试INSERT插入成功说明是首充发双倍插入失败唯一键冲突说明已经用过了发常规量。这个判断和发货必须在同一个事务里否则并发下会重复发双倍。退款的时候要不要清掉这个标记我们的选择是不清。因为玩家退掉首充单之后再充一次还能拿双倍那就是个无限套利的口子。5.3 纪行通行证周期结算与奖励追溯通行证的特点是有经验进度和周期边界。一个周期内玩家通过任务累积经验达到等级后可以领对应档位的奖励其中免费档和付费档分开。技术上要注意的是周期切换时刻的处理。如果玩家在周期结束前 1 分钟购买了通行证但结算任务在结束后 30 秒才跑会不会导致他买了但没拿到对应奖励我们的做法是购买行为绑定购买时刻的周期 ID奖励发放按周期 ID 追溯而不是按当前周期。这样即使结算延迟也能准确找到他该拿的那一期奖励。另外通行证的付费档奖励最好是购买后一次性补发之前已达成等级的奖励。这需要保存每个等级是否已领取的位图或者记录表CREATE TABLE t_pass_reward ( uid BIGINT NOT NULL, season_id INT NOT NULL, level INT NOT NULL, track TINYINT NOT NULL COMMENT 0免费档 1付费档, claim_time DATETIME NOT NULL, PRIMARY KEY (uid, season_id, level, track) ) ENGINEInnoDB;主键本身就是幂等键玩家重复点领取直接被数据库拦掉代码里连状态判断都省了。6. 线上事故排查从玩家反馈到定位根因的完整链路付费系统上线之后最常见的工单就是三类付了没到账、重复扣款、领不了奖励。这三类的排查路径完全不同我把完整的排查链路写出来遇到的时候可以照着走。6.1 三类高频事故和它们的根因分布现象高频根因排查入口付了款没到账回调丢失、异步消费者卡住、发货事务失败t_order状态 消息队列积压量重复扣款渠道重复下单、客户端连点、未做下单限流t_order同 uid 短时间多单奖励领不到权益状态未写入、周期 ID 不匹配、并发冲突权益表 领取流水表这三类里付了没到账占七成以上。所以我第一件事永远是看订单表的状态分布把所有status 1且pay_time超过 3 分钟的记录拉出来如果数量在短时间内激增那基本可以确定是发货链路断了而不是个别玩家的偶发问题。6.2 我的固定排查顺序遇到工单我会按这个顺序走不要跳步先看订单存不存在。如果order_id在库里根本查不到说明问题出在下单或回调入口不是发货。看订单状态和时间戳。pay_time有值但status还是 1说明回调到了、发货没走完。看发货流水表。流水有记录但玩家说没收到那就是加资源的逻辑出问题去查角色数据。看消息队列。如果有一批订单卡在同样的状态大概率是消费者挂了或者消息堆积。看渠道账单。前四步都对不上才去拉渠道那边的流水确认钱到底有没有到。这个顺序的价值在于从近到远。先排除自己系统内部的问题再去查外部渠道能省掉大量沟通成本。我见过有人一上来就找渠道客服来回扯皮两天最后发现是自己消费者线程池被一个慢查询阻塞了。6.3 风控与开关事故的止损手段出事故的时候第一优先级不是修是止血。我们平时会准备好几个开关下单开关一键关闭某个渠道的下单入口防止问题订单继续涌入。发货开关关闭自动发货转为人工审核后补发。商品下架某个档位的配置有问题时直接下架。灰度比例新版本付费逻辑先对 1% 玩家开启。风控侧要盯的几个信号同一uid短时间大量下单、同一设备多账号充值、异常金额的小额订单聚集、退款率突增。这些信号不用做得很复杂先用简单的阈值规则跑起来有数据之后再上模型。7. GM 后台运营真正需要的那几个按钮技术侧做完之后运营能不能自助处理问题决定了这套系统的实际运营成本。我给后台定的原则是能用按钮解决的绝不让运营来找开发。7.1 补单、扣货、退款三个核心操作补单是最常用的。运营输入订单号后台校验订单确实处于已支付未发货状态然后触发一次标准发货流程。注意补单走的必须是和正常发货同一套逻辑不能另写一份否则幂等保证就失效了。扣货要谨慎。给玩家扣结晶之前必须检查当前余额够不够不够的情况要么冻结部分、要么记为负数债务。我们的做法是结晶不足时不允许扣改为给账号打一个待处理标记由人工跟进。退款是三条链路里最复杂的因为它牵扯渠道。后台发起退款请求给渠道渠道回调退款结果然后本地做一次扣货。整个流程必须和发货一样走状态机状态字段加一个退款中避免退款请求重复发起。7.2 看板上真正有用的几个指标后台首页我只留了四个数字当日充值金额与订单数按渠道拆分待发货订单数这个必须实时刷新它是最直接的健康度指标24 小时内发货失败率退款率前两个用来发现异常第三个用来评估链路质量第四个用来判断有没有人在薅羊毛。指标不求多求的是每次打开后台都能一眼看出今天有没有出事。后台的权限也要分。普通客服只能查不能改补单要二级审批退款要财务角色。付费系统的后台权限一旦松了内部风险比外部攻击还大。最后分享一个我在项目里坚持了很久的小习惯任何一次改动付费相关代码上线前必须在测试环境跑一遍完整的下单-支付-回调-发货-对账全链路包括模拟回调重复推送和模拟消息丢失两个异常场景。这两个场景覆盖了我遇到过的八成线上事故跑一遍只要十分钟比线上出事后凌晨爬起来排查划算太多。
热门专题

继续阅读更多专题内容

围绕企业服务、数字化转型与官网运营的常青话题,持续输出深度内容

企业官网建设指南 企业托管服务模式 财税政策与解读 企业数字化转型 官网SEO与获客 网站安全与运维
配套服务

读完这篇文章,了解更多服务

从整站搭建到SEO布局,17项核心服务助您打造高转化的企业官网

01

企业托管整站搭建

从信息架构到栏目预留,搭建可生长的企业站点骨架,每个页面独立原创设计。...

了解详情
02

规整可信网页设计

雪地靴温暖风原创设计,金属铜线条贯穿全页,拒绝通用模板与AI流水线。...

了解详情
03

企业服务SEO布局

关键词体系与语义化结构,从建站源头为搜索排名而生。...

了解详情
04

业务预约咨询表单

多场景表单与线索收集体系,把访问流量转化为可追踪的销售线索。...

了解详情
05

企业服务站点运维

安全巡检、数据备份与内容更新支持,全年守护网站稳定运行。...

了解详情
06

全终端商务适配

电脑、平板、手机一致呈现,移动端体验与转化同样出色。...

了解详情
需要专业建议?

让专业顾问为您解读行业趋势

关于企业官网建设、SEO获客与数字化转型的任何疑问,欢迎一对一咨询我们的专业顾问。