拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

微信支付接入全流程解析:从下单回调到退款对账的实战指南

微信支付接入全流程解析:从下单回调到退款对账的实战指南

说到微信支付,我经手过的项目少说也有十几个了。从最开始的个人公众号H5支付,到后来电商系统里的JSAPI支付和小程序支付,再到退款、转账、对账这些伴生功能,几乎每个环节都踩过坑。微信支付这套接口,官方文档写得不算差,但真正下手去接的时候,你会发现坑全藏在细节里。今天就把我在实际项目里验证过的那套流程和注意事项完整梳理一遍,从账号准备到下单回调,再到退款对账,尽量做到让没接过的人能少走弯路,让已经接了一半的人能理清思路。

1. 微信支付的账本逻辑:一次支付背后到底发生了什么

很多第一次接微信支付的人,最容易犯的毛病是一上来就找接口文档。这没错,但如果你不理解微信支付背后的资金流转逻辑,后面处理订单状态、对账、退款时一定会乱。

1.1 一次支付背后参与的四个角色

微信支付体系里,每次交易至少有四个角色参与:用户、商户、微信支付平台、用户的发卡行或零钱账户。

用户在商户App或小程序里发起支付,实际扣款动作发生在用户侧。微信支付平台做的事情是“撮合”:确认用户资金足够、确认商户合法、记录这笔交易的状态,然后通过异步通知告诉商户“钱已经扣了”。注意,这里有个关键点:微信支付通知商户时,资金实际上还没有结算到商户的银行卡里。

从支付成功到资金结算给商户,中间还有一个“结算周期”和“结算账户”的概念。普通商户一般默认是T+1结算,也就是说今天用户支付成功,钱可能在第二天甚至更晚才打到你的对公账户。如果只知道“支付成功”就发货,大部分业务是没问题的,但如果涉及退款,就要小心:支付成功不等于钱已经到你的银行账户,退款是微信支付在它的账本里做“原路退回”,即使钱还没结算给你,它也能先把这笔交易撤销掉。

1.2 支付状态机与几个容易混淆的状态

我在排查线上问题时,经常看到有人把订单状态写死成“已支付”“未支付”两种。等遇到退款、关闭订单、部分退款这些场景,就彻底糊了。

微信支付的订单状态大体上有这么几类:

  • SUCCESS:支付成功。这是最核心的状态,表示用户的钱已扣,交易成立。
  • REFUND:转入退款。说明这笔订单已经发生了部分或全额退款。
  • NOTPAY:未支付。用户还没完成支付,订单可以继续支付。
  • CLOSED:已关闭。订单超时未支付,或者在未支付状态下被商户主动关闭,不能再发起支付。
  • REVOKED:已撤销。主要是付款码支付场景下,用户扫码后长时间未输入密码或取消支付,微信支付自动撤销了这笔交易。
  • PAYERROR:支付失败。通常是因为余额不足、支付超时、风控拦截等原因。

我建议在业务系统里用两个字段管理订单支付状态:一个是“支付平台状态”(以微信的最终状态为准),一个是“业务状态”(结合自己的发货、退款流程)。不要自己造状态名,直接映射微信支付这一套,后续对账会省掉很多麻烦。

1.3 支付成功到底以谁为准

这是整篇文章最核心的原则:永远以微信支付异步回调为准,不要以用户在前端看到的成功页面为准。

前端页面显示“支付成功”,只表示微信客户端收到了成功结果。但你的服务器不一定收到了通知,通知可能延迟、丢失、重复。如果前端一跳转成功页面就直接发货,就可能在通知丢失时造成“用户付了钱但你没发货”甚至“用户没付钱你发了货”的严重问题。

所以,所有订单状态更新,必须放在你自己的后端接口里,由后端通过回调通知或主动查询接口确认支付状态后再更新。这也是我在后文反复强调回调验签和幂等的原因。

2. 动手前的准备:商户号、API密钥、证书一个都不能少

微信支付不是拿个AppID就能跑的。想要调通接口,你得先有一套完整的商户资质和API凭证。很多新手卡在第一步,就是因为不清楚到底要申请哪些东西,以及这些凭证分别用在什么地方。

2.1 账号体系的三个核心凭证

微信支付涉及三个层级的账号和凭证:

凭证申请位置用途注意事项
小程序/公众号AppID微信公众平台标识你的应用,用户授权登录需要用需要完成微信认证,且与商户号绑定
商户号mch_id商户平台标识你的商户身份,所有支付交易请求都要带由微信支付审核通过后分配,需要营业执照等资质
API密钥/APIv3密钥商户平台设置签名和加解密的数据密钥一个用于APIv2签名,一个用于APIv3签名,两个不要搞混

