ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信小程序付款转二维码:从Native下单到扫码支付全流程解析

微信小程序付款转二维码:从Native下单到扫码支付全流程解析 做微信小程序这行的大概率被客户问过一个问题“用户在小程序里点了付款能不能把它变成一个二维码让对面的人扫一下就能付钱”我第一次听到这个需求是在一个线下门店收银项目里顾客在小程序里下完单收银台想要弹出一张付款二维码省去传手机、搜订单、反复确认金额的麻烦。这个需求听起来简单真正落地的时候牵扯到微信支付下单方式、二维码生成方案、订单过期机制、回调验签一堆细节。这篇就把我实际趟过的方案、踩过的坑、最终跑通的代码一次性整理出来项目标题就叫“微信小程序付款转二维码付款”希望对正在做同类功能的人有点帮助。先从最基础的问题说起在微信小程序生态里把一个“付款动作”转成“二维码给别入扫”其实有两条完全不同的实现路径选错路径会导致后面整个开发方向跑偏。下面我会把两条路径的原理、代码、坑全部拆开讲还会给一张速查表方便你直接选型。1. 先把需求说清楚付款转二维码做的到底是哪件事1.1 微信支付里的两种“码”很多人会把付款码和收款码混在一起实际开发里这是两种完全不同的交互逻辑。微信支付体系里“付款码”是消费者展示给商家扫的属于被扫模式商家用扫码枪或者收银机读取你的18位数字码完成扣款这个场景对应的是微信支付的付款码支付API。“收款码”是商家展示给消费者扫的属于主扫模式消费者用微信扫一扫完成付款这个场景对应的是微信支付的Native下单也就是扫码支付或者小程序内JSAPI跳转。用户问的“小程序付款转二维码付款”绝大多数情况下要的都是第二种小程序端生成一张二维码用户拿微信扫一扫完成支付。我要先把这个概念掰扯清楚因为很多技术同学一上来就去找支付包的“付款码”接口完全走偏了。1.2 路径A经典二维码收银台路径A是最“正统”的做法小程序前端请求自己的后端服务后端调用微信支付Native下单接口微信支付返回一个code_url参数这个参数本质上就是一个指向微信支付收银台的链接。后端把code_url透传给小程序前端小程序用二维码库把这个链接画成一张二维码图片展示在页面上。用户看到这张二维码后用微信扫一扫注意是直接用微信的扫一扫不是在小程序里扫码微信会拉起来的收银台用户确认金额、输入密码支付完成。微信支付服务器异步通知你的后端你的后端修改订单状态小程序再通过轮询或者WebSocket拿到“已支付”的结果页面自动跳转。这就是我在门店收银项目里最终采用的方案。它最大的特点是扫码顾客不需要进入你的小程序扫完直接进微信支付收银台路径最短转化率最高。1.3 路径B把小程序付款页转成二维码让别人扫路径B是另一种看起来很像、实际原理不同的方案小程序把“订单确认页”对应的页面路径生成一个二维码或小程序码用户扫码后先进入你的小程序付款确认页看到订单详情、金额、商品列表再点击按钮拉起微信支付。这个方案等价于“把小程序内部页面分享到线下”。技术上主要靠微信提供的二维码生成接口wxacode.getUnlimited来实现这个接口可以生成一个携带参数的小程序码用户扫进去直接落到对应页面。路径B的优点是体验更丰富扫码用户可以确认商品明细、选择优惠券、填写备注缺点是链路更长扫码进来之后还要点击“立即支付”才能跳微信收银台多一步就会流失一部分转化率。1.4 两条路径怎么选我在实际项目里的判断标准很简单如果场景是“线下收银台、摊位收款、当面付”选路径A让用户扫完直接打开微信支付页一个动作完成不要在小程序里绕。如果场景是“朋友代付、礼物赠送、订单二次确认”选路径B因为需要让付款人先看到订单内容再决定是否支付。两种方案不是互斥的同一个订单接口可以做到后端创建订单时返回orderId、code_urlNative支付链接、小程序码图片URL前端根据用户身份决定展示哪一种。但这也意味着开发和测试成本会翻倍如果不是业务确实需要我建议先集中把一种方案跑通。维度路径A二维码收银台路径B付款页小程序码扫码入口微信扫一扫直接拉起微信支付收银台先进入小程序付款确认页需要用户进入小程序不需要需要支付链路最短较长适合场景线下收银、当面快付好友代付、订单确认、礼物赠予二维码生成方式服务端下单返回code_url前端canvas绘制服务端调用wxacode.getUnlimited生成小程序码开发复杂度中中偏高2. 技术选型前端生成二维码才是正确姿势2.1 三个方案对比确定好走路径A之后第一个技术点就是二维码图片怎么生成。我在项目里对比过三个方案方案一纯前端生成。小程序端引入weapp-qrcode这类canvas绘制库把后端返回的code_url文本画成二维码图片。好处是零后端压力、实时刷新方便、不用额外存储坏处是二维码图片只在当前页面存在如果想让这张二维码长期固定在某处反复使用还得额外做保存相册或者上传服务端。方案二服务端生成二维码图片。后端用node-qrcode、qrcode或zxing等库生成一张图片传到CDN或者对象存储返回图片URL给小程序展示。好处是二维码可以长期复用、可以批量生成、方便做数据统计坏处是增加了后端资源开销每次生成还要考虑缓存策略。方案三直接调用第三方二维码生成API。最快但受限于第三方服务稳定性而且动态二维码链接通常涉及金额我一般不推荐把核心支付链路交给不确定的第三方。最终我选择了方案一前端canvas绘制。理由很直接——二维码内容本身就是微信支付返回的code_url这个链接是一次性的、有效时间短通常两小时内有效根本没必要存储。生成、展示、用完即弃前端成本几乎为零。只有在路径B的长期固定码场景我才建议把小程序码图片上传对象存储做缓存。如果你只是做演示demo用方案三没问题但要是准备上线商用老老实实用方案一或方案二。2.2 weapp-qrcode 接入示例小程序端我用的是weapp-qrcode这个库它在GitHub开源社区很常见底层是Canvas实现接入方式非常简单。先把项目克隆下来把dist目录下的weapp-qrcode.js放到小程序的utils目录里然后在需要生成二维码的页面引入const QRCode require(../../utils/weapp-qrcode.js); Page({ data: { codeUrl: , amount: 0.00, orderId: }, onLoad(options) { this.createOrder(); }, createOrder() { // 调用后端创建订单接口 wx.request({ url: https://api.example.com/order/create, method: POST, data: { goodsId: 123 }, success: (res) { const { orderId, codeUrl, amount } res.data; this.setData({ orderId, codeUrl, amount }); this.drawQrcode(codeUrl); } }); }, drawQrcode(text) { const size wx.getSystemInfoSync().windowWidth * 0.6; const qrcode new QRCode(qrcode-canvas, { usingIn: this, text: text, width: size, height: size, colorDark: #000000, colorLight: #ffffff, correctLevel: QRCode.CorrectLevel.H }); // 保存实例后续canvasToTempFilePath会用到 this.qrcode qrcode; } });对应WXML里的结构view classpay-page view classamount¥{{amount}}/view canvas idqrcode-canvas classqrcode-canvas/canvas view classtip请使用微信扫一扫支付/view /view这个库在实际项目里表现很稳没有奇怪的依赖二维码绘制质量也不错。唯一要注意的是版本兼容老版本对小程序的canvas 2d支持不好建议直接用最新版。2.3 二维码清晰度从像素密度说起你可能想不到二维码功能开发中排查最多的问题是“图片模糊、扫不出来”。这不是库的问题而是canvas尺寸设置的问题。微信小程序的canvas默认宽高单位是px但真机上存在像素密度PixelRatio。如果你画布逻辑尺寸是200px绘制时没有乘上设备像素比那么二维码在Retina屏幕上就会被拉伸边缘发虚识别困难。解决方案是把canvas的实际分辨率乘以设备像素比同时保持CSS显示的尺寸不变。举个例子你要显示一个260px宽的正方形二维码const dpr wx.getSystemInfoSync().pixelRatio; const canvas this.selectComponent(#qrcode-canvas); // 如果使用的是canvas 2d接口 const query wx.createSelectorQuery(); query.select(#qrcode-canvas) .fields({ node: true, size: true }) .exec((res) { const canvasNode res[0].node; const ctx canvasNode.getContext(2d); canvasNode.width 260 * dpr; canvasNode.height 260 * dpr; ctx.scale(dpr, dpr); // 此时再调用二维码库绘制 });如果用的还是老版canvasweapp-qrcode库里会自己处理大部分尺寸逻辑不需要手动乘dpr。但无论哪种方式都要保证二维码两侧留有足够白边微信扫一扫对白边宽度有要求太贴边容易识别失败。3. 核心实战二维码收银台从下单到对账3.1 服务端下单拿到code_url我用的微信支付版本是V3服务端Node.js实现。前端不直接接触微信支付接口所有支付相关请求必须走后端。后端创建订单时调用微信支付Native下单接口核心请求参数大概是这些// 后端代码示意 const url https://api.mch.weixin.qq.com/v3/pay/transactions/native; const body { appid: wx1234567890abcdef, mchid: 1600000000, description: 门店订单-001, out_trade_no: 20250110123456789, notify_url: https://api.example.com/pay/notify, amount: { total: 9900, // 单位是分不能传元 currency: CNY } }; // 请求需要带微信支付要求的Authorization头、签名、平台证书等 // 实际代码需要封装签名逻辑这里不展开 const response await axios.post(url, body, { headers }); const codeUrl response.data.code_url;这一步有几个关键点第一金额单位是分不是元。前端展示时用9.90元接口传参里必须传990否则会出现1000倍的金额差错。第二out_trade_no订单号必须全局唯一一般建议用时间戳随机数用户ID的组合长度不要超过32位不要带特殊字符。第三notify_url是支付结果回调地址必须公网能访问到不能用内网地址。微信支付在用户完成支付后会往这个地址发送通知消息你的后端需要处理这个通知并修改订单状态这个环节俗称“回调”。3.2 小程序端渲染二维码后端把code_url返回给小程序端之后前端就是我在第二章写的drawQrcode方法。这里还有一个小细节二维码图片里面不能放emoji、中文、空格等特殊字符微信支付的code_url理论上都是ASCII安全字符但如果你在测试时手动拼过链接但凡多了一个空格或中文参数微信扫一扫就会报错。渲染完成后用户在页面上看到的是一张带黑色方块的图片。为了让这个页面更像“收银台”我在页面上还放了金额大字、订单编号、倒计时提示和保存二维码按钮。客户反馈这样体验很好确实比直接扔一张干巴巴的二维码图靠谱。值得注意的是微信浏览器和微信扫一扫对二维码的解析是有缓存机制的同一个二维码链接如果被扫过一次第二次扫码有时会命中旧结果尤其是调试阶段改了参数没加版本号。所以我建议生成的code_url如果允许追加参数可以加一个非敏感的随机数query来避免缓存但不要改动微信支付返回的原始code_url本身以防影响支付流程。3.3 轮询支付状态与超时刷新用户在二维码收银台页面上完成了支付前端怎么知道最稳妥的方式是后端回调通知但回调有延迟而且回调到账之后前端不会主动收到消息。实际项目中普遍采用“轮询回调兜底”的组合策略。前端在二维码展示的同时启动一个定时器每隔3秒调用一次后端查单接口startPolling() { this.pollingTimer setInterval(() { wx.request({ url: https://api.example.com/order/status?orderId${this.data.orderId}, success: (res) { if (res.data.status paid) { clearInterval(this.pollingTimer); wx.showToast({ title: 支付成功, icon: success }); wx.navigateBack(); } } }); }, 3000); }, onUnload() { if (this.pollingTimer) { clearInterval(this.pollingTimer); } }轮询间隔不要太短3秒是比较合适的平衡点太短会给后端造成无谓压力太长会让用户觉得支付成功之后反馈太慢。订单过期机制也很重要我没有直接用微信支付的两个小时有效期而是在创建订单时设置了一个更短的过期时间通常是5分钟或10分钟页面显示倒计时超时后二维码自动置灰用户需要点击“刷新二维码”重新生成订单重试。这样做的原因很朴素避免用户拿着截图二维码在几小时后跑来说“我没付钱但二维码扫不了了”之类的扯皮纠纷。3.4 金额防篡改是底线这是一个绝对不能偷懒的地方。前端在任何情况下都不应该自己决定支付金额哪怕你觉得“只是改个数字而已”。正确做法是用户在小程序里发起付款请求时后端根据购物车商品ID、数量、优惠券信息重新计算一遍订单金额生成订单并保存到数据库。前端拿到的是订单号orderId和已算好的金额amount二维码内容只跟orderId或者code_url绑定绝不能在二维码里直接拼接用户传来的金额字段。万一有人截获了code_url链接把里面表示金额的参数改了怎么办不用担心微信支付Native下单返回的code_url真的就是微信支付官方生成的跳转链接它不包含你自定义的金额参数用户扫码后打开的收银台页面展示的金额是在创建订单时提交给微信支付的total不可能被中间人修改。但你自己后端创建的订单金额如果没做二次校验用户在小程序内部伪造一个金额提交给后端下单后端又没查那就会出大事。所以服务端创建订单的逻辑必须是只信任商品ID和数量金额永远后端算。3.5 回调验签与对账支付回调是资金流转的关键环节我不敢马虎。微信支付V3的回调会带HTTP头、时间戳、随机串、签名等一堆验签信息后端收到通知后先验签再用AES-GCM对resource对象解密拿到里面的订单号和实付金额然后拿着订单号去数据库查订单比对金额是否一致、订单是否已支付、商户号是否匹配。只有全部通过才更新订单状态为已支付然后返回200应答给微信支付。如果验签失败或者金额对不上必须返回失败让微信支付过段时间重发通知。这块我一开始偷懒只验了订单号没验金额后来对账发现有一笔单子金额对不上排查半天发现是测试时有人改了数据库。从那以后我就把验签逻辑当成支付系统的防御底线所有回调一律走完整验签链路不通过直接拒绝。4. 进阶把付款确认页生成二维码分享给好友4.1 小程序码与普通二维码的区别路径B用到的不是普通二维码而是微信小程序码。很多人分不清两者的区别普通二维码是通用的编码格式任何App都能扫解码后是一个URL或文本小程序码是微信自家生成的带圆形Logo的码扫码后用微信解释直接打开一个小程序的指定页面。对路径B而言如果你在页面上展示一个普通二维码链接用户微信扫一扫也能通过配置“扫普通链接二维码打开小程序”来进入页面但前提是你得在小程序后台配置对应的二维码链接规则而且每次都要处理链接映射比较麻烦。更省心的是直接用wxacode接口生成小程序码它天然绑定小程序扫完就直接打开指定页面。4.2 用服务端wxacode.getUnlimited生成付款页小程序码wxacode.getUnlimited是微信提供的一个服务端接口需要传小程序的access_token调用后返回的是一张图片的二进制Buffer。我在服务端封装了一个方法生成后直接传给前端以临时图片路径展示或者上传到对象存储拿永久URL。// 后端获取access_token并生成小程序码 async function getMiniProgramCode(orderId) { const token await getAccessToken(); const response await axios.post( https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token${token}, { scene: orderId${orderId}, page: pages/pay/confirm, check_path: false, env_version: release }, { responseType: arraybuffer } ); // 返回Buffer可上传到COS/OSS return response.data; }这里最关键的是scene参数它的值是页面路径上携带的参数长度限制32个可见字符而且只能由英文、数字和部分符号组成不能传中文。付款页拿到的orderId是纯数字或字母组合完全够用。page参数则指定用户扫码后要进入的小程序页面路径不传就默认进入首页。小程序端怎么展示这张图片如果服务端返回的是Buffer可以直接存临时文件const fs wx.getFileSystemManager(); const filePath wx.env.USER_DATA_PATH /pay-qrcode.png; fs.writeFile({ filePath, data: buffer, encoding: binary, success() { that.setData({ qrcodePath: filePath }); } });如果服务端已经传到了CDN直接在image标签的src填上URL就行。4.3 前端展示、长按识别与保存相册路径B在小程序页面里展示的方式和普通图片一样用户在小程序里可以直接长按图片识别小程序码。这个功能不需要额外开发image标签默认支持唯一要注意的是不能把image改成长按编译事件被拦截的状态。如果你的需求是让用户把这张码保存到手机相册然后打印出来贴在线下物料上可以用canvas绘制一张包含扫码指引文字的营销图然后调canvasToTempFilePath导出图片再调saveImageToPhotosAlbum保存。handleSaveQrcode() { wx.canvasToTempFilePath({ canvas: this.canvasNode, success: (res) { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success() { wx.showToast({ title: 已保存到相册, icon: success }); } }); } }); }注意保存相册需要用户授权scope.writePhotosAlbum用户如果拒绝过需要在fail回调里通过openSetting引导用户去设置页重新打开权限我在下一章会把这几个授权坑一起讲。5. 常见问题与踩坑速查5.1 二维码扫不出来或者太模糊这个我前面详细说过九成是canvas像素密度问题。建议画布尺寸不低于200px乘以dpr之后再作为实际绘制尺寸同时给二维码四周至少留出10px白边。还有一个小坑weapp-qrcode这个库如果传入的text太长生成的二维码会非常密集微信扫一扫经常识别失败。微信支付code_url通常不长如果你是自己拼接的自定义链接建议控制在100个字符以内超过这个长度就该考虑缩短链接或用后端生成图片。5.2 canvas白屏与基础库兼容小程序从基础库2.9.0开始引入了Canvas 2D新接口老的canvas组件和新的接口混用会出问题。具体表现就是二维码绘制不出来页面只有背景但canvas区域空白。我查这类问题第一件事是看控制台有没有Canvas相关的报错第二件事是确认自己用的是canvas type2d还是默认的普通canvas。如果你用的是新版canvas 2dweapp-qrcode老版本可能不支持需要传入node、ctx自行绘制如果你用的是普通canvas就保持usingIn的写法。实在兼容不过来最省事的方法是用页面内嵌图片代替canvas让后端生成二维码图片返回给前端虽然多一次网络请求但彻底避开canvas兼容性坑。5.3 生成小程序码报错路径B中调用wxacode.getUnlimited时报错常见有这几个41030invalid page说明page路径不存在或者页面没有配置为小程序页面41031invalid scenescene参数不符合要求比如带了太多字符或者含有非法字符40001access_token无效一般是token过期了需要重新获取。小程序码接口一天有调用次数限制流量主类目申请之后会提高但个人主体限制很严格。上线前一定要在后台确认调用量充足否则活动高峰期会直接打爆。5.4 保存图片到相册失败保存相册失败典型流程是第一次调saveImageToPhotosAlbum用户点了拒绝第二次再调就直接走fail回调弹不出授权框。解决办法是在fail里判断错误信息如果包含“auth deny”或“authorize”就调用wx.openSetting让用户手动打开相册权限。另外小程序必须在《微信小程序用户隐私保护指引》里声明需要用到“相册仅写入权限”的用途不然新版本微信会直接拦截授权弹窗。这个问题在2023年后审核越来越严我身边好几个开发者都中招了。5.5 用户已支付但页面没刷新这种情况不是支付失败而是状态同步延迟。我在项目里遇到过两次第一次是回调地址没通微信支付通知发不出去后端没有成功收到支付成功事件订单一直停在待支付状态。排查方式是去微信商户平台查看回调日志确认notify_url是否公网可达、返回是否成功。第二次是前端轮询逻辑写错了在onShow里启动轮询但用户从微信收银台回到小程序时走的是onShow而不是onLoad导致定时器没有恢复页面一直卡在未支付界面。解决方案是统一在onShow里初始化轮询在onHide里清理定时器并增加一次进入页面时的订单状态主动查询。6. 上线前的检查与体验细节6.1 小程序后台与微信支付配置上线前请把这几个配置项逐项确认小程序已关联微信支付商户号且在商户平台开通了Native支付小程序后台的服务器域名配置包含你的API域名支付回调notify_url配置正确且支持HTTPS如果你的路径B用到了“扫普通链接二维码打开小程序”需要在小程序后台配置对应规则如果用了wxacode.getUnlimited确认调用量配额足够。这些配置任何一项没做好线上就会出现“明明代码没问题但支付就是不通”的诡异问题。6.2 隐私协议必须提前声明2023年之后上线的微信小程序隐私协议审核变得很严格。如果你在代码里调用wx.saveImageToPhotosAlbum、wx.getLocation、wx.chooseImage等接口必须在小程序管理后台填写《微信小程序用户隐私保护指引》声明具体的接口使用目的。否则审核会被拒而且真机运行时接口也会被权限策略拦下来。很多开发者都是在提审被拒之后才回头补建议开发第一天就顺手把隐私声明填好。6.3 体验优化三件套倒计时、音效、角标最后说几个提升二维码收银台体验的小细节。支付页面一定要有倒计时。用户盯着二维码看的时候如果没有倒计时会觉得页面“死”了有了倒计时用户能在最后几秒主动操作“刷新二维码”体验会好很多。倒计时建议用纯数字环形进度条实现难度不高效果却很明显。支付成功后的反馈要强。我习惯在轮询到“已支付”时先做一次震动wx.vibrateShort再弹对角线动画提示有条件的话还可以播放一段短音效。线下场景比较嘈杂震动和音效是顾客感知支付成功的重要方式。二维码区域不要放任何会遮挡码区的弹窗或浮层。有些页面为了营销会在右上角挂一个优惠券悬浮按钮很容易挡住二维码的一部分。微信扫一扫对变形和遮挡很敏感宁可按钮小一点也不要影响扫码区域。整个项目做完我最大的感受是把付款转成二维码这件事二维码本身不是难事真正复杂的永远是订单状态、金额安全、回调对账这些看不见的逻辑。如果你正打算做这个功能先把支付状态机和回调验签设计好再去调UI这样才不会上线之后天天被对账问题折磨。希望这份踩坑记录能让你少走几步弯路。
返回列表