简介:微信小程序支付后端示例基于Spring Boot与Java开发,面向需要接入微信支付的服务器端开发者;项目实现了小程序支付的核心接口,包括统一下单、支付结果查询,并通过集成微信支付SDK完成与微信服务器的通信。资源压缩包共32个文件,其中22个Java源文件构成主要逻辑,另有Maven配置文件、yml环境配置、HTTPS密钥库文件以及项目说明文档和示意图片,整个包仅124KB,轻量紧凑,适合快速导入查看。示例重点展示了Spring Boot接口设计思路、支付SDK的调用方式,还单独说明了server.keystore的HTTPS配置流程,帮助开发者满足微信支付对传输安全的强制要求。对于刚接触微信支付或想参考后端分层写法的初中级工程师,这份demo提供了从工程结构到实际代码的完整参照,包括下单接口如何暴露、支付回调如何处理等细节。目前已有3151人浏览学习,可作为快速上手的实用模板。
1. 微信小程序支付 demo 后端,最被低估的是它的链路长度
微信小程序支付 demo 后端,很多人第一反应是“不就是调个接口吗”。实际上手才发现,一个最小可跑的 springboot 支付工程,横跨了小程序登录态、统一下单、二次签名、回调验签、订单幂等更新五段链路,任何一段理解偏了,前端 wx.requestPayment 就是弹不起来,或者钱扣了订单还是待支付。这个 demo 的价值不在“能支付”,而在让你把这三件事变成肌肉记忆:后端负责拿着商户私钥签名下单、把 5 个参数交给前端调起支付、再靠异步回调确认最终结果。适合谁?刚接触微信支付的后端同学、做毕设需要完整支付闭环的学生,以及想把支付从“会调用”升级到“能排错”的初级工程师。本文只聊 JSAPI 支付,不碰商家转账那套接口,字段别混着抄。
2. 搭建 springboot 支付工程:目录结构、依赖与商户三件套配置
2.1 为什么统一支付链路要放在后端,而不是小程序里直接下单
微信支付的约束很简单:商户私钥不能出现在小程序前端代码里,谁都能解包的小程序一旦存了私钥,等于把资金操作权交给了别人。所以下单请求必须由后端发起,后端负责签名和验签,小程序端只负责把用户带到微信的收银台。
选择一个 springboot 工程来做这层后端,理由也很实际。一是工作室或公司的后端技术栈大多是 Java,支付模块要接入现有的用户体系、订单表、对账任务,springboot 能无缝嵌进去;二是官方提供了 Java 版 SDK,签名、平台证书自动更新这些容易出错的部分有人替你兜底;三是这个 demo 采用前后端分离的写法,springboot 只输出 JSON 接口,前面接原生小程序、uni-app 或者 Vue 外壳都一样,接口不受前端框架绑架。
一个常见的错误认知是“下单之后把 prepay_id 直接返回给前端就行”。prepay_id 只是微信侧生成的预支付单标识,小程序端要拉起收银台,还需要把它包装成 timeStamp、nonceStr、package、signType、paySign 这五个参数,paySign 要用商户私钥对特定格式的字符串做签名。这套“统一下单 + 二次签名”的配合,就是后端存在的核心意义。
2.2 工程目录与依赖:一个 controller、两个 service、一张订单表
支付 demo 不需要复杂的微服务结构,我一般这样组织工程:
wx-pay-demo ├── pom.xml ├── src/main/java/com/example/wxpay │ ├── WxPayApplication.java │ ├── config │ │ ├── WxPayProperties.java // 配置绑定 │ │ └── WxPayConfig.java // SDK Config 初始化 │ ├── controller │ │ └── PayController.java // 下单入口 + 回调入口 │ ├── service │ │ ├── WxOrderService.java // 统一下单、查单 │ │ └── WxNotifyService.java // 回调验签、解密、改单 │ ├── entity │ │ └── Order.java // order_no, amount, status │ └── mapper │ └── OrderMapper.java └── src/main/resources ├── application.yml └── cert/apiclient_key.pem // 商户 API 私钥,别提交到 gitpom.xml 里最要紧的是 spring-boot-starter-web 和官方支付 SDK。注意 SDK 版本不要抄老博客里的旧坐标,以 maven 仓库当前 release 为准,示例里用版本占位符:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.github.wechatpay-apiv3</groupId> <artifactId>wechatpay-java</artifactId> <version>${wechatpay.version}</version> </dependency> </dependencies>这里刻意把 springboot 放在 2.7 而不是 3.x,后面避坑章节会解释为什么。依赖引入后先跑一次空工程,确认 SDK 的 jar 能下载下来,再往下走。
2.3 商户三件套:APIv3 密钥、商户私钥、证书序列号
微信支付 V3 体系里,有三把“钥匙”各管一段,很多 demo 跑不通都是在这里混淆。
第一把是 APIv3 密钥,32 字节随机串,在商户平台里自己设置的,它用于解密微信回调过来的报文,不参与请求签名。第二把是商户 API 证书私钥,最常用的 apiclient_key.pem 文件,所有后端发起下单、查单请求的签名都用它,是一把“主动签名”的钥匙。第三把是平台证书,用来验证微信响应和回调的签名,官方 SDK 的自动更新证书模式可以不手动下载,但原理必须清楚。
配置集中放在 application.yml:
wx: pay: app-id: wx1234567890abcdef # 小程序的 AppID mch-id: 1234567890 # 商户号 api-v3-key: 32位随机字符串 # 商户平台设置的 APIv3 密钥 private-key-path: classpath:/cert/apiclient_key.pem private-key-serial-no: 商户API证书序列号 notify-url: https://api.example.com/wx/pay/notifyprivate-key-serial-no 经常被误解,它不是文件的名称,而是商户平台“API 安全”页里展示的一串十六进制证书序列号。用错了序列号,下单请求会直接返回 401 签名错误。接着写配置绑定类:
@Component @ConfigurationProperties(prefix = "wx.pay") public class WxPayProperties { private String appId; private String mchId; private String apiV3Key; private String privateKeyPath; private String privateKeySerialNo; private String notifyUrl; // getter / setter 省略 }@Configuration public class WxPayConfig { @Bean public RSAAutoCertificateConfig wxPayConfig(WxPayProperties prop) throws Exception { return new RSAAutoCertificateConfig.Builder() .merchantId(prop.getMchId()) .privateKeyFromPath(prop.getPrivateKeyPath()) .merchantSerialNumber(prop.getPrivateKeySerialNo()) .apiV3Key(prop.getApiV3Key()) .build(); } }RSAAutoCertificateConfig 这个类会在首次调用时自动下载微信支付平台证书,并在证书快过期时自动更新,省去了手动换证书的运维操作。如果你的网络环境访问微信平台域名不通,这一步会抛异常,可以先在本地用浏览器确认能访问微信支付相关域名。
2.4 配置完怎么自检:先签名后下单,不急着写业务
配置完成后不要直接冲进业务代码,花两分钟做验证。单元测试里拿配置类签一个明文再验,能通说明私钥、序列号没问题:
@SpringBootTest class WxPayConfigTest { @Test void testSignAndVerify() throws Exception { RSAAutoCertificateConfig config = ...; // 从容器注入 String message = "hello wxpay"; String sign = config.createSigner().sign(message); boolean ok = config.createVerifier().verify(message, sign); System.out.println("sign verify result: " + ok); } }签名能通过,接下来用 Postman 之类工具直接调一次下单接口,观察 HTTP 状态码。返回 200 说明配置链路全通;返回 401 排查序列号或私钥内容;返回 400 排查参数格式。把这一步作为惯例保留,后面每换一个环境(本地、测试服务器、生产)都先跑签名自检,能省掉大量玄学排错时间。
3. 小程序 wx.requestPayment 怎么拉起:统一下单、二次签名与 5 个参数
3.1 下单前先解决 openid:登录接口和获取手机号不是一回事
小程序端 wx.requestPayment 必须携带支付者 openid,这个 openid 来自微信登录流程。很多新手把“获取手机号”当成“获取用户身份”,其实两个接口返回的东西完全不同。手机号是用户主动授权才能拿到的敏感信息,而 openid 是用户在当前小程序下的唯一标识,通过 wx.login 拿到的 code 换 session 接口获得。
后端要拿到 openid,需要在小程序登录时把 code 传过来,后端用 code 调微信的登录接口:
@PostMapping("/wx/login") public Map<String, Object> login(@RequestBody Map<String, String> body) { String code = body.get("code"); String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; // 调用 RestTemplate,解析返回的 openid 和 session_key return result; }注意 secret 是小程序密钥,和商户私钥一样属于后端机密,绝不能出现在小程序代码里。这个接口只解决“知道你是谁”,不涉及支付签名,做 demo 时单独拎出来跑通,后面下单直接复用 openid 即可。
3.2 统一下单:让微信侧返回 prepay_id
openid 就绪后,进入核心下单流程。微信支付 V3 的 JSAPI 下单接口路径是 /v3/pay/transactions/jsapi,官方 Java SDK 把请求签名和响应验签都封装好了,业务代码关注参数即可:
@Service public class WxOrderService { private final RSAAutoCertificateConfig wxPayConfig; private final WxPayProperties wxPayProperties; public PrepayWithRequestPaymentResponse createJsapiPay( String openid, String outTradeNo, long totalFee, String description) throws Exception { JSAPIService service = new JSAPIService.Builder().config(wxPayConfig).build(); PrepayRequest request = new PrepayRequest(); request.setAppid(wxPayProperties.getAppId()); request.setMchid(wxPayProperties.getMchId()); request.setDescription(description); request.setOutTradeNo(outTradeNo); request.setNotifyUrl(wxPayProperties.getNotifyUrl()); Amount amount = new Amount(); amount.setTotal(totalFee); request.setAmount(amount); Payer payer = new Payer(); payer.setOpenid(openid); request.setPayer(payer); return service.prepayWithRequestPayment(request); } }代码里的五个字段逐一说明。description 是商品描述,长度限制 127 个字符,建议格式“商品名-简单规格”,别塞完整订单 JSON。outTradeNo 是商户系统内部的订单号,6 到 32 个字符,同一个商户号下不能重复,重复会让下单接口直接报错,所以“先生成订单号并落库、再调下单”是比较稳妥的写法。totalFee 的单位是分,不是元,传 100 表示一块钱,用 Long 类型避免浮点误差。notifyUrl 是微信异步通知的地址,必须公网可访问,且要能处理 POST 请求。payer 里的 openid 必须与当前小程序的 appid 匹配,用另一个小程序的 openid 来下单,微信直接拒绝。
3.3 二次签名与 5 个参数:新手翻车最多的地方
prepayWithRequestPayment 这个方法名字直白,它帮你完成了“统一下单拿到 prepay_id,再按 JSAPI 调起支付的规则做二次签名”,返回的响应体已经包含了前端需要的全部字段。看这个响应的结构就理解了“为什么不能直接把 prepay_id 抛给前端”:
{ "prepay_id": "wx251234567890abcdef", "timeStamp": "1712345678", "nonceStr": "d4e5f6a7b8c9", "package": "prepay_id=wx251234567890abcdef", "signType": "RSA", "paySign": "Base64编码的签名串" }timeStamp 是秒级时间戳的字符串形式,前端要求 String,不是 Long,传成数字类型会直接导致调起失败。nonceStr 是随机字符串,每次下单都不同。package 必须带 prepay_id= 前缀,只传裸的 prepay_id 也是常见翻车点。paySign 的签名串格式是 appId、timeStamp、nonceStr、package 四个值用换行分隔,然后用商户私钥做 SHA256withRSA 签名。
如果把 SDK 换成手写实现,二次签名的核心逻辑是这段:
private String buildPaySign(String appId, String timeStamp, String nonceStr, String packageStr, PrivateKey privateKey) throws Exception { String message = appId + "\n" + timeStamp + "\n" + nonceStr + "\n" + packageStr + "\n"; Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(privateKey); sign.update(message.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(sign.sign()); }注意这里的 message 不作为 HTTP 请求体发送,它只是本地拼出来的待签名串。初学者容易把它和“下单请求的 HTTP 签名”搞混。下单请求签名用的是 HTTP 方法、请求路径、时间戳、随机串、请求体摘要拼出来的 Authorization 头;二次签名用的是 appId 等四个支付参数。两者都走 SHA256withRSA,但内容不同,签名结果不能互相替换。
有同学会问:官方 SDK 都封装好了,为什么还要自己写一遍签名。答案是排错。哪天 paySign 校验不过,你能快速判断是拼接格式错了还是私钥加载错了,不至于对着黑匣子干瞪眼。
3.4 前端调起支付:只透传后端返回的 5 个参数
后端返回上面的 JSON 后,小程序端调用 wx.requestPayment 只需要原样透传:
const res = await request.post('/wx/pay/create', { openid: this.openid, orderNo: orderNo, totalFee: 1 }); wx.requestPayment({ timeStamp: res.data.timeStamp, nonceStr: res.data.nonceStr, package: res.data.package, signType: res.data.signType, paySign: res.data.paySign, success: () => { /* 提示支付成功,但不要在这里改订单状态 */ }, fail: (err) => { /* 用户取消或支付失败 */ } });前端绝对不要在 success 回调里直接调后端“标记订单已支付”,因为微信官方明确提示,requestPayment 的成功状态只代表调起收银台成功,不能作为支付结果。支付的最终确认必须依赖微信异步通知到后端。这一点在下一章展开,也是很多人做 demo 时“支付成功但订单没变”的根源。
4. 支付回调:验签、解密与订单状态更新的标准写法
4.1 为什么支付结果必须以后端回调为准
小程序端的 wx.requestPayment 成功回调在两种常见场景下会失真:用户支付完成后停留在收银台页面手动关闭,前端收不到 success;或者用户支付成功但手机断网,回调根本没到。唯一可靠的事实来源是微信服务器在支付成功后,用异步 POST 通知后端 notify_url。
这个接口的设计原则是:能快速响应、能验明正身、能防重复消费。微信要求回调接收方在 5 秒内给响应,否则它会按秒级递增的策略重试多次。这就是为什么回调 Controller 里不能串行执行耗时操作,比如同步发短信、调其他业务接口,这些动作应该放到支付状态落库之后异步处理。
4.2 回调接口的位置与路由设计
回调 Controller 需要独立于普通业务接口,原因有两个。其一,回调地址是下单时固定传给微信的,后续更换路径要重新下单才能生效,所以路径一旦确定尽量稳定;其二,回调请求头里带有微信平台的签名信息,拦截器里关于用户登录鉴权、token 校验的逻辑对微信服务器根本不适用,单独建一个 Controller 可以避开这些通用拦截器。
@RestController @RequestMapping("/wx/pay") public class PayNotifyController { @PostMapping("/notify") public ResponseEntity<String> notify(HttpServletRequest request) throws Exception { return wxNotifyService.handleNotify(request); } }4.3 验签与密文解密:回调处理的核心代码
回调报文包含两层保护。第一层是微信平台对报文整体做了 HTTP 签名,放在请求头的 Wechatpay-Signature 字段里,需要用平台证书的公钥去验证,防止第三方伪造通知。第二层是报文的 resource 字段内容被 AES-256-GCM 加密,需要 APIv3 密钥解密,防止传输链路泄露明文。
完整的处理逻辑如下:
@Service public class WxNotifyService { private final RSAAutoCertificateConfig wxPayConfig; private final OrderMapper orderMapper; public ResponseEntity<String> handleNotify(HttpServletRequest request) throws Exception { // 读取请求体 String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); // 构造验签参数 RequestParam param = new RequestParam(); param.setWechatpaySerial(request.getHeader("Wechatpay-Serial")); param.setWechatpaySignature(request.getHeader("Wechatpay-Signature")); param.setWechatpayTimestamp(request.getHeader("Wechatpay-Timestamp")); param.setWechatpayNonce(request.getHeader("Wechatpay-Nonce")); param.setBody(body); // 验签:失败直接返回 400,不再往下处理 if (!wxPayConfig.createVerifier().verify(param.getWechatpaySerial(), param.getWechatpaySignature(), param.getWechatpayTimestamp(), param.getWechatpayNonce(), param.getBody())) { return ResponseEntity.badRequest().build(); } // 解密报文 AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key()); String plaintext = aesUtil.decryptBody(param.getBody()); JSONObject resource = JSON.parseObject(plaintext); // 从 resource 中取出关键字段 String outTradeNo = resource.getString("out_trade_no"); String transactionId = resource.getString("transaction_id"); String tradeState = resource.getString("trade_state"); int totalAmount = resource.getJSONObject("amount").getIntValue("total"); // 只有 SUCCESS 才更新订单,其他状态如 REFUND 另行处理 if ("SUCCESS".equals(tradeState)) { updateOrderIfConsistent(outTradeNo, transactionId, totalAmount); } // 返回 200 空 body,微信收到即停止重试 return ResponseEntity.ok().build(); } private void updateOrderIfConsistent(String outTradeNo, String transactionId, int totalAmount) { Order order = orderMapper.selectByOrderNo(outTradeNo); if (order == null) { // 订单不存在,说明是伪造回调或非法参数,记录告警 return; } // 金额核对:回调金额与本地订单金额必须一致 if (order.getAmount().longValue() != totalAmount) { // 金额不一致,资金风险,记录并触发人工介入 return; } // 状态机:只有待支付状态能流转为已支付 if ("PENDING".equals(order.getStatus())) { order.setStatus("SUCCESS"); order.setTransactionId(transactionId); orderMapper.updateByOrderNo(order); } } }代码里的解密过程是 AES-256-GCM,微信支付规定的细节是:nonce 固定 12 字节,额外认证数据 aad 为空字符串,解密后的明文是 JSON 字符串。AesUtil 类把这些细节封装好了,自己实现时要特别注意 nonce 不能直接当 iv 用,长度和格式必须按 GCM 模式来。
验签这一步绝不能省略。有同学在本地调试时嫌麻烦,直接跳过验签,只解密看字段,这在测试环境也许能跑通,一旦接口暴露在公网,任何人都可以往 notify 地址 POST 伪造报文,配合一个反向代理就能模拟微信的请求格式。金额核对同样关键,只校验 out_trade_no 存在但不管金额,攻击者拿一个真实支付成功的 out_trade_no 伪造小额通知,订单就被错误标记为支付成功。
4.4 幂等更新:防止同一笔支付被重复处理
微信回调会重试,同一个 out_trade_no 可能收到多次通知。重复处理最典型的故障是:订单状态已经被改成 SUCCESS,第二次回调进来又执行一次更新,如果更新逻辑里隐藏着赠品发放、积分累加之类操作,用户就白嫖多次。
代码里加了两道保险。第一道是状态机约束,只允许 PENDING 流转到 SUCCESS,已经 SUCCESS 的单子直接跳过;第二道是数据库层,订单表的 out_trade_no 建唯一索引,更新语句带上 status = 'PENDING' 作为条件,返回更新行数为 0 说明已被处理:
UPDATE tb_order SET status = 'SUCCESS', transaction_id = #{transactionId}, pay_time = NOW() WHERE out_trade_no = #{outTradeNo} AND status = 'PENDING'如果系统里有后续业务,比如支付成功要发消息通知仓储发货,不要直接在回调里同步处理。常见的做法是把 out_trade_no 写入本地消息表,状态标记为待发送,由独立任务消费并允许重试,这一步能把微信 5 秒的响应时限和业务处理解耦开。demo 阶段先不做消息队列,但要把这个设计思想留在代码注释里。
5. 支付 demo 避坑指南:5 个高频翻车点
5.1 把 APIv3 密钥和商户私钥混为一谈
现象:下单返回 401,签名错误,代码反复检查签名逻辑也没问题。
原因:某开发者在商户平台把 APIv3 密钥抄下来,直接当成私钥内容填进了配置。APIv3 密钥是 32 字节的对称密钥,负责解密回调;商户私钥是 PEM 格式的非对称密钥,负责请求签名和二次签名。两边字节格式完全不同,填错位置签名校验当然过不了。
解决:把两个值分开存放。APIv3 密钥是一串随机字符,没有 pem 头;私钥文件以 “-----BEGIN PRIVATE KEY-----” 开头。printf 一下配置加载的内容,用头尾特征一眼就能分辨。
5.2 金额单位错乱:总差 100 倍
现象:下单金额明明传了 1 元,支付时却显示 100 元;或者反过来,回调金额对不上。
原因:微信支付 V3 的金额单位是分,前端展示的 1 元对应整数 100。如果后端代码里直接拿前端传来的元做入库,再原样传给微信,就会差 100 倍。
解决:后端统一定义金额为 Long 型分,入库用分,和小程序交互也只用分。只在展示层做元分转换,转换时用 BigDecimal 的 setScale(2, RoundingMode.HALF_UP) 而不是 Double 运算,防止精度丢失。
5.3 回调收不到:notifyUrl 必须公网可达且路径稳定
现象:支付成功,微信侧显示通知发送成功,后端却始终没收到请求。
原因:notifyUrl 填了 localhost 或内网地址,微信服务器无法访问;或者回调接口前面有网关、防火墙把 POST 请求拦掉了,某些云厂商的默认安全组只放行 80 和 443,没有放行自定义端口。
解决:测试阶段把后端部署到一台有公网地址的测试服务器,用域名或 IP 直连,路径不带自定义端口最省心。收到回调后在 Controller 第一行打 access log,记录请求头和 body,配合日志快速定位是被拦截还是验签失败。
5.4 springboot 版本太高,老教程代码直接编译报错
现象:抄了网上的 demo,在 springboot 3.x 工程里 javax.servlet.* 全部找不到,或者 Redis、切面相关的老写法全都失效。
原因:springboot 3.0 起基础包从 Java EE 迁移到 Jakarta EE,javax 包名整体变成 jakarta。老教程大多基于 springboot 2.x,import javax.servlet.http.HttpServletRequest 在 3.x 下自然编译不过。
解决:做支付 demo 优先用 springboot 2.7 配 Java 8,这是最稳的组合,网上资料最多,排错也快。如果项目必须用 springboot 3,把依赖里的 javax 替换成 jakarta,同时确认支付 SDK 版本兼容 Jakarta 规范。别在支付链路上同时挑战新框架和旧教程,两个变量叠加会让排查难度翻倍。
5.5 回调响应格式用错:V2 的 XML 写法套到 V3 上
现象:回调逻辑明明执行成功,微信却不断重试,日志里同一笔订单刷屏。
原因:微信支付 V2 的回调要求返回 XML 格式的 <return_code> </return_code> ,V3 的规则完全不同,只需要 HTTP 200 且返回空 body 就算确认成功。有的同学看了 V2 资料,把 XML 响应透传出来,反而破坏了 V3 的协议约定。
解决:确认自己对接的版本。新版商户号基本都走 V3,回调方法返回 ResponseEntity.ok().build() 即可,不要再拼 XML。另外注意,回调处理成功和失败都不要抛异常,可以用状态码区分,实在处理不了也要先返回 200 并记录死信数据,人工补偿,比让微信无限重试更有意义。
6. 一个更稳的改法:让支付结果以数据库为准,而不是回调驱动
回调通知本质上是一条不可完全信任的异步消息。微信会重试,网络会抖动,服务器会宕机,单靠回调驱动订单状态,系统就永远有一个“钱付了但订单还挂着”的窗口。生产级做法是把数据库里的订单状态当唯一事实来源,回调负责“尽快推进”,定时任务负责“最终兜底”。
具体改动是在 service 层加一个主动查单的定时任务:
@Scheduled(fixedDelay = 60_000) public void settlePendingOrders() { // 找出超过 5 分钟仍处于 PENDING 的订单 List<Order> pendingOrders = orderMapper.selectPendingOlderThan(5); for (Order order : pendingOrders) { try { QueryOrderResult result = queryOrderByOutTradeNo(order.getOrderNo()); if ("SUCCESS".equals(result.getTradeState())) { updateOrderIfConsistent(order.getOrderNo(), result.getTransactionId(), result.getAmount()); } else if ("CLOSED".equals(result.getTradeState()) || "REVOKED".equals(result.getTradeState())) { order.setStatus("CLOSED"); orderMapper.updateByOrderNo(order); } // NOTPAY 等中间状态继续等待 } catch (Exception e) { // 记录日志,下一次循环继续,不做重试风暴 } } }查单接口和下单接口一样需要商户签名,路径是 /v3/pay/transactions/out-trade-no/{out_trade_no}。定时任务加上回调双重保障后,即使某条回调通知彻底丢失,最坏情况下 5 到 6 分钟也会被主动查单纠正过来。
付款成功后还要处理一个容易被忽略的问题:用户支付成功但前端没有任何反应,停留在订单页。用户不该依赖定时任务的补偿速度,他需要即时反馈。我的习惯是前端在 wx.requestPayment 的 success 回调里不急着提示“支付成功”,而是轮询后端订单状态接口,每秒一次、最多十次,看到后端落库成功再提示,这样体验和事实一致,也绕开了回调延迟带来的虚假等待。
另外建议保留一份 plaintext 解密的原始报文,落库时把 transaction_id、回调时间、验签结果一起存进支付日志表。万一出现对账差异,这份日志能证明“微信确实通知过,后端确实验签成功”,配合主动查单结果,基本可以还原每一笔订单的真实走向。我身边 A 同学的项目就是这样改的,后来线上真遇到一次微信回调网关抖动,定时任务在两轮扫描内自动修复了订单态,比他手动翻数据库改状态快得多。
引用我自己的交付习惯:demo 可以只管回调,面向用户的订单系统必须回调加主动查单双保险。希望这篇笔记能帮你少走几趟弯路,把微信小程序支付后端一次跑顺。
本文还有配套的精品资源,点击获取