Java微信支付V3案例

wen java案例 5

Java微信支付V3案例实战:从零搭建企业级支付对接(附完整代码与避坑指南)


目录导读

  1. 微信支付V3与V2的本质区别:为什么你必须升级?
  2. 环境准备与核心依赖(Maven/Gradle)
  3. Java微信支付V3案例:APIv3密钥与证书的自动更新机制
  4. 核心流程拆解:下单、回调、查单、退款(附可运行代码)
  5. 敏感数据加密:敏感信息加解密与HTTP签名(实操)
  6. 高频坑位警示:回调验签失败、金额分转元、幂等性处理
  7. 性能与安全进阶:连接池配置、证书定时刷新、分布式锁
  8. 常见问题问答(FAQ)与排错口诀

微信支付V3与V2的本质区别:为什么你必须升级?

微信支付V3(APIv3)自2020年全面推行,与V2相比,最核心的变化是通信协议全面升级为HTTP/2 + TLS 1.2及以上,且不再强制要求商户证书文件(p12/apiclient_cert.pem),取而代之的是商户API私钥 + 平台公钥的加解密体系。

Java微信支付V3案例

  • V2:使用MD5/HMAC-SHA256签名,密钥放在本地,一旦泄露无法追溯。
  • V3:使用RSA-OAEP(非对称加密)进行敏感数据传递,使用SHA256withRSA进行请求签名,且引入了APIv3密钥(32位) 用于解密回调数据。

如果你还在维护老系统,请务必升级,因为微信支付已明确宣布V2接口将于2024年逐步下线,新商户只支持V3。

环境准备与核心依赖

以Spring Boot 2.7.x为例,你的 pom.xml 需要引入微信官方SDK(wechatpay-java)或自己封装HTTP客户端。

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.11</version> <!-- 请替换为最新版 -->
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.2.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

关键配置项:商户号(mchid)、商户API私钥路径、商户证书序列号、APIv3密钥(32位随机字符串),特别注意:APIv3密钥不是商户平台登录密码,也不是APIv2密钥,需要单独生成并保存。

Java微信支付V3案例:APIv3密钥与证书的自动更新机制

微信支付平台证书(wechatpay_*.pem)是定期轮换的,手动下载更新会非常痛苦,最佳实践是使用 CertificatesDownloader 实现自动下载并缓存。

@Configuration
public class WechatPayConfig {
    @Bean
    public RSAPublicKey platformPublicKey() throws Exception {
        // 1. 构建商户私钥
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new File("/path/to/apiclient_key.pem"));
        // 2. 构建RSAAutoCertificateProvider,它会自动从微信服务器拉取最新证书
        RSAAutoCertificateProvider provider = new RSAAutoCertificateProvider.Builder()
                .merchantId("your_mch_id")
                .privateKey(merchantPrivateKey)
                .merchantSerialNumber("your_merchant_serial_no")
                .apiV3Key("your_api_v3_key".getBytes(StandardCharsets.UTF_8))
                .build();
        // 3. 获取当前平台证书公钥
        return provider.getAvailableCertificate().getPublicKey();
    }
}

这样每次调用接口时,SDK会自动检查证书有效期,若快过期则自动同步新证书,彻底告别“证书过期导致支付失败”的隐患。

核心流程拆解:下单、回调、查单、退款

以Native支付(扫码支付)为例,完整时序图如下:

A. 创建订单(统一下单)

  • 请求URL:POST https://api.mch.weixin.qq.com/v3/pay/transactions/native
  • 请求体关键字段:appidmchiddescriptionout_trade_noamount.total(单位)、notify_url
public String createNativeOrder(BigDecimal price, String orderNo) throws Exception {
    HttpHeaders headers = buildHeaders(); // 自动签名
    JSONObject body = new JSONObject();
    body.put("appid", appId);
    body.put("mchid", mchId);
    body.put("description", "测试商品");
    body.put("out_trade_no", orderNo);
    body.put("notify_url", notifyUrl);
    JSONObject amount = new JSONObject();
    amount.put("total", price.multiply(BigDecimal.valueOf(100)).intValue()); // 元转分
    body.put("amount", amount);
    String response = restTemplate.postForObject("https://api.mch.weixin.qq.com/v3/pay/transactions/native",
            new HttpEntity<>(body.toJSONString(), headers), String.class);
    return JSONObject.parseObject(response).getString("code_url"); // 返回支付二维码链接
}

B. 支付回调处理(重点) 回调地址必须是公网HTTPS,收到微信POST请求后,需要做两件事:

  1. 验签:用平台公钥验签请求头中的 Wechatpay-Signature
  2. 解密密文:用APIv3密钥解密 resource 字段中的 ciphertext
