Java微信支付V3案例实战:从零搭建企业级支付对接(附完整代码与避坑指南)
目录导读
- 微信支付V3与V2的本质区别:为什么你必须升级?
- 环境准备与核心依赖(Maven/Gradle)
- Java微信支付V3案例:APIv3密钥与证书的自动更新机制
- 核心流程拆解:下单、回调、查单、退款(附可运行代码)
- 敏感数据加密:敏感信息加解密与HTTP签名(实操)
- 高频坑位警示:回调验签失败、金额分转元、幂等性处理
- 性能与安全进阶:连接池配置、证书定时刷新、分布式锁
- 常见问题问答(FAQ)与排错口诀
微信支付V3与V2的本质区别:为什么你必须升级?
微信支付V3(APIv3)自2020年全面推行,与V2相比,最核心的变化是通信协议全面升级为HTTP/2 + TLS 1.2及以上,且不再强制要求商户证书文件(p12/apiclient_cert.pem),取而代之的是商户API私钥 + 平台公钥的加解密体系。

- 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 - 请求体关键字段:
appid、mchid、description、out_trade_no、amount.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请求后,需要做两件事:
- 验签:用平台公钥验签请求头中的
Wechatpay-Signature。 - 解密密文:用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. 查单与退款
- 查单API:
GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchid=xxx,返回结果中若trade_state=SUCCESS则确认支付成功。 - 退款API:
POST /v3/refund/domestic/refunds,请求体需要传入out_refund_no、out_trade_no、amount.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 进行计算,禁止用 Double 或 float,否则会出现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_code 和 err_msg 打印出来,用官方 API文档 对照排查。
微信支付V3已经是非常成熟的对接方案,只要理解“非对称加密验签 + 平台证书自动更新 + APIv3密钥解密”这三板斧,配合官方SDK,开发难度并不高,建议先在沙箱环境(测试号)跑通全流程,再切换生产配置,如果遇到具体错误,欢迎在评论区带上 err_code 提问,我会逐一解答。