首页 / 资讯中心 / 文章详情

微信扫码(Native)支付从零到一:完整Demo源码与联调避坑指南

微信扫码(Native)支付从零到一:完整Demo源码与联调避坑指南 ★ FEATURED ARTICLE
扫码支付这个能力放在五年前还是个新鲜玩意儿现在几乎成了线上收银台的标配。我最早接触 Native 支付是在一个电商后台项目里当时需求方只提了一句“用户要在电脑上扫码付钱”剩下的全得自己啃文档。那会儿踩的坑现在想起来还挺有意思——签名报错查了一整天回调地址配错导致订单状态永远停在“未支付”还有一次把商户证书路径写成了相对路径本地跑得好好的一上服务器就崩。所以看到“微信扫码(Native)支付示例源码”这个标题我特别有共鸣这玩意儿看着简单真要从零跑通细节多得像牛毛。这篇内容我打算按“一个完整可复现的 Native 支付 Demo”来拆从整体设计思路讲到核心参数怎么算、代码怎么组织、回调怎么处理再到实际联调时那些文档里不会写的坑。适合谁看如果你是刚接手支付模块的后端开发或者想在自己的小项目里接一个扫码收款能力再或者你只是好奇“扫个码钱怎么就过去了”这件事背后的链路那这篇应该能帮你省下不少翻文档的时间。下面所有内容都基于常见的 Native 支付接入实践来展开示例代码用 Java 和 Python 两种语言穿插着给方便不同技术栈的朋友对照。1. 整体方案设计与技术选型思路1.1 为什么是 Native 支付而不是别的模式微信支付下面挂了好几种产品形态JSAPI 适合公众号内网页H5 适合手机浏览器APP 支付适合原生应用而 Native 支付专门解决“PC 端网页或者线下大屏展示二维码”这个场景。它的核心特征就一句话商户后台调用统一下单接口拿到一个二维码链接前端把这个链接渲染成二维码图片用户掏出手机扫一下在手机上完成付款商户后台通过回调或者主动查询拿到支付结果。选 Native 的理由很直接。第一它不依赖用户的浏览器环境PC 端 Chrome、Safari、甚至 IE 都能用只要能把字符串画成二维码就行。第二支付动作发生在用户手机上PC 端不需要处理任何敏感信息安全边界清晰。第三二维码链接本身有时效性过期自动失效不用商户自己维护状态。对比 JSAPI 需要用户授权拿 openidNative 完全不需要接入成本低了一大截。不过有一点要提前说清楚Native 支付拿到的是code_url这个链接不是给用户直接点的而是给前端库比如 qrcode.js转成图片的。很多人第一次接的时候会试图把这个链接丢到浏览器里打开结果发现是一串weixin://wxpay/bizpayurl?prxxx的协议头浏览器根本不认。这个设计是有意为之weixin://协议只有微信客户端能识别等于从源头上防止了链接被恶意转发或者钓鱼。1.2 示例源码的整体分层结构一个能跑起来的 Native 支付 Demo我习惯把它拆成四层配置层、签名层、接口调用层、回调处理层。配置层管商户号、API 密钥、证书路径这些敏感信息签名层负责把参数按规则拼成待签名字符串再算摘要接口调用层封装统一下单、查询订单、关闭订单这几个核心接口回调处理层处理微信服务器推过来的支付结果通知。这么分层的好处是每块职责单一调试的时候能快速定位问题出在哪一层。比如签名报错你只需要盯着签名层看不用在业务代码里翻来翻去。示例源码里我建议把签名逻辑单独抽成一个工具类参数组装用有序 Map这样能避免“参数顺序不对导致签名失败”这种低级错误——微信的签名规则要求参数按 ASCII 码从小到大排序用TreeMap天然满足这个条件。技术选型上HTTP 客户端我倾向用 OkHttp 或者 Apache HttpClient前者 API 简洁后者生态成熟看团队习惯。JSON 序列化用 Jackson 或者 Fastjson 都行但要注意微信接口对空值和字段顺序有要求序列化的时候别把 null 字段带进去。二维码生成前端用 qrcode.js 就够了后端不需要生成图片省一层依赖。1.3 支付链路的完整时序梳理把时序理清楚后面写代码就是填空。完整链路是这样的用户在 PC 页面点“去支付”前端请求商户后端下单接口商户后端组装参数商户号、订单号、金额、商品描述、回调地址、交易类型 Native调用微信统一下单接口微信返回code_url商户后端把code_url返回给前端前端用二维码库渲染成图片展示用户手机扫码微信客户端拉起支付确认页用户确认付款微信服务器向商户配置的回调地址 POST 支付结果商户后端验签、核对金额、更新订单状态返回成功应答同时前端可以轮询商户后端的订单查询接口作为回调的兜底。这里有个关键点回调不是百分百可靠的。网络抖动、商户服务器短暂不可用都可能导致通知丢失所以必须配一个主动查询的兜底机制。我的做法是前端展示二维码后每 3 秒调一次商户后端的订单状态接口后端再去查微信或者查本地库查到已支付就跳转成功页。这样即使回调丢了用户也不会卡在“支付中”的界面上干等。2. 核心参数与签名机制的细节拆解2.1 统一下单接口的必填参数清单Native 支付调统一下单参数看着不多但每个都有讲究。下面这张表是我整理的必填项和常见填错点参数名含义常见错误appid应用唯一标识和商户号不匹配导致签名通过但下单报错mch_id商户号抄错一位数字报“商户不存在”nonce_str随机字符串长度超过 32 位或者含特殊字符body商品描述含中文未做编码或者超长被截断out_trade_no商户订单号重复使用第二次下单直接报错total_fee总金额分单位搞错把元当成分传进去spbill_create_ip终端 IP传了内网 IP 或者格式不对notify_url回调地址必须是公网可访问的 http/https 地址trade_type交易类型Native 场景必须传 NATIVEtotal_fee这个字段我单独拎出来说因为它是最容易出问题的地方。微信要求单位是分整数不能带小数点。很多业务系统里金额存的是BigDecimal的元直接toString传过去就变成99.00接口直接报参数格式错误。正确做法是先乘 100 再取整用amount.multiply(new BigDecimal(100)).intValue()注意别用double做乘法浮点精度问题会让你在几分钱上栽跟头。out_trade_no的唯一性也要强调。同一个商户订单号重复下单微信会返回“订单已存在”的错误。所以订单号生成规则要保证全局唯一常见做法是“业务前缀 时间戳 随机数”或者直接用雪花算法。示例源码里我用的是NATIVEyyyyMMddHHmmss 6 位随机数够用且可读。2.2 签名算法的逐步推演微信支付的签名用的是 MD5 或者 HMAC-SHA256示例里我用 MD5 讲因为最直观。步骤是这样的先把所有非空参数按参数名 ASCII 码从小到大排序然后拼成key1value1key2value2的形式最后在末尾拼上key商户API密钥对整个字符串做 MD5结果转大写。举个例子假设参数是appidwxdemo123、mch_id1900000、nonce_strabc123、total_fee100API 密钥是testkey123。排序后拼接得到appidwxdemo123mch_id1900000nonce_strabc123total_fee100keytestkey123对这个字符串算 MD5得到 32 位小写再转大写就是sign字段的值。这里有几个坑必须点出来。第一sign字段本身不参与签名空值参数也不参与但0和空字符串要区分清楚total_fee0是参与签名的。第二参数值里如果有特殊字符不需要做 URL 编码直接原样拼。第三API 密钥是在商户平台单独设置的不是商户号也不是 appsecret很多人第一次接会拿 appsecret 去签怎么都对不上。第四MD5 结果一定要转大写微信校验是大小写敏感的。Python 版本的签名实现大概长这样import hashlib def build_sign(params, api_key): filtered {k: v for k, v in params.items() if v not in (None, ) and k ! sign} sorted_keys sorted(filtered.keys()) raw .join(f{k}{filtered[k]} for k in sorted_keys) raw fkey{api_key} return hashlib.md5(raw.encode(utf-8)).hexdigest().upper()Java 版本用TreeMap天然有序遍历拼接即可。注意字符串拼接时用StringBuilder别在循环里用参数多了性能差异明显。2.3 证书与密钥的安全存放Native 支付本身调统一下单不需要证书只有退款、企业付款这类敏感接口才要双向证书。但示例源码里我还是建议把证书加载逻辑预留出来方便后续扩展。证书文件apiclient_cert.p12的密码默认是商户号加载的时候用KeyStore读取别把证书路径硬编码在代码里放配置文件或者环境变量。API 密钥的存放更是重中之重。绝对不要提交到代码仓库示例源码里我用的是配置文件占位符实际部署时通过环境变量注入。我见过有团队把密钥写在application.properties里然后推到了公开仓库虽然及时删了但历史记录里还能翻出来这种事故处理起来非常麻烦。提示本地调试时可以用一个专门的测试商户号和测试密钥和生产环境完全隔离。测试环境的回调地址可以用内网穿透工具临时映射一个公网地址但要注意这类工具只用于开发调试正式环境必须用备案过的域名。3. 从下单到回调的完整实操流程3.1 统一下单接口的调用与响应解析下单接口的地址是固定的请求体是 XML 格式。对你没看错微信支付 v2 接口用的是 XML不是 JSON。虽然现在 v3 接口已经全面 JSON 化但 Native 支付的很多存量示例还是 v2示例源码里我两种都给先讲 v2 因为资料多、排查方便。请求体组装好之后用 HTTP POST 发出去Content-Type设为text/xml。响应也是 XML解析出return_code、result_code、code_url这几个关键字段。return_code是通信层面的成功标识result_code是业务层面的两个都是SUCCESS才算真正下单成功。很多人只看return_code结果业务报错比如签名错误、参数错误的时候拿不到code_url还以为是网络问题。Java 里解析 XML 可以用dom4j或者 JDK 自带的DocumentBuilder示例里我用dom4j代码短。解析完把code_url取出来返回给前端同时把prepay_id存起来后续查单或者对账会用到。响应里还有个err_code和err_code_des业务失败的时候这两个字段会告诉你具体原因比如OUT_TRADE_NO_USED表示订单号重复SIGNERROR表示签名错误。调试阶段把这两个字段打到日志里能省很多猜测时间。3.2 二维码的生成与前端展示后端把code_url返回给前端之后前端用 qrcode.js 渲染。这里有个细节code_url的有效期默认是 2 小时过期后扫码会提示“二维码已失效”。所以前端展示的时候最好加一个倒计时让用户知道还剩多少时间体验会好很多。二维码的尺寸也有讲究。太小了手机扫不出来太大了页面布局难看。我一般用 200x200 到 260x260 像素之间容错级别用 M 或者 Q保证打印或者屏幕反光的情况下也能识别。qrcode.js 的配置大概是这样new QRCode(document.getElementById(qrcode), { text: codeUrl, width: 240, height: 240, correctLevel: QRCode.CorrectLevel.M });如果是在线下大屏场景比如自助收银机二维码要做得更大同时加一句“请使用微信扫一扫”的提示文案。别小看这句文案实测能显著降低用户的操作困惑。前端展示二维码的同时要启动轮询。轮询间隔我建议 2 到 3 秒太频繁会给后端和微信接口造成压力太慢用户感知差。轮询到已支付状态就停止并跳转结果页。轮询接口要做频率限制防止恶意刷接口。3.3 支付结果回调的验签与幂等处理回调是整条链路里最需要小心的地方。微信服务器会向notify_urlPOST 一段 XML里面包含return_code、result_code、out_trade_no、transaction_id、total_fee、sign等字段。收到之后第一件事是验签用同样的签名算法把收到的参数重新算一遍和sign比对不一致直接丢弃。验签通过之后核对total_fee和本地订单金额是否一致。这一步很多人会漏觉得验签过了就万事大吉。实际上验签只能证明请求来自微信不能证明金额没被篡改虽然概率极低但防御性编程要求我们核对。金额不一致的订单要标记为异常人工介入。然后是幂等处理。微信的回调可能会重复推送同一个transaction_id可能来好几次。所以更新订单状态之前要先查如果已经是“已支付”状态就直接返回成功应答不要重复处理。数据库层面可以用transaction_id做唯一索引插入冲突就说明重复了。回调处理成功后必须返回一段特定的 XML 给微信告诉它“我收到了”xml return_code![CDATA[SUCCESS]]/return_code return_msg![CDATA[OK]]/return_msg /xml如果返回的不是这个格式微信会认为通知失败隔一段时间重试重试策略是 15 秒、15 秒、30 秒、3 分钟、10 分钟、20 分钟、30 分钟、30 分钟、30 分钟一共 9 次。所以回调接口一定要做快速响应业务逻辑能异步就异步别在回调里做耗时操作。3.4 主动查询作为回调的兜底前面说了回调可能丢所以主动查询必须有。查询接口传out_trade_no或者transaction_id返回订单状态。前端轮询的时候后端先查本地库如果本地还是“待支付”就调微信查询接口查到已支付就更新本地状态并返回给前端。查询接口的调用频率要控制微信对查询有频次限制具体数值看官方文档。我的做法是前端轮询间隔 3 秒后端对同一个订单的微信查询做 10 秒缓存避免短时间内重复调微信。缓存用本地ConcurrentHashMap加过期时间就够了示例项目不需要上 Redis。还有一种情况用户扫码后关闭了支付页面没付款。这时候订单会一直停在“待支付”。所以需要一个关单机制超过一定时间比如 30 分钟的未支付订单调关闭订单接口把微信侧的订单关掉本地标记为“已关闭”。这样能避免订单号被长期占用也方便对账。4. 联调排查与常见问题实录4.1 签名错误的排查路径签名错误是 Native 支付联调阶段出现频率最高的问题没有之一。报错信息通常是SIGNERROR但不会告诉你具体哪里错了。我的排查顺序是这样的先确认 API 密钥对不对是不是拿成了 appsecret再确认参数排序是不是 ASCII 升序中文参数有没有被转码然后确认空值参数有没有被排除最后确认 MD5 结果有没有转大写。有个特别隐蔽的坑参数值里如果包含或者拼接的时候会破坏keyvalue的结构导致签名对不上。这种情况要么在业务层限制参数值不能含这些字符要么在拼接前做转义。微信文档里没明说这一点但实际遇到过好几次。还有一个坑是字符编码。签名用的字符串必须是 UTF-8 编码如果你的项目默认编码是 GBKbody字段里的中文在算 MD5 的时候字节序列不一样签名必然错。统一在算签名的地方显式指定 UTF-8别依赖系统默认。4.2 回调收不到的几种可能回调收不到先分清楚是微信没发还是你没收。查微信商户平台的交易记录看订单状态是不是已支付如果是说明钱收到了问题出在通知环节。然后检查notify_url是不是公网可访问用浏览器或者 curl 直接 POST 一下你的回调地址看能不能通。常见原因有这么几个地址填的是内网 IP、端口没开放、防火墙拦截、HTTPS 证书链不完整、回调接口返回了非预期格式导致微信认为失败。HTTPS 证书链这个问题值得单独说。有些证书只配了站点证书没配中间证书浏览器能正常访问因为浏览器会自动补全但微信服务器不会握手直接失败。用在线工具检测一下证书链完整性缺中间证书就补上。还有一种情况是回调接口本身抛异常了比如数据库连接超时、空指针导致返回了 500。微信收到 500 会重试但如果你的接口一直不稳定重试 9 次之后就不再推了。所以回调接口的日志一定要打全收到什么、处理到哪一步、返回什么都要有记录。4.3 金额与订单状态的典型异常金额相关的异常主要有两类单位搞错和精度丢失。单位搞错前面说过了元当成分传用户付了 100 倍的钱这种事故很严重。精度丢失是double运算导致的比如0.1 0.2不等于0.3金额计算一律用BigDecimal并且指定RoundingMode。订单状态异常最常见的是“用户已付款但订单显示未支付”。原因可能是回调丢了、回调处理失败、或者本地事务回滚了。排查的时候先看微信侧订单状态再看本地库再看回调日志。如果确认是回调丢了手动触发一次查询补单。示例源码里我加了一个管理接口输入订单号就能触发补单运维排查很方便。还有一个容易被忽略的点退款和支付的订单状态是联动的。如果一笔订单已经退款再收到支付回调极端情况下的乱序不能把状态改回已支付。所以状态机要设计好已退款是终态不允许回退。4.4 常见问题速查表现象可能原因处理方式下单返回 SIGNERROR密钥错误、排序错误、编码错误逐项核对签名四要素下单返回 OUT_TRADE_NO_USED订单号重复检查订单号生成规则二维码扫不出来code_url 过期或二维码尺寸过小检查有效期调整尺寸和容错级别回调收不到notify_url 不可达或返回格式错误curl 自测检查证书链和返回体订单状态不更新回调丢失或处理异常查日志用主动查询补单金额对不上单位错误或浮点精度问题统一用分和 BigDecimal重复发货回调未做幂等用 transaction_id 做唯一约束注意联调阶段建议把微信返回的原始报文完整打到日志里包括请求和响应。出问题的时候这份日志就是最直接的证据。但生产环境要注意脱敏别把密钥、证书密码这些打进去。5. 源码组织与可复用的工程实践5.1 配置项的集中管理与环境隔离示例源码里我把所有配置项抽到一个WxPayConfig类里字段包括 appid、mchId、apiKey、notifyUrl、certPath、certPassword。这个类从配置文件读取配置文件分dev、test、prod三套通过启动参数指定激活哪套。这样本地调试和线上部署用的是同一份代码只是配置不同避免“本地能跑线上不行”的经典问题。配置项里notifyUrl建议做成可配置而不是硬编码因为不同环境的域名不一样。certPath用绝对路径或者 classpath 相对路径都行但要在启动时校验文件是否存在不存在直接启动失败比运行到一半报错要好排查。5.2 接口调用的统一封装与重试策略统一下单、查询、关闭这三个接口的调用逻辑高度相似组装参数、签名、发 HTTP 请求、解析响应、验签查询和关闭的响应也带签名。所以我抽了一个WxPayClient类内部用一个doPost方法统一处理 HTTP 和 XML 解析上层方法只关心业务参数。重试策略要谨慎。下单接口不建议自动重试因为可能造成重复下单虽然订单号唯一能挡住但会给用户造成困惑。查询接口可以重试失败后隔 1 秒重试一次最多 2 次。回调处理不重试失败了就靠微信自己的重试机制。HTTP 超时时间设置也有讲究。连接超时 5 秒读取超时 10 秒太短了网络抖动就失败太长了用户等待体验差。示例里我用的是 OkHttp配置如下OkHttpClient client new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build();5.3 日志与对账的配套设计支付系统的日志不是“打个 log.info 就完事”要能支撑事后对账和问题追溯。我的做法是每次接口调用记录一条结构化日志包含请求流水号、接口名、请求参数脱敏后、响应码、耗时。请求流水号用 UUID贯穿下单、回调、查询全链路排查的时候一个流水号就能串起来。对账是另一个容易被忽视的环节。每天定时拉取微信侧的账单和本地订单逐笔比对发现差异微信有本地无、本地有微信无、金额不一致就告警。示例源码里我只留了对账的骨架实际落地还需要处理账单文件的下载和解压账单是 gzip 压缩的 CSV。5.4 安全加固的几个实操建议第一回调接口要做来源校验虽然验签已经能挡住伪造请求但再加一层 IP 白名单更稳妥。第二code_url不要在前端日志或者埋点里明文记录虽然它有时效性但泄露了总归不好。第三商户平台的 API 密钥定期轮换轮换期间新旧密钥并行一段时间避免影响存量业务。第四所有涉及金额的接口都要做权限校验防止越权查询他人订单。我在实际项目里还遇到过一个情况测试环境的密钥不小心配到了生产环境导致生产下单全部签名失败。后来我们加了一个启动自检用配置的密钥调一次查询接口查一个不存在的订单如果返回签名错误就拒绝启动。这个自检逻辑很简单但能挡住配置错误这类低级事故。6. 个人实操体会与后续扩展方向Native 支付这个能力本身不复杂复杂的是围绕它的工程细节。我踩过的坑里真正跟支付逻辑相关的不到三成剩下七成都是配置、编码、网络、证书这些外围问题。所以我的建议是第一次接的时候别急着写业务代码先把官方示例跑通把签名、下单、回调这条最小链路走顺再往自己的项目里搬。示例源码的价值不在于代码本身多精妙而在于它把那些散落在文档各处的细节串成了一条能跑通的线。你可以直接拿它当脚手架改改配置就能用也可以只挑其中某一块比如签名工具类、回调处理模板嵌到现有项目里。后续如果要扩展我建议往两个方向走。一是接入 v3 接口v3 用 JSON、用平台证书、用 AES 加密回调安全性更高也是官方主推的方向。二是把支付能力抽象成独立的服务下单、查询、退款、对账都通过 RPC 暴露业务方不用关心支付细节这样多业务线复用的时候会清爽很多。不过这是后话了先把 Native 这条链路跑稳比什么都实在。
阅读完成 · 觉得有帮助?
咨询建站