news 2026/10/1 20:19:52

微信小程序付款转二维码:canvas 2D 生成与支付落地全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信小程序付款转二维码:canvas 2D 生成与支付落地全指南

这需求我太熟悉了。做小程序电商、线下点餐、活动票务的同学,基本都会碰到同一个产品需求:用户在小程序里下了单,但付款方式要从“小程序内直接调起支付”变成“生成一个二维码,让用户自己拿去扫”。最常见的场景就是线下扫码购、面对面收款、活动签到付费,甚至有些人会想在小程序里生成一个聚合收款码,发给客户扫完付款。我最初接手这个需求时也踩了不少坑,尤其是二维码在微信小程序里的生成方式和普通H5完全不同,不能直接套用 qrcode.js 的 DOM 方案。这篇就把我做小程序付款转二维码付款的完整思路、代码实现和踩坑记录整理出来,给同样遇到这个需求的朋友一份可以直接上手的参考。

1. 先搞明白:付款转二维码到底解决的是什么问题

1.1 两种典型业务场景,别一上来就写代码

我接到这个需求之后第一件事不是开IDE,而是找产品确认场景。因为“付款转二维码”这句话在不同业务里,落地形态差得非常远。

第一种是“订单生成二维码,别人扫了替他付款”的转赠/代付场景。用户自己选好商品、生成订单,但不想或者不能自己付款,系统把一个订单标识编码成二维码,他把二维码发给朋友,朋友微信扫一扫,跳转到付款确认页完成支付。这种场景核心是把“订单信息”转成二维码。

第二种是“收银台被扫”场景。用户到店消费完,在商家的小程序里核对账单后,屏幕展示一个二维码,用户用自己的微信扫码,弹出的其实是“向商家付款”的确认页或者商家关联的收款账户,扫码的人完成付款。这种场景核心是把“收款身份/支付凭据”转成二维码。

还有一种比较边缘但很常见的情况——你的小程序本身没有微信支付资质,或者只是个人开发者的工具型小程序,不能直接调起支付。你要做的其实是把一笔订单标记成“待线下支付”,然后生成一个二维码包含订单号、金额、收款方信息,由另一个有支付能力的主体(比如商户App、服务号H5)去承接实际付款流程。这种情况下,你生成二维码的目的就是“跨端传递支付上下文”。

搞清楚场景再设计技术方案,否则你写出来的二维码很可能是一个“看起来能扫、扫完没反应”的废码。

1.2 为什么不能直接把小程序码当付款码用

很多非技术人员会问:小程序不是自带“小程序码”吗?把那个码发给用户扫不就行了?

原理上可以,但实际行不通。小程序码扫完之后进的是小程序页面,它需要用户先登录、再找到那笔订单、再手动支付,链路太长。而且如果你需要的是“指定订单、指定金额、一次性有效”的动态支付码,小程序码根本做不到。小程序码只是标识小程序页面路径,它不携带订单状态、金额限制、过期时间这些动态数据的语义。

所以付款转二维码本质要做的,是用二维码承载一个“统一支付链接”或者“支付参数串”,这个链接可以是微信支付Native支付的 code_url,也可以是你自己服务端生成的短链,扫码后跳到支付确认页。做技术方案之前,先把这个逻辑捋清楚,后面才不会做偏。

1.3 核心链路设计:小程序、服务端、扫码端三方协作

我在动手前画了一条完整链路(这里不贴图,用文字描述):小程序端确认订单后,请求服务端创建支付单;服务端调用微信支付下单接口拿到支付链接或者支付参数;服务端返回一个该订单唯一关联的支付token;小程序拿到token后生成二维码;扫码端使用微信扫一扫,微信解析二维码跳转到支付链接;支付完成后,微信支付回调服务端;小程序通过轮询或者微信支付结果通知查询订单状态,刷新页面。

这里最关键的一点是:二维码里到底装什么。我建议不要直接把完整支付链接塞进二维码,而是通过服务端把订单信息编码成短token,然后拼成一条短链。原因有三个:二维码容量有限,链接太长会降低二维码密度导致扫码识别率下降;完整支付链接可能包含签名参数,直接暴露在二维码里有安全风险;短链便于后端在扫码跳转时做统计、风控和订单状态校验。

2. 小程序里生成二维码的技术选型,为什么我最终选了 canvas 2d

2.1 H5 的 qrcode.js 方案在微信小程序里为什么直接报废

在普通网页里生成二维码,最常用的做法是引入 qrcodejs,新建一个 div,再调用库把二维码画成 canvas 或者直接输出图片标签。但是在微信小程序里,这套东西完全不适用。小程序没有 DOM 节点概念,qrcodejs 依赖 window 和 document 来创建 canvas 元素,在小程序环境里这两个对象都不存在,运行到一半就会报错。

