
简介这是一套基于微信小程序平台的二维码支付与识别系统源码面向希望快速搭建线上线下收款工具的商户、个人开发者及小程序学习者。系统整合了二维码生成、扫描解码、支付码创建、收款码制作、条形码识别、图像处理与支付接口集成等模块可用于店铺前台、广告牌、名片等场景的快捷收款也适合作为移动支付类课程设计或小程序练手项目。压缩包共18个文件约61KB以js逻辑脚本、wxss样式、json配置、wxml页面结构为主另含png图标素材、说明文档与README目录按pages、utils、images等模块划分qrcode.js与util.js承担核心编解码与工具函数。目前已有87人学习下载。读者可从中获取完整的二维码生成与识别实现思路、小程序页面组织方式、支付码与收款码的创建流程以及图像优化提升扫描准确率的处理参考便于二次开发或集成到自有收款场景中。1. 二维码支付与识别系统从扫码到收款一个小程序怎么把整条链路跑通线下收款台前贴着静态收款码顾客扫完还要手动输入金额商户想做个能自动带金额的动态码又不想接一堆硬件。这个场景下微信小程序做二维码支付与识别系统是性价比很高的方案小程序负责展示收款码、调起相机扫码、解析二维码内容后端负责生成带参数的支付码和对接支付接口。整套链路的核心技术点就四个——二维码生成、二维码扫描、条形码识别、支付接口集成。适合谁有微信小程序开发基础、想给商户做收款工具或个人收款助手的开发者。这篇笔记按「概念选型 → 生成与扫描实现 → 支付接口对接 → 避坑 → 进阶验证」的顺序把每一步的命令、参数和翻车点讲清楚新手能照着跑熟手能直接看边界。2. 二维码生成与扫描的技术选型为什么不用原生 API 硬扛2.1 二维码生成前端画还是后端出图二维码生成有两条路。第一条是纯前端用 canvas 画微信小程序里常见做法是引入一个轻量 QR 码库把字符串转成点阵再绘制到 canvas 上。第二条是后端生成图片小程序用image组件加载。两条路的差别不在能不能用而在参数控制权。前端生成的好处是零网络延迟改内容即时重绘适合「收款码内容随金额变化」这种高频刷新场景。坏处是小程序包体积会涨而且 canvas 在不同机型上的像素对齐有玄学偶尔出现码边缘模糊导致扫不出来。后端生成的好处是图片质量稳定、可以缓存、可以带 logo坏处是每次改金额都要请求一次接口。我一般这样分静态收款码商户固定码走后端生成一次存 CDN动态支付码带订单号、金额走前端生成减少接口往返。下面是一个前端生成的最小实现思路用 canvas 2D 接口绘制。// 小程序页面内生成二维码点阵并绘制到 canvas // 依赖一个纯 JS 的 QR 编码函数输入字符串返回二维 0/1 矩阵 const qr require(../../utils/qr-encoder.js); Page({ data: { payContent: }, onReady() { this.drawQr(https://pay.example.com/order?id1001amt9.90); }, drawQr(content) { const matrix qr.encode(content, { errorCorrectLevel: M }); const ctx wx.createCanvasContext(qrCanvas); const size 220; // 画布逻辑尺寸单位 px const count matrix.length; // 矩阵行列数决定单格边长 const cell size / count; ctx.setFillStyle(#FFFFFF); ctx.fillRect(0, 0, size, size); ctx.setFillStyle(#000000); for (let r 0; r count; r) { for (let c 0; c count; c) { if (matrix[r][c]) { // 加 0.5 偏移避免相邻格之间出现白缝 ctx.fillRect(c * cell, r * cell, cell 0.5, cell 0.5); } } } ctx.draw(); } });逻辑说明qr.encode把字符串转成布尔矩阵errorCorrectLevel控制纠错等级L/M/Q/H 对应约 7%/15%/25%/30% 的容错。收款码建议用 M兼顾密度和容错。cell 0.5是血泪经验不加的话部分安卓机渲染时格子之间会有 1px 白线扫码识别率下降。参数上size不要低于 200低于这个值在 5.5 寸屏上单格小于 2px相机对焦稍慢就扫不出。2.2 二维码扫描wx.scanCode 的能力边界微信小程序扫码用wx.scanCode这是官方 API不需要额外权限申请但有几个参数必须搞清楚。wx.scanCode({ onlyFromCamera: false, // false 允许从相册选图true 只允许相机 scanType: [qrCode, barCode], // 同时支持二维码和条形码 success(res) { // res.result 是扫码内容字符串 // res.scanType 是实际识别到的类型如 QR_CODE 或 EAN_13 console.log(扫码结果, res.result, res.scanType); this.handleScanResult(res.result); }, fail(err) { // 用户取消也会进 fail需要区分 if (err.errMsg.indexOf(cancel) -1) { wx.showToast({ title: 识别失败请重试, icon: none }); } } });逻辑说明scanType数组里同时写qrCode和barCode就能在一个扫码入口里同时处理二维码和商品条形码。onlyFromCamera设 false 时用户可以从相册选图做收款工具时建议设 true防止用户拿一张截图来伪造支付码。fail回调里用户主动取消也会触发必须用errMsg判断否则每次取消都弹「识别失败」很烦。参数边界scanCode返回的res.result是原始字符串没有做任何解析。如果扫的是支付码内容通常是 URL 或特定格式的字符串需要自己写解析逻辑。条形码识别返回的是纯数字长度 8 到 13 位不等做商品查询时要按位数区分 EAN-8 和 EAN-13。2.3 条形码识别与二维码解码的差异处理条形码和二维码在识别后的处理逻辑完全不同。二维码内容可以是任意字符串条形码基本是数字编码。做商户收款工具时扫到条形码大概率是商品码应该走商品查询扫到二维码才走支付解析。区分方式就是看res.scanType。handleScanResult(result, scanType) { if (scanType QR_CODE) { // 支付码解析约定格式 pay://order?noxxxamtxxx if (result.startsWith(pay://)) { const query result.split(?)[1]; const params new URLSearchParams(query); this.confirmPayment(params.get(no), params.get(amt)); } else { wx.showToast({ title: 非本系统支付码, icon: none }); } } else if (scanType EAN_13 || scanType EAN_8) { // 商品条码走商品库查询 this.queryProduct(result); } }逻辑说明用pay://前缀做协议约定避免扫到其他平台的码误触发支付。URLSearchParams在小程序基础库 2.10.0 以上可用低版本需要自己写 split 解析。条形码分支单独走商品查询不要和支付逻辑混在一起否则扫到商品码会报「支付码格式错误」。3. 支付接口集成从统一下单到回调验签的完整链路3.1 支付接口集成的三个必调接口微信支付在小程序里的接入核心是三个接口统一下单、调起支付、支付回调。统一下单在后端做调起支付在小程序端做回调也在后端做。很多人卡在签名和证书上其实流程本身不复杂。后端统一下单以常见的服务端语言为例这里用伪代码说明参数结构# 后端统一下单请求构造示意具体签名算法按支付平台文档实现 import hashlib, time, requests def create_order(out_trade_no, total_fee, openid): params { appid: APPID, mch_id: MCH_ID, nonce_str: generate_nonce(), # 随机字符串长度不超过32 body: 收款-订单 out_trade_no, # 商品描述 out_trade_no: out_trade_no, # 商户订单号唯一 total_fee: total_fee, # 金额单位分 spbill_create_ip: 127.0.0.1, # 调用端 IP notify_url: https://your.domain/pay/notify, # 回调地址 trade_type: JSAPI, # 小程序支付固定 JSAPI openid: openid # 用户 openid } params[sign] sign(params, API_KEY) # 按 key 排序后拼接再加密 resp requests.post(https://api.mch.weixin.qq.com/pay/unifiedorder, datato_xml(params)) return parse_xml(resp.text)逻辑说明out_trade_no必须全局唯一重复下单会报错。total_fee单位是分前端传金额时要乘 100 并取整浮点数直接乘会有精度问题用Math.round(amount * 100)。trade_type小程序固定JSAPI不要写成NATIVE。notify_url必须是 HTTPS 且公网可达本地开发用内网穿透工具临时映射一个地址。小程序端调起支付// 后端返回 prepay_id 和签名后小程序调起支付 wx.requestPayment({ timeStamp: String(res.timeStamp), // 秒级时间戳字符串 nonceStr: res.nonceStr, // 随机字符串 package: prepay_id res.prepayId, signType: RSA, // 新版用 RSA老版用 MD5 paySign: res.paySign, // 后端算好的签名 success() { /* 支付成功但以后端回调为准 */ }, fail(err) { /* 用户取消或支付失败 */ } });逻辑说明timeStamp必须是字符串传数字在部分安卓机上会失败。package格式固定prepay_idxxx。success回调只代表用户完成了支付动作不代表钱到账最终状态以后端收到的支付回调为准。这是最容易翻车的地方很多人只信前端 success 就发货结果回调延迟或失败时对不上账。3.2 支付回调验签与幂等处理支付回调是后端接口支付平台会 POST 一个 XML 过来。处理逻辑分三步验签、更新订单、返回成功。# 支付回调处理示意 def pay_notify(request): xml_data request.body data parse_xml(xml_data) # 第一步验签防止伪造回调 if not verify_sign(data, API_KEY): return xml_response(FAIL, 签名错误) # 第二步幂等处理同一订单回调可能重复到达 out_trade_no data[out_trade_no] if order_already_paid(out_trade_no): return xml_response(SUCCESS, OK) # 已处理过直接返回成功 # 第三步更新订单状态 if data[result_code] SUCCESS: mark_order_paid(out_trade_no, data[transaction_id]) return xml_response(SUCCESS, OK)逻辑说明验签用支付平台提供的公钥或 API 密钥不验签等于把订单状态暴露给任何人。幂等处理必须有支付平台在没收到 SUCCESS 响应时会按策略重试同一笔订单可能回调多次不判重会重复发货。返回内容必须是 XML 格式的 SUCCESS返回 JSON 或纯文本支付平台不认会一直重试。参数上transaction_id是支付平台的流水号和商户的out_trade_no一起存对账时用。回调地址不要做登录鉴权支付平台不会带你的 token做了鉴权反而收不到回调。3.3 收款码与支付码的内容设计收款码和支付码是两种东西。收款码是商户展示给顾客扫的内容是商户标识支付码是系统生成给顾客扫的内容是订单信息。做商户收款工具时收款码一般做成静态的内容里带商户 ID顾客扫码后进入小程序输入金额。支付码是动态的内容里带订单号和金额扫完直接确认支付。内容格式建议用自定义协议前缀加参数比如pay://merchant?idM001表示商户收款码pay://order?no20240101amt9.90表示订单支付码。解析时先判断前缀再解析参数。不要用纯 URL 做内容因为 URL 可能被其他应用拦截自定义协议在小程序内解析更可控。4. 避坑与排查二维码支付系统里最容易翻车的五件事4.1 扫码识别率低尤其是弱光环境现象同一张收款码在室内正常光线下能扫到了傍晚或灯光偏暗的摊位就频繁识别失败。原因wx.scanCode的识别依赖相机对焦和曝光弱光下对焦慢加上二维码本身对比度不够比如深灰色底配黑色码识别率断崖下降。解决生成二维码时保证前景纯黑#000000、背景纯白#FFFFFF不要用品牌色做码点。码周围留至少 4 个模块宽度的白边静区否则相机取景框切到码边缘时无法定位。如果场景确实暗在小程序扫码页加一个「打开闪光灯」的按钮用wx.setKeepScreenOn保持屏幕常亮减少用户操作中断。4.2 支付回调收不到订单一直待支付现象用户前端显示支付成功但后端订单状态还是待支付查日志没有收到回调。原因notify_url配置的是内网地址或本地 localhost支付平台从公网访问不到。或者回调接口做了登录鉴权支付平台的请求被拦截返回 401。解决notify_url必须是公网 HTTPS 地址本地开发用内网穿透工具映射一个临时域名上线后换成正式域名。回调接口单独放行不做 token 校验只做验签。排查时看支付平台的商户后台有没有回调记录有记录但你的服务没收到就是网络或鉴权问题没记录就是地址配错了。4.3 金额精度问题导致对账差一分钱现象订单金额 9.9 元前端传 9.9后端乘 100 变成 989.9999999999999取整后变 989 分少收一分。原因JavaScript 浮点数运算精度问题9.9 * 100不等于 990。解决前端传金额时就用整数分或者用字符串传金额后端用Decimal类型解析后再乘 100。不要在前端做amount * 100后直接传用Math.round(amount * 100)取整。后端存金额统一用分做单位展示时再除以 100。4.4 二维码内容过长导致码密度过高扫不出现象支付码里塞了订单号、金额、时间戳、签名等一堆参数生成的二维码密密麻麻手机稍微远一点就扫不出。原因二维码容量有限内容越长模块越多同样尺寸下单个模块越小相机解析难度越大。解决支付码内容只放必要参数订单号用短编码签名可以不放二维码里扫码后后端校验。如果参数确实多用短链接服务把长内容转成短 URL 再生成二维码。或者把纠错等级从 H 降到 M减少冗余模块。实测内容控制在 100 字符以内220px 画布下识别率比较稳。4.5 安卓和 iOS 扫码行为不一致现象同样的扫码代码iOS 上正常安卓上偶尔扫不出来或者返回的scanType不一样。原因不同机型相机对焦策略不同安卓碎片化严重部分机型对条形码和二维码的识别优先级不同。解决scanType数组里把qrCode放前面让相机优先尝试二维码识别。测试时至少覆盖华为、小米、OPPO、vivo 各一台真机不要只用开发者工具模拟。如果某机型持续有问题在扫码页加一个「手动输入」入口兜底用户扫不出时可以手动输码。5. 进阶验证用日志和压测确认支付链路真的可靠5.1 给扫码和支付链路加可观测日志上线前我在扫码成功、下单请求、支付回调三个节点各埋一条日志格式统一带traceId方便串起来查。// 小程序端扫码成功后生成 traceId 并透传 const traceId scan_ Date.now() _ Math.random().toString(36).slice(2, 8); wx.scanCode({ success(res) { wx.request({ url: https://your.domain/api/order, data: { content: res.result, traceId }, success(orderRes) { console.log([trace], traceId, order created, orderRes.data.orderNo); } }); } });后端收到traceId后在下单日志和回调日志里都打出来。排查时用grep traceId就能把一次支付的全链路串起来。参数上traceId不要用纯时间戳并发高时可能重复加随机后缀。5.2 用沙箱环境做支付回调的重复投递测试支付回调的幂等逻辑必须验证。我一般写一个脚本模拟支付平台连续 POST 三次同样的回调数据到notify_url看订单状态是否只更新一次、是否每次都返回 SUCCESS。# 模拟重复回调连续发三次相同数据 for i in 1 2 3; do curl -X POST https://your.domain/pay/notify \ -H Content-Type: application/xml \ -d notify_sample.xml echo --- round $i done --- done逻辑说明notify_sample.xml是一份合法的回调数据样本从支付平台文档里抄一份改一下订单号。三次请求后查数据库订单状态应该只变一次transaction_id只写一次。如果第二次回调把状态又改了一遍或者报了唯一键冲突说明幂等没做好。5.3 二维码生成质量的快速验证方法生成二维码后不要只用微信扫用系统相机也扫一遍。系统相机对二维码的识别算法和微信不同能过系统相机的基本上所有扫码器都能过。另外把二维码截图放大到 200% 看边缘如果码点边缘有灰边或锯齿说明 canvas 绘制时没有做像素对齐调整cell 0.5的偏移量或者改用image方式由后端生成。我自己的习惯是每次改完二维码生成逻辑先在一台旧安卓机上用系统相机扫过了再上微信扫。旧机型能过新机型基本没问题。这个习惯帮我省了很多「用户反馈扫不出」的售后。希望帮到你。本文还有配套的精品资源点击获取