第一次接支付需求那天我花了一下午时间把微信支付和支付宝开放平台的文档翻了个遍。文档都写得挺规范但问题在于它们是两套不同的体系一个讲证书、一个讲公钥一个单位用分、一个单位用元一个回调加密、一个回调明文。真正动手的时候让我上头的不是某个接口的代码量而是这几套东西之间的逻辑关系。这篇内容就把我梳理好的主链路完整写一遍资质准备、参数体系、下单、支付、回调、查单、退款以及联调时容易翻车的细节。适合三类人看正要接手支付模块的后端开发、给小程序或App接入支付的同学、还有准备自己维护支付系统的独立开发者。1. 对接前的硬性门槛资质、账号、产品选型和成本核算很多人一上来就打开API文档开始写代码等到要真实验证才发现商户号还没申请甚至营业执照都没有。支付对接的第一步其实不是技术而是先确认你有没有资格去申请这些能力。1.1 主体资质要求微信支付商户号和支付宝商家账号申请的前提都是有一个真实经营主体。企业营业执照和个体工商户都可以申请但提交的主体信息必须和后续结算账户保持一致。名称、法人、证件号任何一个对不上审核都会打回来。结算账户这块注意一下企业主体基本是对公账户个体工商户在部分平台可以结算到法人本人的银行账户这个以申请页面的实际提示为准。个人开发者如果暂时没有主体可以先用沙箱环境或者测试商户号把技术链路跑通但上线正式交易肯定还是得补上商事主体资质。1.2 申请流程的几个关键点微信侧通常是先有一个公众号、小程序或者App然后在后台找到微信支付入口提交主体信息、经营信息、结算账户审核通过后拿到商户号。支付宝侧则是注册商家账号后在开放平台创建应用完成配置后再签约对应的支付产品。这个流程一般一两天能过慢的话三到五个工作日也正常。资料照片要拍清楚营业执照不能过期法人身份证要和执照一致。我见过最多的驳回原因是结算账户开户名和营业执照主体不一致或者法人姓名填成了经营者姓名以外的字。别笑真有人把“经营者”填成“法人代表”然后被打回来。1.3 支付产品选型不是每个产品都要接微信和支付宝都按照使用场景拆分了多个支付产品先列一张表使用场景微信侧产品支付宝侧产品微信内公众号/小程序支付JSAPI支付小程序支付微信外手机浏览器支付H5支付手机网站支付App内支付APP支付App支付电脑网站扫码支付Native支付电脑网站支付对接之前先想清楚自己的业务到底在哪个场景出现。如果只有一个小程序就只需要微信用JSAPI、支付宝用小程序支付其他的产品一概不用签减少审核时间和维护成本。如果业务是PC网站那就接微信Native支付和支付宝电脑网站支付。1.4 费率与结算周期费率一般在签约页面能看到普通行业约0.6%部分优惠类目能做到更低。结算周期常见的T1也就是第二天到账节假日顺延。这个费率要计入项目成本尤其你做的是低毛利商品手续费会直接影响利润测算。还有一个容易忽略的点退款时原手续费退不退不同平台不同产品的规则不完全一样。有些是退款成功后按退款金额等比退还手续费有些则不退。这个细节建议在开发退款功能前问清楚客服或在页面确认避免上线后财务对不上账。2. 参数与密钥体系把AppID、商户号、证书、公钥先串起来支付对接最劝退新手的就是参数太多而且微信和支付宝叫法还完全不一样。这里我直接把两边各自要用的核心参数拆开讲讲清楚每个东西到底干嘛用的。2.1 微信支付侧参数全家桶AppID你那个公众号或小程序的应用ID用来标识“你是哪个应用”。商户号mchid微信支付分配给商户的唯一编号标识“你是哪个商户”。APIv3 Key32字节的对称密钥主要是解密微信支付回调通知里加密的订单数据。商户API证书包含apiclient_cert.pem和apiclient_key.pem两个文件其中私钥文件用于请求接口时的签名。证书序列号微信服务端验证你请求签名时用。微信支付平台公钥/证书用于验证回调通知的签名确认通知真的是微信发来的。这里有一个重要认知商户API证书和APIv3 Key是两个东西用途完全不同。初学者经常把APIv3 Key填到签名私钥位置然后报一堆看不懂的错。2.2 支付宝侧参数全家桶支付宝的体系相对直白AppID开放平台应用的标识。应用私钥你自己生成并保存在服务端的密钥用来给请求参数签名。应用公钥把私钥对应的公钥上传到支付宝开放平台支付宝用它验证你的请求。支付宝公钥支付宝提供给你的公钥用来验证支付宝回调通知的签名。普通模式下支付宝没有像微信那样的“证书对称密钥”概念就是典型的非对称RSA2签名体系。理解了这个区别后面看文档会轻松很多。2.3 签名与加密的设计逻辑用生活类比解释一下你给平台发的请求相当于寄一封要盖章的信。你的私钥是你的章你盖上章说明这封信确实是你发的平台那边拿着你上传的公章印模应用公钥来核对。反过来平台发给你的回调通知平台用自己的私钥盖章你用平台给的公章印模支付宝公钥或微信平台公钥来验证。微信多了一层加密它回调的数据体本身被AES-256-GCM加密了所以在验签之后还要解密才能看到订单内容。支付宝普通模式回调是明文表单验签通过后直接读参数即可。明白了这套逻辑之后就不会再把“应用公钥”和“支付宝公钥”混用了。这两个东西在支付宝后台长得有点像但一个是验你的请求一个是验平台的回调拿错了就会一直验签失败。2.4 密钥保管的底线要求私钥文件绝不能提交到Git仓库哪怕私有仓库也建议排除通过配置中心或环境变量注入。开发环境、测试环境、生产环境的密钥要分开尤其是支付宝沙箱和正式环境的AppID、私钥完全不能混用。APIv3 Key和应用私钥要有轮换机制至少半年到一年换一次换的时候注意提前过渡。3. 核心链路从下单到回调的完整拼图把前置条件准备好之后真正开发的核心链路其实就四步后端调统一下单接口、前端拉起支付、支付完成后平台异步回调通知、后端处理回调并更新订单状态。3.1 统一下单微信JSAPI支付示例以微信JSAPI支付为例后端需要请求POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi请求体大概是这样的{ appid: 你的AppID, mchid: 你的商户号, description: 商品描述, out_trade_no: 商户订单号, notify_url: https://api.example.com/pay/wx/notify, amount: { total: 100 }, payer: { openid: 用户的openid } }注意这里的amount.total单位是分也就是1元要传100。这个“分”和“元”的坑几乎每个接支付的人都踩过。这个请求需要在Header里带上签名核心是构造一个待签字符串用商户私钥做SHA256-RSA签名import time, secrets, base64 def build_message(method, url, timestamp, nonce, body): return f{method}\n{url}\n{timestamp}\n{nonce}\n{body}\n def sign_request(method, url, body, private_key): timestamp str(int(time.time())) nonce secrets.token_hex(16) message build_message(method, url, timestamp, nonce, body) signature base64.b64encode( private_key.sign( message.encode(utf-8), padding.PKCS1v15(), hashes.SHA256() ) ).decode(utf-8) sign_str ( fWECHATPAY2-SHA256-RSA2048 fmchid{mchid}, fnonce_str{nonce}, fsignature{signature}, ftimestamp{timestamp}, fserial_no{cert_serial_no} ) return sign_str看起来麻烦但逻辑就是把请求方法、URL路径、时间戳、随机串、请求体拼成一个字符串用私钥签名再把签名结果和其他信息拼成Authorization头。这里有个细节URL用的是去掉域名的路径部分比如/v3/pay/transactions/jsapi。支付宝侧的思路类似不过它把签名结果放在请求参数里而不是HTTP头。下单请求的biz_content里包含out_trade_no、total_amount、subject等字段其中total_amount单位是元比如0.01。用应用私钥对参数做SHA256withRSA签名Base64编码后放入sign参数。3.2 前端拉起支付后端拿到参数后的处理微信JSAPI下单成功后返回prepay_id。这个prepay_id不能直接给前端用后端还要用它重新生成调起支付的必要参数比如timeStamp、nonceStr、packagepackage的值为prepay_idxxx、signType、paySign。然后用当前商户的私钥对这五个字段做签名再交给前端调用小程序或公众号的支付SDK。支付宝侧的处理方式略有不同。App支付下单后会返回orderStr前端直接拿orderStr调起支付宝SDK手机网站支付会返回一段自动提交的表单HTML后端把它直接输出到浏览器就行。这里有一个安全原则下单和签名必须在后端完成前端不要接触任何私钥。我看到有人把私钥放在前端代码里去生成签名这是严重的资金级安全漏洞。3.3 异步回调通知核心中的核心支付完成后微信和支付宝会向下单时提交的notify_url发送异步通知。这个回调如果处理不好订单状态就会错乱。微信回调的body是加密的。处理流程是先验签再用APIv3 Key解密最后校验参数。解密核心逻辑from cryptography.hazmat.primitives.ciphers.aead import AESGCM # resource字段包含ciphertext、nonce、associated_data # ciphertext需要base64解码 key api_v3_key.encode(utf-8) aesgcm AESGCM(key) plaintext aesgcm.decrypt( nonce, # 回调报文里的noncebytes b64decode(ciphertext), # 解码后的密文tag associated_data # 回调报文里的associated_databytes ).decode(utf-8)解密后拿到的字段里有out_trade_no商户订单号、transaction_id微信支付订单号、trade_state交易状态、amount.total支付金额单位分等。后端要做的事情很明确check签名和解密是否成功校验商户订单号是否存在校验支付金额是否和订单金额一致更新订单状态为已支付返回响应告诉微信“我处理成功了”。这里有一个非常容易错的点微信要求收到回调后HTTP响应必须是2xx且返回体符合要求处理失败时不要直接返回错误页面而是记录日志后返回失败状态让微信稍后再次通知。如果后端回调服务崩溃了或者返回了500微信会按策略重发多次通知。支付宝的回调是明文表单验签方式是把回调参数里的sign和sign_type去掉剩余参数按key的ASCII码升序排列拼成keyvaluekeyvalue形式用支付宝公钥做RSA2验签。验签通过后再校验app_id、out_trade_no、trade_status和total_amount。注意支付宝的trade_status只有TRADE_SUCCESS和TRADE_FINISHED才表示交易成功。处理完毕后响应体要原样输出英文单词success不能返回JSON、不能输出HTML、不能重定向否则支付宝会认为回调失败并重发通知。3.4 主动查单、退款和对账兜底回调是主路径但网络世界没有绝对可靠所以查单接口是重要的兜底。微信支付查询接口可以通过out_trade_no查询交易状态支付宝有alipay.trade.query。建议在以下场景主动查单用户支付后回调迟迟没来、订单处于中间状态超过一定时间、用户反馈已付款但订单未更新。退款链路也是一定要提前联调的。微信支付退款接口是POST /v3/refund/domestic/refunds支付宝对应alipay.trade.refund。退款一般原路返回不是即时到账需要关注退款状态异步通知。部分退款时累计退款金额不能超过原订单总额这个校验后端要做。3.5 两边核心差异对照环节微信支付支付宝下单接口示例v3/pay/transactions/jsapialipay.trade.create金额单位分整数元两位小数回调数据AES-256-GCM加密明文表单验签方式微信支付平台公钥验签支付宝公钥验签成功响应返回200和SUCCESS原样输出success4. 边界处理与安全设计支付系统不能只做到“能付通”把主链路跑通只是开始真正上线前还要处理一堆边界情况和安全问题。支付系统最怕的不是功能没有而是边界没想清楚。4.1 金额单位与精度问题微信用分支付宝用元这在同一个项目里其实是个大坑。我自己的做法是数据库统一以分存储用整数类型或长整型避免浮点数精度问题。如果有对外的金额展示再转换成元。数据库存金额不要用float或double你可以用decimal或者干脆都存整数分。所有金额运算用整型分完成最后展示层再做格式化。4.2 重复通知与数据幂等微信和支付宝的通知机制都允许重复通知同一个订单的支付成功通知可能收到多次。后端处理回调时不能每次通知都直接更新订单状态必须做幂等控制。最基本的方式是回调处理时先查询订单当前状态如果订单已经是“已支付”直接返回成功响应不再重复处理。更严谨的方式是给支付流水表加唯一索引以transaction_id作为订单维度的唯一键重复通知插入时直接报错回滚再根据唯一索引冲突判断为重复。4.3 订单状态机设计支付相关订单状态建议设计成待支付、已支付、已关闭、退款中、已退款。用户支付成功前如果主动取消订单订单进入已关闭。已支付后用户申请退款订单进入退款中退款完成进入已退款。这里有一个值得注意的操作顺序用户支付成功的那个瞬间如果用户同时点击了取消订单后端要以回调通知为准而不是以前端取消操作为准。前端可以发关闭请求但后端要判断订单当前是否已支付已支付订单不能关要引导走退款流程。4.4 回调验签与日志脱敏所有回调接口都必须验签这是非商量项。不验签的后果是伪造通知任意改订单状态等于把资金安全裸奔。日志方面回调原文、响应结果、异常堆栈都要记录但是不能打印完整的签名参数、APIv3 Key、应用私钥。订单号、金额可以打完整卡号、密钥类字段必须脱敏。线上排查问题的时候日志是否足够决定了你的排障时间是十分钟还是两个小时。5. 联调阶段常见的坑与完整排查思路这里把我在联调阶段踩过的坑和排查链路整理出来每个问题都给到排查顺序方向对了答案自然出来。5.1 签名报错“验签失败”微信侧排查顺序确认你签名用的私钥还是不是商户API证书里的私钥新人经常会拿错成平台证书私钥。确认待签字符串的格式尤其是URL部分要写路径而不是完整域名method要大写。确认时间戳是秒级且服务器时间偏差不能太大。确认Authorization头里serial_no用的是证书序列号不是商户号。支付宝侧排查顺序确认拼接验签串时去掉了sign和sign_type两种参数。确认value部分没有做二次URL编码就用原始的键值。确认用的是支付宝公钥验签不是应用公钥。如果你的支付宝公钥是从后台复制的注意去掉多余的换行符。5.2 回调收不到或回调报错回调收不到先别怀疑平台按这个顺序自查回调地址在公网能否访问用的HTTPS证书是否有效自签名证书平台会拒绝。商户平台配置的notify_url和下单请求里传的notify_url是否一致两边会校验。服务器安全组、防火墙是否放行了443端口反向代理有没有把POST请求转发到后端。接口是否返回了非200状态或者返回了HTML内容。微信和支付宝对响应格式有严格要求。有没有加全局重定向比如HTTP跳HTTPS、加www跳转这些都会截断回调。调试阶段用内网穿透工具把本地服务暴露出去是最快的但生产环境老老实实上正规域名和HTTPS证书不要省这个钱。5.3 微信回调解密失败微信回调解密失败几乎都是这三类原因第一APIv3 Key复制不全多复制了空格、漏了几个字符密钥是32字节错了任何一个字节解密都会失败。建议把密钥放到配置中心后写一个自检接口测试加解密不要肉眼比对。第二密文没有用原始body字节很多框架会先把请求体解析成JSON对象你再把这些字段重新序列化去解密结果字节顺序变了导致解密失败。正确做法是直接用request.body的原生字节。第三AES-256-GCM的参数顺序写错。decrypt()方法接收的顺序是nonce、data密文tag、associated_data这三个字段来自微信回调的resource节点别把顺序搞反。5.4 支付宝notify验签失败支付宝notify验签失败我见过最多的是这两个原因一是用JSON解析了请求体。支付宝notify是以application/x-www-form-urlencoded格式POST过来的应该用request.POST或对应语言的表单解析方式拿到参数而不是把body当JSON解析。你用JSON解析出来的键值对大概率没问题但有些字段值里的符号会发生变化导致验签串不一致。二是验签用的公钥不对。支付宝后台有两个公钥展示位一个是应用公钥是你上传上去的一个是支付宝公钥是支付宝提供的。验签要用后者。把两个搞混会一直验签失败。5.5 沙箱环境和正式环境混用支付宝沙箱环境提供了一套独立的AppID和密钥跟正式环境完全隔离。联调时用沙箱配置、上线前换成正式配置经常有人换漏了一个参数。我的建议是把环境配置集中到一个文件或配置中心切换环境时一次性替换整套参数不要分散写在代码里手动改。微信侧相对简单一般直接用正式商户号在测试模式下产生少量真实订单来验证所以环境隔离的坑没有支付宝那么明显但测试订单要记得处理掉别影响线上数据。6. 上线前自查清单与值得长期保留的习惯支付模块上线前的自查我整理了一份清单照着过一遍能挡掉绝大多数低级问题[ ] 回调接口验签、解密逻辑完整且经过测试[ ] 重复回调幂等验证过用工具或脚本连续推送两次同一通知[ ] 金额单位统一分和元的转换没有遗漏[ ] 下单、回调、查单、退款全链路联调通过[ ] 退款链路走通包含部分退款场景[ ] 密钥不落在代码仓库生产配置加密[ ] 支付相关日志完整但敏感字段已脱敏[ ] 对账任务已部署差异告警有人处理最后分享三个我长期保留的习惯。第一个习惯是建支付流水表。每一笔下单请求的请求参数、返回结果、回调原文、处理结果都记录到一张独立的流水表里排障时不需要翻服务器日志直接查这张表就能串起来整条链路。第二个习惯是回调处理先落库再报错。回调进来后先把原始报文存下来再去做验签和业务处理即使后面业务处理出了问题原始数据也留住了。第三个习惯是真实环境用小额订单验证全链路。微信支付最低可以交易1分钱支付宝也支持0.01元在正式上线前用真实环境产生一笔小额订单跑通下单、支付、回调、对账全流程后再发起退款这套组合拳比任何代码review都管用。我在实际维护支付模块时有个体会支付链路里越简单越可靠回调处理顺序执行、先落库再返回、查单兜底定时跑做到这些再复杂的对接也就是几个接口的事。支付系统不需要炫技稳定和安全才是第一位。