另一个思路是用后端生成二维码图片返回给前端展示。这个方案我早期也用过,稳定但有两个痛点:一是每张二维码都要走一次网络请求,弱网环境下图片加载慢,影响用户付款体验;二是后端生成图片需要考虑存储和过期清理,加了一堆运维成本。如果你的列表页或弹窗里要同时展示很多二维码,这个方案会非常吃力。

所以最终方向锁定为:小程序前端本地生成二维码。

2.2 本地生成方案横向对比

我在实测阶段对比过三类前端生成方案:纯JS 移植库、第三方 UI 组件、原生 canvas 绘制。

纯JS移植库里最知名的是 weapp-qrcode,这个库专门为小程序环境做了适配,去掉了对 DOM 的依赖,直接把二维码绘制逻辑改写成小程序 canvas API。它的优点是轻量,不需要额外 UI 框架,也不依赖服务端。缺点是它最初适配的是旧版 canvas 接口,新版 canvas 2D 接口需要一些额外的初始化处理,这个我后面详细讲。

第三方 UI 组件比如 tki-qrcode,优点是封装完整,插件市场直接安装就能用,组件化调用,代码量最少。缺点是你为了一个二维码功能引入一个没有长期维护的第三方组件,一旦基础库升级可能被动背锅,我试过其中一个组件在 iPhone 14 Pro 上画出来的二维码密度异常,排查了半天才发现是组件内部还在用已经废弃的旧版 canvas API。

原生 canvas 绘制方案最可控,性能也最好,但需要你自己实现二维码编码算法(包括数据编码、纠错码生成、矩阵排列、掩码处理这一整套东西),工作量非常大。除非你是想彻底掌握二维码原理或者有其他特殊需求,否则我不建议从零造轮子。

我的结论是:用 weapp-qrcode 作为核心绘制库,手动配合新版 canvas 2D 接口初始化。既能拿到本地生成的优势,又不需要为团队引入维护风险大的第三方组件。

2.3 新旧 canvas 接口的兼容问题

这里必须单独说一截,因为这个坑几乎让所有人翻车。小程序早期提供的 canvas 接口是 wx.createCanvasContext,导出图片用 wx.canvasToTempFilePath,这套接口已经在基础库 2.9.0 开始被官方标记为“不推荐使用”,新项目建议使用 canvas 2D 接口。

为何这个切换影响很大?因为 weapp-qrcode 的老版本只支持旧接口,而很多网上的教程也还停留在旧接口时代。如果你照葫芦画瓢把旧代码粘贴到新项目里,懒加载模式下会出现画布空白、画完就消失等诡异问题。

我在项目里统一做了封装,底层使用新版 canvas 2D 接口,通过 wx.createSelectorQuery 获取 canvas 节点,然后调用 node.getContext('2d') 拿到 2D 渲染上下文,再传给 weapp-qrcode 内部的绘制逻辑。后面给的代码里我会完整展示这套封装,拿去就能直接用。

3. 完整实操:从支付参数到付款二维码落地

3.1 服务端返回什么数据,前端要拿什么

先说服务端要做什么。小程序点击“生成付款二维码”后,请求服务端接口,服务端逻辑大概是:校验用户身份和订单归属;核对订单未支付且未过期;调用微信支付下单接口,申请预支付交易单;拿到微信返回的 code_url 参数(Native 支付的核心字段),这个 code_url 就是扫码后直接拉起支付确认页的链接;把 code_url 和你自己的订单 token 绑定,返回给小程序。

前端真正需要的其实就是一个字符串:要么是完整的 code_url,要么是你自己拼接的支付跳转短链。我比较推荐后者:服务端把订单号、金额、收款方ID 编码成一个 token,然后生成 https://你的域名/pay/{token} 这样的短链返回给前端。二维码内容就算被别人拿到,他也只能在这个短链里做有限操作,核心支付数据不会直接暴露。

还有一个容易被忽略的点:服务端要给每笔订单设置二维码过期时间。微信支付 Native 支付的 code_url 有效期一般是2小时,如果你自己拼短链,也要在服务端维护过期时间,过期后扫码跳转提示“订单已失效”。前端拿到这个有效期后,要在二维码下方展示倒计时,时间到了自动销毁二维码并刷新。

3.2 前端核心代码实现(可直接复制改造)

下面这段是我封装好的二维码生成组件核心逻辑,适配新版 canvas 2D 接口,使用时传入一个 DOM id 和二维码内容字符串即可。

