简介:微信支付V2 Java代码是一份面向Java后端开发者的支付接入示例,适用于电商、内容付费、小程序等需要快速开通微信支付的业务场景。压缩包共23个文件,包含17个Java源文件、3个JSP页面、2个Jar依赖和1个XML配置文件,整体大小约208KB,结构清晰,便于直接对照或嵌入现有项目。资源覆盖统一下单、签名生成、异步回调验签、支付结果查询与退款等核心流程,并对MD5/HMAC-SHA256签名算法、回调数据校验、证书使用、异常处理等易错环节给出代码实现;JSP文件可作为联调入口,帮助理解客户端与服务端的交互过程,XML配置则方便快速调整商户参数。目前已有1027人学习下载,适合具备基础Java知识、希望快速掌握微信支付V2服务端逻辑的开发者参考,也可作为排错与二次开发时的实用手册。
1. 微信支付 V2 在 Java 里的真实处境:老协议为什么还值得花力气
先说一个反直觉的结论:微信支付 V2 协议虽然官方已不再推荐新商户接入,但存量商户、第三方支付渠道包、以及大量外包项目中它依然是绝对主力。原因很直接——V3 要求 APIv3 密钥、双向证书、微信支付平台证书轮换,而 V2 只需一个 API 密钥加一个 apiclient_cert.p12,很多老系统的支付模块从 2017 年跑到现在没动过。做 Java 开发的,尤其是接外包或维护老项目的,手上这套 V2 代码基本是吃饭的家伙。
这套资源解决的核心问题很具体:微信支付 V2 从统一下单、回调验签、订单查询到退款,全流程的 Java 实现代码直接可用,省去对着官方文档翻来翻去、踩签名坑的时间。适合三类人:第一次在 Spring Boot 里接微信支付的新手,需要把老项目从 V2 迁移到 V3 但暂时不能动的熟手,以及要写支付模块给别人做二次开发的技术负责人。本章先把整体流程捋清楚,后续从准备参数开始逐步落到代码和坑。
2. 支付前置准备:证书、密钥与参数配置的完整梳理
2.1 商户平台里到底要拿哪几个参数
很多新手在微信支付 V2 的准备工作里卡住,不是因为代码难,而是不知道去商户平台哪个菜单拿哪几个值。这里把 V2 必须的参数列一份清单,按这个去对就不会漏:
- 商户号 mch_id:商户平台首页右上角,一串 10 位以内的数字。
- API 密钥 key:商户平台 -> 账户中心 -> API 安全 -> 设置APIv2密钥,32 位字符串,自己设置的。
- AppID:公众号或小程序的 AppID,在公众平台拿,不是商户平台。
- apiclient_cert.p12:商户平台 -> 账户中心 -> API 安全 -> API证书 -> 下载证书,含商户号的 p12 文件。
- apiclient_key.pem 和 apiclient_cert.pem:V2 虽用 p12 居多,但某些第三方 SDK 或 HTTP 工具要求 pem 格式,下载的证书包里直接有。
这里有一个选型要点:V2 的请求签名用的是 MD5 或 HMAC-SHA256,而 V3 用的是 RSA-SHA256。你这套代码如果可能被拿去做二次开发,建议直接统一用 HMAC-SHA256,因为 MD5 在部分新接口已经不支持,而且 HMAC-SHA256 安全性更高。
2.2 Java 侧的配置文件与初始化
拿到上述参数后,写一个配置类把这些值统一管理。常见做法是放在 application.yml 里,然后用 @ConfigurationProperties 绑定。下面这份配置类可以直接用到代码包里:
@Component @ConfigurationProperties(prefix = "wechat.pay.v2") public class WechatPayConfig { private String appId; private String mchId; private String apiKey; private String certPath; // p12 证书路径,如 /config/apiclient_cert.p12 private String certPassword; // 证书密码,默认就是商户号 mch_id private String notifyUrl; // 支付回调地址,公网可访问 // getter and setter 省略 }对应的 application.yml 配置:
wechat: pay: v2: app-id: wx1234567890abcdef mch-id: 1600000000 api-key: 32位密钥字符串 cert-path: /config/apiclient_cert.p12 cert-password: 1600000000 notify-url: https://api.yourdomain.com/pay/notify逻辑说明:这个配置类把证书路径、API 密钥等敏感信息从代码里抽出来,改环境时只动配置文件,不用重新编译。证书密码这里特别注意——微信官方生成的 p12 证书密码固定为商户号 mch_id 本身,不是你自己设置的任意值,写错的话初始化证书时会直接报错。
参数说明:certPath 建议放在 resources 目录下,打包时注意不要被 maven 的 filter 机制误处理二进制文件;notifyUrl 必须是 HTTPS 且公网可达,微信服务器回调不认 HTTP 和内网地址。API 密钥泄露的风险远大于证书泄露,因为有了 key 就可以自己构造签名。
2.3 HTTP 客户端与 XML 工具的准备
V2 协议的消息格式是 XML 不是 JSON,这是很多从 V3 转头用 V2 的人最不适应的点。所有请求和响应都是 XML 字符串,请求体还要做签名,所以需要准备两个基础设施:一个能发 HTTPS 请求的 HTTP 客户端,一个能解析和构建 XML 的工具类。
public class HttpUtil { private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .sslContext(createSslContext()) .build(); private static SSLContext createSslContext() { try { // 加载 p12 证书,双向 TLS 认证 KeyStore keyStore = KeyStore.getInstance("PKCS12"); try (InputStream ins = new FileInputStream(certPath)) { keyStore.load(ins, certPassword.toCharArray()); } KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(keyStore, certPassword.toCharArray()); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), null, null); return sslContext; } catch (Exception e) { throw new RuntimeException("初始化微信支付证书失败", e); } } public static String postXml(String url, String xml) { // 使用 HTTP_CLIENT 发送 POST 请求,body 是 XML 字符串 // 返回响应体的 XML 字符串 } }逻辑说明:微信支付 V2 的部分接口需要双向证书认证,也就是客户端要拿着 apiclient_cert.p12 证明自己是合法商户。这段代码把 p12 加载进 KeyStore,构建带客户端证书的 SSLContext。后续所有请求都通过这个 HTTP_CLIENT 发送,不用每次请求都重新加载证书。
参数说明:HTTP 请求超时一般设 10 秒,微信支付接口偶尔会延迟,太短容易误报超时,太长影响用户体验。这里有实际踩坑——某个项目把超时设成 5 秒,高峰期微信接口响应 8 秒左右,导致订单实际支付成功但本地查询超时,后来统一改成 10 秒并加一次主动查询兜底。
XML 工具这边,用原生 DOM 解析即可,不需要引额外的库。注意 V2 响应的 CDATA 节点处理,比如 prepay_id、code_url 等字段,用 XPath 取值时要注意节点类型。
3. 统一下单与支付参数生成:核心链路的代码实现
3.1 统一下单请求的签名逻辑与字段组装
统一下单是 V2 里最核心的接口,交互流程是:后端组装订单参数 -> 签名 -> POST 到微信支付接口 -> 拿到 prepay_id -> 再生成前端 JSAPI 拉起支付用的参数。这个过程拆开看就是两个签名环节,第一个环节就是下面这段代码:
public String unifiedOrder(UnifiedOrderParam param) { // 1. 组装请求参数 SortedMap<String, String> params = new TreeMap<>(); params.put("appid", config.getAppId()); params.put("mch_id", config.getMchId()); params.put("nonce_str", generateNonceStr()); // 32位随机字符串 params.put("body", param.getBody()); // 商品描述 params.put("out_trade_no", param.getOutTradeNo()); // 商户订单号 params.put("total_fee", String.valueOf(param.getTotalFee())); // 金额,单位分 params.put("spbill_create_ip", param.getSpbillCreateIp()); // 终端IP params.put("notify_url", config.getNotifyUrl()); params.put("trade_type", param.getTradeType()); // JSAPI / NATIVE / MWEB // 2. 生成签名 String sign = WechatPaySignUtil.sign(params, config.getApiKey(), "HMAC-SHA256"); // 3. 拼接 XML 请求体 params.put("sign", sign); String xml = XmlUtil.buildXml(params); // 4. 发送请求并解析响应 String respXml = HttpUtil.postXml(UNIFIED_ORDER_URL, xml); Map<String, String> resp = XmlUtil.parseXml(respXml); // 5. 校验响应签名(防止中间人篡改响应) String respSign = resp.get("sign"); resp.remove("sign"); if (!WechatPaySignUtil.verify(resp, config.getApiKey(), "HMAC-SHA256", respSign)) { throw new RuntimeException("统一下单响应签名校验失败"); } return resp.get("prepay_id"); }逻辑说明:这里的关键是 SortedMap,签名规则要求把所有参与签名的参数按字典序排序,然后拼成 key=value&key=value 格式,末尾拼接 key=API密钥,再做 HMAC-SHA256 或 MD5,最后转大写。TreeMap 天然保证 key 按字典序排列,省掉手动排序的麻烦。
参数说明:total_fee 单位是分,不是元,这里几乎每个人都会踩一次——金额 10.5 元要传 1050,如果直接传入 10.5 或 "10.5" 会被微信直接拒绝,报「订单金额不正确」。out_trade_no 商户订单号要保证唯一,长度限制 32 位以内,建议用「商户号+时间戳+随机数」的拼法,不要用 UUID 直接做订单号,因为 UUID 里有横杠,后续在退款、对账查询里容易出问题。
3.2 前端拉起支付的二次签名怎么补
拿到 prepay_id 之后,前端 JSAPI 拉起支付还需要一套参数:appId、timeStamp、nonceStr、package(值是 prepay_id=xxx)、signType,然后对这五个参数再做一次签名。这次签名的 key 和刚才的一样,但是参与签名的字段完全不同,很多人在这一步翻车——用统一下单的签名结果直接给前端,结果拉起支付失败报「签名错误」。
public Map<String, String> buildPayParams(String prepayId) { SortedMap<String, String> params = new TreeMap<>(); params.put("appId", config.getAppId()); params.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); params.put("nonceStr", generateNonceStr()); params.put("package", "prepay_id=" + prepayId); params.put("signType", "HMAC-SHA256"); String sign = WechatPaySignUtil.sign(params, config.getApiKey(), "HMAC-SHA256"); params.put("paySign", sign); return params; }逻辑说明:这段代码返回的 Map 直接传给前端,前端 wx.choosePayment 接口的入参就是这五个字段。注意 timeStamp 单位是秒,如果用毫秒会导致签名过期,微信那边校验时间戳发现不合法直接报错。
参数说明:signType 要和统一下单时一致,统一下单用的什么算法,这里必须同样用那个算法,否则签名校验必失败。前端拉起支付失败时报的「invalid signature」基本是这里的问题。
3.3 NATIVE 扫码支付的差异点
如果是 PC 端扫码支付,trade_type 传 NATIVE,统一下单成功后返回的不是 prepay_id 而是 code_url,前端把这个字符串生成二维码让用户扫。无需二次签名。很多在 Java 里做收银台的人会误以为 NATIVE 也要走 JSAPI 的二次签名流程,其实不需要。
NATIVE 模式下,拿到 code_url 后直接返回给前端生成二维码即可。下单成功后 code_url 的有效期是 2 小时,过期需要重新下单。如果项目有 PC 端和移动端两套界面,建议分开两个下单入口,因为 trade_type 不同,前端处理逻辑也不同。
3.4 完整下单流程的时序校验
把上面几段串起来,一个完整的下单用例大致是:前端请求后端下单接口 -> 后端校验商品状态和库存 -> 调微信统一下单 -> 拿到 prepay_id 或 code_url -> 如果是 JSAPI 再生成二次签名 -> 返回给前端拉起支付或展示二维码。
这里有个容易被忽略的操作:统一下单之前要把订单状态先落库,状态设为「待支付」。不是因为微信要求这样做,而是你的回调处理逻辑需要一个订单状态字段来保证幂等——如果订单还没落库就先调微信下单,那回调来了你都不知道这笔订单是哪个用户的。
4. 回调验签与订单查询:别把支付结果寄托在回调一次送达上
4.1 回调验签的完整逻辑:先验签、再解密、后更新
支付成功后的回调通知是 V2 里最容易被轻视的环节。很多人在回调里直接解析 XML、拿 out_trade_no 更新订单状态就算完事,这是不对的——回调是可以伪造的,而且回调通知可能重复发送。正确顺序是先验签再处理业务。
@RequestMapping("/pay/notify") public String payNotify(HttpServletRequest request) throws Exception { // 1. 读取请求体 XML String xml = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); // 2. 解析 XML 并取出 sign 字段 Map<String, String> params = XmlUtil.parseXml(xml); String sign = params.get("sign"); params.remove("sign"); // 3. 验签 boolean verify = WechatPaySignUtil.verify(params, config.getApiKey(), params.get("sign_type") == null ? "MD5" : params.get("sign_type"), sign); if (!verify) { return failResp("签名验证失败"); } // 4. 校验关键业务字段 if (!"SUCCESS".equals(params.get("return_code"))) { return failResp("通信失败"); } if (!"SUCCESS".equals(params.get("result_code"))) { return failResp("支付失败"); } // 5. 校验金额,防止金额被篡改 String outTradeNo = params.get("out_trade_no"); Order order = orderService.findByOutTradeNo(outTradeNo); if (order.getTotalFee() != Integer.parseInt(params.get("total_fee"))) { return failResp("金额不一致"); } // 6. 更新订单状态(幂等处理) if ("PAID".equals(order.getStatus())) { return successResp(); } orderService.updateStatus(outTradeNo, "PAID"); // 7. 返回成功标识 return successResp(); }逻辑说明:第 5 步金额校验很多人会漏掉。回调里带着商户订单号总金额,如果你在更新订单状态前不去比对数据库里的订单金额,就等于把一个不设防的支付入口打开——理论上攻击者构造一个同样 out_trade_no 的假回调,金额是 0.01 元,如果没有金额校验,这笔订单就可能被标记为已支付。正常微信支付回调里金额和本地一致才会放行。
第 6 步是幂等操作的核心。微信官方说明回调通知可能发送多次,且不保证按顺序到达,也可能你处理完业务后网络异常导致微信没收到你的成功响应,微信会重新发送回调。所以更新前先判断订单状态,已经是 PAID 直接返回成功,不再重复处理。
参数说明:failResp 和 successResp 返回的不是 JSON,而是 XML 字符串,格式要求是给微信回 <return_code> </return_code> 这样的。微信支付如果没有收到 SUCCESS 响应,会按照一定时间间隔重复通知,间隔逐步拉长——15 秒、15 秒、30 秒、3 分钟、10 分钟、20 分钟、30 分钟、30 分钟、30 分钟、60 分钟、3 小时、3 小时、3 小时、6 小时、6 小时,一天内最多通知 15 次。
这里的常见错误是把验签放在 return_code 判断之后。如果请求本身是伪造的,return_code 可能成功也可能失败,验签应该最先做,不能因为 return_code 不是 SUCCESS 就跳过验签流程。
4.2 订单查询接口:给回调兜底的主动方案
回调通知机制在极端场景下会丢——微信服务器推送失败,或者你自己服务器在回调那一刻宕机了,恢复后不做主动查询,这笔订单就永远卡在「待支付」。所以正规做法是定时任务主动查微信订单状态来兜底。
@Component public class OrderQueryTask { @Scheduled(fixedRate = 60000) // 每分钟跑一次 public void queryPendingOrders() { // 1. 找出数据库中超时未支付的订单 List<Order> pendingOrders = orderService.findTimeoutOrders(5, TimeUnit.MINUTES); for (Order order : pendingOrders) { // 2. 调用微信查单接口 Map<String, String> result = queryWechatOrder(order.getOutTradeNo()); String tradeState = result.get("trade_state"); if ("SUCCESS".equals(tradeState)) { // 3. 微信侧已支付,但本地回调没收到,补更新订单状态 orderService.updateStatus(order.getOutTradeNo(), "PAID"); } else if ("NOTPAY".equals(tradeState)) { // 4. 用户确实没支付,可以做关闭订单或标记超时处理 orderService.updateStatus(order.getOutTradeNo(), "CLOSED"); } } } private Map<String, String> queryWechatOrder(String outTradeNo) { SortedMap<String, String> params = new TreeMap<>(); params.put("appid", config.getAppId()); params.put("mch_id", config.getMchId()); params.put("out_trade_no", outTradeNo); params.put("nonce_str", generateNonceStr()); String sign = WechatPaySignUtil.sign(params, config.getApiKey(), "HMAC-SHA256"); params.put("sign", sign); String xml = XmlUtil.buildXml(params); String respXml = HttpUtil.postXml(QUERY_ORDER_URL, xml); Map<String, String> resp = XmlUtil.parseXml(respXml); // 验签逻辑与下单类似,省略 return resp; } }逻辑说明:这个定时任务作为回调的补偿机制,解决的是「微信支付成功但回调没更新订单」的场景。每 60 秒扫描一次数据库中超过 5 分钟还处于待支付状态的订单,然后主动问微信这笔订单的真实状态。这里 5 分钟不是拍脑袋定的,而是给用户扫码或者拉起支付留出充足时间,如果一上来就把 1 分钟内的订单查一遍,大概率是用户还在输入密码的路上。
参数说明:查询接口的返回字段 trade_state 有多个取值:SUCCESS、REFUND、NOTPAY、CLOSED、REVOKED、USERPAYING、PAYERROR。其中 USERPAYING 表示用户正在支付中,这种状态不要立刻关单,等下一次轮询再查。NOTPAY 可以做关单,但要注意如果用户正在用微信扫码支付,关单动作会直接中止交易。
4.3 查询接口的边界条件:超时、异常与订单不存在
查单接口还有几个边界条件要处理:微信返回 OUT_TRADE_NO_NOT_EXIST 表示订单号不存在,可能是商户订单号确实没下单成功,也可能是查单参数拼错——比如 out_trade_no 带了空格或者不可见字符。另一个情况是查单返回 SYSTEMERROR,这种建议隔几秒重试,不要直接把订单标成失败。
实际操作中,我一般会把查单逻辑做成一个独立方法,供定时任务和用户手动查询按钮共用。用户端如果支付后页面卡住,前端可以做一次主动查单请求,这比让用户干等回调体验好很多。
5. 微信支付 V2 避坑指南:沙箱测试、金额单位、签名大小写与回调幂等的五个真实案例
5.1 沙箱环境的密钥与真实环境不一致
现象:用微信支付 V2 沙箱环境测试,统一下单一直报「签名错误」,但切换到真实环境同样的代码却能通。
原因:微信支付 V2 的沙箱环境并不是直接把真实环境的 API 密钥拿来用,而是需要先调用沙箱密钥获取接口,用真实密钥去换沙箱专用 key。很多人不知道这一点,直接用真实密钥去沙箱环境验签,自然全部失败。沙箱环境的 AppID 和商户号还是真实的那套,但 API 密钥必须换成沙箱 key。
解决:代码里做环境隔离,沙箱配置和真实配置分开成两套,沙箱用 sandboxkey,真实环境用 apiclient key。如果项目已经接了配置中心,把这个切换做成动态开关,测试联调用沙箱,生产切回真实。
5.2 金额单位不一致导致的金额校验失败
现象:本地订单金额是 10.5 元,微信回调里 total_fee 是 1050,数据库里存的是 10.5,做金额比对时直接不相等,订单永远无法更新为已支付。
原因:微信支付 V2 所有金额字段单位都是分,包括下单、回调、查询、退款。但很多团队在数据库里用 BigDecimal 存元,后端编码时忘记转换,或者在比对时一个转了一个没转,两个值错位。
解决:统一在数据库里用 int 存分,实体类上和微信交互的字段全部用 Integer 类型。如果历史表已经用 BigDecimal 存元,比对时手动乘以 100,代码里写明确注释——防止三个月后你自己都忘了这行乘 100 是干什么的。这是支付代码里最玄学的一个点,但排查起来就几行日志就能定位。
5.3 签名结果为小写字母
现象:同样的参数和密钥,本地工具类算出的签名报文里带着 a、b、c 等小写字母,微信返回「签名错误」。
原因:微信支付 V2 的签名规则中,MD5 和 HMAC-SHA256 计算结果都需要转成大写后再参与传输和校验,但部分方式生成的签名默认是小写 Hex 格式。很多脚手架代码里直接返回 Hex 编码结果,没有调 toUpperCase()。
解决:签名工具类里统一处理,生成签名的最后一行强制 sign.toUpperCase()。验签时微信返回的 sign 是大写的,本地的签名串也要转大写后再比较。日志打印时保持原始大小写,方便排查时对齐看。
5.4 回调重复通知导致重复发货
现象:业务上用户支付成功后发送积分、会员充值等动作执行了两遍,排查日志发现微信回调同一笔订单通知了三次,每次都走了完整业务。
原因:回调处理里没有幂等判断,直接把「更新订单状态」和「发积分」放在同一个事务里。第二次通知来了之后,订单状态虽然已经变成 PAID,但发积分只检查了订单是否存在,没有检查积分是否已发放。
解决:所有回调业务处理前先查订单状态,PAID 直接 return SUCCESS 不再重复执行。如果有多步业务动作(状态更新、发积分、发短信),建议引入消息队列或单独的事务表做幂等控制。这里有一个细节——不要在回调里做耗时过长的操作,微信支付等待响应有时间限制,超时后会立刻重发通知,如果回调 10 秒没响应,相当于自己触发了一次重复通知。
5.5 回调验签报「验证失败」但日志里签名明明是对的
现象:回调验签一直失败,把参数打出来人工拼一遍签名跟微信返回的 sign 完全一致,代码里却校验不过。
原因:XML 解析出来的参数值可能带了 \n 或空格,尤其是商品描述字段 body 里有换行时,签名串拼接会把换行一起拼进去,算出的签名和微信原始签名完全对不上。还有参数顺序问题——TreeMap 排序后拼出的原文顺序,与微信服务器签名时使用的字段顺序可能不一致,比如 sign_type 这个字段某些接口不参与签名、某些接口参与,漏掉或多余一个字段,签名就对不上。
解决:验签前对参数值做 trim,body 等中文描述字段尤其要注意。写一个签名 debug 方法,把最终拼接的待签名串完整打出来,放到一个在线的 HMAC-SHA256 计算器里跟微信返回的 sign 做比对,哪一步不一样一目了然。日志打印时不要用 JSON 序列化,直接逐行打印 key=value 的拼接串,方便复制出来对照。
5.6 JSAPI 拉起支付报「支付验证签名失败」
现象:统一下单成功,prepay_id 拿到了,二次签名也生成了,但前端 wx.choosePayment 返回「支付验证签名失败」。
原因:二次签名的参数顺序和字段名与统一下单不同。统一下单签名时是 appid、mch_id,二次签名时是 appId、package(prepay_id=xxx)。部分老代码里二次签名用的字段名写错,把 package 写成了 packages,或者 timeStamp 用毫秒传了 13 位数字。
解决:二次签名严格按微信文档的五个字段来:appId、timeStamp、nonceStr、package、signType。timeStamp 用 System.currentTimeMillis() / 1000 取秒。package 的值必须带上前缀 prepay_id=,拼完后整体参与签名,不要单独拿 prepay_id 的原始值去签。
6. 从 V2 平稳迁向 V3 的三个务实建议与一个调试思路
先说迁移评估。V2 转 V3 不是改个 URL 和加密方式那么简单,两类核心差异决定了工作量:第一,V3 的请求签名从 MD5/HMAC-SHA256 换成了 RSA-SHA256,签名工具类要重写,p12 证书换成 pub_key.pem、key.pem、平台证书三个文件,并且平台证书有轮换机制,本地代码要做证书自动更新或定期手动替换。第二,V3 的请求和响应都是 JSON 格式,回调通知的数据是 AES-256-GCM 加密,解密逻辑比 V2 的 XML 解析多了两个步骤。如果项目只是给内部商户用的收银台,V2 短期内继续跑没有大问题,但如果是新开发的支付平台,从第一天起就该接 V3。
给正在迁移的人三个建议。第一个建议是先把回调验签部分抽出来独立成一个服务,V2 和 V3 的验签入口分开,业务逻辑共用。这样迁移过程中可以只切一部分流量到 V3 验签入口,观察一段时间再全量切换。第二个建议是共用订单表和状态机,不要为了适配 V3 另建一套订单表。V2 的 out_trade_no 规则、回调幂等逻辑、对账逻辑在 V3 下完全复用,重写一遍反而容易引入金额单位、状态不统一这种低级错误。第三个建议是退款接口优先迁移,V2 的退款需要加载证书发起请求,V3 改成了 API 私钥签名,代码结构会更干净。
最后说一个调试思路——所有支付相关的联调问题,第一件事不是看代码,而是开一个抓包工具的代理,把请求和响应的原始报文完整录一遍。微信支付 V2 是 XML 明文传输,V3 是 JSON 明文传输,回调通知里带的是密文但请求头能看到密钥 ID。录下来的报文里有微信服务器返回的完整签名字段、时间戳、随机串,把这些原样拿去跟自己的工具类比对。这个习惯帮我解决了大量排查超过半小时的签名类问题。每次遇到支付接口报错,先录报文,再看日志,最后再看代码。从那以后我每次接支付相关的需求都强制走一遍这个流程。
希望这个 V2 代码包和这套调试思路能帮你少走点弯路。
提示:V2 的 API 密钥一旦泄露,攻击者可以伪造支付回调,泄露后请立即到商户平台重置,重置后所有需要签名的接口都要用新 key。
本文还有配套的精品资源,点击获取