
说到微信支付我经手过的项目少说也有十几个了。从最开始的个人公众号H5支付到后来电商系统里的JSAPI支付和小程序支付再到退款、转账、对账这些伴生功能几乎每个环节都踩过坑。微信支付这套接口官方文档写得不算差但真正下手去接的时候你会发现坑全藏在细节里。今天就把我在实际项目里验证过的那套流程和注意事项完整梳理一遍从账号准备到下单回调再到退款对账尽量做到让没接过的人能少走弯路让已经接了一半的人能理清思路。1. 微信支付的账本逻辑一次支付背后到底发生了什么很多第一次接微信支付的人最容易犯的毛病是一上来就找接口文档。这没错但如果你不理解微信支付背后的资金流转逻辑后面处理订单状态、对账、退款时一定会乱。1.1 一次支付背后参与的四个角色微信支付体系里每次交易至少有四个角色参与用户、商户、微信支付平台、用户的发卡行或零钱账户。用户在商户App或小程序里发起支付实际扣款动作发生在用户侧。微信支付平台做的事情是“撮合”确认用户资金足够、确认商户合法、记录这笔交易的状态然后通过异步通知告诉商户“钱已经扣了”。注意这里有个关键点微信支付通知商户时资金实际上还没有结算到商户的银行卡里。从支付成功到资金结算给商户中间还有一个“结算周期”和“结算账户”的概念。普通商户一般默认是T1结算也就是说今天用户支付成功钱可能在第二天甚至更晚才打到你的对公账户。如果只知道“支付成功”就发货大部分业务是没问题的但如果涉及退款就要小心支付成功不等于钱已经到你的银行账户退款是微信支付在它的账本里做“原路退回”即使钱还没结算给你它也能先把这笔交易撤销掉。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.pemAPIv3还会用到一个平台证书用来验签。我见过有团队把这些证书文件直接提交到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是公众号或小程序的AppIDmch_id是商户号out_trade_no是商户自己的订单号必须唯一total_fee是订单金额单位是分注意必须是整数且不能为0body是商品描述会展示在支付页面上notify_url是异步通知回调地址trade_type是JSAPI、NATIVE、APP、MWEB等openid条件必填trade_type为JSAPI时必须传即用户的openidspbill_create_ip建议填用户下单的IP有助于风控time_expire建议填订单失效时间比如下单后15分钟支付超时所有参数除sign外按ASCII字典序排序拼接成keyvaluekeyvalue之后在末尾再拼上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_idwx201410272009395522657e690389285100, signType: RSA, paySign: 用APIv3或APIv2规则生成的签名 }这里最容易出错的点是package参数。很多人以为是直接传prepay_id其实它要拼成prepay_idxxx。另外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。先把回调处理的完整步骤整理出来验签确认这个通知确实来自微信支付。在APIv3下用平台证书验证通知自带的签名在APIv2下用API密钥重新计算签名进行比对。解密APIv3的通知报文是加密的需要结合APIv3密钥进行解密得到明文订单数据。校验业务参数重点核对out_trade_no是不是你自己的订单、total_fee是否与订单金额一致、mch_id是否匹配。幂等处理微信支付的通知可能重复发送服务端必须保证重复通知不会重复发货、重复加余额。返回应答处理成功后返回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_feerefund_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且不要关闭证书校验。最后分享一个小技巧在企业微信里加一个机器人把支付回调、退款回调的高优先级异常直接推到群里。这样哪怕半夜出问题你也能第一时间知道。微信支付这套东西说难不难说简单也绝不简单但只要把“状态机、回调幂等、金额校验、日志对账”这四件事做好它就能跑得很稳。