简介:针对企业微信交易场景封装的Java开发工具类,涵盖微信支付V3版、微信退款V3版、交易状态查询与企业打款到个人零钱(旧版)四大功能。这套代码源自作者真实企业项目中的自我封装,将微信官方接口调用逻辑统一归纳为可直接调用的方法,使用时传入对应业务参数即可完成交易闭环,省去大量重复对接工作,显著降低微信支付接入门槛,适合需要快速集成支付能力的后端工程师。资源共7个文件,其中5个Java文件为核心工具类实现,iml与xml文件承担工程配置与依赖声明,整体压缩包仅11KB,结构轻量、便于阅读。目前已有2277人学习下载,读者既能直接参考封装思路,也可复制方法至自身项目快速验证,遇到问题还可在评论区留言讨论。
1. 微信支付工具类 v3 版:为什么 Java 项目越来越需要自己封装
微信支付 v3 接口跟 v2 不是换个地址那么简单:请求签名被放进了 Authorization 头,回调报文变成了 AES-GCM 加密的 JSON,证书还被拆成了商户证书和微信支付平台证书两套。很多 Java 项目从网上复制过来的老工具类,一旦碰上证书更新、退款回调、交易状态主动查询,就是各种验签失败和黑匣子一样的报错。这篇文章要拆的,是一个能覆盖微信支付 v3 版下单、微信退款 v3 版、微信交易状态查询、企业打款到零钱全场景的工具类封装方案,每一段代码为什么这么写、参数怎么取、失败了先看哪里,都会讲透。适合自己维护支付模块、需要二次开发,或想摆脱官方 SDK 黑匣子的中级 Java 工程师。
2. 先把签名体系焊死:初始化 v3 工具类的证书与 Authorization 头
微信支付 v3 工具类最核心的不是 HTTP 请求本身,而是那套 RSA 签名。很多项目移植官方 SDK 时遇到SIGN_ERROR,最后定位下来都是签名串拼错、私钥格式不对或证书序列号带进了冒号。这一章先把签名基础打牢,再做具体接口。
2.1 v2 到 v3 的变化:签名挪进了 Authorization,证书分成了商户和平台两把
v2 的报文是 XML,商户用 MD5 或 HMAC-SHA256 把签名放在请求体里,一个 API 密钥就能跑。v3 把签名从报文体里挪了出来,放进 HTTP 头的Authorization,算法换成 RSA-SHA256,并且要求每次请求都带时间戳和随机串来防止重放。请求体统一是 JSON,回调通知如果涉及敏感数据,还会用 AES-GCM 再加密一层。
每次请求前,微信要求按固定顺序拼一个待签名串:
POST /v3/pay/transactions/jsapi 1715000000 9d0f5c3c9c124d0b9e6c3a0c7f {"appid":"wx8888","mchid":"1900009191"}也就是HTTP方法 + "\n" + 规范化URL + "\n" + 时间戳 + "\n" + 随机串 + "\n" + 请求体 + "\n"。注意 URL 要做编码,比如商户号里的特殊字符不能直接粘进去;请求体必须和实际发送的 JSON 完全一致,多一个空格都会让验签失败。GET 请求的请求体为空,但最后的空行必须保留。
对应的请求头长这样:
Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="9d0f5c3c9c124d0b9e6c3a0c7f",timestamp="1715000000",serial_no="1AB2C3D4...",signature="BASE64签名"Header 里的键名是固定的,nonce_str、timestamp、serial_no、signature一个都不能少,顺序错了也可能被拒。这里签名用的私钥是商户 API 证书私钥,而回调验签用的却是微信支付平台证书的公钥,两把钥匙各管一段,不能混用。
2.2 写一个 PayV3Signer:加载私钥、构造签名串、生成请求头
在工具类里,我一般会把“签名”单独抽成一个类,后续所有支付、退款、查询、打款接口都复用同一个实例。下面是最小可用的PayV3Signer:
import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; import java.util.UUID; public class PayV3Signer { private final String mchId; // 商户号,例如 1900009191 private final String serialNo; // 商户 API 证书序列号,注意不能带冒号 private final PrivateKey privateKey; public PayV3Signer(String mchId, String serialNo, String pemPath) throws Exception { this.mchId = mchId; this.serialNo = serialNo; this.privateKey = loadPrivateKey(Files.readString(Paths.get(pemPath))); } /** 微信支付 v3 要求 PKCS8 格式私钥,很多 v2 老代码里是 PKCS1,这是验签失败的最大原因 */ private PrivateKey loadPrivateKey(String pem) throws Exception { String body = pem .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s", ""); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(body)); return KeyFactory.getInstance("RSA").generatePrivate(spec); } /** * 组装 Authorization 头 * @param method POST/GET * @param url 规范化 URL,例如 /v3/pay/transactions/jsapi * @param body 请求体 JSON,GET 请求传空字符串 */ public String buildAuthHeader(String method, String url, String body) throws Exception { String timestamp = String.valueOf(System.currentTimeMillis() / 1000); String nonce = UUID.randomUUID().toString().replace("-", ""); String message = method + "\n" + url + "\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()); return "WECHATPAY2-SHA256-RSA2048 " + "mchid=\"" + mchId + "\"," + "nonce_str=\"" + nonce + "\"," + "timestamp=\"" + timestamp + "\"," + "serial_no=\"" + serialNo + "\"," + "signature=\"" + signature + "\""; } }逻辑说明:loadPrivateKey把 PEM 文本里的头和尾剥掉,剩下的 Base64 用 PKCS8 解码。微信商户平台下载的apiclient_key.pem默认就是 PKCS8,但如果你从别处拷贝过私钥,或者经过旧版 OpenSSL 转换,很可能变成 PKCS1,导致invalid key format,这个问题在避坑章节还会展开。
参数说明:serialNo是商户 API 证书序列号,不是证书文件里openssl x509 -noout -serial打印出来的serial=AB:CD:...。复制出来后要去掉冒号,只留十六进制字母,否则签名头里的序列号匹配不上。nonce用 UUID 去掉横线就够用,微信没有要求非要用 SecureRandom,但如果你对安全等级敏感,可以换SecureRandom生成 16 字节十六进制串。
实际发送请求时,用 Java 11+ 的HttpClient就能实现,替换成 OkHttp 或 Feign 也只是一行改动:
HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.mch.weixin.qq.com" + canonicalUrl)) .header("Authorization", signer.buildAuthHeader("POST", canonicalUrl, jsonBody)) .header("Content-Type", "application/json") .header("Accept", "application/json") .header("User-Agent", "java-pay-v3-demo/1.0") .POST(BodyPublishers.ofString(jsonBody)) .build();2.3 商户证书、平台证书与 APIv3 密钥:一张表分清三把“钥匙”
工具类配置里最容易混的就是这三样东西。很多新手以为回调验签用的是自己下载的那个证书,实际上完全不是一回事。
| 配置项 | 来源 | 用途 |
|---|---|---|
| 商户 API 证书私钥 | 商户平台 → API安全 → API证书 | 发起所有请求时生成 Authorization 签名 |
| 商户 API 证书序列号 | 证书详情页 | 放进 Authorization 头的 serial_no |
| 微信支付平台证书公钥 | 证书下载接口或商户平台 | 验证微信回调通知和平台下发的响应签名 |
| APIv3 密钥 | 商户平台 → API安全 → APIv3密钥 | AES-GCM 解密回调里的 resource 密文 |
商户 API 证书是客户端身份凭证,用来证明这是你;平台证书是服务端身份凭证,用来证明响应真的来自微信。两者都可以轮换,但轮换节奏不一样。APIv3 密钥则完全不参与签名,只负责解密和加密,建议用独立的随机字符串,别跟 API 密钥混用。
工具类初始化时,通常会把商户号、AppId、证书序列号、私钥路径、APIv3 密钥放成一个配置对象。平台证书不建议写死在代码里,因为微信会不定期更新,后面避坑章节会说怎么处理。
3. 用工具类跑通微信支付 v3:下单、调起支付与回调验签解密
支付是整套工具类的主干,跑通一次下单、收到一次回调并成功解密,后面退款和查询基本就是复制这个骨架。
3.1 构造 JSAPI 下单参数:金额单位、openid 和回调地址别踩坑
JSAPI 下单对应POST /v3/pay/transactions/jsapi,适合公众号、小程序里发起支付。最精简的请求体长这样:
{ "appid": "wx8888888888888888", "mchid": "1900009191", "description": "测试商品-001", "out_trade_no": "T202406120001", "notify_url": "https://api.example.com/pay/notify", "amount": { "total": 1, "currency": "CNY" }, "payer": { "openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o" } }amount.total的单位是分,1 就是 0.01 元;out_trade_no是商户侧订单号,同一商户号下必须唯一,这是后续查询和退款的幂等键;description长度限制 1~127 个字符;notify_url不能带自定义 query 参数,回调地址在 v3 里只认固定格式,带?from=xx这类参数微信会直接拒收。
金额计算我建议全程用 BigDecimal,用 String 构造而不是new BigDecimal(double)。单价 0.29 元乘以 3 这种计算,如果中间用了浮点,最后转分就可能是 86 而不是 87,这类问题在避坑章节有详细案例。
3.2 发送下单请求并生成调起支付参数
发送逻辑跟其他 POST 接口一样,把签名头带上,解析返回的prepay_id:
public static JSONObject createJsapiOrder(PayV3Signer signer, JSONObject orderBody, String canonicalUrl) throws Exception { String auth = signer.buildAuthHeader("POST", canonicalUrl, orderBody.toString()); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.mch.weixin.qq.com" + canonicalUrl)) .header("Authorization", auth) .header("Content-Type", "application/json") .header("Accept", "application/json") .header("User-Agent", "java-pay-v3-demo/1.0") .POST(BodyPublishers.ofString(orderBody.toString())) .build(); HttpResponse<String> resp = client.send(request, HttpResponse.BodyHandlers.ofString()); return new JSONObject(resp.body()); }常见做法是把这个方法封装成通用的postJson(signer, url, body),所有接口复用。下单成功后,接口会返回prepay_id,但不能直接把prepay_id给前端,还需要用 AppId、时间戳、随机串、package=prepay_id=xxx再签一次名,才能用于小程序或 App 的调起支付:
String prepayId = result.getString("prepay_id"); String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = UUID.randomUUID().toString().replace("-", ""); // App 调起支付参数里的 paySign 签名串 String payMessage = appId + "\n" + timeStamp + "\n" + nonceStr + "\n" + "prepay_id=" + prepayId + "\n"; // 用 PayV3Signer 里的同一个私钥按 SHA256withRSA 签名参数说明:这里的签名串跟请求头签名串不同,它是给前端 SDK 做验签用的,所以 key 顺序是 AppId、timeStamp、nonceStr、package。前端拿到paySign后唤起微信,用户完成支付,微信会把结果异步 POST 到notify_url,那才是真正的“支付成功”依据。
3.3 回调通知:先验签再解密,解密后按 out_trade_no 幂等
回调通知的报文外面包了一层加密结构,请求头里有Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature、Wechatpay-Serial四个字段。第一步是用Wechatpay-Serial找到对应的平台证书,然后拼验签串:
String message = timestamp + "\n" + nonce + "\n" + requestBody + "\n"; Signature sig = Signature.getInstance("SHA256withRSA"); sig.initVerify(platformCert.getPublicKey()); sig.update(message.getBytes(StandardCharsets.UTF_8)); boolean ok = sig.verify(Base64.getDecoder().decode(signatureFromHeader));验签通过后,再解析请求体拿到resource里的ciphertext、nonce、associated_data,用 APIv3 密钥做 AES-GCM 解密:
public static String decryptNotify(String apiV3Key, JSONObject resource) throws Exception { String ciphertext = resource.getString("ciphertext"); String nonce = resource.getString("nonce"); // 注意:取 resource.nonce,不是请求头的 nonce String associatedData = resource.optString("associated_data", ""); byte[] key = apiV3Key.getBytes(StandardCharsets.UTF_8); byte[] iv = nonce.getBytes(StandardCharsets.UTF_8); byte[] cipherBytes = Base64.getDecoder().decode(ciphertext); Cipher gcm = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv); gcm.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec); gcm.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8)); return new String(gcm.doFinal(cipherBytes), StandardCharsets.UTF_8); }解密后的 JSON 里能拿到out_trade_no、transaction_id、trade_state、amount.total等关键字段。这里要特别注意顺序:先验签,后解密,再改订单状态。如果验签不过,说明这条通知可能来自伪造请求,直接丢弃并返回 401 或 500。处理成功后要返回 HTTP 200 和{"code":"SUCCESS","message":"成功"},否则微信会按 15 秒、15 秒、30 秒的节奏重试,最长可能推 24 小时,直到你返回成功。
4. 微信退款 v3 与交易状态查询:把支付闭环补成对账闭环
支付跑通只是开始,退款和查单才是日常运维里最常用的能力。微信退款 v3 版不需要再拼 XML,也不用像 v2 那样双证书同时上,工具类里直接复用同一个签名器即可。
4.1 微信退款 v3 版:退款接口、out_refund_no 与金额校验
退款接口是POST /v3/refund/domestic/refunds,一个方法就能覆盖全额退款和部分退款:
public static JSONObject refund(PayV3Signer signer, String outTradeNo, String outRefundNo, int refundFee, int totalFee, String refundNotifyUrl) throws Exception { JSONObject amount = new JSONObject(); amount.put("refund", refundFee); // 退款金额,单位分 amount.put("total", totalFee); // 原订单金额,单位分 amount.put("currency", "CNY"); JSONObject body = new JSONObject(); body.put("out_trade_no", outTradeNo); // 原交易订单号 body.put("out_refund_no", outRefundNo); // 退款单号,同一商户下唯一 body.put("amount", amount); body.put("notify_url", refundNotifyUrl); String url = "/v3/refund/domestic/refunds"; String auth = signer.buildAuthHeader("POST", url, body.toString()); // 沿用上一章的 postJson 方法发送即可 return postJson(auth, url, body); }逻辑说明:out_refund_no是退款请求的幂等键,重复提交同一个退款单号不会生成多笔退款,而是返回相同的退款单。这个字段设计得非常巧妙,可以当作一次失败重试的“后悔药”。amount.refund是本次要退的金额,amount.total是原订单实付金额,两边的单位都是分。部分退款时refund小于total,全额退款时两者相等。
参数说明:如果没有传notify_url,退款结果就只能靠主动查询,我是建议每次都传独立退款回调地址。退款回调通知的验签和解密方式与支付回调完全一致,只是解密后的event_type是REFUND.SUCCESS,original_type是refund,里面有refund_status字段。
退款接口返回的响应体里有一个status字段,通常一开始是PROCESSING,最终变成SUCCESS或CLOSED。需要注意,退款成功不代表钱马上到账,尤其是退到银行账户可能有 T+1 延迟,所以业务上要以退款回调或查询结果为准。
4.2 交易状态查询:主动查询的时机和 trade_state 判断
查询订单状态用GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={mchid},这个接口专门解决回调漏单、回调乱序、用户支付后没等回调就先跳回页面等问题:
public static JSONObject queryTrade(PayV3Signer signer, String outTradeNo, String mchId) throws Exception { String url = "/v3/pay/transactions/out-trade-no/" + outTradeNo + "?mchid=" + mchId; // GET 请求的 body 传空字符串,但签名串末尾的空行不能丢 String auth = signer.buildAuthHeader("GET", url, ""); HttpResponse<String> resp = getJson(auth, url); return new JSONObject(resp.body()); }返回的 JSON 中,trade_state是核心字段,常见取值如下:
| trade_state | 含义 | 业务动作 |
|---|---|---|
| SUCCESS | 支付成功 | 发货、放行 |
| REFUND | 已退款 | 关闭权限、标记退款 |
| NOTPAY | 未支付 | 可继续等待或关闭 |
| CLOSED | 已关闭 | 不再接受支付 |
| USERPAYING | 用户支付中 | 等待结果,稍后再查 |
| PAYERROR | 支付失败 | 引导用户重新支付 |
小程序支付有一个典型场景:用户输入密码后还没等回调回来就杀掉了 App,服务端一直没收到通知,订单躺在NOTPAY状态。如果只依赖回调,这个订单就永远无法发货。血泪经验告诉我,下单后必须留一个手动“补查”入口,或者在订单创建 5 分钟后跑一个定时任务,把超时未支付但可能已付款的订单捞出来主动查询,查询结果SUCCESS就直接走支付成功流程,把状态补上。
如果要查询退款结果,对应接口是GET /v3/refund/domestic/refunds/{out_refund_no},同样无 query 参数,直接用退款单号拼在路径里。返回值里的status和refund_account能告诉你退款单是处理中、成功还是已关闭。
5. 微信支付工具类避坑清单:签名失败、回调解密、金额精度是前三名
这一章记录的是我自己踩过、以及帮别人排查过的真实翻车点。每一条都按现象、原因、解决这样的顺序写,方便你对着问题直接定位。
5.1 坑一:Authorization 一直 401,错误码 SIGN_ERROR
现象:同样的请求体拿 Postman 测就能过,Java 代码发出去就报401 Unauthorized,响应体里code是SIGN_ERROR。
原因:最常见是私钥格式不对。微信某些下载渠道提供的私钥是 PKCS1 格式,开头是-----BEGIN RSA PRIVATE KEY-----,而 Java 的PKCS8EncodedKeySpec只认-----BEGIN PRIVATE KEY-----。其次,serial_no被复制成了带冒号的格式,或者签名串里拼的 URL 和实际请求 URL 不一致。还有少数情况是请求体里的 JSON 被框架自动加了多余空格或重排了字段,导致签名串里的 body 和实际发出去的 body 不同。
解决:先把私钥转成 PKCS8,用 OpenSSL 一条命令就能校验:
openssl pkcs8 -topk8 -inform PEM -in apiclient_key.pem -out apiclient_key_pkcs8.pem -nocrypt然后把serial_no里的冒号、空格全部去掉。最后,如果你用的是 Jackson 或 Fastjson 序列化请求体,记得在发送时把“序列化后的字符串”原样交给签名方法和 HTTP 请求,不要签完名又让框架重新序列化一遍。
5.2 坑二:回调解密报 AEADBadTagException 或解密出来是乱码
现象:回调验签通过了,但执行decryptNotify抛javax.crypto.AEADBadTagException,或者解密出来的字符串不是合法的 JSON。
原因:绝大多数是把 GCM 解密用的 nonce 取错了。微信回调里有三个 nonce 概念:请求头Wechatpay-Nonce、resource.nonce、以及你请求签名时自己生成的随机串。AES-GCM 解密必须用resource.nonce,如果误用了请求头的Wechatpay-Nonce,GCM 的 tag 校验必然失败,报 AEADBadTagException。另一个原因是associated_data处理不当,微信文档说它可能是空字符串,但有些实现直接把associated_data拼进密文一起解密,这会把分组边界破坏。
解决:严格按上一章代码段的写法,nonce从resource里取,associated_data用optString兜底空串,加解密都使用AES/GCM/NoPadding,tag 长度固定 128 位。另外,ciphertext是标准 Base64 编码,不要先做 URL decode,直接Base64.getDecoder().decode即可。
5.3 坑三:金额精度丢失,1 元支付变成 0.99 元
现象:退款计算时明明是按 0.30 元退的,到微信侧却变成 29 分;或者购买多个商品后,订单金额跟前端展示的总额差 1 分。
原因:业务系统把金额在数据库里存成decimal,但读到 Java 里用了Double做运算,0.1 * 3在二进制浮点里并不精确等于 0.3。转分时如果直接(int) (amount * 100),1.03 元会变成 102 分而不是 103 分,这就是金额“玄学”差异的来源。
解决:统一用分作为存储和传输单位,接口层只收整数;如果业务层必须以元为单位展示,用BigDecimal且必须以 String 构造:
BigDecimal yuan = new BigDecimal("1.03"); int fen = yuan.setScale(2, RoundingMode.HALF_UP) .multiply(new BigDecimal("100")) .intValue(); // 结果是 103,不是 102凡是涉及微信支付 v3 的金额字段,请求前都打一条日志把分打印出来,减少排查成本。
5.4 坑四:证书更新后回调验签全部失败,请求却一切正常
现象:某天支付请求还能正常发起,但所有回调验签都返回失败,查看日志发现Wechatpay-Serial对应的证书已经过期。
原因:微信支付平台证书是有有效期的,轮换周期通常几个月一次。如果工具类里把平台证书的公钥写死成常量,证书更新后旧的验签公钥自然失效。而商户 API 证书更新和平台证书更新是两条线,所以会出现“请求能用、回调全挂”的割裂状态。
解决:给工具类加一个“平台证书缓存”。处理回调时先看Wechatpay-Serial是否在缓存里,不在就去拉平台证书列表。证书下载接口返回的证书需要解密后才能拿到公钥,解密用的还是 APIv3 密钥。稳妥的做法是:启动时拉一次,运行期发现未知序列号再拉一次,缓存刷新周期设 12 小时。
5.5 坑五:同一笔订单回调重复推送,订单状态被覆盖
现象:订单已经标记为“已支付”,几分钟后又收到一次TRANSACTION.SUCCESS通知,把订单状态重新刷成“待发货”,并发场景下甚至出现两条支付流水。
原因:微信支付回调没有 exactly-once 语义,业务方必须在回调里做幂等。如果你处理回调直接update order set status = 支付成功,后到的重复通知就可能覆盖掉已经推进到下一步的状态。
解决:用out_trade_no + event_type作为消费去重键,处理前先查当前订单状态,只有“待支付”才允许流转到“已支付”;已经“已支付”的订单收到重复通知直接返回SUCCESS,不再更新状态。退款回调同理,out_refund_no就是天然的幂等键,重复通知直接丢弃。
6. 企业打款到零钱:批次转账的幂等与资金核对
企业打款在微信支付 v3 里对应的是“商家转账到零钱”,典型场景是返利、佣金、报销款直接打到用户微信零钱。接口路径是POST /v3/transfer/batches,和前面的支付、退款一样用的是同一套 RSA 签名,但报文结构多了批次和明细两层,转账时要特别关注幂等设计。
JSONObject detail = new JSONObject(); detail.put("out_detail_no", "D202406120001"); // 明细单号,幂等键 detail.put("transfer_amount", 100); // 单笔转账金额,单位分 detail.put("transfer_remark", "6月返利"); detail.put("openid", "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"); JSONObject body = new JSONObject(); body.put("appid", appId); body.put("out_batch_no", "B202406120001"); // 批次单号,批次幂等键 body.put("batch_name", "6月返利批次"); body.put("batch_remark", "6月返利发放"); body.put("total_amount", 100); // 批次总金额,单位分 body.put("total_num", 1); // 转账明细条数 body.put("transfer_detail_list", new JSONArray().put(detail)); String url = "/v3/transfer/batches"; String auth = signer.buildAuthHeader("POST", url, body.toString()); // 返回 out_batch_no 与 out_detail_no,后续用它们查询批次状态逻辑说明:批次号和明细号都是幂等键,重复提交同一组编号不会重复打款。返回后的batch_status只有WAIT_PAY、ACCEPTED、FINISHED、CLOSED等几种,打款结果最终要以批量查询接口为准。如果明细里需要做实名校验,且转账金额达到平台规定阈值,就得把user_name先用微信平台公钥做非对称加密再传,不要直接放明文。资金类接口我还会额外打一条包含out_batch_no、请求体哈希、响应码的日志,排查时不用去翻黑匣子。
最后说一个我个人的习惯:每当在新的支付项目里把 v3 工具类搭好,第一件事不是写业务接口,而是先写失败用例。比如用一个已知错误的私钥去调签名方法,确认异常;用一段被篡改的回调 body 去验签,确认返回 false;再把固定的一组ciphertext、nonce、associated_data灌进解密方法,断言明文 JSON 跟预期一致。这样做一次,后面证书更新、SDK 升级时能立刻知道是哪一环出了问题。微信支付这种链路,靠的不只是文档,还有这些能复现的最小验证用例,希望帮到你。
本文还有配套的精品资源,点击获取