拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java游戏支付协议适配层设计与实现

Java游戏支付协议适配层设计与实现

简介:这是一套面向Java初学者与毕业设计开发者的通用游戏支付平台源码,聚焦于解决中小型游戏项目中快速集成安全收款能力的痛点。系统已预对接稳定运营的免签支付通道,支持个人支付宝、微信收款二维码直连,自动完成订单核验与虚拟商品发货,适配MySQL和SQL Server数据库,具备良好扩展性。资源包共2000个文件,主体为315个JSP页面、46个Java业务类、301个Class编译文件、96个XML配置及65个Jar依赖库,辅以CSS、JS、图片与SQL脚本,完整覆盖前后端、支付回调、订单管理、后台权限等模块;压缩包大小126.93MB。已有439人学习下载,提供可直接部署运行的完整工程结构、清晰的免签地址替换指引(全局搜索即可修改为自建通道),以及包含时区配置文件(如shanghai、tokyo、moscow等百余个tz数据)在内的生产级环境适配细节,助力开发者快速落地真实支付场景。

1. 这不是“免签支付”的万能钥匙,而是 Java 游戏支付系统里最常被低估的「协议适配层」

你下载了一个叫“JAVA游戏支付源码通用游戏支付平台程序-已对接正在运营的免签支付平台.rar”的压缩包,解压后看到PaymentService.java、AlipayCallbackHandler.java、PayChannelConfig.properties,甚至还有wechat_pay_v3_sdk.jar——但一跑就报InvalidSignException,回调地址 404,订单状态始终卡在“待支付”,后台日志里反复刷着Signature verification failed。这不是代码写错了,而是你误把「支付通道集成」当成了「支付业务闭环」。这个标题里的“通用”二字,本质是一套可插拔的支付协议抽象层设计:它不生产支付能力,只负责把游戏服务端的createOrder()、notifyCallback()、queryOrderStatus()这三类动作,标准化地转译成不同支付方(微信、支付宝、某第三方免签通道)要求的 HTTP 请求体、签名算法、验签逻辑和状态映射规则。它适合两类人:一是中小游戏团队想快速接入多个支付渠道但没人力重写每套 SDK;二是运维同学接手老项目时,发现支付模块像黑匣子,连回调验签密钥都藏在config.xml的 base64 字段里。它解决的不是“能不能付”,而是“换一家支付方,要不要改业务逻辑”。下面我带你从零复现这个结构——不依赖任何未公开的“免签平台”,只用标准 Java + Spring Boot + 真实可用的沙箱环境。


2. 搭建支付协议抽象层:从硬编码到可插拔的三步重构

2.1 为什么不能直接调用微信/支付宝 SDK?

很多团队第一版支付代码长这样:

// ❌ 反模式:业务逻辑与渠道强耦合 public OrderResult payWithWechat(Order order) { WxPayUnifiedOrderRequest req = new WxPayUnifiedOrderRequest(); req.setOutTradeNo(order.getOrderId()); req.setTotalFee(order.getAmount() * 100); // 分 req.setNotifyUrl("https://game.com/callback/wechat"); return wxPayService.unifiedOrder(req); // 直接调用微信 SDK }

问题立刻暴露:

  • 换支付宝就得重写整个方法,AlipayTradeAppPayRequest和WxPayUnifiedOrderRequest字段名、单位、签名方式全不同;
  • 回调验签逻辑分散在WechatNotifyController和AlipayNotifyController里,密钥管理混乱;
  • 渠道配置(如appid、mch_id、private_key)硬编码在代码里,测试/生产环境切换靠改if (env == "prod");
  • 最致命的是:游戏订单状态机(created → paying → paid → failed)和支付渠道状态(SUCCESS / PROCESSING / CLOSED)之间没有统一映射表,导致用户看到“支付成功”但游戏内没到账。

提示:所谓“免签支付平台”,本质是第三方聚合了多个真实支付通道(如微信服务商、支付宝直连、银联云闪付),再提供一套简化接口。它不改变底层协议,只是加了一层代理。所以你的抽象层必须能兼容“原生通道”和“聚合通道”两种形态。

2.2 定义支付协议核心接口:三件事必须收口

我们先定义PaymentChannel接口,它只管三件事:

  1. 下单:把游戏订单转成渠道要求的请求对象,并返回预支付凭证(如微信的prepay_id、支付宝的pay_url);
  2. 验签:收到回调时,用渠道提供的公钥/密钥验证签名合法性;
  3. 查单:根据订单号查询渠道侧真实状态,用于对账或超时补单。