@PostMapping("/notify")
public String handleNotify(@RequestBody String body, @RequestHeader("Wechatpay-Signature") String sign,
                           @RequestHeader("Wechatpay-Timestamp") String timestamp,
                           @RequestHeader("Wechatpay-Nonce") String nonce) throws Exception {
    // 1. 构造验签名串
    String message = timestamp + "\n" + nonce + "\n" + body + "\n";
    boolean verify = RSAUtils.verify(message, sign, platformPublicKey()); // 必须使用SHA256withRSA
    // 2. 解密
    JSONObject resource = JSONObject.parseObject(body).getJSONObject("resource");
    String plainText = AesUtil.decryptToString(resource.getString("ciphertext"), apiV3Key);
    JSONObject payResult = JSONObject.parseObject(plainText);
    String outTradeNo = payResult.getString("out_trade_no");
    String transactionId = payResult.getString("transaction_id");
    // 3. 业务处理(幂等:先查本地订单状态,若已处理则直接返回成功)
    if (orderService.isProcessed(transactionId)) {
        return "{\"code\":\"SUCCESS\",\"message\":\"\"}";
    }
    orderService.paySuccess(outTradeNo, transactionId);
    return "{\"code\":\"SUCCESS\",\"message\":\"\"}"; // 必须返回此格式,否则微信会重试
}

C. 查单与退款

  • 查单APIGET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchid=xxx,返回结果中若 trade_state = SUCCESS 则确认支付成功。
  • 退款APIPOST /v3/refund/domestic/refunds,请求体需要传入 out_refund_noout_trade_noamount.refund(退款金额)、amount.total,注意退款接口也走APIv3签名,且需要平台证书来加密敏感字段(如退款原因可选)。

敏感数据加密:敏感信息加解密与HTTP签名

在V3中,商户向微信发送请求时,需要对商户API私钥进行RSA签名,具体流程:

Authorization: WECHATPAY2-SHA256-RSA2048 mchid="商户号",nonce_str="随机串",signature="签名",timestamp="时间戳",serial_no="商户证书序列号"

签名串格式为 HTTP方法 + "\n" + URL路径 + "\n" + 时间戳 + "\n" + 随机串 + "\n" + 请求体(若为空则为空字符串) + "\n",官方SDK封装了这一切,但如果你是自研,请务必用下面公式验测:

String message = requestMethod + "\n" + urlPath + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n";
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(message.getBytes(StandardCharsets.UTF_8));
String signature = Base64.getEncoder().encodeToString(sign.sign());

而对于回调响应,微信要求的返回格式必须是 { "code": "SUCCESS", "message": "成功" },不能返回其他格式,否则会视为失败并重试。

高频坑位警示:回调验签失败、金额分转元、幂等性处理

坑1:验签失败 —— 90%的原因是使用了V2的HMAC-SHA256验签方式,或者平台证书未及时更新。切记:验签必须用平台证书公钥,而不是商户证书公钥。

坑2:金额精度 —— APIv3规定金额单位是,但数据库存储常用元。必须用 BigDecimal 进行计算,禁止用 Doublefloat,否则会出现0.1+0.2=0.30000000000000004的问题。

坑3:幂等性 —— 微信回调可能重复推送(尤其网络抖动时),必须在处理回调时加分布式锁(如Redis)并检查本地订单状态,否则会导致用户充值两次。

坑4:证书路径 —— 本地开发时习惯用绝对路径,但生产环境建议用 ClassPathResource 加载,避免因文件路径变更导致找不到私钥。

性能与安全进阶:连接池配置、证书定时刷新、分布式锁

  • 连接池:使用 HttpClient5 连接池,设置最大连接数(如200),并且开启连接复用,否则高并发下会创建大量TCP连接导致端口耗尽。
  • 证书刷新:利用上面的 RSAAutoCertificateProvider 定时任务(每12小时)自动调用微信接口更新证书,避免到期前手动替换。
  • 分布式锁:推荐 Redis SETNX + 过期时间(例如30秒)来保证回调处理的原子性。

常见问题问答(FAQ)与排错口诀

Q1:为什么回调总是验签失败? A:检查三点:① 是否用了微信支付平台证书(不是商户证书);② 时间戳是否与微信服务器时间相差超过5分钟(需NTP同步);③ 随机串nonce_str是否与请求头中的完全一致。

Q2:接口返回 SIGN_ERROR 是什么原因? A:大概率是签名串格式有误,请逐行对比官方文档,特别注意URL路径不含域名,且以 开头。/v3/pay/transactions/native,不能写 https://api.mch.weixin.qq.com/v3/...

Q3:退款时提示 AMOUNT_INVALID 如何处理? A:确认退款金额 refund 必须小于等于 total,且 total 必须与下单时的金额完全一致(包括分),如果订单已全额退款,再次退款会报错。

Q4:如何快速定位问题? A:牢记口诀:“一查证书、二查签名、三查金额、四查幂等”,优先将微信返回的 err_codeerr_msg 打印出来,用官方 API文档 对照排查。


微信支付V3已经是非常成熟的对接方案,只要理解“非对称加密验签 + 平台证书自动更新 + APIv3密钥解密”这三板斧,配合官方SDK,开发难度并不高,建议先在沙箱环境(测试号)跑通全流程,再切换生产配置,如果遇到具体错误,欢迎在评论区带上 err_code 提问,我会逐一解答。

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