Native(PC 扫码)支付集成指南
在 CloudBase 控制台创建「微信支付」集成后,系统会自动生成一个 HTTP 云函数(函数名形如 <集成名>-<随机串>,本文统称 pay-common)。本指南介绍如何基于该云函数完成 Native 支付——即 PC 网页/桌面应用展示二维码,用户用手机微信扫码完成支付的接入闭环。
适用场景:PC 收银台、桌面客户端、自助终端、广告屏等所有"非微信内浏览器"的扫码支付需求。 不适用:微信内 H5 → 用 JSAPI 网页支付;小程序 → 用小程序支付;微信外手机浏览器 → 用 H5 支付。
整体调用链路:
- 正向请求:网页 → CloudBase 云 API 网关(accessToken 鉴权)→
pay-commonHTTP 云函数 → 微信支付 API(/v3/pay/transactions/native) - 异步回调:微信支付 → 集成中心(验签 + 解密)→
pay-common→ 业务侧 - 状态同步:网页轮询查单接口或前端 WebSocket/SSE 推送,发现支付完成后跳转结果页
架构总览
┌──────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ PC 收银台 │ │ CloudBase 云 API │ │ pay-common │
│ 网页/客户端 │ │ 网关 │ HTTP │ (HTTP 云函数) │
│ │ Bearer │ accessToken │ invoke │ SDK 自签 │
│ fetch ─────►│ Token │ 鉴权 │─────────►│ /v3/pay/transactions/native
│ │ └──────────────────┘ └─────────┬──────────┘
│ │ │ 统一下单
│ │ ▼
│ │ ┌────────────────────────┐
│ │ ◄──── 返回 code_url ──────────────── │ 微信支付 API │
│ │ │ api.mch.weixin.qq.com │
│ 渲染二维码 │ └──────────┬─────────────┘
│ (qrcode.js) │ │ 异步支付回调
│ │ ┌── 用户用手机微信扫码 ──┐ ▼
│ │ │ │ ┌────────────── ──────────┐
│ 轮询查单 │ ▼ │ │ 集成中心 │
│ 或推送状态 │ 手机微信完成支付 │ │ (验签 + 解密) │
│ │ └─────────────────────────┘ └──────────┬─────────────┘
│ │ │ 明文转发
│ │ ▼
│ │ ┌────────────────────────┐
│ │ │ pay-common │
│ │ │ /wechatpay/order │
│ │ │ → orderService 处理 │
└──────────────┘ └────────────────────────┘
Native 支付的关键特点:
| 维度 | 与 JSAPI/小程序的区别 |
|---|---|
| 下单接口 | /v3/pay/transactions/native(不同接口) |
| 下单返回 | { code_url: "weixin://wxpay/bizpayurl?pr=..." },需要前端生成二维码 |
| 是否需要 openid | 不需要,无 payer.openid 字段 |
appid | 公众号 AppID 或小程序 AppID(已与商户号关联即可) |
| 支付完成感知 | 没有前端调起回调,需要轮询查单或服务端推送 |
| 运行环境 | 任意浏览器/客户端,无微信容器依赖 |
Native 支付的服务端逻辑与小程序/JSAPI 走的是同一个 pay-common 云函数模板,只是
_action不同,路由内部走不同的下单 strategy。
前置条件
| 项 | 要求 |
|---|---|
| 公众号 / 小程序 / 移动应用 | 至少其一已通过认证;其 AppID 将作为下单 appid |
| 微信支付 Native 支付权限 | 商户平台「产品中心」中开通 Native 支付(一般认证后默认开通) |
| 微信支付商户号 | 已申请并完成与上述 AppID 的关联 |
| 商户超管权限 | 用于下载 API 证书、设置 APIv3 密钥 |
| 前端二维码渲染 | 任意 QR Code 库(qrcode / qrcode.js / 后端生成 PNG) |
第一步 · 准备商户凭证
| # | 字段 | 说明 |
|---|---|---|
| 1 | appId | 公众号 / 小程序 / 移动应用 AppID(已与商户号关联) |
| 2 | merchantId | 10 位数字商户号 |
| 3 | apiV3Key | 32 字符 APIv3 密钥 |
| 4 | merchantSerialNumber | 40 位十六进制证书序列号 |
| 5 | privateKey | apiclient_key.pem 完整内容 |
| 6 | wxPayPublicKey | 微信支付公钥 PEM |
| 7 | wxPayPublicKeyId | 公钥 ID(PUB_KEY_ID_...) |
Native 支付下,
appId可以是公众号或小程序 AppID(甚至开放平台移动应用 AppID),只要在商户平台「Native 支付」产品下与商户号完成关联即可。
第二步 · 在 CloudBase 创建集成
与小程序支付指南完全相同。集成创建后,系统自动生成:
- HTTP 云函数
pay-common - 回调基础域名
https://<集成标识>.integration-callback.tcloudbase.com - 支付回调路径
/wechatpay/order - 退款回调路径
/wechatpay/refund
完整支付回调 URL 需填入微信支付商户平台 → 产品中心 → 开发配置 → 支付通知 URL。
第三步 · 了解云函数和环境变量
完全复用小程序指南所述源码结构与环境变量注入逻辑。Native 下单已在 pay-common 中实现,对应 _action: wxpay_order_native。
第四步 · 接入 PC 收银台
pay-common 部署完成后,PC 端的接入分四步:调用 pay-common 拿 code_url → 前端渲染二维码 → 轮询查单 → 跳转结果页。
4.1 支付相关路由
Native 与其他支付方式共享同一套 pay-common 路由,差异只在下单 _action:
_action | 类别 | 说明 |
|---|---|---|
wxpay_order_native | 下单 | Native 扫码下单(本指南使用) |
wxpay_query_order_by_out_trade_no | 查询 | 按商户订单号查单(Native 必备,前端轮询使用) |
wxpay_query_order_by_transaction_id | 查询 | 按微信订单号查单 |
wxpay_close_order | 关单 | 关闭订单(10 分钟未支付建议主动关闭) |
wxpay_refund | 退款 | 申请退款 |
wxpay_refund_query | 退款 | 查询退款 |
与 JSAPI/小程序最大差别:Native 下单走
wxpay_order_native,返回{ code_url }而非{ timeStamp, paySign, ... }。
所有路由通过 body 中的 _action 字段区分,除回调路由外均需携带 Authorization: Bearer {access_token}。
4.2 回调处理
完全复用小程序指南所述 services/orderService.js 中的方法(handlerUnified / handlerUnifiedTrigger / handlerRefund / handlerRefundTrigger),原则相同:尽早返回、幂等、金额校验、状态原子更新。
Native 支付与 JSAPI/小程序回调结构 100% 一致——都是
event_type: TRANSACTION.SUCCESS,明文中拿到out_trade_no/transaction_id/amount等字段。业务侧无需为支付方式区分回调代码。
4.3 完整调用示例
适合 PC 网页场景。下面是从下单到展 示二维码、轮询查单的端到端示例。
关键约束(与 pay-common 源码行为一致,详见 GitHub pay-common):
ENV_ID:云开发环境 IDFN_NAME:集成创建后自动生成的 HTTP 云函数名(形如<集成标识>-<随机串>)- 下单请求体必填 3 个字段:
description(商品描述,≤ 127 字符)、amount.total(正整数,单位分)、out_trade_no(6–32 位字母/数字/下划线/连字符,全局唯一) - 无需
payer.openid(仅 JSAPI/小程序需要)、无需appid(pay-common 自动注入集成配置的 AppID)、无需notify_url(pay-common 自动使用集成中心的回调地址) - accessToken 必须来自「认证登录」——匿名登录(
signInAnonymously)拿到的 token 无权调用云函数,网关会直接返回 401。具体登录方式(邮箱、手机号、微信公众号/开放平台、CloudBase 自定义登录等)请参考 CloudBase Web SDK · 认证登录。 - 返回结构有三层嵌套:
{ code, msg, data: { status, data: { code_url } } }——云 API 网关外层{ code, msg, data }、wechatpay-node-v3 中间层{ status, data }、最内层是微信返回的{ code_url } - 二维码内容直接用
code_url原文(形如weixin://wxpay/bizpayurl?pr=...),不要做 URL 编码 - 支付状态以服务端回调为准,前端轮询查单只是 UI 提示
<!-- pc-cashier.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>扫码支付</title>
<style>
.cashier { text-align: center; padding: 40px; }
#qrcode { margin: 24px auto; }
.status { color: #888; font-size: 14px; margin-top: 12px; }
</style>
</head>
<body>
<div class="cashier">
<h2>请使用微信扫码支付</h2>
<h3>金额:<span id="amount">¥ 0.20</span></h3>
<div id="qrcode"></div>
<div class="status" id="status">等待扫码……</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script src="https://static.cloudbase.net/cloudbase-js-sdk/latest/cloudbase.full.js"></script>
<script>
const ENV_ID = 'your-env-id'
const FN_NAME = 'pay-common' // 集成创建后生成的实际函数名
const POLL_INTERVAL = 2000 // 查单间隔 2s
const POLL_TIMEOUT = 600000 // 10 分钟后停止轮询并关单
const app = cloudbase.init({ env: ENV_ID })
const auth = app.auth({ persistence: 'local' })
// Web 端拿 accessToken
// ⚠️ 匿名登录(signInAnonymously)拿到的 accessToken 无法通过云 API 网关调用云函数,
// 云函数侧会返回 401。必须使用「认证登录」获取 accessToken。
// 可选登录方式(邮箱、手机号、微信公众号/开放平台、CloudBase 自定义登录等)与完整代码,
// 参见 CloudBase Web SDK 文档:
// https://docs.cloudbase.net/api-reference/webv2/authentication#%E8%AE%A4%E8%AF%81%E7%99%BB%E5%BD%95
async function getAccessToken() {
// 示例:使用邮箱 + 验证码登录,获取可访问云函数的 accessToken
// const loginState = await auth.signIn({
// username: 'user@example.com',
// verification_code: '...',
// verification_token: '...',
// })
// return loginState.credential.accessToken
throw new Error('请参考 CloudBase Web SDK 文档实现认证登录后返回 accessToken')
}
// 统一封装:调用 pay-common 的任意路由
async function callPayCommon(token, payload) {
const res = await fetch(
`https://${ENV_ID}.api.tcloudbasegateway.com/v1/functions/${FN_NAME}?webfn=true`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
}
)
return res.json()
}
// 从 pay-common 返回结构中解包业务数据
// pay-common 返回: { code: 0, msg: 'success', data: { status: 200, data: <业务> } }
function unwrap(body) {
if (!body || body.code !== 0) return null
const inner = body.data
return inner && inner.data ? inner.data : inner
}
;(async () => {
const token = await getAccessToken()
const out_trade_no = 'ORDER_' + Date.now()
// 1. Native 下单(无需 openid / appid / notify_url)
const orderRes = await callPayCommon(token, {
_action: 'wxpay_order_native',
description: '测试商品',
out_trade_no,
amount: { total: 20, currency: 'CNY' }, // 0.2 元
})
const order = unwrap(orderRes)
if (!order || !order.code_url) {
document.getElementById('status').textContent = '下单失败:' + (orderRes.msg || '')
return
}
// order.code_url 形如 'weixin://wxpay/bizpayurl?pr=...'
// 2. 渲染二维码(code_url 原样作为二维码内容,不要 URL 编码)
QRCode.toCanvas(document.getElementById('qrcode'), order.code_url, { width: 240 })
// 3. 轮询查单
const startedAt = Date.now()
const timer = setInterval(async () => {
// 超时关单
if (Date.now() - startedAt > POLL_TIMEOUT) {
clearInterval(timer)
await callPayCommon(token, {
_action: 'wxpay_close_order',
out_trade_no,
})
document.getElementById('status').textContent = '订单已超时关闭'
return
}
const queryRes = await callPayCommon(token, {
_action: 'wxpay_query_order_by_out_trade_no',
out_trade_no,
})
const detail = unwrap(queryRes)
const state = detail && detail.trade_state
if (state === 'SUCCESS') {
clearInterval(timer)
document.getElementById('status').textContent = '支付成功,正在跳转……'
location.href = `/order/result?out_trade_no=${out_trade_no}`
} else if (state === 'CLOSED' || state === 'PAYERROR' || state === 'REVOKED') {
clearInterval(timer)
document.getElementById('status').textContent = '订单已 ' + state
} else if (state === 'USERPAYING') {
document.getElementById('status').textContent = '用户支付中……'
}
// NOTPAY:继续等待
}, POLL_INTERVAL)
})()
</script>
</body>
</html>
实现要点:
code_url直接用作二维码内容,不要 URL 编码、不要嵌入 HTTPS 链接,扫码后微信会自行跳转支付页。- 三层解包:示例中的
unwrap()函数封装了固定的三层结构——云 API 网关的{ code, msg, data }→ wechatpay-node-v3 的{ status, data }→ 微信原始业务数据。所有 pay-common 路由的返回都遵循这个结构。 - 轮询查单是 Native 支付的常规感知方式,建议 2–3 秒一次,10 分钟未支付则前端主动调
wxpay_close_order关闭订单。 - 更优体验:服务端在
handlerUnifiedTrigger收到回调后,通过 WebSocket / SSE 主动推送结果到该用户对应的 PC 会话,可省去轮询;本指南为简化未演示。 - 最终发货等业务副作用以服务端回调为准,前端轮询到
SUCCESS仅用于跳转 UI。
多 AppID 场景
pay-common 支持通过 useServiceAccount 参数动态切换 appid(需在云函数环境变量中额外配置 service_app_id)。典型场景:集成里默认是小程序 AppID,但某些 Native 场景希望用服务号 AppID 下单:
// 下单时带上 useServiceAccount: true
await callPayCommon(token, {
_action: 'wxpay_order_native',
useServiceAccount: true, // 用 service_app_id 下单
description: '测试商品',
out_trade_no,
amount: { total: 20, currency: 'CNY' },
})
若未配置 service_app_id 环境变量,pay-common 会直接报错返回 useServiceAccount=true 但未配置 service_app_id 环境变量。
4.4 真机测试
Native 支付测试比 JSAPI/小程序简单:
- 用任意桌面浏览器(Chrome/Safari)打开
pc-cashier.html,看到二维码即下单成功。 - 手机微信「扫一扫」二维码,弹出支付页,输入密码完成支付。
- PC 端应在 2–4 秒内显示"支付成功"并跳转结果页(取决于轮询间隔)。
- 同步检查云函数日志:
tcb fn log <FN_NAME>中应能看到handlerUnifiedTrigger - 支付结果: ... SUCCESS表示回调链路也成功。
测试金额建议同小程序:使用 0.1–1 元,避免 0.01 元触发风控;同一账号单日扫码支付次数也有限制。
常见问题 FAQ
Q1:返回的 code_url 形如 weixin:// 开头,能直接放到 img src 吗
不能。weixin:// 是私有 schema,不是 HTTPS URL。必须用前端 QR Code 库(qrcode.js / canvas)把字符串绘制成二维码图片。
Q2:扫码后微信提示「该链接无法访问」
通常是把 code_url 做了 URL 编码或包了一层跳转链接。code_url 必须原样作为二维码内容,连去除前后空白都不要做。
Q3:扫码后微信提示「支付链接已失效」
code_url 默认 2 小时后过期;如果用户长时间没扫,需要重新下单生成新链接。前端应同时提供"刷新二维码"按钮。
Q4:手机扫码后没反应,没弹支付页
可能原因:
- 商户号未开通 Native 支付权限(商户平台「产品中心」→ Native 支付 → 申请开通)
appId与商户号未完成关联- 二维码生成时使用了错误编码(比如 base64)导致内容损坏
Q5:PC 网页轮询查单报 401 / token 过期
CloudBase Web SDK 拿到的 accessToken 默认 2 小时过期。简单做法:每次调用前重新拿 token;更优做法:在 auth.onLoginStateChanged 中监听并刷新。
Q6:能否不轮询,只靠服务端回调通知前端
可以,常见方案:
- WebSocket / SSE:业务后端在
handlerUnifiedTrigger收到回调后,通过 ws/sse 推送给该out_trade_no对应的 PC 会话。 - CloudBase 实时数据库:服务端把订单状态写到云数据库,PC 前端用实时 SDK
db.collection('orders').doc(id).watch()监听变更。
两种都能省去前端轮询,体验最好。轮询作为兜底依然建议保留。
Q7:Native 支付能否用同一个集成同时支持小程序支付
能。pay-common 模板中 Native 与 JSAPI/小程序下单走同一个 SDK,只是路由(_action)不同。注意:
- 集成中心填的
appId决定下单时的appid。如果该 AppID 在商户平台已开通 Native 与 JSAPI 两种支付,pay-common 就能同时支持。 - 如果 Native 想用一个 AppID(如开放平台移动应用 AppID)、小程序支付想用另一个 AppID,需要两个独立集成或改 pay-common 代码动态切换。
Q8:Native 支付的回调与小程序支付有什么差别
没有差别。微信支付 V3 的回调对所有支付方式(JSAPI / Native / H5 / APP)都是同一份结构 TRANSACTION.SUCCESS,验签机制、解密方式完全一致。集成中心已代做验签和解密,pay-common 的 handlerUnifiedTrigger 拿到的明文格式与下单方式无关。
附录 · 与其他支付方式的差异速查
| 维度 | 小程序 | JSAPI 网页 | Native(本指南) |
|---|---|---|---|
_action | wxpay_order | wxpay_order | wxpay_order_native |
| 下单接口 | /v3/pay/transactions/jsapi | /v3/pay/transactions/jsapi | /v3/pay/transactions/native |
| 是否需 openid | 是 | 是 | 否 |
| 下单返回 | prepay_id + 5 字段 | prepay_id + 5 字段 | code_url |
| 调起方式 | wx.requestPayment | WeixinJSBridge.invoke | 二维码 + 用户扫码 |
| 支付完成感知 | 前端 success 回调 | JSBridge ok 回调 | 轮询查单 / 服务端推送 |
| 运行环境 | 微信小程序 | 微信内置浏览器 | 任意浏览器/客户端 |
| 回调结构 | TRANSACTION.SUCCESS | TRANSACTION.SUCCESS | TRANSACTION.SUCCESS(一致) |