Java实现微信支付回调案例

wen java案例 5

Java实现微信支付回调全攻略:从签名验签到幂等处理的实战案例


目录导读

  1. 微信支付回调机制核心原理
  2. 环境准备与依赖引入(Spring Boot + Maven)
  3. 回调接口设计:URL配置与安全限制
  4. 核心代码实现:解密、验签、业务处理
  5. 常见坑点:重复通知、网络超时、参数错位
  6. 高频问答:回调失败如何排查?
  7. 生产级回调的可靠性设计

微信支付回调机制核心原理

微信支付在用户完成支付后,会通过异步通知的方式向商户后台发送支付结果,这个回调(Webhook)机制是支付成功状态的唯一可靠来源(官方文档强调:不能仅依赖前端跳转)。
关键点:回调可能多次发送(频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h,总计24次),且数据是AES-256-GCM加密的密文,商户必须处理后返回{"code":"SUCCESS","message":"成功"},否则微信会持续重试。

Java实现微信支付回调案例

环境准备与依赖引入

以Spring Boot 2.7 + 微信支付v3 API为例,在pom.xml中加入官方SDK:

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.11</version>
</dependency>

同时需要在application.yml中配置商户号、APIv3密钥、商户私钥路径和证书序列号。

回调接口设计:URL配置与安全限制

在微信商户平台配置回调地址时必须使用HTTPS(生产环境),且路径不能携带查询参数,Controller示例:

@RestController
@RequestMapping("/api/pay")
public class PayCallbackController {
    @PostMapping("/callback")
    public String callback(@RequestBody String body,
                           @RequestHeader("Wechatpay-Signature") String signature,
                           @RequestHeader("Wechatpay-Timestamp") String timestamp,
                           @RequestHeader("Wechatpay-Nonce") String nonce,
                           @RequestHeader("Wechatpay-Serial") String serial) {
        // 处理逻辑
    }
}

核心代码实现:解密、验签、业务处理

第一步:验签防伪造(必须做,防止黑客伪造通知),利用SDK的NotificationParser

// 初始化Config
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream("/path/to/apiclient_key.pem"));
AutoCertificateService certificatesService = new AutoCertificateService.Builder()
        .merchant(merchantId, merchantSerialNumber, merchantPrivateKey)
        .build();
NotificationParser parser = new NotificationParser(certificatesService);

第二步:解密资源(关键代码):

public PayCallbackVo parseCallback(String body, String signature, String timestamp, String nonce, String serial) {
    RequestParam requestParam = new RequestParam.Builder()
            .serialNumber(serial)
            .nonce(nonce)
            .signature(signature)
            .timestamp(timestamp)
            .body(body)
            .build();
    try {
        // 验签并解密
        Notification notification = parser.parse(requestParam);
        // 解密后的明文是JSON字符串
        JSONObject resource = JSON.parseObject(notification.getDecryptData());
        String outTradeNo = resource.getString("out_trade_no");
        String transactionId = resource.getString("transaction_id");
        String tradeState = resource.getString("trade_state");
        // ... 提取金额、附加数据等
        return new PayCallbackVo(outTradeNo, transactionId, tradeState);
    } catch (Exception e) {
        log.error("回调验签或解密失败", e);
        throw new RuntimeException("验签失败");
    }
}

第三步:幂等业务处理(防止重复回调导致重复发货):

@Transactional
public String processBusiness(PayCallbackVo vo) {
    // 1. 查询本地订单状态
    Order order = orderMapper.selectByOutTradeNo(vo.getOutTradeNo());
    if (order == null) {
        return "订单不存在";  // 返回错误,微信会重试
    }
    // 2. 核心防重:如果订单已支付,直接返回成功,不再处理
    if ("PAID".equals(order.getStatus())) {
        return "SUCCESS";
    }
    // 3. 校验金额是否一致(防止篡改)
    if (order.getAmount().compareTo(vo.getAmount()) != 0) {
        log.error("金额不一致,存在风险");
        return "FAIL";
    }
    // 4. 更新订单状态 + 发放积分/发货等业务操作
    order.setStatus("PAID");
    order.setTransactionId(vo.getTransactionId());
    orderMapper.updateById(order);
    // 其他业务(如消息队列通知)
    return "SUCCESS";
}

最终Controller返回:

Map<String, String> result = new HashMap<>();
result.put("code", "SUCCESS");
result.put("message", "成功");
return JSON.toJSONString(result);

常见坑点:重复通知、网络超时、参数错位

  • 坑1:验签时忽略Wechatpay-Serial:证书轮换时,必须用对应的证书验证,否则报错。
  • 坑2:返回字符串格式错误:必须是{"code":"SUCCESS","message":"成功"},不能用纯文本success
  • 坑3:订单状态锁缺失:高并发下两个回调同时进入,导致重复发货,解决方案是在orderMapper使用SELECT ... FOR UPDATE行锁。
  • 坑4:解密失败后未记录原始报文:导致无法排查,务必用MDC或日志AOP记录body和headers。

高频问答:回调失败如何排查?

Q1:回调一直返回FAIL,微信会停止重试吗?
答:不会,微信会按官方频率重试24次,直到返回SUCCESS,若超过24次仍失败,需在商户平台手动发起“订单查询”来补偿。

Q2:如何区分支付成功退款成功的回调?
答:回调资源中trade_state字段值为SUCCESS表示支付成功;退款回调则走独立的refund接口(/api/v3/refund/notify),勿混用

Q3:本地开发环境没有公网IP,如何测试完整回调流程?
答:使用内网穿透工具(如frp、ngrok)暴露本地端口,并在商户平台将回调URL设为https://你的临时域名/api/pay/callback,注意必须用HTTPS,且证书有效。

Q4:回调解密时遇到java.security.InvalidKeyException: Illegal key size
答:微信支付v3使用AES-256,需要JDK安装Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy文件,JDK8需手动替换,JDK9+默认支持。

Q5:如何处理“网络闪断”导致回调丢失?
答:设计补偿任务,定时(如每小时)调用“订单查询API”关单,对比本地订单,将已支付但本地未更新的订单做补单处理。

生产级回调的可靠性设计

真正的生产环境回调,应具备以下能力:

  • 完全异步化:Controller内只做验签+解密,然后发送MQ消息,立即返回SUCCESS,业务消费者负责幂等处理,这样即使业务逻辑耗时,也不阻塞微信重试。
  • 多层日志追踪:使用traceId贯穿回调→业务处理→DB操作,便于事后全链路排查。
  • 监控告警:对“回调失败率”“解密失败次数”“订单状态异常”设置阈值告警(如使用Prometheus)。
  • 数据库唯一约束:在订单表加transaction_id唯一索引,从数据库层面兜底防重。

回调是支付闭环的“最后一公里”,踩过坑才能写出健壮的代码,建议收藏本案例,在项目初始化时直接复用核心逻辑,再根据业务定制扩展。


(本文基于微信支付APIv3,非v2版,所示代码片段均已简化,生产环境请参考官方SDK文档及严格的安全规范。)

抱歉,评论功能暂时关闭!