ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

免签支付接入游戏后端:JAVA源码解决回调幂等与掉单补偿

免签支付接入游戏后端:JAVA源码解决回调幂等与掉单补偿 简介这是一套面向Java开发者与毕业设计学生的通用游戏支付平台源码核心解决游戏充值收款与自动发货问题。系统已对接正在运营的个码免签支付通道使用个人支付宝、微信收款二维码即可完成自动过发货资金直接进入个人账户无需第三方中转若不想使用自带通道也可自行搭建免签系统只需全局搜索源码中的免签支付地址并替换为自己的即可。资源兼容MySQL与SQLServer数据库适配多数游戏服务端。压缩包共约2000个文件整体126.93MB涵盖jsp页面、class编译文件、java源码、jar依赖包、xml与properties配置、css与js前端资源、sql脚本及大量gif、jpg、png图片素材另含exe工具与多语言时区数据文件结构完整便于二次开发。目前已有440人学习下载适合需要快速搭建游戏支付模块、研究免签对接流程或完成相关毕业设计的读者参考借鉴。1. 免签支付接入游戏后端这套 JAVA 源码到底解决了什么问题做游戏后端的人大多碰过这个场景玩家充值下单成功钱到没到、订单状态怎么同步、回调丢了怎么办。传统做法是接支付宝或微信的官方商户接口要企业资质、要签约、要等审核个人开发者和小团队基本走不通。于是「免签支付平台」成了很多中小游戏项目的现实选择——它用个人收款码做资金归集再通过监听或回调把到账结果通知给业务系统。这套 JAVA 游戏支付源码要解决的就是把「游戏下单」和「免签平台回调」这两端接起来让充值流程能自动闭环。它适合谁一是手里有 JAVA 游戏服务端、想快速加充值能力的开发者二是需要多游戏共用一套支付中台的团队三是想研究支付回调幂等、订单对账这类工程问题的人。核心难点不在写接口而在回调验签、订单幂等、掉单补偿这三件事上后面几章会逐个拆开讲。免签支付平台本身有合规边界本文只谈技术对接落地前请自行确认业务合法性。2. 免签支付的下单与回调链路从玩家点击到账务入账2.1 免签支付和官方支付在链路上的本质差别官方支付的链路是服务端调统一下单接口 → 拿到预支付凭证 → 前端唤起收银台 → 用户付款 → 官方服务器异步回调你的 notify 地址 → 你验签改单。整条链路里支付平台是可信第三方回调带签名你只需要验签。免签支付的链路多了一层不确定性资金走的是个人收款码平台靠「监听收款通知」或「用户手动确认」来判断到账然后再以 HTTP 回调的形式通知你。这意味着两件事——第一回调可能延迟几秒到几分钟都正常第二回调可能重复也可能丢。所以你的订单系统不能假设「回调一定来、且只来一次」必须自己兜底。理解这个差别后面所有设计才有依据。很多人第一次接免签直接照搬官方支付的写法结果上线就遇到重复加币、掉单不补的问题这就是没吃透链路差异。2.2 一次完整充值的时序拆解把一次充值拆成六步每一步都要有明确的落库动作玩家在游戏内点充值前端带productId、gameId、serverId请求你的下单接口。你的支付中台生成内部订单号orderNo状态置为CREATED落库。中台调用免签平台的「创建订单」接口把orderNo、金额、回调地址传过去拿到平台返回的支付链接或二维码。前端展示二维码玩家扫码付款。免签平台检测到账后回调你的notify地址带上平台订单号和签名。你验签、查单、幂等判断通过后把订单置为PAID再通知游戏服发货。关键在第 6 步。发货动作必须和订单状态变更在同一个事务语义里或者用「状态机 消息队列」保证最终一致。我一般会把「改单」和「发货」拆成两步先原子地把订单从CREATED改成PAID用带条件的 UPDATE改成功的那一次才去触发发货这样天然幂等。2.3 订单表结构该怎么设计免签场景下订单表要能扛住重复回调和人工对账字段不能省。下面是我常用的最小字段集字段名类型说明idbigint自增主键order_novarchar(32)内部订单号唯一索引platform_order_novarchar(64)免签平台订单号可空game_idint游戏标识server_idint区服标识player_idvarchar(64)玩家标识amountdecimal(10,2)金额单位元statustinyint0创建 1已支付 2已发货 3失败notify_countint回调次数排查用create_timedatetime创建时间pay_timedatetime支付时间order_no上加唯一索引是底线防止并发下单产生重复单。notify_count看着不起眼但排查「到底回调了几次」时非常有用属于血泪经验换来的字段。3. 用 Spring Boot 搭支付中台下单接口与回调验签的落地代码3.1 下单接口的实现与参数校验下单接口要做的三件事参数校验、生成订单、调平台。下面是一个精简版实现基于 Spring Boot MyBatis 的常见组合。RestController RequestMapping(/pay) public class PayController { Autowired private OrderService orderService; Autowired private MianQianClient mianQianClient; PostMapping(/create) public Result createOrder(RequestBody Valid CreateOrderReq req) { // 1. 参数校验由 Valid 完成金额必须为正 if (req.getAmount().compareTo(BigDecimal.ZERO) 0) { return Result.fail(金额非法); } // 2. 生成内部订单落库状态 CREATED Order order orderService.createOrder(req); // 3. 调用免签平台创建订单拿支付链接 String payUrl mianQianClient.create(order.getOrderNo(), req.getAmount()); return Result.ok(payUrl); } }逻辑说明createOrder内部要先做一次「同玩家同商品短时间重复提交」的拦截避免玩家连点生成多单。参数说明amount用BigDecimal而不是double金额计算绝不能用浮点orderNo建议用「时间戳 游戏ID 随机数」拼长度控制在 32 位以内兼容多数平台。3.2 回调验签与幂等改单回调接口是整个支付里最容易被攻击和最容易出 bug 的地方。验签必须做幂等必须做。PostMapping(/notify) public String notify(RequestParam MapString, String params) { // 1. 验签按平台规则拼接参数 密钥做 MD5 if (!SignUtil.verify(params, SECRET_KEY)) { return fail; } String orderNo params.get(out_trade_no); String platformNo params.get(trade_no); // 2. 幂等改单只有 CREATED 状态才能改成 PAID int updated orderMapper.markPaid(orderNo, platformNo); if (updated 1) { // 3. 只有真正改单成功的那一次才触发发货 orderService.deliver(orderNo); } return success; }逻辑说明markPaid对应的 SQL 必须带状态条件例如UPDATE orders SET status1 WHERE order_no? AND status0返回影响行数。参数说明SECRET_KEY从配置中心读不要硬编码返回给平台的字符串要严格按平台要求多数免签平台要求返回success才停止重推。3.3 掉单补偿的定时任务回调可能丢所以必须有一个主动查单的兜底任务。用 Spring 的定时任务即可。Scheduled(fixedDelay 60000) public void compensate() { // 查 5 分钟前仍是 CREATED 的订单 ListOrder list orderMapper.findStaleCreated(5); for (Order o : list) { // 主动向平台查单 String status mianQianClient.query(o.getOrderNo()); if (PAID.equals(status)) { int updated orderMapper.markPaid(o.getOrderNo(), null); if (updated 1) { orderService.deliver(o.getOrderNo()); } } } }逻辑说明fixedDelay保证上一次执行完再等 60 秒避免任务堆积。参数说明findStaleCreated(5)里的 5 是分钟数太短会误查还没付款的单太长掉单恢复慢一般 3 到 10 分钟之间。这个任务和回调共用同一套幂等改单逻辑所以不会重复发货。4. 多游戏共用一套支付中台的隔离与配置4.1 按 gameId 做数据与密钥隔离一套中台接多个游戏时最容易出的问题是「A 游戏的回调改了 B 游戏的单」。解决办法是在订单表里冗余game_id所有查询和改单都带上它。密钥也要按游戏隔离每个游戏在免签平台注册独立的商户号回调地址里带上gameId参数验签时用对应游戏的密钥。配置上我一般用一张game_pay_config表存game_id、app_id、secret_key、notify_url启动时加载进本地缓存避免每次回调都查库。密钥轮换时更新表并刷新缓存即可。4.2 回调地址的动态路由回调地址建议统一成一个入口用路径变量区分游戏例如/pay/notify/{gameId}。这样平台侧只配一个域名新增游戏不用改平台配置。路由到具体处理逻辑时先按gameId取密钥验签再走统一的幂等改单流程。要注意的是gameId不能只从回调参数里取最好从 URL 路径取因为参数可能被篡改。路径里的gameId配合验签能挡住大部分伪造回调。5. 免签支付对接的避坑清单5 个真实翻车点5.1 回调重复导致重复发货现象玩家充值一次游戏里到账两次甚至多次。原因免签平台在没收到success响应时会重推或者平台本身逻辑就重推。解决改单 SQL 必须带status0条件用影响行数判断是否是首次处理只有首次才发货。这是最经典也最容易犯的错。5.2 验签顺序错误导致全部回调失败现象所有回调都验签不通过订单一直不掉。原因免签平台拼接签名的参数顺序、是否包含空值、是否 URL 编码和你实现的不一致。解决严格按平台文档的拼接规则来把平台给的示例参数拿来单测一遍别凭感觉写。不同平台规则差异很大换平台一定要重测。5.3 金额用 double 导致对账差一分钱现象对账时发现订单金额和平台金额差 0.01。原因double精度问题。解决全链路用BigDecimal数据库用decimal和平台交互时金额统一转成「分」的整数传输避免小数。5.4 回调接口没做限流被刷现象日志里大量伪造回调CPU 飙高。原因回调地址暴露被扫描到后恶意请求。解决验签失败的请求直接返回 fail 并记录 IP加一层限流如 Guava RateLimiter 或网关限流必要时对来源 IP 做白名单。验签本身就是第一道防线但限流能防住量。5.5 定时补偿任务和回调并发改单现象偶发重复发货日志显示补偿任务和回调几乎同时执行。原因两个线程同时读到CREATED状态。解决改单用数据库行锁或乐观锁UPDATE ... WHERE status0本身就是原子的只要发货逻辑挂在「改单成功」分支下就不会重复。别用「先查再改」的两步写法。6. 把支付中台做成可复用的 SDK一个进阶技巧前面几章的代码是「能跑」但如果你手里不止一个游戏项目每次都复制一遍 Controller 和 Service 就很痛苦。我的习惯是把支付中台抽成一个独立的 Spring Boot Starter业务方只依赖 jar 包、加几行配置就能接入。具体做法把下单、回调、补偿三块逻辑封装进pay-core模块对外只暴露一个PayService接口和一组配置项。业务方实现一个DeliverCallback接口负责发货中台在改单成功后回调它。配置用ConfigurationProperties绑定密钥从配置中心注入。public interface DeliverCallback { // 业务方实现收到这个回调时给玩家发货 void onPaid(String orderNo, String gameId, String playerId, BigDecimal amount); }这样做的价值在于回调验签、幂等、补偿这些容易写错的逻辑只维护一份业务方不用关心支付细节只关心「发货」这一件事。新增游戏时接入成本从「改一堆代码」降到「加一段配置 实现一个接口」。验证方法也简单写一个 Mock 平台模拟重复回调、延迟回调、伪造签名三种情况跑一遍看订单状态和发货次数是否符合预期。我一般会把这个 Mock 做成单测每次改支付逻辑都跑一遍比上线后救火强得多。最后说个教训支付这块千万别图快。我早期接过一个项目回调没做幂等上线第一天就被玩家刷了重复到账赔了不少。后来所有支付相关的改动我都坚持先写幂等测试再写业务代码。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进