小程序支付接入全流程:下单、签名与回调

admin 微信QQ技巧 2026-10-25 2 0

支付联调最耗时间的往往不是下单,而是那句「签名错误」。你照着文档拼了字符串,用私钥签了,请求发出去还是被拒。原因通常是拼接时的换行符少了、字段顺序错了,或者你签的是商户 API 证书私钥、验的却是平台侧的签名。这一篇按真实接入顺序走一遍:从准备材料到下单调起,再到回调处理,把每个环节里最容易写错的那一处单独指出来。

接入前要准备的四样东西

  • 商户号:支付的主体,所有金额结算到这里。它和你小程序的 appid 是两个独立实体。
  • appid 与商户号的绑定关系:在小程序侧或商户平台侧完成关联,没绑定会在下单时直接报错,且错误信息不太直观。
  • APIv3 密钥:一个 32 位的自定义字符串,用于回调数据的解密和部分签名。它只在设置时可见,之后无法查看,只能在需要时重设,重设后旧密钥立刻失效。
  • 商户 API 证书与私钥:用于请求签名,私钥只留在你自己的服务器上。同时要拿到平台侧的公钥或平台证书,用于验证回调与应答的真实性。

四样东西的自检顺序

建议顺序是:先确认绑定关系,再用一个最小请求验证签名能通过,最后再写业务。跳过前两步直接写业务,你会在几百行代码里找一个字符串拼接错误。

下单请求体的关键字段

下单是一个 JSON POST,签名放在请求头里。

POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
Authorization: WECHATPAY2-SHA256-RSA2048 mchid="...",nonce_str="...",
               signature="...",timestamp="...",serial_no="..."

{
  "appid": "小程序appid",
  "mchid": "商户号",
  "description": "商品描述",
  "out_trade_no": "商户订单号",
  "notify_url": "https://your.domain/pay/notify",
  "amount": { "total": 1, "currency": "CNY" },
  "payer": { "openid": "用户的openid" }
}

几个必须说清楚的点:amount.total 的单位是分,一块钱写 100,写成 1 就是收了一分钱,这类差错在上线初期很常见;payer.openid 必须来自同一个小程序,用公众号的 openid 会报错;out_trade_no 是你的主键,长度和字符集都有限制,不要用中文、不要用可预测的自增;notify_url 必须是公网可访问的 https 地址,带 query 参数有时会被截断,尽量保持干净。

请求头的签名串不是 JSON 本身,而是方法、路径、时间戳、随机串、请求体这五行用换行符连接的结果,最后一行之后不要再加换行。路径要带 query,但不带域名。这四行换行符是签名错误的第一大来源,很多人手写字符串时少了最后一个换行或者多了一个空格。

从 prepay_id 到前端五个参数

下单成功后拿到的不是「可以付了」,而是一个 prepay_id,有效期通常很短。你要用它再签一次名,把这五个值给前端:

  • timeStamp:秒级时间戳,字符串形式。
  • nonceStr:随机串,长度控制在有限范围内。
  • package:固定写成 prepay_id=xxx 的形式。
  • signType:小程序侧只能填 RSA。
  • paySign:用商户私钥对四行内容做 SHA256 签名的 Base64 结果。
message = appId + "\n" + timeStamp + "\n" + nonceStr + "\n" + package + "\n"
paySign = base64(sha256withrsa_sign(message, 商户私钥))

注意这里参与签名的第一行是小程序 appid,不是商户号;package 里包含 prepay_id 前缀。前端调起就一行 wx.requestPayment,把五个参数原样传进去,不要在客户端做任何二次加工或重新签名——前端既没有私钥也不该有。

前端调起支付的签名与 v3 的差别

老版本用的是 MD5 或 HMAC 类的对称签名,密钥就是 API 密钥;现在这条链路全部换成非对称签名:请求签名用商户私钥、微信用你的公钥验;应答和回调的签名则是微信用平台私钥签、你用平台公钥验。方向是相反的,这是最容易混的一处。

所以你在代码里需要两组密钥材料:自己的私钥(签出去)和平台公钥或平台证书(验回来)。不要试图用一把密钥搞定两边,也不要把商户私钥打进任何前端产物或镜像层的公开配置里。serial_no 要与你实际使用的证书序列号对应,证书轮换之后忘了同步序列号,签名会全部失败。

支付回调的验签与解密

回调是 POST JSON,但业务数据在 resource 字段里,是加密的。处理顺序是:先验签,再解密,最后才改业务状态。

  1. 取出请求头里的签名、时间戳、随机串、证书序列号,构造验签串并校验。
  2. 用 AEAD_AES_256_GCM 解密 resource:密钥是 APIv3 密钥,需要 nonce、associated_data、ciphertext 三样。
  3. 解密后得到 JSON,校验里面的商户号、appid 与你自己的一致,再校验金额与订单号。
  4. 确认 trade_state 确实是成功,才更新订单状态。

验签失败就直接拒绝,不要「先处理再看日志」。时间戳过旧的请求要拒绝,防止重放。金额校验这一步尤其不能省:回调里的金额和你本地订单金额不一致时必须挂起人工核查,这是防篡改的最后一道闸。

回调必须做的三件事

这三件事少一件就会出问题,而且往往在流量大的时候才暴露。

  • 验签:证明这条回调确实来自微信,而不是别人构造的请求。
  • 幂等:同一个订单的成功回调可能来多次。用订单号做唯一约束,先原子地把状态从「待支付」改为「已支付」,改成功才发权益,改失败说明已处理过,直接跳过。
  • 应答:处理完必须返回一个带成功标识的 HTTP 响应。返回不成功或者超时,微信会按策略重试,你的接口会被反复打。

幂等键放在哪里

实践上建议:回调里只做「落库 + 标记」,发货、加积分、发消息这些动作交给异步任务消费,用同一个订单号做幂等键。回调接口要能极快返回,并且对异常返回失败而不是空响应,让对方重试——你要的是可重试的失败,不是静默的丢失。

out_trade_no 与常见问题

订单号生成建议用「业务前缀 + 时间戳 + 随机后缀」的组合,保证全局唯一且不可枚举。不要用自增 ID,也不要让两个业务共用同一套订单号空间,否则对账时你会花大量时间在归属判断上。

环节参与方要做什么
创建订单你的服务端生成订单号,落库为待支付
统一下单你的服务端 → 微信签请求头,拿到 prepay_id
二次签名你的服务端生成五个调起参数下发前端
调起支付小程序前端wx.requestPayment 原样传参
异步通知微信 → 你的服务端验签、解密、幂等更新、返回成功
主动兜底你的服务端定时查询未支付订单,补状态

最后三个高频问题:签名错误九成是签名串换行或字段顺序,先打印出待签串逐字节比对;回调收不到先看 notify_url 是否公网可达、是否被鉴权中间件拦掉;金额对不上先看单位是不是忘了乘 100。把这三条做成上线前的检查清单,能省掉大半联调时间。