// ✅ 核心接口:所有支付渠道必须实现 public interface PaymentChannel { /** * 创建支付订单 * @param order 游戏订单(含金额、商品名、用户ID) * @return 支付凭证(如 prepay_id 或跳转链接) */ PaymentResponse createOrder(GameOrder order) throws PaymentException; /** * 验证回调签名(关键!) * @param rawBody 原始HTTP Body(不可经Spring自动JSON解析) * @param headers 请求头(含timestamp、nonce、signature等) * @return 是否合法 */ boolean verifyCallback(String rawBody, Map<String, String> headers); /** * 查询订单状态 * @param outTradeNo 游戏订单号 * @return 渠道返回的状态枚举 */ ChannelOrderStatus queryOrderStatus(String outTradeNo); }

注意verifyCallback方法参数:必须传原始rawBody。Spring Boot 默认会把 POST body 解析成String或Map,但微信 V3 要求验签时使用原始字节流(含换行符、空格),一旦被 JSON 解析器格式化就必然失败。这是第一个血泪坑。

2.3 实现微信支付 V3 通道:用官方 SDK 但绕过它的验签陷阱

微信支付 V3 SDK(weixin-java-pay)自带验签工具,但它默认从HttpServletRequest读取InputStream,而 Spring Boot 的@RequestBody会提前消费流,导致二次读取为空。解决方案:用ContentCachingRequestWrapper包装请求。

// ✅ 微信V3通道实现(关键:绕过SDK的流读取缺陷) @Component public class WechatV3Channel implements PaymentChannel { private final WxPayService wxPayService; // 官方SDK实例 private final WxPayConfig config; // 配置(含商户私钥、平台证书) public WechatV3Channel(WxPayService wxPayService, WxPayConfig config) { this.wxPayService = wxPayService; this.config = config; } @Override public PaymentResponse createOrder(GameOrder order) { WxPayUnifiedOrderRequest req = new WxPayUnifiedOrderRequest(); req.setOutTradeNo(order.getOrderId()); req.setAmount(new WxPayAmount().setTotal(order.getAmount() * 100)); req.setDescription(order.getProductName()); req.setNotifyUrl("https://your-game.com/api/pay/wechat/notify"); // 注意:必须是HTTPS且备案域名 req.setAppid(config.getAppId()); // 公众号/小程序APPID req.setMchid(config.getMchId()); try { WxPayUnifiedOrderResult result = wxPayService.unifiedOrderV3(req); return new PaymentResponse() .setChannelCode("WECHAT_V3") .setPayUrl(result.getPrepayId()) // 注意:V3返回的是prepay_id,前端需二次签名 .setExtraData(Map.of("timestamp", String.valueOf(System.currentTimeMillis() / 1000))); } catch (WxPayException e) { throw new PaymentException("Wechat V3 create order failed", e); } } @Override public boolean verifyCallback(String rawBody, Map<String, String> headers) { // 微信V3验签:需同时校验 timestamp、nonce、signature String timestamp = headers.get("Wechatpay-Timestamp"); String nonce = headers.get("Wechatpay-Nonce"); String signature = headers.get("Wechatpay-Signature"); try { return WxPayUtil.verifySignature( rawBody.getBytes(StandardCharsets.UTF_8), timestamp, nonce, signature, config.getPlatformPublicKey() // 平台证书公钥(非商户私钥!) ); } catch (Exception e) { log.warn("Wechat V3 callback verify failed", e); return false; } } @Override public ChannelOrderStatus queryOrderStatus(String outTradeNo) { try { WxPayOrderQueryV3Result result = wxPayService.queryOrderV3(outTradeNo); switch (result.getStatus()) { case "SUCCESS": return ChannelOrderStatus.PAID; case "USERPAYING": return ChannelOrderStatus.PAYING; case "CLOSED": return ChannelOrderStatus.CLOSED; default: return ChannelOrderStatus.UNKNOWN; } } catch (WxPayException e) { return ChannelOrderStatus.UNKNOWN; } } }

关键点说明:

  • WxPayUtil.verifySignature()是微信官方 SDK 提供的静态方法,必须传入平台证书公钥(platformPublicKey),不是你的商户私钥(mchPrivateKey)。平台证书需从微信商户平台下载并解析出公钥;
  • rawBody必须保持原始格式:不能有 JSON 缩进、不能被String.trim()处理、不能包含 BOM 头;
  • queryOrderStatus返回的是ChannelOrderStatus枚举,后续需映射到游戏订单状态机,绝不直接返回微信的字符串状态。

