小程序付款转二维码:Native支付实现扫码收款全流程解析
发布时间:2026/10/1 3:22:24来源:尧图网络
上个月一个做餐饮SaaS的朋友来找我问了个特别实际的问题他们店里有个收银场景店员在小程序里点一下“收款”屏幕上立刻弹出一个二维码顾客拿自己微信扫一下钱就付了。他问我这功能是怎么实现的能不能把“小程序付款”直接转成“二维码付款”。这个需求听起来很直白但真做起来坑不少。微信支付的产品形态分了好几种付款码、扫码支付、小程序支付看着都是二维码实现路径完全不同。这篇我就把“小程序付款转二维码付款”这条链路完整拆开讲从场景选型、后端下单、小程序端二维码渲染到支付结果同步和上线避坑一次性说清楚。适合正在做收银系统、点单系统、聚合支付或者单纯被老板一句“做个收款码弹出来”难住的朋友参考。1. “店员点收款顾客扫码付”先看清这个需求长什么样1.1 三个真实的落地点位我总结下来“小程序付款转二维码付款”这个需求主要出现在这三类场景里餐饮门店收银顾客在收银台点单店员在ipad或收银机上打开小程序或配套后台输入金额后屏幕弹出二维码顾客扫码付款。这个场景不用额外买扫码枪一台装了小程序的设备就是收银终端。代客下单比如服装店、美容院的店员替顾客在手机小程序里录入商品生成一个付款二维码顾客用自己手机扫码完成支付。好处是订单留在系统里后续退换货、售后对账都方便。临时收款点展会、市集、活动摊位不方便部署完整收银硬件商家拿一个小程序页面当收款入口顾客扫页面上的动态码付款。这些场景的共同点是支付动作发生在小程序生态之外——付款的人不一定要打开商户的小程序他只需要用微信“扫一扫”就可以付钱。这就是标题里“付款转二维码付款”的意思。1.2 主扫、被扫、小程序内支付三种模式别搞混微信支付里和二维码有关的方式很多人一上来就绕晕我先用一张表把概念钉死模式官方叫法谁扫谁典型入口代码入口二维码收款主扫Native支付顾客扫商家的码商家展示二维码后端Native下单返回code_url付款码支付被扫付款码支付商家扫顾客的码顾客展示付款码商家端调用付款码支付接口小程序内支付JSAPI支付无扫码用户点击小程序内调起收银台小程序wx.requestPayment注意微信官方并没有“把小程序支付转成二维码”这样一个一键能力。小程序内支付用的是wx.requestPayment它只能在当前小程序页面里调起微信支付收银台生成不了可供另一个微信扫的二维码。而你真正需要的是第一种Native支付。商户后台先向微信支付下单拿到一个支付链接官方叫code_url再把这个链接画成二维码展示出来。顾客用微信扫码后微信支付收银台会自动弹出完成付款。这就是“小程序付款转二维码付款”的完整实现方式。1.3 为什么不能把微信支付的“付款码”截成二维码贴出去有些产品经理会提一个需求“把用户的微信付款码转成二维码发给顾客扫不就行了”这里必须泼一盆冷水。微信支付的安全策略明确不允许这么做。用户微信里的“收付款-向商家付款”那个条形码/二维码是动态付款码每分钟自动刷新而且它设计出来的语义是被扫——商家用扫码枪读取再配合商户号和终端设备调用付款码支付接口。一旦截图、转发、非本人实时展示微信风控大概率会拦截。也就是说付款码是一个有时效、有定向使用场景的动态凭证把它固化成一张静态图是完全不可行的这是微信支付的安全底线不是技术问题。不要浪费时间在这上面琢磨。2. 直接在前端把小程序支付转二维码为什么走不通2.1 不是没人试过是方向就错了我自己第一次接类似需求时也想了个“偷懒”方案既然小程序里能调wx.requestPayment发起支付那能不能把JSAPI支付需要的参数拼成一个链接再生成二维码顾客扫了直接付款结果是走不通的。wx.requestPayment依赖当前小程序的运行上下文它在底层会校验发起方是不是在微信客户端内的某个小程序webview里并且需要openid等用户身份信息。把这些参数塞进二维码另一个微信扫出来后没有任何合法载体可以承载这个支付请求。微信不会认顾客扫了只会看到一段乱码或者“无法识别”。这件事给我的启发是微信支付的不同模式对应的是不同的资金流和信息流入口不能靠前端把A模式伪装成B模式。想实现“扫码即付”就老老实实用微信支付为这个场景专门设计的Native支付。2.2 前置条件里最容易漏的是商户号和支付证书Native支付不是小程序端单独能完成的它需要一个已开通微信支付、且开通了Native支付权限的商户号商户API证书用于请求签名API v3密钥一个公网可访问的HTTPS回调地址。很多人拿着小程序AppID就去请求Native下单报错appid与mchid不匹配就是因为小程序的AppID和商户号没有完成绑定关联。在小程序后台的“微信支付-商户号管理”里要把商户号关联到小程序AppID下这一步漏掉的话后面所有请求都白搭。2.3 走通的正路链路图我先用文字把这套链路完整画出来后面每一章再展开店员在小程序里点击“收款”输入金额小程序把订单信息金额、商品、桌号等提交给商户后端商户后端调微信支付Native下单接口传入appid、mchid、out_trade_no、amount.total、notify_url等参数微信支付返回一个code_url格式类似weixin://wxpay/bizpayurl?pr...商户后端把code_url响应给小程序小程序端用二维码库把code_url渲染成图片展示在屏幕上顾客用自己微信“扫一扫”扫这个码微信支付收银台自动弹出顾客输入密码完成支付微信支付服务器异步通知商户后端notify_url告知支付成功小程序端同时启动轮询查询订单状态发现已支付后展示“支付成功”反馈。看到第9步和第10步同时存在有人会问既然微信会回调为什么小程序还要轮询因为微信支付回调是发给后端服务器的微信不会主动给小程序前端推消息。小程序页面要实时感知支付结果只能靠前端轮询后端接口。这个联动细节我在第5章细讲。2.4 容易混淆的另一条路线“扫普通链接二维码打开小程序”再付款做扫码点餐或者扫码进入小程序领券的朋友经常会接触到微信公众平台的另一个能力扫普通链接二维码打开小程序。这个能力和Native支付容易混在一起因为都涉及“二维码小程序”。但它们的区别关键维度Native支付扫普通链接二维码打开小程序扫码后效果直接弹出微信支付收银台先打开某个小程序页面适合场景收银台收款、代客下单扫码点餐、扫码预约、扫码核销支付方式顾客直接完成支付不强制进入商户小程序进入小程序后再走wx.requestPayment支付二维码内容weixin://wxpay/bizpayurl?pr支付链接绑定了小程序路径的普通链接或小程序码如果你的需求是“顾客扫桌上的码先打开点餐小程序选完菜再付款”那走的是第二种普通链接二维码小程序内wx.requestPayment。如果你的需求是“店员在收银端生成一个码顾客扫了直接付钱”那走的是第一种Native支付。一句话总结标题里“小程序付款转二维码付款”标准解法就是Native支付。3. 后端这一步统一下单的完整参数与代码3.1 先确认四个前置条件动手写代码前把下面几项准备好不然后面每一步都会卡商户号mchid微信支付商户平台的商户号格式是10位数字。商户API证书在微信支付商户平台-账户中心-API安全里申请得到apiclient_cert.pem证书文件和apiclient_key.pem私钥文件。API v3密钥在商户平台自行设置的32位密钥用于回调数据解密。小程序AppID与商户号绑定在小程序后台-微信支付里完成绑定。Java后端推荐直接用微信支付官方Java SDKwechatpay-java它会自动处理签名、验签和证书轮换比自己手工拼签名串省太多事。下面的示例代码是官方SDK v2版本的调用方式不同版本API名略有差异以你依赖的实际版本为准。3.2 请求参数逐项解读调微信支付POST https://api.mch.weixin.qq.com/v3/pay/transactions/native核心请求体如下{ appid: wx8888888888888888, mchid: 1900000109, description: 堂食订单-D001-3号桌, out_trade_no: ORD202405010001, notify_url: https://api.yourdomain.com/wxpay/notify, amount: { total: 1, currency: CNY } }这里每个字段都有讲究appid和mchid必须配对就是前面说的绑定关系description会显示在顾客的支付账单里建议包含订单号、桌号等可识别信息方便对账out_trade_no是商户订单号自己生成一次订单一个号绝不能重复字符长度6~32位notify_url是支付结果回调地址必须是HTTPS且公网可访问amount.total的单位是分不是元。比如收款1元传100还有两个可选的attach字段可以放一些自定义数据支付回调时会原样带回适合放业务标识。3.3 Java示例一次性把下单写通RSAAutoCertificateConfig config new RSAAutoCertificateConfig.Builder() .merchantId(mchId) .privateKeyFromPath(/path/to/apiclient_key.pem) .merchantSerialNumber(mchSerialNo) .apiV3Key(apiV3Key) .build(); NativeService service new NativeService.Builder().config(config).build(); Transaction transaction service.create( new CreateOrderRequest() .setAppid(appid) .setMchid(mchId) .setDescription(堂食订单-3号桌-宫保鸡丁套餐) .setOutTradeNo(orderNo) .setNotifyUrl(notifyUrl) .setAmount(new Amount() .setTotal(amountInFen) .setCurrency(CNY))); // 返回的是 weixin://wxpay/bizpayurl?prxxxx 形式的支付链接 String codeUrl transaction.getCodeUrl();下单成功后后端把它返回给小程序。这里有个额外提示不要让小程序端直接拿商户证书去请求微信支付。证书和密钥应该永远只存在后端服务器小程序只接收后端处理好的结果否则一旦小程序被反编译或调试证书泄露风险极大。3.4 拿到code_url之后别对它做多余的处理code_url是什么它是一段以weixin://开头的支付链接。这个链接不需要你解析、不需要你改写成普通https开头的地址也不要去调它。你只需要原样把它交给二维码生成库画成二维码图。有人会问weixin://协议微信能扫吗能。顾客用微信扫一扫识别二维码后微信客户端自己会识别这个协议的语义弹起收银台。如果用普通二维码工具去解析只会看到一个weixin://wxpay/bizpayurl?pr...的字符串这是正常的。有一点要注意code_url的有效期官方默认是2小时但实际业务里我们通常会把它做短一点。为什么顾客扫旧码会形成坏账、对单困难而且码泄漏出去就意味着别人可以在某个时间窗口内用你的订单号付款。后面第4章我会讲怎么配合120秒过期策略。4. 小程序端把code_url变成屏幕上的付款二维码4.1 三种渲染方案怎么选拿到code_url之后小程序端要把它画成二维码。实现方式有三种方案优点缺点前端引入weapp.qrcode用canvas绘制不消耗后端资源样式可定制可动态刷新需要在小程序端集成库canvas API有基础库版本差异后端生成二维码base64图片前端image展示前端代码最少兼容性最好每次重新下单都要请求图片浪费流量样式改动要发版后端后端返回二维码矩阵前端逐格绘制灵活性最高实现工作量最大不推荐我的建议是第一种。收款码这种场景通常金额动态、有效期短、还要能刷新前端库生成最顺手。第二种适合二维码内容几乎不变的场景比如门店固定收款码但那更推荐直接申请微信官方商户收款码。4.2 weapp.qrcode接法weapp.qrcode是一个移植到小程序环境的二维码生成库内部原理是解析文本内容生成二维码矩阵再用canvas绘制出来。如果你的项目用的是老版canvas接口代码风格是这样const QRCode require(weapp.qrcode) new QRCode({ canvasId: payQrcode, ctx: wx.createCanvasContext(payQrcode), width: 260, height: 260, text: codeUrl, correctLevel: QRCode.CorrectLevel.H, colorDark: #000000, colorLight: #ffffff, callback: () { console.log(二维码绘制完成) } })如果你的基础库在2.9.0以上小程序推荐用Canvas 2D接口代码要这么写const QRCode require(weapp.qrcode) wx.createSelectorQuery() .select(#payQrcode) .fields({ node: true, size: true }) .exec((res) { const canvas res[0].node const ctx canvas.getContext(2d) new QRCode({ canvas: canvas, ctx: ctx, width: 260, height: 260, text: codeUrl, correctLevel: QRCode.CorrectLevel.H }) })对应的WXML里要放一个canvas节点canvas type2d idpayQrcode stylewidth: 260px; height: 260px;/canvas老版canvas接口和Canvas 2D接口在new QRCode的入参上不一样前者传canvasId和ctx后者传canvas节点和ctx。如果你在项目里遇到“真机上不显示、工具上正常”的诡异问题十有八九是基础库版本和canvas接口不匹配。4.3 二维码大小、容错率和留白这些细节二维码能扫出来不只是库调用对就行。收款码的展示有几个经验值我是吃过亏的尺寸建议不低于240x240px最好做到260~300px。店里顾客扫码距离一般20~50cm码太小或太密都容易识别失败。容错率选择QRCode.CorrectLevel.H最高容错。收款码贴在屏幕上可能遇到反光、遮挡、顾客扫码角度偏高容错率能抵抗一定程度的图像污损。颜色深色模块用纯黑#000000浅色背景用纯白#ffffff不要做反色不要加彩色渐变。扫码识别依赖明暗对比花哨样式好看但是会牺牲识别率。留白二维码四周至少留出4个模块宽度的白边这个白边是定位用的很多库默认会留但如果你自己用canvas画矩阵千万别为了排版把白边裁掉。不要在二维码上覆盖商家Logo做品牌宣传可以把Logo放旁边但别压在中央覆盖大量模块。真要放也保持Logo尺寸不超过二维码宽度的五分之一不然识别率掉得厉害。4.4 动态二维码建议做完120秒自动过期我强烈建议把一个订单的二维码有效期控制在90秒到120秒之间。为什么一是防误扫顾客离店了、服务员已经用其他方式收完款这个码还摆在屏幕上下一个人扫了就会形成一个说不清的交易。二是防截图动态码过期机制天然限制了截图的利用价值。如果有人拍了收银台的码过两分钟再扫就失效了安全风险小很多。实现方式很简单小程序端拿到code_url后启动一个120秒倒计时倒计时结束就调用后端的关单接口把该订单关闭前端也同步把二维码置灰或者切换成“二维码已过期点击刷新”状态。如果顾客在过期前完成了支付倒计时结束前前端轮询到支付成功自然取消倒计时。5. 支付完成怎么通知小程序回调、轮询与界面联动5.1 后端必须接好支付结果回调下单时传的notify_url就是用来接收支付结果通知的。微信支付在用户完成支付后会向这个地址发送一个POST请求。Java后端接收回调解密后的核心逻辑大概是// 1. 从请求头拿到微信支付平台证书序列号验签 // 2. 用API v3密钥解密resource节点 // 3. 解析出 out_trade_no、transaction_id、trade_state、amount.total // 4. 校验订单存在、金额一致、trade_state SUCCESS // 5. 更新数据库订单状态为已支付 // 6. 返回 {code: SUCCESS} 给微信告诉它不要再重试这个回调有几个必须注意的地方收到通知后先解密、验签再更新库顺序不能反必须校验out_trade_no对应订单是否存在且回调里的金额和订单金额一致更新订单状态时要用条件更新比如UPDATE orders SET statusPAID WHERE out_trade_no? AND statusPENDING防止回调重复发送时重复处理处理成功要返回{code: SUCCESS}否则微信会按一定时间间隔重试15秒/15秒/30秒/3分钟/10分钟/20分钟/30分钟/30分钟/30分钟/60分钟/3小时/3小时/3小时/6小时/6小时直到收到成功响应。5.2 页面轮询前端等结果的标准姿势后端回调做得再好小程序前端也是不知道的。最常见也最省事的做法就是小程序展示二维码的同时开一个定时器每隔3秒请求一次后端的订单查询接口。const timer setInterval(async () { const res await request.get(/api/orders/status, { orderNo }) if (res.data.status PAID) { clearInterval(timer) wx.showToast({ title: 支付成功, icon: success }) // 跳转到成功页或打印小票 } else if (res.data.status CLOSED || res.data.status EXPIRED) { clearInterval(timer) // 提示用户二维码已失效 } }, 3000)轮询频率建议3~5秒一次不要1秒一次收银高峰期多个店员同时开小程序1秒轮询会造成很大的无效请求容易把后端接口打挂。整个轮询的终止条件有两个订单变成已支付/已关闭或者二维码过期倒计时结束。除此之外页面切到后台时小程序定时器会被系统挂起回到前台要做一次立即查询避免明明已经支付了但界面还停在“待付款”。5.3 回调与轮询的前后端协作整条链路上最怕出现“轮询查到已支付但回调还没写库”的错位。举个例子用户刚完成付款轮询请求到了后端但此时微信回调还没到达或者正在解密处理中订单状态还是PENDING前端显示“未支付”用户实际上已经扣款成功。这就尴尬了。解决思路有两个后端在查询接口里做兜底如果订单状态是PENDING且下单时间距今超过一定阈值比如60秒可以主动调用微信支付查单接口GET /v3/pay/transactions/out-trade-no/{out_trade_no}以微信支付侧的结果为准刷新本地状态再返回给前端。前端给支付结果一个缓冲期不要因为一次轮询查到PENDING就给用户强反馈“支付失败”而是一直轮询到二维码过期为止。很多收银台页面倒了过期前1秒还在轮询直到拿到明确状态才切换界面。我在项目里给前端返回的状态字段固定是{ orderNo: ORD202405010001, status: PENDING, paidAmount: 100, expireAt: 2024-05-01 12:02:00 }前端拿到statusPENDING且未过期就继续等待拿到PAID就引导成功页拿到CLOSED或EXPIRED就引导重新下单。界面不要用“支付失败”这种词建议用“订单未支付成功”不然顾客在支付状态还没最终落定时看到失败容易反复重试导致重复下单。5.4 从“待付款”到“支付成功”的用户界面细节收款二维码页面虽然简单但直接影响收银效率。我提几个做得好的页面必备元素二维码下方大字显示金额让顾客扫码前心里有数避免扫完码发现金额不对产生纠纷。显示订单号和桌号/台号店员和顾客都能快速核对特别是多桌并发收款时。二维码旁边放一个倒计时比如“二维码剩余 80 秒”给顾客一个心理预期。支付成功后声音反馈很多收银环境嘈杂店员不一定盯屏幕支付成功时播放提示音非常关键。小程序里可以用wx.createInnerAudioContext放一段极短的提示音。6. 联调和上线时我在代码里补齐这些防线6.1 金额单位是分必须用整数计算微信支付所有金额相关字段的单位都是分而且要求是整数。不同语言里最常见的错误是用浮点数做乘法再转整型比如// 错误示范 int total (int)(amountYuan * 100); // 正确示范 int total BigDecimal.valueOf(amountYuan) .multiply(BigDecimal.valueOf(100)) .setScale(0, RoundingMode.HALF_UP) .intValue();用浮点数算分可能因为精度问题出现9.9元变成989分这种低级错误。在订单表里金额字段也建议直接用BIGINT存分避免所有下游系统再做一次元到分的转换。6.2 幂等out_trade_no和回调去重同一个订单号微信支付不允许重复下单。如果用户生成了二维码但没付重新点了一次收款后端如果仍用同一个out_trade_no请求Native下单微信会报“订单已存在”或“重复商户订单号”。建议每次收款都生成新订单号哪怕金额一样。回调端重复问题更隐蔽。微信支付为了保证送达会按规则重试通知。如果你的处理逻辑不是幂等的可能出现一次支付被记两次、库存扣两次的严重事故。所以更新订单状态务必用条件更新UPDATE orders SET status PAID, transaction_id #{transactionId}, paid_at NOW() WHERE order_no #{orderNo} AND status PENDING影响行数为1才说明这次更新是有效的否则说明之前已经处理过直接返回SUCCESS给微信即可。6.3 后端必须校验金额防“改价下单”“小程序下单接口直接传金额”是我提醒很多朋友的一个重要问题。收款场景下金额必须由后端从订单上下文计算出来不能相信前端传来的amount字段。打个比方顾客买了58元的套餐如果前端能随意下单传total1那顾客就可以在正式付款前先构造一个1分的订单然后扫码付掉。虽然这会少付钱而且订单内容对不上但已经形成了坏账。正确姿势是小程序下单时提交的是订单标识比如桌号、菜单快照ID、商品编码后端自己查定价表、算优惠、得出最终金额再调微信支付下单。回调时再用这个金额和微信支付返回的amount.total比对不一致就告警。6.4 测试环境与回调调试的坑Native支付联调最容易卡在回调上。微信支付notify_url要求公网可访问的HTTPS地址本地开发环境收不到回调。比较稳的做法是开发阶段用内网穿透工具把本地服务映射成公网HTTPS地址将穿透地址填到notify_url。但穿透服务的稳定性参差不齐如果回调接收断断续续排查问题会怀疑人生。我建议后端把支付回调的原始报文和完整响应日志打好一旦线上顾客反馈“付了钱但订单还是待支付”先查日志里到底有没有收到回调、解密和校验卡在哪一步。另外微信支付回调要求HTTPS证书必须是有效的测试时不要自签名证书微信支付不会信任。6.5 二维码要配合关单别留着过期订单一个订单生成二维码后顾客迟迟不付这个订单会一直挂在订单列表里。我的建议是二维码有效期120秒过期后前端提示刷新刷新时如果原订单还是PENDING后端调用微信支付关单接口POST /v3/pay/transactions/out-trade-no/{out_trade_no}/close关单后生成新订单号再走一遍下单流程展示新码。关闭订单接口只对PENDING状态的订单有效如果订单已经支付成功关单会报错。所以关单前最好先查一下支付状态或者捕获微信返回的错误码避免把已支付订单当未支付处理。7. 最后分享几个项目里的实操体会7.1 先跑通Native再想聚合做收银相关功能一开始不要急着上服务商模式或者聚合支付平台。先把微信支付原生Native跑通确认整个链路能走通再考虑要不要接服务商接口。服务商模式涉及特约商户、分账、抽佣等概念复杂度和普通商户直连不是一个量级的。很多时候原生Native已经能覆盖门店90%的需求。7.2 支付结果反馈要充分别只靠一个Toast收款场景里支付结果的反馈越强越好。视觉上支付成功页不要只弹一个wx.showToast最好有独立成功页面金额展示听觉上加一个清脆的提示音如果是在收银一体机上还可以联动打印机自动出小票。顾客扫码支付之后如果商家端和顾客端都没有即时的强烈反馈整个收银体验会显得很“虚”。7.3 这套方案还能扩展到哪里思路打通之后能玩的花样不少。比如一桌一码的升级版顾客扫桌上的小程序码进入点餐页选完菜后走wx.requestPayment。这和Native支付的定位不同但可以互补——线上点餐用JSAPI线下收银用Native。预约单付款顾客在小程序里预约服务后端提前生成一个Native支付的code_url通过小程序订阅消息或扫码方式展示顾客线下到店扫码付款。批量收款在一个页面上并列展示多张二维码分别对应不同订单收银高峰期可以多线收款这个场景我见过不少展会摊位在用。最后分享一个真实的教训第一版做这个功能时我图省事想把后端生成二维码图片的接口和小程序直接对接结果每次重新下单、刷新二维码都要重新请求一次图片高峰期后端带宽被二维码图片占掉不少。后来改成前端用weapp.qrcode生成后端只返回几百字节的code_url压力小了一个量级。支付和二维码本身都不是新东西但把它们在小程序这个载体上组合好能解决非常具体的线下经营问题。希望这篇能帮你把“小程序付款转二维码付款”的需求一步到位落地少踩几个我已经踩过的坑。
网站建设高端定制企业官网