ARTICLE DETAIL

资讯详情

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

微信支付v3 Java工具类:签名、回调解密与商家转账实战指南

微信支付v3 Java工具类:签名、回调解密与商家转账实战指南 简介面向Java开发者的微信支付V3版工具类资源围绕企业项目中最常遇到的支付、退款与对账场景封装了微信支付V3下单、微信退款V3、交易状态查询以及企业打款到个人零钱旧版等核心能力可作为快速接入微信支付体系的现成参考。使用时直接调用封装方法并传入对应参数即可免去自行处理签名、证书、回调通知等繁琐流程。压缩包共7个文件以5个Java源文件为主辅以IML工程配置与XML配置文件整体仅11KB轻量易用无冗余内容。这些工具类源于作者真实企业项目实践工程采用标准Maven结构源码与配置层次清晰方便直接导入项目后按需修改复用。目前已有2277人学习下载对需要实现微信支付联调、退款对账或企业打款的开发者具有较高参考价值。1. 微信支付v3工具类先搞清楚这套东西解决什么问题一个Java服务要同时接微信支付v3版下单、微信退款v3版、微信交易状态查询和企业打款到零钱时最先崩掉的一定是签名那套东西。v2版只要一个API密钥加MD5签名v3版把安全体系拆成商户私钥签名、平台证书验签、APIv3密钥解密三层老工具类基本作废。标题里的「工具类v3版」本质就是把这套签名、请求、解密、回调逻辑收敛到一段可复用的Java代码里让业务侧只关心参数。适合正在从v2迁移、或新项目直接对v3的Java后端照着结构改参数就能落地。下面的方案覆盖支付下单、回调、退款、查单、企业打款五个动作顺带把对接中的坑都标出来尽量少走弯路。2. v3签名机制与请求封装商户私钥、平台证书和APIv3密钥的分工2.1 三把钥匙的分工私钥签名、平台证书验签、APIv3密钥解密微信支付v3的签名体系跟v2完全是两回事。v2大家习惯用MD5或HMAC-SHA256对报文做摘要v3换成了RSA-SHA256而且一次完整交互里要管三种密钥材料商户API私钥负责生成请求签名平台证书负责验证微信返回的响应签名和回调签名APIv3密钥负责解密回调通知里的加密报文。这三样东西的角色不能混混了就是各种看不懂的报错。商户API证书在商户平台「账户中心-API安全-API证书」下载拿到的是apiclient_key.pem和apiclient_cert.pem。请求签名用的是apiclient_key.pem里的私钥这个文件只能放在服务端走配置中心或环境变量注入绝不能进GIT仓库。平台证书反过来是微信支付平台的证书用来验签的网关证书会不定期更新建议在程序里做定时刷新不要只把证书文件静态放在resources目录里等微信换证书后回调突然全挂。APIv3密钥是你在商户平台手动设置的一个32字节字符串不参与网络签名只用于解密回调通知里的resource.ciphertext。很多新手拿APIv3密钥去验签或者拿商户私钥去解密方向反了自然全错。三者用途整理成一张表材料来源用途保存方式商户API私钥商户平台下载生成请求签名服务端私密存储平台证书接口/自动更新验签响应和回调服务端可公开APIv3密钥商户平台手动设置AES-GCM解密回调私密存储这里还有个容易忽略的点平台证书的拉取接口/v3/certificates本身也要走商户私钥签名请求返回的加密报文又要用APIv3密钥解密。所以工具类必须先把签名和请求封装好再做证书轮换这两个能力天然是耦合的。我见过有人把证书下载单独写个脚本跑一次然后把证书文件扔到服务器上结果证书过期后线上全挂临时手忙脚乱。正确做法是把证书拉取和解密也收进工具类里定时任务去刷新业务代码无感。2.2 用Java生成Authorization签名头签名串与RSA-SHA256v3的每个请求头里都带一个Authorization格式是固定的WECHATPAY2-SHA256-RSA2048 mchid1900009191,nonce_str5K8264ILTKCH16CQ2502SI8ZNMTM67VS,timestamp1651113310,serial_no1DDE55AD98C71D7269AF5BDA408F6C,signature...签名串由五段组成段间用换行符\n连接HTTP方法、URL路径带查询参数、请求时间戳、请求随机串、请求报文主体。报文主体为空时这一行就是空的但最后那个换行符不能丢。用Java实现如下public class WechatPayV3Signer { private final PrivateKey privateKey; private final String mchId; private final String serialNo; public WechatPayV3Signer(PrivateKey privateKey, String mchId, String serialNo) { this.privateKey privateKey; this.mchId mchId; this.serialNo serialNo; } public String buildAuthorization(String method, String urlPath, String body, String timestamp, String nonce) throws Exception { // 签名串固定五段最后以换行符结尾 String message method \n urlPath \n timestamp \n nonce \n body \n; Signature signature Signature.getInstance(SHA256withRSA); signature.initSign(privateKey); signature.update(message.getBytes(StandardCharsets.UTF_8)); String signValue Base64.getEncoder().encodeToString(signature.sign()); return WECHATPAY2-SHA256-RSA2048 mchid\ mchId \, nonce_str\ nonce \, timestamp\ timestamp \, serial_no\ serialNo \, signature\ signValue \; } }这段代码里最容易坑人的是body参数。很多人先构造Map签名时序列化一次发送时HttpClient又把Map序列化一次两次结果只要差一个空格或者字段顺序变化签名必挂。我的习惯是先把请求体序列化成String签名和发送都用同一个String不做二次转换。另外serial_no必须是商户API证书的证书序列号不是文件名编号更不是商户号。查证书序列号可以直接用openssl x509 -in apiclient_cert.pem -noout -serialJava侧也可以从X509Certificate里读。这个填错微信返回的永远是英文报错SIGN_ERROR不带任何额外细节。工具类里最好把timestamp和nonce的生成也收进来timestamp用秒级System.currentTimeMillis() / 1000nonce用UUID.randomUUID().toString().replace(-, )长度32位以内。时间戳偏移超过5分钟微信会直接拒绝所以服务器时间要做NTP同步这个属于玄学但确实常发生。2.3 一个通用的v3请求客户端统一签名、超时与错误处理有了签名生成器再封装一个execute方法把HttpClient细节全部收起来业务侧只需要关心接口路径和请求体public class WechatPayV3Client { private final CloseableHttpClient httpClient; private final WechatPayV3Signer signer; private static final String BASE_URL https://api.mch.weixin.qq.com; public String execute(String method, String urlPath, String body) throws Exception { long timestamp System.currentTimeMillis() / 1000; String nonce UUID.randomUUID().toString().replace(-, ); String authorization signer.buildAuthorization(method, urlPath, body, String.valueOf(timestamp), nonce); HttpUriRequest request buildRequest(method, BASE_URL urlPath, body); request.setHeader(Authorization, authorization); request.setHeader(Accept, application/json); if (body ! null body.length() 0) { request.setHeader(Content-Type, application/json); } try (CloseableHttpResponse response httpClient.execute(request)) { String responseBody EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); int statusCode response.getStatusLine().getStatusCode(); if (statusCode 200 statusCode 300) { return responseBody; } throw new WechatPayException(wechat pay error, status statusCode , body responseBody); } } private HttpUriRequest buildRequest(String method, String url, String body) { if (GET.equalsIgnoreCase(method)) { return RequestBuilder.get(url).build(); } RequestBuilder builder RequestBuilder.create(method.toUpperCase()); builder.setUri(url); if (body ! null body.length() 0) { builder.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON)); } return builder.build(); } }execute方法最关键的设计是urlPath由调用方传入签名用的路径和实际请求用的路径是同一个变量天然保证一致。我见过有人让调用方传完整URL内部再截取一段去签名结果两边对不上所有请求全部Invalid Signature。这里统一只传路径域名在客户端内部拼能省掉一大批签名对齐问题。GET请求的body传空字符串签名串里对应那一段就是空的但换行符还在。请求头发送时GET不需要Content-Type所以上面代码里body为空就不设置这个头。微信对GET请求带不带Content-Type不报错但多一事不如少一事统一这么处理。超时配置单独提一下连接超时3秒读超时10秒。微信支付接口偶尔响应超过5秒读超时设太短会误判失败连接超时不能太长否则请求会堆积在连接池。生产环境我用连接池每个路由最多200个连接瞬时大并发不会打挂国内的支付域名。错误响应体这里要保留原始body因为v3的4xx错误不一定都是JSON{code:SIGN_ERROR,message:...}这种要能原样打出来排查时直接复制给微信技术看。3. 微信支付、退款和交易状态查询工具类的三个高频接口3.1 JSAPI下单与小程序调起支付参数JSAPI下单是公众号小程序场景最常见的接口路径POST /v3/pay/transactions/jsapi核心参数是appid、mchid、description、out_trade_no、notify_url、amount和payer。写进工具类就是一次普通调用public String createJsapiOrder(String openid, String description, String outTradeNo, Integer totalFeeFen, String notifyUrl) throws Exception { MapString, Object payer new LinkedHashMap(); payer.put(openid, openid); MapString, Object amount new LinkedHashMap(); amount.put(total, totalFeeFen); amount.put(currency, CNY); MapString, Object bodyMap new LinkedHashMap(); bodyMap.put(appid, appId); bodyMap.put(mchid, mchId); bodyMap.put(description, description); bodyMap.put(out_trade_no, outTradeNo); bodyMap.put(notify_url, notifyUrl); bodyMap.put(amount, amount); bodyMap.put(payer, payer); String body JSON.toJSONString(bodyMap); String resp client.execute(POST, /v3/pay/transactions/jsapi, body); return JSON.parseObject(resp).getString(prepay_id); }这里我故意用LinkedHashMap不是为了签名是为了让日志里的JSON字段顺序固定排查问题时直观。amount.total的单位是分整数类型不要传String更不要传1.00。out_trade_no是商户订单号6到32位同一商户号下必须唯一description也不能为空支付收银台会直接展示这个字段命名规范建议「商品名-订单号后四位」。拿到prepay_id后小程序端要调起支付还需要一组参数并且这组参数要再做一次签名。这次签名串跟请求签名不一样只有四段appId、timeStamp、nonceStr、package。package的值是prepay_idxxxsignType固定成RSApublic String buildMiniProgramPayParams(String prepayId, String timestamp, String nonce) throws Exception { String packageStr prepay_id prepayId; String signMessage appId \n timestamp \n nonce \n packageStr \n; Signature signature Signature.getInstance(SHA256withRSA); signature.initSign(privateKey); signature.update(signMessage.getBytes(StandardCharsets.UTF_8)); String paySign Base64.getEncoder().encodeToString(signature.sign()); MapString, String payParams new LinkedHashMap(); payParams.put(appId, appId); payParams.put(timeStamp, timestamp); payParams.put(nonceStr, nonce); payParams.put(package, packageStr); payParams.put(signType, RSA); payParams.put(paySign, paySign); return JSON.toJSONString(payParams); }注意这里的签名和2.2节的请求签名区别没有HTTP方法没有URL没有请求体就是四个字段各占一行。用同一个商户私钥做RSA-SHA256签名但签名串结构完全不同。很多从v2过来的人习惯把v2那套按字典序拼接的方式套上来这里v3明确规定按appId、timeStamp、nonceStr、package这个顺序不能自己调整。3.2 回调验签与AES-GCM解密支付成功微信会异步通知notify_url通知体的resource是加密结构。处理回调必须两步走先验签再解密。验签要读四个请求头Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Serial。签名串是时间戳\n随机串\n请求体\n用平台证书公钥验签不是商户私钥public String decryptCallback(String serialHeader, String signatureHeader, String timestampHeader, String nonceHeader, String requestBody) throws Exception { // 第一步用平台证书验签 X509Certificate certificate platformCertificateProvider.getBySerial(serialHeader); String message timestampHeader \n nonceHeader \n requestBody \n; Signature signature Signature.getInstance(SHA256withRSA); signature.initVerify(certificate); signature.update(message.getBytes(StandardCharsets.UTF_8)); if (!signature.verify(Base64.getDecoder().decode(signatureHeader))) { throw new WechatPayException(wechat pay callback verify failed); } // 第二步AES-GCM解密 JSONObject resource JSON.parseObject(requestBody).getJSONObject(resource); String ciphertext resource.getString(ciphertext); String associatedData resource.getString(associated_data); String nonce resource.getString(nonce); Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); SecretKeySpec key new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), AES); cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8))); if (associatedData ! null associatedData.length() 0) { cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8)); } byte[] plainBytes cipher.doFinal(Base64.getDecoder().decode(ciphertext)); return new String(plainBytes, StandardCharsets.UTF_8); }AAD参数是重灾区。回调通知的resource里自带一个associated_data字段支付回调和退款回调的值不一样AES-GCM解密时的AAD必须取它不是请求头Wechatpay-Serial更不能传null。我当时第一版写成了空字符串解密一直报错后来对照文档才发现resource里还有个associated_data。如果这个字段为null才允许传空串。nonce的处理也容易踩坑这里的nonce是明文字符串直接getBytes(StandardCharsets.UTF_8)当GCMParameterSpec初始化向量不需要Base64解码。习惯用支付宝AES工具的人第一次都会在这翻车。最后验签通过后还要检查Wechatpay-Timestamp和服务器当前时间差超过5分钟直接拒绝这是防重放的基本要求别省。3.3 微信退款与交易状态查询单位用分、单号必填、掉单兜底退款接口是POST /v3/refund/domestic/refunds请求体里必须同时传原订单金额total和退款金额refund单位都是分。封装成Java方法public String refund(String outTradeNo, String outRefundNo, Integer totalFen, Integer refundFen, String notifyUrl) throws Exception { MapString, Object amount new LinkedHashMap(); amount.put(refund, refundFen); amount.put(total, totalFen); amount.put(currency, CNY); MapString, Object bodyMap new LinkedHashMap(); bodyMap.put(out_trade_no, outTradeNo); bodyMap.put(out_refund_no, outRefundNo); bodyMap.put(notify_url, notifyUrl); bodyMap.put(amount, amount); String body JSON.toJSONString(bodyMap); return client.execute(POST, /v3/refund/domestic/refunds, body); }退款接口的幂等靠out_refund_no退款单号要商户自己生成同一个单号重复提交微信会报「单号已被使用」不会重复退款。业务上为了防止操作员点两遍造成重复退款生成退款单号可以用「原订单号日期随机数」并在库里做唯一索引。退款的到账不是实时的响应里status可能是PROCESSING、SUCCESS、CLOSED或ABNORMAL不应该同步等SUCCESS要接退款结果回调或者定时查退款单状态。交易状态查询是掉单兜底的关键手段接口是GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchidxxxpublic String queryOrder(String outTradeNo) throws Exception { String path /v3/pay/transactions/out-trade-no/ URLEncoder.encode(outTradeNo, StandardCharsets.UTF_8.name()) ?mchid mchId; return client.execute(GET, path, ); }注意两点第一路径里的out_trade_no要URL编码虽然我们用的单号都是字母数字但编码一下更保险第二签名串里的路径必须包含?mchidxxx2.2节签名时路径就是整个带query的路径。查询返回的trade_state有七种SUCCESS、REFUND、NOTPAY、CLOSED、REVOKED、USERPAYING、PAYERROR。只有SUCCESS算支付成功NOTPAY和USERPAYING都不能直接判定失败USERPAYING是用户正在输入密码或指纹这时候轮询间隔放大到5分钟比较稳不然用户还没付完就被你关单了。4. 企业打款到零钱v3商家转账API的实现与参数4.1 商家转账API和旧版企业付款的区别企业打款到零钱在v3叫「商家转账」接口链路和v2的mmpaymkttransfers差别很大。v2时代发XML报文、同步拿结果v3改成JSON、RSA签名、批次加明细两层单号结构。从工具类角度看支付退款和打款共用同一套签名请求逻辑这是个明显优势不需要维护两套安全体系。对比项v2企业付款v3商家转账签名方式MD5/HMAC-SHA256RSA-SHA256报文格式XMLJSON单号结构单个商户订单号批次号明细号两级结果获取同步返回批次查询明细查询收款人校验弱可传姓名证件强校验v3的批次和明细两级结构意味着一个请求可以带多个收款人部分明细失败不影响其他明细。我们项目里单笔打款比较常见批次里放一个明细但数据结构仍然保持两层这样后续扩展批量报销、批量佣金结算时不用改接口。这里要提前确认的事商家转账需要在商户平台「产品中心-商家转账」里开通产品权限并配置结算规则。没开通就调用接口会返回NO_AUTH错误不是代码问题。另外不同行业类目对转账用途有限制比如有些类目不支持把打款描述写成「返利」「佣金」这个在配置结算规则时商户平台会有提示别等上线后被拒才发现。4.2 发起转账批次单号、明细单号和金额参数发起转账的接口是POST /v3/transfer/batches请求体里批次和明细的参数有严格校验public String transfer(String openid, Integer amountFen, String remark, String outBatchNo, String outDetailNo) throws Exception { MapString, Object detail new LinkedHashMap(); detail.put(out_detail_no, outDetailNo); detail.put(transfer_amount, amountFen); detail.put(transfer_remark, remark); detail.put(openid, openid); MapString, Object bodyMap new LinkedHashMap(); bodyMap.put(appid, appId); bodyMap.put(out_batch_no, outBatchNo); bodyMap.put(batch_name, 报销打款); bodyMap.put(batch_remark, remark); bodyMap.put(total_amount, amountFen); bodyMap.put(total_num, 1); bodyMap.put(transfer_detail_list, Collections.singletonList(detail)); String body JSON.toJSONString(bodyMap); return client.execute(POST, /v3/transfer/batches, body); }out_batch_no是商户侧生成的批次单号out_detail_no是明细单号两个都不能跟已有单号重复。total_amount是批次总金额单位分必须等于所有明细transfer_amount之和批次里只有一笔时就是这一笔的金额多笔时一定要循环求和别拿第一笔的金额填进去。batch_name和batch_remark有长度限制也不能带特殊符号我们曾经在备注里加了「报销#3月」结果报参数格式错误去掉#号就好了。openid必须是当前appid下获取的用户openid。企业打款和支付用的是同一套AppID体系如果你拿的是另一个小程序或公众号下的openid直接传接口会报用户信息不匹配。如果需要强实名核验可以传user_name这类敏感字段但必须用平台证书公钥做RSAES-OAEP加密后再传加密结果Base64编码明文传会被直接拒绝。大部分场景可以不传姓名让微信侧按openid对应的实名信息校验打款给未实名用户会失败这种失败是异步体现在明细状态里的要专门处理。4.3 转账结果查询批次与明细两级的查单接口转账发起后不能马上下结论说成功了。批次状态需要主动查询接口是GET /v3/transfer/batches/out-batch-no/{out_batch_no}public String queryTransferBatch(String outBatchNo) throws Exception { String path /v3/transfer/batches/out-batch-no/ URLEncoder.encode(outBatchNo, StandardCharsets.UTF_8.name()); return client.execute(GET, path, ); }批次查询返回的batch_status有ACCEPTED、FINISHED、CANCELING、CANCELED等。ACCEPTED是批次已受理但还在处理中FINISHED才是批次完成。如果只想确认某一笔明细是否到账用明细查询接口GET /v3/transfer/batches/batch-id/{batch_id}/details/detail-id/{detail_id}detail_status为SUCCESS才代表真的到账。我的处理策略是发起转账先落一张待确认表定时任务每5分钟跑一次查批次或明细状态SUCCESS就更新打款状态FAIL就告警并进入人工复核流程。批次查询接口响应体很大一个大批次可能几百KB轮询频率别太高更建议用明细查询按单核对。另外微信侧的转账结果也有异步回调可以接但回调不是必达的定时兜底一定要留这个和支付查单是一个思路。5. 避坑指南v3工具类接入中常见的5个翻车现场5.1 现象请求全部返回Invalid Signature所有POST和GET请求都报签名无效但签名代码看起来没有任何问题。这种问题大概率出在签名串和实际请求不一致。最常见的是请求体被JSON工具二次序列化先构造Map签名时序列化一次发送时HttpClient又序列化一次两次结果只要差一个空格或字段顺序变化签名就过不了。第二个高频原因是查询接口的URL路径没带query参数比如查单的路径是/v3/pay/transactions/out-trade-no/{out_trade_no}?mchidxxx签名串里的路径必须包含问号后的部分很多人只签了问号前那段。解决body统一用String签名和发送共用同一个String禁止在中间环节做任何处理urlPath在客户端内部拼接签名和请求用同一个变量这是2.3节设计的核心原因。5.2 现象回调验签失败回调接口频繁验签失败但平台证书、签名代码都检查过看起来没问题。原因多半是本地存的平台证书和微信当前签名用证书对不上。回调头Wechatpay-Serial标识了微信本次签名用的证书序列号如果你在商户平台下载过一次平台证书就再也没更新过微信侧换证书后旧证书验新回调必然失败。解决平台证书要做轮换通过/v3/certificates接口动态拉取按序列号存Map验签时从Map里取取不到就先拉一次再取。日志里把Wechatpay-Serial打出来排查时直接对比本地证书的序列号一秒钟就能定位。5.3 现象AES-GCM解密抛AEADBadTagException回调报文能验签通过但解密时报AEADBadTagException说明GCM解密过程中认证失败几乎都是AAD参数传错。常见错误写法是拿请求头Wechatpay-Serial当AAD或者干脆传null。正确做法是取回调JSON里resource.associated_data字段支付回调和退款回调里这个字段都存在支付是transaction退款是refund就把这个字符串作为AAD。解决解密方法统一写成String aad resource.getString(associated_data)为null时才用空串不要从请求头里找。5.4 现象退款报「订单金额不正确」退款接口一直提示订单金额不正确但传的金额看起来没问题。原因是微信退款要求amount里同时传total和refundtotal必须等于原订单实付金额refund是本次退款金额。很多人把total填成了退款金额或者把元当分传v3所有金额字段都是分没有小数点。另一种情况是退款金额大于订单可退余额比如已经退过一部分再全额退就超了。解决金额字段统一用Long类型参数命名带Fen后缀避免和元混淆比如totalFen、refundFen。退款前先调交易状态查询拿订单当前实付金额做一次本地校验再发请求。5.5 现象企业打款报「用户姓名不匹配」或「该用户未实名」发起商家转账时报用户姓名不匹配或未实名但openid明明是从前端拿到的。原因通常有两个一是openid来源AppID和当前商户配置不一致openid是AppID维度生成的换一个AppID就换一批openid拿旧的传必然不匹配二是传了user_name但没有加密或者加密用的证书不对v3转账里姓名、身份证号这类敏感字段必须用平台证书公钥做RSAES-OAEP加密后再传明文传会被拒。解决先确认openid确实是从当前appid对应的小程序或公众号里获取的。不强校验姓名的场景就不传微信侧会按openid对应实名信息校验必须传姓名时用平台证书公钥加密并保证加密时用的证书序列号与当前商户使用的平台证书版本一致。6. 不花真钱也能验证本地Mock回调和解密逻辑微信支付v3没有支付宝那种完整沙箱但可以用本地Mock把最复杂的两段逻辑先验证掉回调验签和AES-GCM解密。思路是反向构造一份加密回调再喂给工具类看它能不能正确验签和解密。先生成一对测试RSA密钥一个充当「模拟微信平台私钥」一个充当「模拟平台公钥」注入工具类的证书Provider。然后按AES-GCM加密一段构造好的支付成功通知得到密文。加密代码和解密代码用同一套参数规范这样本地方案和线上逻辑完全一致private String mockCiphertext(String plainJson, String nonce, String aad) throws Exception { Cipher cipher Cipher.getInstance(AES/GCM/NoPadding); SecretKeySpec key new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), AES); cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8))); if (aad ! null) { cipher.updateAAD(aad.getBytes(StandardCharsets.UTF_8)); } return Base64.getEncoder().encodeToString(cipher.doFinal(plainJson.getBytes(StandardCharsets.UTF_8))); }拿到密文后组装成微信回调的resource结构再用模拟平台私钥生成验签签名头POST到本地回调接口。这一步能一次性验证三件事平台证书序列号解析对不对、验签签名串结构对不对、AES-GCM解密参数对不对。跑通之后再上真实商户号充一分钱做端到端联调只验证微信侧到服务器这一段的网络和证书链路。我现在的习惯是每个新项目接支付先把这一套JUnit测试跑绿再碰真实金额。回调类的问题在本地暴露比线上少很多惊心动魄的时刻。企业打款涉及真实资金没有Mock环境我的做法是先用1分钱的测试单走完转账、查批次、查明细全流程确认状态流转正常后再放量。工具类的边界、签名规范和加密细节都定好了后面接新接口就只是加参数的事。希望帮到你。本文还有配套的精品资源点击获取
返回列表