简介:这是一套基于Java开发的通用游戏支付平台源码,面向游戏运营者、独立开发者及中小型游戏团队,用于解决游戏内虚拟商品购买与自动发货的支付对接问题。系统已对接正在运营的免签支付平台,用户使用个人支付宝或微信收款二维码即可完成支付,款项直接进入个人账户,省去第三方托管环节。源码兼容MySQL与SQLServer数据库,若需更换支付通道,只需全局搜索安装文件中的免签支付地址并替换为自有接口即可,扩展性较强。压缩包共约2000个文件,涵盖jsp页面、class编译文件、java源码、jar依赖包、xml与properties配置、sql脚本及大量gif、jpg、png图片资源,整体约151.86MB,并附有详细安装说明。目前已有412人学习下载,适合具备一定Java基础、希望快速搭建游戏支付系统的开发者参考与二次开发。
1. 一套 JAVA 游戏支付源码,为什么绕不开免签支付平台
做游戏联运或者私服起盘的人,十有八九在支付这一关卡过壳。你手上有一套 JAVA 游戏支付源码,逻辑写得再漂亮,只要收款通道没打通,玩家点充值就是转圈。而正规支付接口对游戏类商户的审核越来越严,个人或小团队根本拿不到,于是「免签支付平台」成了绕不开的选项——它不需要你提供营业执照和对公账户,靠监听个人收款码的到账通知来确认订单。
这套「JAVA游戏支付源码通用游戏支付平台程序-已对接正在运营的免签支付平台」讲的就是这么一件事:用一套通用的 JAVA 支付平台程序,把游戏服务端和免签支付通道对接起来,让充值订单能自动回调、自动发货。适合谁?适合手里有游戏服务端、想自己搭一套充值中台的后端开发,也适合想理解支付回调链路的产品和运维。下面我按实际落地的顺序,把选型、建表、对接、回调、对账和踩坑一条条拆开讲。
2. 通用游戏支付平台的技术选型与订单模型
2.1 为什么用 Spring Boot + MyBatis 而不是裸 Servlet
游戏支付平台的核心诉求是「稳」和「可查」。玩家充值失败要能立刻定位是哪一步断了,运营要能按订单号、按渠道、按时间维度拉数据。裸 Servlet 写起来快,但事务、连接池、日志、定时任务全得自己搭,后期加一个渠道就要动一次底层。
常见做法是 Spring Boot + MyBatis。Spring Boot 负责把 Web 层、定时任务、配置管理串起来,MyBatis 负责订单和渠道配置的持久化。这里不追求微服务,单体足够——支付平台的并发量级通常远低于游戏战斗服,瓶颈在通道稳定性和对账准确性,不在吞吐。选 MyBatis 而不是 JPA,是因为支付场景里大量手写 SQL 做条件筛选和统计,XML 映射比注解更可控,尤其是分页和动态条件。
依赖上,除了 spring-boot-starter-web 和 mybatis-spring-boot-starter,还需要一个 HTTP 客户端去请求免签平台的接口,用 OkHttp 或 Hutool 的 HttpUtil 都行。定时对账用 Spring 自带的 @Scheduled 就够,不必上 XXL-JOB。
2.2 订单表、渠道表、回调日志表怎么设计
支付平台的数据模型不复杂,但字段设计直接决定后面排查顺不顺手。我一般拆三张核心表:订单表、渠道配置表、回调日志表。
订单表t_order关键字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| order_no | varchar(32) | 平台订单号,唯一,主键或唯一索引 |
| out_trade_no | varchar(64) | 免签平台返回的流水号 |
| user_id | bigint | 玩家 ID |
| game_id | varchar(32) | 游戏标识,多游戏共用一套支付时必填 |
| amount | decimal(10,2) | 订单金额,单位元 |
| real_amount | decimal(10,2) | 实付金额,免签可能扣手续费 |
| status | tinyint | 0 待支付 1 已支付 2 已发货 3 失败 4 已退款 |
| channel_code | varchar(32) | 渠道编码,对应渠道表 |
| notify_time | datetime | 回调到账时间 |
| create_time | datetime | 下单时间 |
渠道配置表t_channel存免签平台的接口地址、商户号、密钥、回调地址模板、是否启用。回调日志表t_notify_log把每次回调的原始报文、签名、处理结果、耗时都落库,这是排查「钱到了但没发货」的黑匣子。
提示:order_no 一定要用「业务前缀 + 时间戳 + 随机数」生成,别用自增 ID 暴露给外部,免签平台回调时靠它定位订单。
2.3 下单接口的最小实现
下单接口做三件事:校验参数、落库生成待支付订单、请求免签平台拿到收款二维码或跳转链接。
@PostMapping("/pay/create") public Result createOrder(@RequestBody CreateOrderReq req) { // 1. 参数校验:金额必须大于 0,游戏 ID 不能为空 if (req.getAmount() == null || req.getAmount().compareTo(BigDecimal.ZERO) <= 0) { return Result.fail("金额非法"); } // 2. 生成平台订单号,落库为待支付 String orderNo = "G" + System.currentTimeMillis() + RandomUtil.randomNumbers(6); Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(req.getUserId()); order.setGameId(req.getGameId()); order.setAmount(req.getAmount()); order.setStatus(0); order.setChannelCode(req.getChannelCode()); orderMapper.insert(order); // 3. 调用免签平台下单,拿到支付链接 Channel channel = channelMapper.selectByCode(req.getChannelCode()); String payUrl = mianqianClient.createPay(orderNo, req.getAmount(), channel); return Result.ok(payUrl); }逻辑说明:先落库再请求外部接口,是为了防止请求超时后订单丢失——只要订单在库里,就能靠定时任务补查。参数说明:channelCode决定走哪个免签通道,多通道时前端可以让用户选,也可以按金额区间自动路由。mianqianClient是封装好的 HTTP 客户端,内部负责拼签名和解析返回。
3. 对接免签支付平台:签名、下单与回调
3.1 免签支付的到账确认原理
免签支付平台不直连银行,它的核心是「监听」。你在平台绑定一张个人收款码,平台用一台常驻设备或云端监控这个收款码的到账通知,一旦有匹配金额的入账,就回调你的服务器。所以它的确认依据是「金额 + 时间窗口」,不是订单号——订单号是你和平台之间约定的,平台回调时会带上。
这就带来一个经典问题:同一时间两个玩家充了相同金额,平台怎么区分?答案是平台侧通常要求金额精确到分,或者让你在金额后加随机尾数(比如 100.37),用尾数做匹配。对接前一定要问清楚平台是哪种模式,这决定了你下单时金额怎么算。
3.2 签名算法与下单请求
免签平台的签名大同小异,基本是「参数按 key 排序 + 拼接 + 拼密钥 + MD5 或 HMAC-SHA256」。下面是一个通用封装:
public String buildSign(Map<String, String> params, String secret) { // 1. 过滤空值,按 key 字典序排序 List<String> keys = new ArrayList<>(params.keySet()); Collections.sort(keys); StringBuilder sb = new StringBuilder(); for (String k : keys) { String v = params.get(k); if (v == null || v.isEmpty() || "sign".equals(k)) { continue; } sb.append(k).append("=").append(v).append("&"); } // 2. 拼接密钥后做 MD5,转小写 sb.append("key=").append(secret); return DigestUtil.md5Hex(sb.toString()).toLowerCase(); }逻辑说明:排序是为了保证双方拼出的字符串一致,任何一方顺序不同签名就对不上。参数说明:secret是渠道表里存的商户密钥,绝不能硬编码在代码里,要从数据库或配置中心读。注意sign字段本身要排除,否则会把自己算进去。
下单请求把orderNo、amount、notifyUrl、returnUrl、sign一起 POST 给平台,平台返回一个payUrl或二维码内容。notifyUrl是你的回调地址,必须是公网可访问的,本地调试用内网穿透工具临时映射一个。
3.3 回调接口的幂等与验签
回调是整个链路最容易翻车的地方。免签平台可能因为网络重试多次推送同一笔订单,你的接口必须幂等。
@PostMapping("/pay/notify") public String notify(@RequestParam Map<String, String> params) { // 1. 验签,防止伪造回调 String sign = params.get("sign"); String calcSign = buildSign(params, channel.getSecret()); if (!calcSign.equals(sign)) { log.warn("回调验签失败: {}", params); return "fail"; } // 2. 幂等:先查订单状态,已处理直接返回 success String orderNo = params.get("orderNo"); Order order = orderMapper.selectByOrderNo(orderNo); if (order == null || order.getStatus() >= 1) { return "success"; } // 3. 更新订单为已支付,触发发货 orderMapper.updateStatus(orderNo, 1, params.get("outTradeNo")); gameNotifyService.deliver(order); return "success"; }逻辑说明:验签放在最前面,任何不通过的直接拒绝。幂等靠订单状态判断,status >= 1说明已经处理过,直接返回 success 让平台停止重试。参数说明:返回给平台的必须是平台约定的成功标识,通常是字符串success,返回别的平台会一直重推。发货逻辑要单独抽成 service,方便加事务和重试。
注意:回调接口不要做耗时操作,发货如果涉及调用游戏服务端,建议先更新订单状态再异步发货,避免回调超时导致平台重推。
4. 发货、对账与订单状态机
4.1 发货为什么要和支付回调解耦
很多新手把发货直接写在回调里,回调一慢,平台就重推,重推又触发一次发货,玩家白拿一份。正确做法是回调只负责「确认到账」,发货交给一个独立的任务去消费。
我一般用一张t_deliver_task表,回调成功后插入一条待发货任务,定时任务扫描这张表去调用游戏服务端的充值接口。这样即使游戏服务端临时挂了,任务还在,恢复后自动补发。发货成功后把订单状态从 1 改成 2,任务标记完成。
4.2 订单状态机的合法流转
状态乱跳是支付平台最隐蔽的 bug。把合法流转固定下来:
| 当前状态 | 允许流转到 | 触发条件 |
|---|---|---|
| 0 待支付 | 1 已支付 | 回调验签通过 |
| 0 待支付 | 3 失败 | 超时未支付,定时关单 |
| 1 已支付 | 2 已发货 | 发货任务成功 |
| 1 已支付 | 4 已退款 | 运营手动退款 |
| 2 已发货 | 4 已退款 | 运营手动退款 |
任何不在表里的流转都要拒绝并告警。比如订单已经是 2 已发货,又收到一个回调想改成 1,这种要么是重复回调(幂等已挡),要么是有人伪造,必须记日志。
4.3 定时对账:主动查单补漏
回调不是 100% 可靠,网络抖动、平台故障都可能丢回调。所以必须有一个主动查单的定时任务,扫描「创建超过 5 分钟且状态仍为 0」的订单,去免签平台查询真实状态。
@Scheduled(fixedDelay = 60000) public void reconcile() { // 1. 查出 5 分钟前创建、仍未支付的订单 List<Order> pending = orderMapper.selectPendingBefore( LocalDateTime.now().minusMinutes(5)); for (Order order : pending) { // 2. 主动向免签平台查单 String status = mianqianClient.query(order.getOrderNo()); if ("PAID".equals(status)) { // 3. 补一次发货流程,走和回调相同的幂等逻辑 orderMapper.updateStatus(order.getOrderNo(), 1, null); deliverTaskService.create(order); } else if (isTimeout(order)) { orderMapper.updateStatus(order.getOrderNo(), 3, null); } } }逻辑说明:查单接口和回调走同一套状态更新逻辑,保证幂等。参数说明:fixedDelay设 60 秒,太频繁会给免签平台压力,太慢玩家等得急。超时时间一般设 15 到 30 分钟,看平台规则。
5. 免签支付对接的避坑与排查清单
5.1 回调收不到:先查网络再查签名
现象:玩家付款成功,订单一直待支付。原因通常有三层:一是notifyUrl不是公网地址,平台推不过来;二是服务器防火墙或安全组挡了入站;三是验签失败被你自己拒绝了。解决:先在回调接口入口打一行日志,确认请求有没有到达。到达了但验签失败,把双方拼接的原始串打出来逐字符比对,八成是编码或空值处理不一致。
5.2 金额对不上:尾数模式和手续费
现象:平台说收到 100 元,你库里订单是 100.00,但匹配不上。原因是免签平台可能扣了手续费,实际到账 99.5,或者平台用的是尾数匹配模式,要求你下单金额带随机尾数。解决:对接前确认平台的匹配规则,订单表里amount和real_amount分开存,对账时用real_amount比对。
5.3 重复发货:幂等没做在正确的层
现象:玩家收到两份充值。原因是回调重推时,你的幂等判断和发货不在同一个事务里,两个线程同时通过了状态检查。解决:幂等要用数据库的唯一约束或乐观锁兜底,比如update t_order set status=1 where order_no=? and status=0,靠影响行数判断是否抢到,而不是先 select 再 update。
5.4 定时任务重复执行:多实例部署的坑
现象:对账任务在每台机器上都跑,同一笔订单被查了多次。原因是@Scheduled是单机任务,多实例部署时会并发。解决:要么用数据库行锁选主,要么引入分布式调度,要么干脆把对账任务拆成独立单实例服务。小团队最省事的做法是加一张t_lock表,任务执行前抢锁。
5.5 密钥泄露:配置写死在代码里
现象:渠道密钥出现在 Git 提交记录里。原因是图省事直接写在常量类。解决:密钥全部进数据库或配置中心,代码里只留读取逻辑。已经泄露的立即在免签平台后台重置密钥,别抱侥幸。
6. 把支付平台跑稳的一个进阶习惯:全链路压测与灰度
前面讲的都是「能跑通」,但支付平台真正难的是「跑稳」。我踩过最深的一次坑,是上线当天通道没问题、代码没问题,结果玩家集中充值把回调接口打满,Tomcat 线程池耗尽,后面所有回调全部超时。那次之后我养成了一个习惯:上线前用脚本模拟并发回调,把回调接口单独压一遍。
具体做法是用 JMeter 或简单的多线程脚本,按真实回调报文构造请求,逐步加压到日常峰值的 3 倍,观察三件事:接口平均耗时、数据库连接池占用、发货任务的积压量。如果回调接口 P99 超过 500ms,就要考虑把验签和落库拆开,或者给回调接口单独配一个线程池,别和普通业务接口抢资源。
灰度这块,新接一个免签通道时不要全量切。先让 10% 的订单走新通道,观察一天的对账差异率和回调成功率,没问题再逐步放大。渠道表里加一个weight字段做权重路由,出问题改权重就能秒切回老通道,比改代码重新发版快得多。
还有一个我个人的习惯:每笔订单从创建到发货,全链路打一个 traceId,下单、请求平台、回调、发货、对账每个环节都带上。出问题时拿订单号一搜,整条链路的时间线和每步的入参出参全在眼前,比翻三台机器的日志快十倍。支付这东西,后悔药就是日志,平时多打一点,出事少熬一夜。
这套 JAVA 游戏支付源码加免签平台对接的方案,值不值得做取决于你的量级——日订单几百笔,单体加定时对账完全够用;上千笔再考虑拆服务和引入消息队列。别一上来就上重型架构,先把订单状态机和幂等做扎实,比什么都强。希望帮到你。
本文还有配套的精品资源,点击获取