扫码支付接入实战从微信支付宝API到前端防重复提交细节

梓淇的笔记 移动 阅读 1,395
赞 27 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

扫码支付在移动端其实就两件事:生成二维码、等用户扫完回调。但真做起来,你会发现微信和支付宝的 SDK 不兼容、H5 调起原生扫码卡顿、iOS Safari 对 canvas 渲染二维码有白边、甚至有些安卓机扫完没反应……我上个项目上线前两天还在改扫码逻辑,最后定稿的方案是「纯前端生成 + 后端轮询」,不依赖任何 SDK,亲测有效,连 2017 款红米 Note 5 都能扫得稳稳的。

扫码支付接入实战从微信支付宝API到前端防重复提交细节

核心思路很简单:前端调用后端接口生成一个带订单号的支付链接(比如 https://jztheme.com/pay?order_id=xxx),然后用 qrcode.js 在页面里画出这个链接的二维码,同时前端启动轮询查支付状态。整个过程不跳转、不唤起 App、不弹权限提示——对用户最友好,对开发最省心。

核心代码就这几行

先装依赖(别用太老的版本,qrcode.js v1.5.3 之后才修复了 iOS canvas 抗锯齿问题):

npm install qrcode

然后是关键渲染逻辑(Vue 3 setup script 示例,React 同理,重点看逻辑):

import { ref, onMounted } from 'vue'
import QRCode from 'qrcode'

const qrCodeUrl = ref('')
const orderStatus = ref('pending') // pending / paid / expired
const orderId = 'ORD_20240521_8892'

onMounted(async () => {
  try {
    // 1. 先请求后端生成带签名的支付页地址
    const res = await fetch('/api/v1/payment/qrcode', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ order_id: orderId })
    })
    const data = await res.json()
    qrCodeUrl.value = data.qr_url // e.g. https://jztheme.com/pay?order_id=ORD_20240521_8892&sign=xxx

    // 2. 生成二维码到 canvas
    const canvas = document.getElementById('qr-canvas')
    await QRCode.toCanvas(canvas, qrCodeUrl.value, {
      width: 240,
      margin: 2,
      color: {
        dark: '#2c3e50',
        light: '#ffffff'
      }
    })

    // 3. 启动轮询(注意防抖+超时)
    startPolling()
  } catch (err) {
    console.error('生成二维码失败', err)
  }
})

const startPolling = () => {
  const poll = async () => {
    try {
      const res = await fetch(/api/v1/payment/status?order_id=${orderId})
      const data = await res.json()
      if (data.status === 'paid') {
        orderStatus.value = 'paid'
        clearInterval(pollTimer)
        // 这里跳转成功页 or 弹 toast
        alert('支付成功!')
      } else if (data.status === 'expired') {
        orderStatus.value = 'expired'
        clearInterval(pollTimer)
        alert('二维码已过期,请刷新重试')
      }
    } catch (e) {
      // 网络失败不中断轮询,继续试
    }
  }

  let pollTimer = setInterval(poll, 3000)
  // 最多轮询 120 秒(40 次 × 3s),避免用户一直挂着
  setTimeout(() => clearInterval(pollTimer), 120000)
}

HTML 就一行:

<canvas id="qr-canvas" width="240" height="240"></canvas>

这个场景最好用:嵌入 WebView 的混合 App

我们当时做的是一款银行合作的轻量级理财 H5,要求必须跑在他们自己的 App WebView 里。一开始想用 JSBridge 调他们的扫码 SDK,结果发现不同安卓厂商 WebView 内核差异大,有的不支持 window.AlipayJSBridge,有的调用后黑屏。折腾半天,换成上面这套「纯前端二维码 + 轮询」方案,三天就上线了。

关键点在于:后端返回的 qr_url 必须是能直接被微信/支付宝识别的 scheme 或标准 URL。实测下来,只要满足以下任一条件,扫码就能自动唤起支付:

  • URL 是 HTTPS 协议,且域名已备案(微信强制要求)
  • URL 包含支付宝合法的 alipays:// scheme(需提前在支付宝开放平台配置)
  • URL 是微信支付的 weixin:// scheme(同理需配置,但 H5 场景不推荐,兼容性差)

我们选的是第一种:HTTPS 标准网页。后端生成的 https://jztheme.com/pay?order_id=xxx 页面本身是个支付中转页,加载时自动调起微信/支付宝唤起逻辑(用官方 JS-SDK),用户扫完就能进支付流程——整个链路完全可控,不甩锅给客户端。

踩坑提醒:这三点一定注意

第一,canvas 在 iOS 上默认有白边,导致扫码率下降。不是所有机型都这样,但 iPhone XR 及以下概率很高。解决方案不是换库,而是加 CSS 强制重绘:

#qr-canvas {
  image-rendering: -webkit-optimize-contrast;
  image-rendering: crisp-edges;
  background: #fff;
}

另外,qrcode.js 的 margin: 2 参数不能设为 0,否则 iOS Safari 渲染时会把最外一圈像素“吃掉”,建议保持 1~3。

第二,轮询不能只靠前端 timer,要配合后端幂等 + 状态缓存。我们第一次上线时,用户手快点了两次“重新生成”,后端没做幂等,导致生成了两个支付单,但前端轮询只认第一个 order_id。后来加了 Redis 缓存:每次生成二维码前先查 cache:get:order_id_${orderId},存在就直接返回旧地址,不存在才走支付单创建流程。简单粗暴,但管用。

第三,别信“扫码后自动跳转”这种理想状态。微信浏览器里扫完确实可能自动跳支付页,但支付宝在某些安卓机上就是不跳,用户得手动点右上角“在支付宝中打开”。所以 UI 上一定要显式提示:“请用支付宝/微信扫描,如未自动跳转,请手动打开对应 App”。我们加了底部浮动提示条,点击还能复制链接——真实用户里有 12% 是手动粘贴进 App 完成的,不是玄学数据,是埋点统计出来的。

高级技巧:让二维码带 logo,又不影响扫码

运营同学说“没 logo 不像我们品牌”,技术说“加 logo 会降低扫码成功率”。最后折中方案:logo 放中间,尺寸控制在二维码面积的 15% 以内,且只覆盖中心 4×4 的模块区域(qrcode.js 支持自定义 maskPattern)。实测加了 32×32 像素的透明 png logo 后,扫码成功率从 99.2% 降到 98.7%,可接受。

代码就多一行:

await QRCode.toCanvas(canvas, qrCodeUrl.value, {
  width: 240,
  margin: 2,
  color: { dark: '#2c3e50', light: '#ffffff' },
  errorCorrectionLevel: 'H', // 必须设为 H,容错率最高
  image: '/logo-small.png', // 必须是同域图片,跨域会报 canvas tainted
  imageOptions: {
    crossOrigin: 'anonymous',
    margin: 0
  }
})

这个技术的拓展用法还有很多,后续会继续分享这类博客

比如:如何用 service worker 缓存二维码减少首屏延迟;如何结合 Web Bluetooth 实现扫码+蓝牙设备联动;还有更狠的——用 WebRTC 拍摄用户付款码反向识别(别笑,某便利店项目真这么干过,扫码速度比传统方式快 1.8 秒)。以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论