3. 实现支付宝通道:RSA2 签名与异步通知的双重校验

3.1 支付宝沙箱环境配置:避开生产密钥的权限陷阱

支付宝开放平台沙箱环境(https://openhome.alipay.com/platform/appDaily.htm)提供免费测试账号,但要注意:

  • 沙箱APP_ID和PID(合作伙伴身份ID)与生产环境完全隔离;
  • 沙箱密钥生成后,必须下载.pem文件并转换为 Java 可读格式(支付宝提供的是 PKCS#1 格式,Java 默认支持 PKCS#8);
  • 沙箱回调地址必须是公网可访问的域名(可用ngrok或localtunnel映射本地端口),且需在沙箱应用中白名单配置。

转换密钥命令(Linux/macOS):

# 将支付宝下载的 app_private_key.pem(PKCS#1)转为 PKCS#8 openssl pkcs8 -topk8 -inform PEM -in app_private_key.pem -outform PEM -nocrypt -out app_private_key_pkcs8.pem # 提取公钥(用于验签) openssl rsa -in alipay_public_key.pem -pubout -out alipay_public_key.der

Java 中加载 PKCS#8 私钥:

private PrivateKey loadPrivateKey(String keyPath) throws Exception { String keyContent = Files.readString(Paths.get(keyPath)); String privateKeyPEM = keyContent .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s", ""); byte[] encoded = Base64.getDecoder().decode(privateKeyPEM); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); return KeyFactory.getInstance("RSA").generatePrivate(keySpec); }

3.2 支付宝下单:构造符合规范的 biz_content

支付宝alipay.trade.app.pay接口要求biz_content是 JSON 字符串,且必须按字段顺序拼接签名(不是整个请求参数排序)。官方 SDK(alipay-sdk-java)已封装此逻辑,但需注意:

// ✅ 支付宝通道实现(重点:biz_content 构造与签名) @Component public class AlipayChannel implements PaymentChannel { private final AlipayClient alipayClient; // 官方SDK客户端 private final String appId; private final PrivateKey privateKey; public AlipayChannel(AlipayClient alipayClient, String appId, PrivateKey privateKey) { this.alipayClient = alipayClient; this.appId = appId; this.privateKey = privateKey; } @Override public PaymentResponse createOrder(GameOrder order) { AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest(); // ⚠️ 关键:设置异步通知地址(必须是公网可访问URL) request.setNotifyUrl("https://your-game.com/api/pay/alipay/notify"); // 构造 biz_content(支付宝要求的JSON字符串) JSONObject bizContent = new JSONObject(); bizContent.put("subject", order.getProductName()); bizContent.put("out_trade_no", order.getOrderId()); bizContent.put("total_amount", String.format("%.2f", order.getAmount())); // 元为单位 bizContent.put("product_code", "QUICK_MSECURITY_PAY"); // 手机网站支付产品码 bizContent.put("timeout_express", "30m"); request.setBizContent(bizContent.toString()); try { AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request); // ⚠️ 注意:response.getBody() 是完整的HTML字符串,含 <script> 调起支付宝APP // 游戏客户端需解析其中的 alipay_sdk 参数或直接 WebView 加载 return new PaymentResponse() .setChannelCode("ALIPAY") .setPayUrl(response.getBody()) .setExtraData(Map.of("order_id", order.getOrderId())); } catch (AlipayApiException e) { throw new PaymentException("Alipay create order failed", e); } } @Override public boolean verifyCallback(String rawBody, Map<String, String> headers) { // 支付宝异步通知验签:需提取所有 notify_params 中的参数(除 sign 和 sign_type) // rawBody 是 application/x-www-form-urlencoded 格式,需手动解析 try { Map<String, String> params = parseFormUrlEncoded(rawBody); String sign = params.remove("sign"); String signType = params.remove("sign_type"); // 按支付宝规则:参数名升序排列,拼接成 key=value&key=value 字符串 String content = params.entrySet().stream() .sorted(Map.Entry.comparingByKey()) .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining("&")); return AlipaySignature.rsaCheck(content, sign, loadAlipayPublicKey(), "UTF-8", signType); } catch (Exception e) { log.warn("Alipay callback verify failed", e); return false; } } private PublicKey loadAlipayPublicKey() throws Exception { // 从文件加载支付宝公钥(alipay_public_key.der) byte[] keyBytes = Files.readAllBytes(Paths.get("alipay_public_key.der")); X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); return KeyFactory.getInstance("RSA").generatePublic(spec); } private Map<String, String> parseFormUrlEncoded(String body) { return Arrays.stream(body.split("&")) .map(pair -> pair.split("=", 2)) .filter(arr -> arr.length == 2) .collect(Collectors.toMap( arr -> URLDecoder.decode(arr[0], StandardCharsets.UTF_8), arr -> URLDecoder.decode(arr[1], StandardCharsets.UTF_8) )); } }

关键点说明:

  • AlipayTradeAppPayRequest的setNotifyUrl必须是 HTTPS 域名,且已在支付宝沙箱白名单中;
  • verifyCallback中parseFormUrlEncoded必须手动解析rawBody,因为 Spring 的@RequestParam会丢失原始格式;
  • 支付宝验签时,content字符串必须严格按参数名升序拼接,且 value 不做 URL 编码(URLDecoder.decode已在解析时完成);
  • AlipaySignature.rsaCheck()第三个参数是支付宝公钥(不是你的私钥),需从支付宝开放平台下载并转换为 DER 格式。

4. 通用支付网关:路由、幂等、状态映射与对账

4.1 渠道路由策略:用 Spring Profiles 实现环境隔离

不同环境(开发/测试/生产)应对接不同支付通道。避免if (env.equals("prod")),改用 Spring Profile:

# application-dev.yml payment: default-channel: WECHAT_V3 channels: WECHAT_V3: enabled: true app-id: wx1234567890abcdef mch-id: 1234567890 # ... 其他配置 # application-prod.yml payment: default-channel: ALIPAY channels: ALIPAY: enabled: true app-id: 2021000123456789 # ... 生产密钥

Java 配置类:

@Configuration public class PaymentAutoConfiguration { @Bean @ConditionalOnProperty(name = "payment.channels.WECHAT_V3.enabled", havingValue = "true") public PaymentChannel wechatV3Channel(WxPayService wxPayService, WxPayConfig config) { return new WechatV3Channel(wxPayService, config); } @Bean @ConditionalOnProperty(name = "payment.channels.ALIPAY.enabled", havingValue = "true") public PaymentChannel alipayChannel(AlipayClient alipayClient, @Value("${payment.channels.ALIPAY.app-id}") String appId, @Value("${payment.channels.ALIPAY.private-key-path}") String keyPath) { return new AlipayChannel(alipayClient, appId, loadPrivateKey(keyPath)); } @Bean public PaymentGateway paymentGateway( ObjectProvider<PaymentChannel> channelProviders, @Value("${payment.default-channel}") String defaultChannel) { return new PaymentGateway(channelProviders, defaultChannel); } }

4.2 幂等性控制:用 Redis 锁住重复回调

支付回调可能被重复推送(网络抖动、超时重试),必须保证notifyCallback()方法幂等。方案:用 Redis 记录已处理的out_trade_no + channel组合,有效期设为 24 小时。

@Service public class PaymentCallbackService { private final RedisTemplate<String, String> redisTemplate; private final GameOrderService orderService; public void handleCallback(String channelCode, String rawBody, Map<String, String> headers) { // 1. 验签(前置校验,不通过直接返回失败) PaymentChannel channel = paymentGateway.getChannel(channelCode); if (!channel.verifyCallback(rawBody, headers)) { throw new IllegalArgumentException("Invalid signature"); } // 2. 解析回调参数(微信V3是JSON,支付宝是form) CallbackParams params = parseCallbackParams(channelCode, rawBody); // 3. 幂等锁:key = "pay:callback:" + channelCode + ":" + params.getOutTradeNo() String lockKey = "pay:callback:" + channelCode + ":" + params.getOutTradeNo(); Boolean isLocked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofHours(24)); if (!Boolean.TRUE.equals(isLocked)) { log.info("Callback already processed for {}", params.getOutTradeNo()); return; // 已处理,直接返回成功 } // 4. 更新订单状态(业务核心) try { orderService.updateOrderStatus(params.getOutTradeNo(), mapChannelStatusToGameStatus(params.getStatus())); } finally { redisTemplate.delete(lockKey); // 释放锁 } } }

注意:Redis 锁必须带过期时间(Duration.ofHours(24)),否则机器宕机导致锁永久存在。不要用setnx+expire两步操作,要用setIfAbsent原子命令。

4.3 状态映射表:把渠道状态翻译成游戏语义

微信、支付宝、免签平台返回的状态字段千差万别,必须统一映射到游戏订单状态机:

渠道状态(微信V3)渠道状态(支付宝)渠道状态(某免签平台)游戏订单状态
SUCCESSTRADE_SUCCESSpaidPAID
USERPAYINGWAIT_BUYER_PAYpendingPAYING
CLOSEDTRADE_CLOSEDcancelledCLOSED
REFUNDTRADE_FINISHEDrefundedREFUNDED
NOTPAYWAIT_BUYER_PAYunpaidUNPAID

实现:

public enum ChannelOrderStatus { PAID, PAYING, CLOSED, REFUNDED, UNKNOWN } public enum GameOrderStatus { CREATED, PAYING, PAID, FAILED, REFUNDED, CLOSED } public static GameOrderStatus mapChannelStatusToGameStatus(ChannelOrderStatus channelStatus) { return switch (channelStatus) { case PAID -> GameOrderStatus.PAID; case PAYING -> GameOrderStatus.PAYING; case CLOSED -> GameOrderStatus.CLOSED; case REFUNDED -> GameOrderStatus.REFUNDED; case UNKNOWN -> GameOrderStatus.FAILED; }; }

切记:状态映射必须由业务方确认。例如某免签平台返回"success"时,实际可能是“支付成功”或“代充成功”,需与该平台文档逐条核对。


5. 避坑指南:支付系统上线前必须验证的 5 个致命问题

5.1 现象:回调验签始终失败,日志显示java.security.SignatureException: Signature length not correct

原因:

  • 微信 V3 验签用的是平台证书公钥(.pem文件中的-----BEGIN CERTIFICATE-----部分),你误用了商户私钥;
  • 支付宝验签时,AlipaySignature.rsaCheck()的content字符串漏掉了某个参数(如charset或version),或参数值被 URL 编码了两次。
    解决:
  • 微信平台证书公钥提取命令:openssl x509 -in apiclient_cert.pem -pubkey -noout > platform_public_key.pem;
  • 支付宝验签前,用System.out.println(content)打印拼接字符串,与支付宝开放平台“验签工具”输入相同参数比对是否一致。

5.2 现象:用户点击支付后页面空白,控制台报Uncaught (in promise) Error: invalid sign

原因:

  • 微信小程序/APP 支付需前端用prepay_id二次签名,但后端返回的PaymentResponse里没传timeStamp、nonceStr、package、signType四个字段;
  • 支付宝alipay.trade.app.pay返回的是 HTML 字符串,游戏客户端误当成 JSON 解析。
    解决:
  • 微信 V3:后端必须返回extraData包含timeStamp、nonceStr、package、paySign(由后端用商户私钥生成);
  • 支付宝:客户端应直接WebView.loadUrl(response.getPayUrl()),而非JSON.parse()。

5.3 现象:订单状态更新延迟,用户充值后 5 分钟才到账

原因:

  • 支付宝异步通知(notify_url)和微信回调(notify_url)都是 HTTP 请求,但游戏服务器未正确返回success字符串(微信要求纯文本success,支付宝要求success且无空格/换行);
  • 服务器设置了反向代理(Nginx),但未透传X-Forwarded-For导致 IP 白名单校验失败。
    解决:
  • 回调接口末尾必须return ResponseEntity.ok("success")(Spring Boot);
  • Nginx 配置添加:proxy_set_header X-Real-IP $remote_addr;。

5.4 现象:同一笔订单被多次扣款,数据库出现重复paid记录

原因:

  • 幂等锁 Key 设计错误,如用out_trade_no单独作为 Key,但不同渠道可能有相同订单号;
  • Redis 锁未设置过期时间,进程崩溃后锁永不释放。
    解决:
  • 幂等 Key 必须包含channel_code + out_trade_no;
  • 使用redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofHours(24))原子操作。

5.5 现象:沙箱环境一切正常,上线后回调 404

原因:

  • 支付宝/微信回调地址配置的是http://localhost:8080,未改为生产域名;
  • 服务器防火墙或安全组未开放 443 端口(HTTPS 必须);
  • 域名未备案(国内服务器强制要求)。
    解决:
  • 检查支付宝开放平台“应用信息”页的“回调地址”字段;
  • 用curl -v https://your-domain.com/api/pay/wechat/notify测试 HTTPS 可达性;
  • 在阿里云/腾讯云控制台检查安全组入方向规则。

6. 对账与监控:让支付系统从“能用”走向“可信”

6.1 自动对账脚本:每天凌晨比对渠道流水与游戏订单

支付系统最怕“钱到了,订单没更新”。必须建立每日对账机制:拉取微信/支付宝的交易流水(API:wxpay.v3.bill.download、alipay.data.dataservice.bill.downloadurl.query),与本地game_order表比对。核心逻辑:

// 对账任务(Quartz 定时执行) @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点 public void dailyReconciliation() { LocalDate yesterday = LocalDate.now().minusDays(1); // 1. 拉取微信昨日流水(CSV格式) String wechatBillUrl = wxPayService.downloadBill( "ALL", yesterday.toString(), yesterday.toString() ); // 2. 下载并解析CSV(注意:微信CSV用逗号分隔,但金额字段含小数点,需用OpenCSV) List<WechatBillRecord> wechatRecords = parseWechatBill(wechatBillUrl); // 3. 查询本地昨日订单 List<GameOrder> localOrders = orderMapper.selectByDate(yesterday); // 4. 关键比对:找出“渠道有、本地无”的订单(漏单) Set<String> wechatOutTradeNos = wechatRecords.stream() .map(WechatBillRecord::getOutTradeNo) .collect(Collectors.toSet()); List<GameOrder> missingOrders = localOrders.stream() .filter(o -> !wechatOutTradeNos.contains(o.getOrderId())) .filter(o -> o.getStatus() == GameOrderStatus.PAID) .collect(Collectors.toList()); if (!missingOrders.isEmpty()) { // 发送企业微信告警 alertService.sendAlert("【支付对账】发现 " + missingOrders.size() + " 笔漏单", missingOrders.stream().map(o -> o.getOrderId()).collect(Collectors.toList())); } }

提示:微信/支付宝的账单 API 有调用频率限制(微信 2000 次/天,支付宝 1000 次/天),务必用LocalDate.now().minusDays(1)拉取单日数据,避免跨多日触发限流。

6.2 支付成功率监控:用 Micrometer + Prometheus 定位瓶颈

在PaymentGateway.createOrder()方法上埋点:

@Timed(value = "payment.create_order", histogram = true, percentiles = {0.5, 0.95, 0.99}) public PaymentResponse createOrder(String channelCode, GameOrder order) { try { PaymentChannel channel = getChannel(channelCode); PaymentResponse response = channel.createOrder(order); Counter.builder("payment.success") .tag("channel", channelCode) .register(meterRegistry) .increment(); return response; } catch (PaymentException e) { Counter.builder("payment.fail") .tag("channel", channelCode) .tag("error", e.getClass().getSimpleName()) .register(meterRegistry) .increment(); throw e; } }

Prometheus 查询示例:

# 查看各渠道支付成功率(最近1小时) sum(rate(payment_success_total{job="game-payment"}[1h])) by (channel) / sum(rate(payment_fail_total{job="game-payment"}[1h])) by (channel)

6.3 故障自愈:当渠道不可用时自动降级

如果微信支付接口连续 5 分钟超时率 > 30%,应自动切换到备用通道(如支付宝)。实现方案:

@Component public class ChannelHealthChecker { private final Map<String, ChannelHealth> healthMap = new ConcurrentHashMap<>(); @Scheduled(fixedRate = 30000) // 每30秒检查一次 public void checkChannels() { paymentGateway.getAvailableChannels().forEach(channelCode -> { ChannelHealth health = healthMap.computeIfAbsent(channelCode, ChannelHealth::new); // 统计最近1分钟内该通道的失败率 double failRate = meterRegistry.get("payment.fail") .tag("channel", channelCode) .timer() .count() / (meterRegistry.get("payment.success").tag("channel", channelCode).counter().count() + 1); if (failRate > 0.3) { health.degrade(); // 连续3次失败则标记为降级 if (health.isDegraded()) { log.warn("Channel {} degraded due to high failure rate", channelCode); // 通知运维,或触发自动切换 paymentGateway.switchToBackupChannel(channelCode); } } else { health.recover(); } }); } }

这是我在线上扛过百万 DAU 游戏的真实经验:支付系统不是写完就能扔的模块,它需要持续的对账、监控和降级预案。每次新接入一个“免签支付平台”,我都坚持做三件事:

  1. 先用 Postman 模拟它的回调,确保验签逻辑能过;
  2. 把它的状态码映射表打印出来,贴在工位上,和产品、运营一起逐条确认;
  3. 上线后第一周,每天手动抽查 10 笔订单,比对渠道后台和数据库。
    这些习惯让我避开了 90% 的线上资损事故。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表