补充一个容易忽略的点:AppID和商户号之间需要“绑定授权”。如果你用的是别人开发的小程序,或者商户号是总公司统一申请的,一定要确认AppID和mch_id已经建立绑定关系,否则下单时会直接报“商户号与AppID不匹配”。

2.2 APIv2和APIv3的选型:新项目无脑选v3

经常有人问我:文档里接口既有v2又有v3,到底用哪个?

我的答案是:新项目全部用APIv3,老项目没坏就不折腾。

这两个版本核心差异在签名和报文格式上:

维度APIv2APIv3
报文格式XMLJSON
签名算法MD5或HMAC-SHA256,靠一个API密钥RSA-SHA256,靠商户私钥和平台公钥
凭证要求API密钥,退款等敏感操作需要商户证书双向认证商户API证书、商户私钥、平台证书
回调验签用API密钥算签名比对用平台证书验签,更安全
敏感信息加密一般明文(手机号等除外)敏感字段用公钥加密,如银行卡号、姓名
推荐程度老接口,代码简单但安全性一般新接口,安全和规范更好

APIv3的签名逻辑我第一次看的时候也觉得绕,但用顺手之后会发现它其实更清晰:用商户私钥对“请求方法、URL、时间戳、随机串、请求体”拼接成的字符串签名,然后把商户号、时间戳、随机串、证书序列号、签名放进Authorization请求头里。

Authorization: WECHATPAY2-SHA256-RSA2048 mchid="****" nonce_str="***" timestamp="***" serial_no="***" signature="***"

接收回调时,再用微信支付平台证书验证平台签名,确保回调确实来自微信支付官方。这个双向验证机制,比v2那种“双方拿同一个密钥算MD5”要可靠得多。

2.3 证书和密钥的安全存放:我见过最野的操作

商户平台可以下载一个apiclient_cert.pem或apiclient_key.pem,APIv3还会用到一个平台证书用来验签。我见过有团队把这些证书文件直接提交到Git仓库里,或者放在前端静态目录里,这是绝对绝对不能做的事。

建议的存放方式:

  • 商户私钥和API密钥存到环境变量、配置中心或KMS密钥管理服务里,不要硬编码在代码里。
  • 服务器上给证书文件加白名单权限,只允许应用进程读取。
  • 定期更换API密钥,尤其在人员变动时。
  • 商户平台开启IP白名单,只允许你自己的服务器IP调用接口。

说句实在话,微信支付接口本身的安全性设计不差,大多数被“盗刷”“被退款”的事故,都是商户自己把密钥给丢了。

3. 主流程拆解:从统一下单到支付结果回调的完整链路

搞定账号和凭证之后,就可以走主流程了。这里以最常见的JSAPI支付为例——也就是公众号、H5里用户在微信内打开的支付页面,小程序支付流程与之类似。

3.1 统一下单:参数、签名、prepay_id

前端要拉起微信支付,服务端必须先替用户向微信支付发起“统一下单”请求。这一步的目的是让微信支付后台生成一个预支付订单,并返回一个prepay_id。

以APIv2为例,请求地址是:

https://api.mch.weixin.qq.com/pay/unifiedorder

请求体是XML,但核心参数就那么几个,我列一下最常用的:

参数是否必填说明
appid是公众号或小程序的AppID
mch_id是商户号
out_trade_no是商户自己的订单号,必须唯一
total_fee是订单金额,单位是分,注意必须是整数,且不能为0
body是商品描述,会展示在支付页面上
notify_url是异步通知回调地址
trade_type是JSAPI、NATIVE、APP、MWEB等
openid条件必填trade_type为JSAPI时必须传,即用户的openid
spbill_create_ip建议填用户下单的IP,有助于风控
time_expire建议填订单失效时间,比如下单后15分钟支付超时

所有参数(除sign外)按ASCII字典序排序,拼接成key=value&key=value之后,在末尾再拼上&key=你的API密钥,然后MD5或HMAC-SHA256算出来的就是sign。

下单成功后,微信会返回:

<xml> <return_code><![CDATA[SUCCESS]]></return_code> <result_code><![CDATA[SUCCESS]]></result_code> <prepay_id><![CDATA[wx201410272009395522657e690389285100]]></prepay_id> </xml>

这个prepay_id就是调起前端支付的凭证。prepay_id有效期一般是2小时,且只能用一次,所以不要在缓存里存太久,也不要试图重复使用。

3.2 拿到prepay_id之后,前端如何调起支付

服务端拿到prepay_id后,不能直接把prepay_id丢给前端就完事,还需要再生成一组“调起支付参数”。

以JSAPI为例,后端要返回给前端这几个字段:

{ "appId": "wx************", "timeStamp": "1640995200", "nonceStr": "随机字符串", "package": "prepay_id=wx201410272009395522657e690389285100", "signType": "RSA", "paySign": "用APIv3或APIv2规则生成的签名" }

