公众号 JSAPI 网页支付集成指南
在 CloudBase 控制台「集成中心」创建微信支付集成后,平台会自动为你生成一个 HTTP 云函数,封装好下单、查单、退款、回调等全部能力。本指南介绍如何在**微信公众号网页(H5 within WeChat)**端使用 JSAPI 支付,基于该云函数完成从下单到回调的完整支付闭环。
关于函数名的约定:集成中心生成的云函数名形如
<集成名>-<随机串>(例如miniapp-wxpay-rwmx67sc)。为方便讲解,本文统一称其为pay-common——你在自己的代码中替换为实际函数名即可。
适用场景:用户在微信浏览器内打开的公众号 H5 页面发起支付(最常见的服务号支付)。
不适用:
- 微信外浏览器(Chrome/Safari/系统浏览器)→ 应使用 H5 支付下单接口
- PC 网页 → 应使用 Native(扫码)支付
整体调用链路:
- 正向请求:网页 → 业务后端获取微信网页授权 → CloudBase 云 API 网关(accessToken 鉴权)→
pay-commonHTTP 云函数 → 微信支付 API - 异步回调:微信支付 → 集成中心(验签 + 解密)→
pay-common→ 业务侧
架构总览
整体分为正向下单与异步回调两条链路(公众号 JSAPI 比小程序多了「网页授权换 openid」与「网关 Bearer 鉴权」两步):
JSAPI 与小程序支付的关键区别:
| 维度 | 小程序支付 | 公众号 JSAPI 网页支付 |
|---|---|---|
| 下单接口 | /v3/pay/transactions/jsapi | /v3/pay/transactions/jsapi(同一个) |
appid | 小程序 AppID | 公众号 AppID |
| openid 来源 | signInWithOpenId() 直接拿 | 微信网页授权(OAuth2.0)code → openid |
| 调起支付 API | wx.requestPayment | WeixinJSBridge.invoke('getBrandWCPayRequest', ...) |
| 运行环境 | 微信小程序容器 | 微信内置浏览器(必须) |
因此 pay-common 函数本身无差异,差异在「openid 获取」与「前端调起 API」两处。
职责分工:
| 角色 | 职责 |
|---|---|
| 公众号网页 | 通过微信网页授权拿 code → 业务后端换 openid;携带 Bearer Token 调用 pay-common;用 WeixinJSBridge.invoke 调起支付 |
| 业务后端(自备) | 持有公众号 AppSecret,调用 sns/oauth2/access_token 用 code 换 openid(不能让 AppSecret 出现在前端) |
| 云 API 网关 | 校验 Bearer {accessToken},将请求转发到 HTTP 云函数 |
| pay-common 云函数 | 处理下单、查单、退款;接收并处理回调 |
| 微信支付 API | 支付业务实际执行方 |
| 集成中心 | 统一托管凭证;接收回调后完成验签与解密,并以明文转发至云函数 |
前置条件
| 项 | 要求 |
|---|---|
| 公众号 | 服务号(订阅号不支持 JSAPI 支付)已通过微信认证 |
| 公众号 JSAPI 支付权限 | 在公众平台「微信支付」中开通并通过审核 |
| 微信支付商户号 | 已申请并在商户平台完成与上述公众号的绑定 |
| 公众号支付授权目录 | 商户平台 → 产品中心 → JSAPI 支付 → 添加授权目录(必须与下单页面 URL 前缀一致,含末尾 /) |
| 公众号网页授权域名 | 公众平台 → 设置与开发 → 公众号设置 → 功能设置 → 网页授权域名 |
| 商户超管权限 | 用于下载 API 证书、设置 APIv3 密钥 |
| 业务后端(任选) | 用于换 openid 的轻量服务(自有 Node/Java/Go 服务,或一个独立的云函数) |
第一步 · 准备商户凭证
需要获得 7 项凭证:
| # | 字段 | 说明 |
|---|---|---|
| 1 | appId | 公众号 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_...) |
⚠️
appId必须是公众号 AppID,且该 AppID 已在商户平台「JSAPI 支付」产品下与商户号关联。详见小程序指南附录的 AppID 区别说明。
第二步 · 在 CloudBase 创建集成
与小程序支付指南完全相同。集成创建后,系统自动生成:
- HTTP 云函数
pay-common - 回调基础域名
https://<集成标识>.integration-callback.tcloudbase.com - 支付回调路径
/wechatpay/order - 退款回调路径
/wechatpay/refund
完整支付回调 URL 需填入商户平台「产品中心 → 开发配置 → 支付通知 URL」。
第三步 · 了解 pay-common 云函数
完全复用微信小程序支付集成指南所述源码结构与环境变量注入逻辑。pay-common 中/v3/pay/transactions/jsapi 下单的实现对小程序与公众号网页是同一份代码(路由 _action: wxpay_order)。
唯一区别:调用方传入的 payer.openid 必须是该公众号 AppID 下用户的 openid。
第四步 · 接入公众号网页
pay-common 部署完成后,公众号网页的接入分四步:网页授权拿 openid → 调用 pay-common 下单 → JSBridge 调起支付 → 服务端处理回调。
4.1 配置公众号支付授权目录与网页授权域名
支付授权目录(必须):
路径:商户平台 → 产品中心 → JSAPI 支付 → 添加授权目录。
填写调起支付的页面所在 URL 前缀,必须以 / 结尾。例如页面是 https://shop.example.com/pay/cashier.html,则授权目录填 https://shop.example.com/pay/。
网页授权域名(必须):
路径:公众平台 → 设置与开发 → 公众号设置 → 功能设置 → 网页授权域名。
4.2 微信网页授权拿 openid
公众号 JSAPI 支付要求 payer.openid,必须通过微信网页授权(OAuth2.0)获取。换 openid 这一步不能在前端做——会泄漏公众号 AppSecret。下面给两种实现方案:
推荐方案:使用微信公众号集成获取 openid
CloudBase 平台提供的「微信公众号」集成(offiaccount-common 云函数)已经把网页授权链路封装好,业务方无需自建后端、无需保管 AppSecret。
详细接入步骤参见 公众号集成接入指南 · 网页授权篇。
✅ 优势:AppSecret 留在 offiaccount-common 云函数环境变量中,不出集成中心;网页授权、用户资料拉取、token 续期等能力开箱即用。
备选方案:自建业务后端换 openid
如果业务侧已有独立后端服务,且不想引入公众号集成,可自行调用微信网页授权接口。重点:换 openid 必须在后端做,前端只做跳转和回调处理。
1. 网页跳转:https://open.weixin.qq.com/connect/oauth2/authorize
?appid=<公众号AppID>
&redirect_uri=<URL编码后的业务回跳地址>
&response_type=code
&scope=snsapi_base ← 静默授权,仅拿 openid,无需用户确认
&state=<自定义>
#wechat_redirect
2. 微信回跳到 redirect_uri 时附带 ?code=xxx&state=xxx
3. **业务后端**用 code 调微信接口换 openid(不要在前端调,会泄漏 AppSecret):
GET https://api.weixin.qq.com/sns/oauth2/access_token
?appid=<公众号AppID>
&secret=<公众号AppSecret>
&code=<上一步的 code>
&grant_type=authorization_code
返回:{ access_token, openid, refresh_token, expires_in, scope, ... }
4. 业务后端把 openid 写入会话/cookie 或返回给前端
scope=snsapi_base不需要用户确认弹窗,只要用户在公众号会话中打开,跳转一瞬间就拿到了 openid,体验最好。如果业务还需要昵称/头像等用户信息,用scope=snsapi_userinfo,会多一个授权弹窗。
4.3 支付相关路由
JSAPI 网页支付与小程序支付走同一套路由,差异仅在客户端调起方式。常用路由:
_action | 类别 | 说明 |
|---|---|---|
wxpay_order | 下单 | JSAPI / 小程序下单(本指南使用) |
wxpay_query_order_by_out_trade_no | 查询 | 按商户订单号查单 |
wxpay_query_order_by_transaction_id | 查询 | 按微信订单号查单 |
wxpay_close_order | 关单 | 关闭订单(10 分钟未支付建议主动关闭) |
wxpay_refund | 退款 | 申请退款 |
wxpay_refund_query | 退款 | 查询退款 |
所有路由通过 body 中的 _action 字段区分,除回调路由外均需携带 Authorization: Bearer {access_token}。
4.4 回调处理
完全复用微信小程序支付集成指南所述 services/orderService.js 中的钩子方法(handlerUnified / handlerUnifiedTrigger / handlerRefund / handlerRefundTrigger),实现幂等、金额校验、状态原子更新等原则。
JSAPI 支付与小程序支付回调结构 100% 一致——都是
event_type: TRANSACTION.SUCCESS,明文中拿到out_trade_no/transaction_id/amount等字段。业务侧无需为支付方式区分回调代码。
4.5 完整调用示例
适合原生 H5 自研工程。下面是从拿到 openid 到调起支付的端到端示例。
关键约束:
ENV_ID:云开发环境 IDFN_NAME:集成创建后自动生成的 HTTP 云函数名(形如<集成标识>-<随机串>)- 必须在微信内置浏览器中打开页面,否则
WeixinJSBridge不存在 - 必须在
payer.openid中传入公众号 AppID 下的 openid - accessToken 必须来自「认证登录」——匿名登录(
signInAnonymously)拿到的 token 无权调用云函数,网关会直接返回 401。具体登录方式(邮箱、手机号、自定义登录等)请参考 CloudBase Web SDK · 认证登录。
<!-- pay.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>公众号支付</title>
</head>
<body>
<button id="payBtn">微信支付 0.2 元</button>
<script src="https://cdn.jsdelivr.net/npm/@cloudbase/js-sdk/dist/cloudbase.full.js"></script>
<script>
const ENV_ID = "your-env-id";
const FN_NAME = "pay-common";
// 通过业务后端拿到的当前用户 openid(公众号 AppID 维度)
const OPENID = window.__USER_OPENID__; // 由业务后端模板渲染或 cookie 写入
// 1. 初始化 CloudBase Web SDK,拿 accessToken
const app = cloudbase.init({ env: ENV_ID });
const auth = app.auth({ persistence: "local" });
async function getAccessToken() {
// ⚠️ 匿名登录(signInAnonymously)拿到的 accessToken 无法调用云函数(401)
// 必须使用「认证登录」。可选登录方式(邮箱、手机号、自定义登录等)参见:
// https://docs.cloudbase.net/api-reference/webv3/authentication#%E8%AE%A4%E8%AF%81%E7%99%BB%E5%BD%95
throw new Error(
"请按 CloudBase Web SDK 文档实现认证登录后返回 accessToken"
);
}
document.getElementById("payBtn").onclick = async () => {
const accessToken = await getAccessToken();
const out_trade_no = "ORDER_" + Date.now();
// 2. 调用 pay-common 下单
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 ${accessToken}`,
},
body: JSON.stringify({
_action: "wxpay_order",
description: "测试商品",
out_trade_no,
amount: { total: 20, currency: "CNY" }, // 0.2 元
payer: { openid: OPENID },
}),
}
);
const body = await res.json();
if (body.code !== 0) {
alert(body.msg || "下单失败");
return;
}
const p = body.data && body.data.data ? body.data.data : body.data;
// p = { timeStamp, nonceStr, package, signType, paySign }
// 3. 调起 JSBridge 支付
callJsBridgePay(p, out_trade_no);
};
function callJsBridgePay(p, outTradeNo) {
function onBridgeReady() {
WeixinJSBridge.invoke(
"getBrandWCPayRequest",
{
appId: "<公众号AppID>", // 必须与下单时一致
timeStamp: p.timeStamp,
nonceStr: p.nonceStr,
package: p.package, // 'prepay_id=wx20...'
signType: p.signType || "RSA",
paySign: p.paySign,
},
function (resp) {
if (resp.err_msg === "get_brand_wcpay_request:ok") {
// 支付成功(前端仅用于 UI;最终以服务端回调为准)
// 建议 1-2 秒后调用 wxpay_query_order_by_out_trade_no 主动查单兜底
location.href = `/order/result?out_trade_no=${outTradeNo}`;
} else if (resp.err_msg === "get_brand_wcpay_request:cancel") {
alert("用户取消支付");
} else {
alert("支付失败:" + resp.err_msg);
}
}
);
}
if (typeof WeixinJSBridge === "undefined") {
document.addEventListener(
"WeixinJSBridgeReady",
onBridgeReady,
false
);
} else {
onBridgeReady();
}
}
</script>
</body>
</html>
实现要点:
WeixinJSBridge仅在微信内置浏览器中存在,外部浏览器需切到 H5 支付方案(不在本指南范围)。appId字段在getBrandWCPayRequest入参中必须与 下单时使用的公众号 AppID 完全一致,否则会报"appid 和 openid 不匹配"。package字段直接用接口返回值,请勿自行拼接prepay_id=...。- 最终支付状态以服务端回调为准,前端
ok仅用于跳转结果页;服务端收到回调后再执行发货等副作用。
4.6 真机测试
JSAPI 支付的调试链路相比小程序更繁琐:
- 必须在微信内置浏览器中访问页面,桌面浏览器无法调起
WeixinJSBridge。 - 微信开发者工具有「公众号网页调试」模式(菜单 → 公众号网页项目 → 微信网页调试),可模拟微信 UA,但仍无法发起真实支付——必须真机。
- 真机访问页面前,确认:
- 域名已配在公众号「网页授权域名」白名单
- 页面 URL 前缀已在商户平台「JSAPI 支付授权目录」中(含末尾
/) - 用户公众号关注关系不影响(
snsapi_base不强制关注)
测试金额建议同小程序:使用 0.1–1 元,避免 0.01 元触发风控。
常见问题 FAQ
Q1:调起支付报 appid 和 openid 不匹配
最常见错误。getBrandWCPayRequest 入参中的 appId、下单时填入的 appId、以及换 openid 时使用的 AppID 必须三处一致且都是公众号 AppID。
排查:
- 集成中心填写的是公众号 AppID,不是小程序 AppID
- 业务后端 OAuth2 用的也是同一个公众号 AppID + AppSecret
- 前端
getBrandWCPayRequest入参中没硬编码错误的 appId
Q2:报 APPID_MCHID_NOT_MATCH 或商户不存在
公众号 AppID 未在商户平台与商户号完成关联。商户平台 → 产品中心 → JSAPI 支付 → 关联 AppID,提交申请,由公众号管理员审批后生效。
Q3:报当前 URL 不在支付授权目录
调起支付的页面 URL 不在「JSAPI 支付授权目录」白名单。商户平台 → 产品中心 → JSAPI 支付 → 添加授权目录,填该页面 URL 的前缀(含末尾 /)。生效需 5 分钟左右。
Q4:网页授权回跳后没拿到 code
redirect_uri 域名不在公众号「网页授权域名」白名单,或域名校验文件未上线。补完网页授权域名验证后重试。
Q5:WeixinJSBridge is not defined
页面在非微信内置浏览器中打开。JSAPI 支付只能在微信浏览器中跑,外部浏览器需走 H5 支付(/v3/pay/transactions/h5)。
Q6:模拟器/桌面 Chrome 修改 UA 伪装微信,能调起支付吗
不能。WeixinJSBridge 是微信原生注入的全局对象,UA 伪装无法注入它。开发期可用微信开发者工具「公众号网页项目」模式联调页面渲染与请求,但支付调起必须真机。
Q7:是否需要 wx.config 或 JSSDK 鉴权
JSAPI 支付(getBrandWCPayRequest)不需要 wx.config。只有需要调用拍照、定位、分享等微信 JSSDK 能力时才需要。
Q8:能否复用小程序支付的同一个集成
能,但要看你的 appId 配置。pay-common 集成里只有一个 appId 字段,填的是哪个就只能服务对应应用。如果要同时支持小程序与公众号 JSAPI,两种做法:
- 创建两个独立集成(一个填小程序 AppID、一个填公众号 AppID),分别服务两端
- 改 pay-common 代码,按调用方动态切换
appId,环境变量增加jsapiAppId等
附录 · 与小程序支付的差异速查
| 维度 | 小程序 | 公众号 JSAPI 网页 |
|---|---|---|
集成中心 appId | 小程序 AppID | 公众号 AppID |
| openid 来源 | signInWithOpenId() | OAuth2.0 网页授权 + 业务后端换取 |
| 拉起支付 API | wx.requestPayment | WeixinJSBridge.invoke('getBrandWCPayRequest') |
| 运行环境 | 小程序容器 | 微信内置浏览器 |
| 域名白名单 | 小程序 request 合法域名 | 公众号网页授权域名 + 支付授权目录 |
| pay-common 路由 | _action: wxpay_order | _action: wxpay_order(同一个) |
| 回调结构 | TRANSACTION.SUCCESS | TRANSACTION.SUCCESS(完全一致) |