// utils/qrcode.js import QRCode from './weapp-qrcode'; function drawQrcode(option) { const { canvasId, text, size, success } = option; // 新版canvas通过SelectorQuery获取节点 const query = wx.createSelectorQuery(); query.select('#' + canvasId).fields({ node: true, size: true }) .exec((res) => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = wx.getSystemInfoSync().pixelRatio; // 设置canvas的实际渲染尺寸,避免在高清屏上出现模糊 canvas.width = size * dpr; canvas.height = size * dpr; ctx.scale(dpr, dpr); // 调用weapp-qrcode绘制二维码 QRCode({ ctx, text, width: size, height: size, correctLevel: QRCode.CorrectLevel.M, // 可以加logo模块,我这里先跳过 callback(qrcode) { if (success) success(qrcode); } }); }); } export default drawQrcode;

画布组件侧写法注意,新版 canvas 需要设置 type="2d" 和 id 属性,不能用旧版的 canvas-id。样式尺寸直接用 css 控制,但内部绘制尺寸要用上面的代码乘以 dpr 做适配,否则会模糊。

<view class="qrcode-box"> <canvas type="2d" id="payQrcode" style="width: 400rpx; height: 400rpx;"></canvas> </view>

小程序部分调用:

import drawQrcode from '../../utils/qrcode'; Page({ onLoad(query) { const orderNo = query.orderNo; this.generatePayQrcode(orderNo); }, generatePayQrcode(orderNo) { wx.showLoading({ title: '生成中' }); wx.request({ url: 'https://api.example.com/pay/qrcode', data: { orderNo }, success: (res) => { const { payUrl, expireAt } = res.data; wx.hideLoading(); // 画二维码 drawQrcode({ canvasId: 'payQrcode', text: payUrl, size: 160, // 绘制区域逻辑像素 success: () => { this.startCountdown(expireAt); this.startPolling(orderNo); } }); } }); } });

这里有个细节,size 参数我传的是 160,这是 canvas 内部的逻辑尺寸,和400rpx样式尺寸是两回事。内部逻辑尺寸决定了二维码矩阵的渲染精度,过小会导致二维码太密扫不出来,过大浪费性能,实测常规下单场景 160-200 足够。

3.3 二维码弹窗展示的交互细节

支付二维码一般不会直接放在页面上,而是通过弹窗展示,用户完成支付后自动关闭。弹窗组件我用的是自定义半透明遮罩加居中卡片,这有一个好处:弹窗里放 canvas 画布不会有层级问题。

千万别用 wx.showModal 做二维码弹窗,它不支持自定义内容;也不要直接在 scroll-view 里放 canvas 画长页,canvas 天然是原生组件,层级最高,滚动过程中会出现悬浮和撕裂问题。虽然新版 canvas 2D 不再是原生组件(它走同层渲染),但在 iOS 的 web-view 里仍然会遇到兼容问题。

弹窗显示之后,要在底部放两个按钮:一个是“保存二维码/分享”,一个是“刷新二维码”。保存功能我后文专门讲实现。刷新按钮是为了配合过期逻辑,用户超时未支付可以主动换一个新码。

3.4 订单状态同步:轮询的正确写法

二维码展示出来后,小程序端需要持续监听支付结果。做移动端支付,最稳妥的方案就是后端收到微信支付异步通知后,小程序再通过轮询订单状态接口拿到结果。轮询不能用 setInterval,因为 setInterval 即使在上一次请求还没返回时也会继续触发定时器,出现请求堆积。

我比较推荐用 setTimeout 递归:

startPolling(orderNo) { if (this._polling) return; this._polling = true; const poll = () => { wx.request({ url: 'https://api.example.com/order/status', data: { orderNo }, success: (res) => { const { status } = res.data; if (status === 'PAID') { this._polling = false; wx.showToast({ title: '支付成功' }); setTimeout(() => { this.closeQrcodeModal(); wx.redirectTo({ url: '/pages/order/detail?orderNo=' + orderNo }); }, 1000); return; } if (status === 'CLOSED') { this._polling = false; wx.showModal({ title: '提示', content: '订单已关闭' }); return; } // 未支付,继续轮询,间隔2秒 setTimeout(poll, 2000); }, fail: () => { // 网络错误,继续轮询,间隔放长 setTimeout(poll, 4000); } }); }; poll(); }

轮询结束后,一定记得在页面 onUnload 和 onHide 里清理标志位,否则从支付结果回调返回页面时会重复开启新的轮询。

4. 踩坑复盘:每个坑都是钱买来的经验

4.1 canvas 画出来是空白,或者是模糊的马赛克

这个现象在新旧接口混用时代非常常见。我排查这类问题有一个固定套路:先看开发者工具是否有报错,确保 weapp-qrcode 引入路径正确;检查 canvas 节点是否成功获取,在 exec 回调里打印 canvas 对象是否存在;然后确认 canvas 的 width/height 是否被正确设置为大于0的值;最后检查页面的 canvas 是否渲染完成。如果 canvas 放在弹窗里,而弹窗初始状态是 display:none,SelectorQuery 获取节点时可能拿到的是空节点。

弹窗里的 canvas 必须在弹窗显示后再绘制,不能在 wx:if 控制显示的同时立刻去拿节点。我的处理方式是:弹窗显示动画结束后触发一个事件,再在这个事件回调里调用生成二维码函数。

模糊问题更直接,就是没有设置 canvas.width 为 css 尺寸乘 dpr。在 iPhone 上 dpr 是3,如果不乘3,画出来的二维码在物理屏幕上就只有逻辑尺寸三分之一的大小,看起来就是糊的。

4.2 二维码生成出来了,但总是扫不出来

大部分情况是二维码的纠错级别设置太低,或者尺寸太小。weapp-qrcode 支持 L、M、Q、H 四个级别,级别越高容错能力越强,但图案越密集。我实测在手机扫码场景,M 级别最均衡。尺寸方面,canvas 内部绘制尺寸低于 120 像素时,图案密集度会明显上升,用微信扫一扫在弱光或者纸张有纹理的情况下识别率很低。

还有一个高频问题:二维码内容里包含中文字符或者特殊字符。二维码对UTF-8编码的中文支持没问题,但某些老二维码生成库默认使用 ISO-8859-1 编码,中文就会变成乱码,扫码后跳转链接直接 break。所以我建议二维码内容固定为 ASCII 字符集的短链,从源头规避。

4.3 用户长按保存二维码,发现保存的是空白图

这是最容易让测试同学崩溃的一个问题。小程序里的 canvas 不是普通图片,用户长按弹出来的菜单里没有“保存图片”选项,只能通过代码调用 wx.canvasToTempFilePath 把画布导出成临时图片,再交给用户保存或者转发。

这里有一个隐患:新版 canvas 2D 接口下,wx.canvasToTempFilePath 需要传 canvas 对象,而不是 canvasId。老接口传 canvasId 就能用,新接口传了 canvasId 反而导出空白。正确做法是导出前先通过 SelectorQuery 拿到 canvas 节点对象,再传给 canvasToTempFilePath 的 canvas 参数。

exportQrcodeImage() { const query = wx.createSelectorQuery(); query.select('#payQrcode').fields({ node: true }) .exec((res) => { wx.canvasToTempFilePath({ canvas: res[0].node, success: (r) => { wx.saveImageToPhotosAlbum({ filePath: r.tempFilePath, success: () => wx.showToast({ title: '保存成功' }) }); } }); }); }

4.4 轮询导致服务端接口被刷爆

上线第一天,我就发现下单量涨上去之后服务器的支付状态查询接口 QPS 飙升。排查发现是多个页面同时开启了轮询,同一个用户开了两个订单页,每个页面都在每2秒请求一次订单状态接口,稀疏到极值时叠加了支付回调统计,服务端压力剧增。

解决思路有三层:页面级控制,同一个页面在 onShow 时检查 _polling 标志位,正在轮询就不重复发起;订单级节流,服务端对同一订单的查询接口做 3 秒缓存,前端频繁查也只回一个响应;环境变量控制,小程序切后台时用 wx.onHide 暂停轮询,回到前台再恢复,毕竟用户切到微信扫码后如果一直轮询,订单状态接口会白白跑很多无效请求。

4.5 已支付成功后,二维码仍然能扫出来

这是业务上的严重漏洞。有些用户支付成功后没有立即关闭弹窗,或者支付成功后返回订单页,这个时候二维码还在,如果被旁边的朋友扫走,微信会提示该订单已支付无法重复支付。虽然是安全兜底,但体验很差。

我在发二维码时会在服务端记录一个“已支付”状态,前端轮询到支付成功后不要立刻销毁二维码,而是先把二维码内容变成一个“核销完成”的占位图,或者直接调用支付结果页遮挡。我这里直接在弹窗里加了遮罩层,显示“已完成支付,请勿重复扫码”。

4.6 不同手机屏幕上的显示问题

canvas 画布尺寸我一开始写死 400rpx,但部分安卓机型上 400rpx 对应的像素尺寸小于 canvas 内部逻辑尺寸,导致二维码被裁剪。这个问题是在做兼容测试时发现的。后来我统一用 CSS 的 flex 布局控制画布外层容器宽度,让 canvas 的 css 尺寸跟随容器自适应,而不是写死 rpx 值。CSS 画布宽度设为容器宽度的百分比,内部尺寸通过 getBoundingClientRect 动态获取。

drawQrcode({ canvasId: 'payQrcode', text: payUrl, size: Math.floor(containerWidth * dpr), ... });

5. 扩展:二维码付款的更多玩法

5.1 动态刷新二维码

如果业务要求每笔订单金额会变,或者同一个码只能使用一次,可以做成动态二维码:每次点击刷新按钮,服务端重新生成一个支付 token,旧 token 立即失效。核心是让服务端支持 token 的撤销接口,前端刷新时先调用撤销接口再生成新的支付码。

5.2 带 Logo 的专属收款码

很多商家希望二维码中间放自己的店铺 Logo。weapp-qrcode 画完二维码之后,可以在 canvas 中心再绘制一个图片或文字。注意 logo 区域大小不能超过二维码整个矩阵的 1/4,否则会遮挡纠错区域导致扫码失败。我一般取总尺寸的 1/5 作为 logo 宽度。

5.3 二维码内容支持多种支付渠道

如果扫码用户可能没有微信支付,只支持支付宝,可以考虑二维码内容链接到一个 H5 中转页,在这个页面解析浏览器 UA 后,自动跳转到对应的支付渠道。这种方案在技术上有点绕,需要处理好不同浏览器的支付唤起限制,但业务兼容性确实强很多。这个方向我还在尝试,等稳定了再单独写一篇分享。

回到付款转二维码这件事本身,我最大的感受是:需求听起来简单,真正落地时细节比想象多得多。好在核心方案是成熟的,你只需要把 canvas 2D 的适配、二维码内容规范、轮询逻辑这三点做扎实,这个功能就能跑得又稳又顺。如果照着这篇做完还有卡壳的地方,大概率是 canvas 新老接口或者弹窗渲染时序的问题,可以按我前面给的排查顺序从前往后捋一遍,应该能找到问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 20:19:35

C语言函数从入门到实战:声明、传参、指针与递归

学C语言学到“函数”这一章&#xff0c;很多人第一次有了“编程不是写作文&#xff0c;而是搭积木”的感觉。前面变量、循环、数组还能顺着写&#xff0c;到了函数突然冒出声明、定义、形参实参、传值传址、局部变量静态变量&#xff0c;甚至还有函数指针和回调&#xff0c;一套…

作者头像 李华
网站建设 2026/10/1 20:19:24

ABP集成模块如何融入ASP.NET Core:从原理到PDF导出实战

ABP框架的ASP.NET Core集成模块&#xff0c;听起来有点绕&#xff0c;但它背后对应的是一个非常具体的类&#xff1a;一个继承自AbpModule的类。很多人第一次接触ABP&#xff0c;看到项目里一堆Module结尾的类&#xff0c;第一反应往往是“这是什么东西”&#xff0c;后来才明白…

作者头像 李华
网站建设 2026/10/1 20:17:17

# 智诺方AI|开题报告也会查AIGC?开题阶段文本优化思路

智诺方AI&#xff5c;开题报告也会查AIGC&#xff1f;开题阶段文本优化思路&#xff0c;智诺方ai官网www.znfai.cn 微信公众号搜一搜 智诺方ai 很多同学只关注毕业论文终稿的查重和AIGC检测&#xff0c;却忽略开题报告、中期检查这些前置材料。实际上&#xff0c;不少高校在开题…

作者头像 李华
网站建设 2026/10/1 20:17:16

阿里通义千问,彻底爆了!(本地部署+实测)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 20:17:12

ISP标定-NR标定(Noise Reduction,降噪校准)

NR标定&#xff08;Noise Reduction&#xff0c;降噪校准&#xff09;功能说明降噪校准&#xff08;NR&#xff09;是通过识别并抑制图像中的随机噪声&#xff0c;提升图像的清晰度和质量。NR结合空间降噪和时间降噪算法&#xff0c;在保持图像细节的同时&#xff0c;有效减少由…

作者头像 李华
网站建设 2026/10/1 20:16:36

工业以太网温湿度传感器的架构设计与工程落地

1. 这不是“连个传感器”的事&#xff1a;工业以太网温湿度感知层的真实战场你手头那台标着“支持Modbus TCP”的温湿度传感器&#xff0c;真能直接插进车间交换机就跑起来&#xff1f;我见过太多项目——PLC工程师说“协议没问题”&#xff0c;电气工程师说“供电已预留”&…

作者头像 李华