这里最容易出错的点是package参数。很多人以为是直接传prepay_id,其实它要拼成prepay_id=xxx。另外timeStamp是秒级时间戳,不是毫秒,前端做Number()转换时别被坑了。

生成paySign的签名规则在不同版本里不同,APIv3下是把appId、timeStamp、nonceStr、package按特定方式拼起来,再用商户私钥签名。签名算法错了,前端会一直停在“支付失败”但后端明明下单成功,这种问题我排查过好几次,最后发现是签名串格式里多了一个换行符。

小程序端调起支付的写法是:

wx.requestPayment({ timeStamp: res.data.timeStamp, nonceStr: res.data.nonceStr, package: res.data.package, signType: 'RSA', paySign: res.data.paySign, success: function (res) { // 这里不要急着更新订单状态,等后端回调 }, fail: function (err) { console.error('支付失败', err) } })

3.3 异步回调:验签、金额校验、幂等处理

下单之后最关键的环节就是异步回调。微信支付会在用户支付成功后,以POST方式把结果发送到你在统一下单时填写的notify_url。

先把回调处理的完整步骤整理出来:

  1. 验签:确认这个通知确实来自微信支付。在APIv3下,用平台证书验证通知自带的签名;在APIv2下,用API密钥重新计算签名进行比对。
  2. 解密:APIv3的通知报文是加密的,需要结合APIv3密钥进行解密,得到明文订单数据。
  3. 校验业务参数:重点核对out_trade_no是不是你自己的订单、total_fee是否与订单金额一致、mch_id是否匹配。
  4. 幂等处理:微信支付的通知可能重复发送,服务端必须保证重复通知不会重复发货、重复加余额。
  5. 返回应答:处理成功后返回SUCCESS,否则回调会按间隔重试(15秒/15秒/30秒/3分钟/10分钟/20分钟/30分钟/30分钟/30分钟/60分钟/3小时/3小时/3小时/6小时/6小时)。

这里我特别想强调幂等。微信支付为了保证回调可靠送达,会多次通知。我第一次接支付的时候就没做幂等,用户支付成功后连续收到了两次回调,于是数据库里加了两次余额,第二天对账才发现。从那以后,我在回调入库前一律先查一次订单状态,只有“待支付”才更新为“已支付”,否则直接返回SUCCESS。

另外,回调接口的耗时尽量控制在2秒以内。如果你在回调里又去调别的业务接口、发短信、同步ERP,一旦响应慢了,微信那边会继续重试,最终造成大量重复通知堆积。

4. 不只是收钱:退款、转账与对账这几个伴生操作

很多人把“微信支付详解”理解成“怎么收钱”。但真实线上业务里,退款、转账、对账这三个操作才是最容易出生产事故的地方。

4.1 退款:原路退回的细节

退款接口在APIv2里有单独的地址:

https://api.mch.weixin.qq.com/secapi/pay/refund

注意域名里多了个secapi,说明这个接口对安全性要求更高。APIv2下退款需要加载商户证书做双向TLS认证,直接用HTTP客户端请求是过不去的。

退款核心参数:

参数说明
out_trade_no原商户订单号
transaction_id微信支付订单号,二选一即可
out_refund_no商户退款单号,需要自己维护
total_fee原订单金额,单位分
refund_fee退款金额,单位分,不能大于total_fee
refund_desc退款原因,部分渠道展示给用户
notify_url退款结果异步回调地址

退款还有个很容易踩的坑:退款金额、退款单号必须要有幂等设计。如果你因为网络超时重试退款,给同一个订单发两次相同金额的退款,微信不会自动帮你合并。所以out_refund_no一定要用能保持稳定的、不会因重试而变化的编号。

另外,退款也不是立刻到账的。微信支付会异步处理退款,部分银行渠道可能需要1-5个工作日。业务系统里千万别在提交退款请求成功后就以为“已退款”,要以退款回调为准。

4.2 企业付款到零钱:微信红包和转账的差别

企业付款到零钱,也就是“微信支付商户号向外转钱”,比如返利、提现、报销等场景。这个接口叫:

https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers

这个接口同样是双向证书调用,且对商户号和收款用户的实名要求非常严格。参数里需要openid、amount、desc,以及可选的真实姓名re_user_name。注意:金额单位是分,跟你收钱时保持一致,千万别在转账时当成元用。

这个接口目前有几个限制:收款用户必须是微信实名用户;商户一定有足够可转出的余额;转账涉及风控,频繁大额转账很容易触发人工审核。我做提现功能时,就曾经因为单笔金额过大被限制,后来拆分成多笔才通过。

4.3 对账:每天的对账单怎么用

跟钱打交道,对账是底线。微信支付提供了下载对账单的接口:

https://api.mch.weixin.qq.com/pay/downloadbill

对账单分日账单和申请资金账单,按天下载下来是一个TXT文件(CSV格式),每行包含交易时间、商户号、订单号、微信订单号、支付方式、金额、手续费、状态等字段。维护一个每日定时任务,把对账单下载下来和自己数据库里的账单比对,是排查“用户付了钱但订单未支付成功”“金额不一致”最直接的手段。

我自己一般每周跑一次手动对账,每月做一次全面核对。别嫌麻烦,线上支付没有对账机制,等于大半夜不锁门。

5. 线上常见的坑:从签名错误到重复通知

微信支付的坑,很多不是文档没写,而是藏在各种边界情况里。这一节专门记录我踩过或帮别人排查过的高频问题。

5.1 签名错误:大多数人忽略的换行符与编码问题

签名错误是新手遇到最多的报错。这里分享几个容易被忽略的原因:

  • 拼接参数时,参数值里有中文、空格、特殊字符,没有做URL编码或直接用了驼峰命名。
  • APIv3的签名串是按“实际请求路径+请求体”拼的,如果你用HTTP客户端时URL被加上了多余的/,签名就会失败。
  • 生成MD5时,原来的API密钥用的是32位字符,但如果你在商户平台把密钥改成43位,那就会变成APIv3的密钥,v2接口就不能用了。

我在排查别人代码时,发现最典型的问题就是:代码里用了String.trim()去掉参数首尾空格,但微信的签名原串要求严格保留原始请求体。千万别做二次格式化。

5.2 回调重复通知与并发问题

前面已经说过重复通知的幂等。这里再补充一个并发场景:如果同一个用户快速重复支付,或者两次回调在同一秒到达,你的查询订单状态和更新状态如果不是一个原子操作,仍然可能造成重复发货。

解决方案很简单,在数据库更新时带上状态条件:

UPDATE orders SET pay_status = 'PAID', transaction_id = 'xx' WHERE order_id = 'xx' AND pay_status = 'UNPAID'

受影响行数为1才表示本次更新成功。用这种方式,即使回调过来10次,也只会成功执行一次。

5.3 证书过期与密钥轮换

很多人在代码上线时没注意证书和密钥的有效期。APIv2的商户证书、APIv3的平台证书都有有效期,到期之后所有需要证书的接口都会突然失败。如果项目里证书是手动下载的,一定要在证书到期前做提醒计划。

这里说一个实用做法:把证书过期时间写进监控指标,按周检查;同时预留好证书轮换接口,确保在到期前可以平滑切换。否则订单数据都在正常跑,突然某天转账、退款全挂,会非常被动。

6. 上线前的自查清单与几条个人经验

最后这部分写给即将上线或者正在联调的同学。微信支付接口虽然只有那么几个,但从开发到上线,很多细节是做之前想象不到的。我把自己的经验整理成一份“上线前清单”,尽量帮大家减少交学费的概率。

6.1 日志:关键数据必须打全

支付相关日志必须包含以下内容,缺一个你会后悔:

  • 请求参数和响应参数原文(脱敏后),特别是out_trade_no、transaction_id、total_fee、return_code。
  • 每次回调的完整报文、验签结果、解密后的订单数据。
  • 支付接口上下游的耗时,包括下单接口、回调处理接口。
  • 订单状态迁移的前后值,方便追踪整个生命周期。

日志和监控是支付线上事故定位的生命线。没有日志,出问题只能干瞪眼。

6.2 测试:真实验证不了,就做边界模拟

微信支付没有很好的全量沙箱环境,很多测试依赖真实的小额支付。我的经验是:

  • 准备一个测试商户号,所有功能联调都用测试号。
  • 在测试环境里模拟用户取消支付、支付超时、重复回调、金额不一致等各种异常场景。
  • 用几笔1分钱的真实支付验证全链路,包括下单、回调、退款、对账单下载。
  • 确认退款可以全额退、部分退,并且退款成功后原订单状态正确。

6.3 安全红线:防刷单、防篡改、防越权

微信支付接口是公开的,如果服务端不校验,很容易被别人刷。上线前一定要检查这几点:

  • 统一下单接口必须校验用户登录态,防止别人恶意下单。
  • 回调通知校验金额时,必须用数据库订单金额为准,不能直接信任回调里的total_fee。我见过有人把回调里的金额直接写进订单,结果被构造假通知刷了货。
  • 涉及退款的接口,必须做管理员权限校验,且验证退款金额不能大于原始订单金额。
  • 服务器与微信接口的通信必须走HTTPS,且不要关闭证书校验。

最后分享一个小技巧:在企业微信里加一个机器人,把支付回调、退款回调的高优先级异常直接推到群里。这样哪怕半夜出问题,你也能第一时间知道。微信支付这套东西,说难不难,说简单也绝不简单,但只要把“状态机、回调幂等、金额校验、日志对账”这四件事做好,它就能跑得很稳。

返回列表