小程序虚拟支付集成指南
在 CloudBase 控制台「集成中心」创建小程序虚拟支付集成后,平台会自动生成一个 HTTP 云函数,封装签名、查单与回调能力。本文介绍从创建集成到小程序端拉起收银台的完整流程。
云函数名形如
<集成名>-<随机串>(如xpay-demo-rwmx67sc),本文统一称xpay-common,代码中请替换为实际函数名。
虚拟支付面向虚拟商品(道具直购、代币、会员订阅),底层打通微信支付与 Apple IAP,同一套 wx.requestVirtualPayment 在安卓与 iOS 上都能拉起收银台。
整体调用链路:
- 正向请求:小程序 →
wx.cloud.callHTTPFunction(平台自动鉴权 + 注入 openid)→xpay-commonHTTP 云函数 → 返回signData+ 双签名 - 支付拉起:小程序拿到签名参数后,由客户端调用
wx.requestVirtualPayment自行完成下单(服务端不参与下单) - 异步回调:微信消息推送 → 集成中心网关(验签 + 解密)→
xpay-common→ 业务侧发货
架构总览
职责分工:
| 角色 | 职责 |
|---|---|
| 小程序 | 通过 callHTTPFunction 获取签名参数,再调 wx.requestVirtualPayment 拉起收银台 |
| xpay-common 云函数 | 生成 signData 与双签名;提供查单接口;接收回调后完成幂等与发货 |
| 微信虚拟支付 | 支付业务实际执行方(安卓走微信支付,iOS 走 Apple IAP) |
| 集成中心 | 托管凭证与环境变量;代收回调并完成验签、解密,以明文转发至云函数 |
| 业务侧 | 维护道具清单(productId + 价格)、订单状态机、幂等发货与风控 |
这是虚拟支付与普通微信支付最大的差异。普通支付由服务端调 V3 接口拿 prepay_id;虚拟支付由服务端只产出签名参数,真正的下单动作发生在客户端调用 wx.requestVirtualPayment 时。
与微信支付集成的差异
| 维度 | 微信支付(实物) | 虚拟支付 |
|---|---|---|
| 适用商品 | 实物商品、线下服务 | 道具直购、代币、会员订阅等虚拟商品 |
| 客户端 API | wx.requestPayment | wx.requestVirtualPayment |
| 下单动作 | 服务端调 V3 接口拿 prepay_id | 服务端本地构造 signData + 双签名,客户端自行拉起 |
| 服务端接口 | /v3/pay/transactions/* | /xpay/*(米大师) |
| 回调通道 | 微信支付回调 + AES-GCM 解密 | 小程序消息推送 + AES-256-CBC 解密 |
| 核心凭证 | 商户号、APIv3 密钥、商户证书、支付公钥 | OfferID、AppKey、AppID / AppSecret |
| 费率与结算 | 约 0.6%,T+1 | 安卓 1%,T+3;iOS 12%,约 45–60 天 |
前置条件
| 项 | 要求 |
|---|---|
| 小程序 | 已通过微信认证,并已开通虚拟支付能力(类目与主体要求以微信公众平台为准) |
| 虚拟支付商户信息 | 已在 MP 后台「虚拟支付 → 基本配置」获取 OfferID 与 AppKey |
| 道具 | 已在 MP 后台创建并发布道具,取得 productId 与价格;发布后同步需数分钟至半小时 |
| 小程序基础库 | ≥ 3.15.2(wx.cloud.callHTTPFunction 的前提条件) |
| 微信开发者工具 | 最新稳定版;支付需在真机验证 |
iOS 端通过 Apple IAP 收款,最低金额为 1 元(100 分)。低于 1 元的道具档位在 iPhone 上必然失败;且 Apple 支付不支持沙箱环境,开发调试请在安卓端完成或使用现网小额道具。
第一步 · 准备虚拟支付凭证
登录 微信公众平台,按以下各处取值。所有值将在第二步填入集成中心表单。
1.1 虚拟支付基本配置
路径:MP 后台 → 虚拟支付 → 基本配置。
| # | 凭证 | 说明 |
|---|---|---|
| 1 | offerId | 虚拟支付商户号(米大师应用 ID) |
| 2 | appKey | 支付签名密钥,用于计算 paySig,只放服务端 |
1.2 小程序基本信息
路径:MP 后台 → 设置 → 基本设置。
| # | 凭证 | 说明 |
|---|---|---|
| 3 | appId | 小程序 AppID |
| 4 | secret | 小程序 AppSecret,用于换取 sessionKey,只放服务端 |
1.3 消息推送配置(回调)
路径:MP 后台 → 虚拟支付 → 基本配置 → 基础配置 → 发货推送配置。
| # | 凭证 | 说明 |
|---|---|---|
| 5 | token | 消息推送 Token,用于验签 |
| 6 | msgSecret | EncodingAESKey(43 位),用于安全模式解密 |
| 7 | msgEncryptType | 加密方式:plaintext(明文)/ compatible(兼容)/ aes(安全模式),须与微信后台所选项一致;不传时默认按安全模式(aes)处理 |
虚拟支付的「发货推送」复用公众号消息推送的验签与解密机制,所以用的是同一套 Token、EncodingAESKey 与加密方式。
区别只在配置入口:这几项要在「虚拟支付 → 基本配置 → 基础配置 → 发货推送配置」里填写。
采用集成中心托管回调(system 模式)时,上述参数由平台注入并由网关使用,业务代码无需实现任何密码学逻辑。
1.4 凭证清单
| # | 集成中心字段 / 环境变量 | 取值出处 |
|---|---|---|
| 1 | offerId | 1.1 |
| 2 | appKey | 1.1 |
| 3 | appId | 1.2 |
| 4 | secret | 1.2 |
| 5 | token | 1.3 |
| 6 | msgSecret | 1.3 |
| 7 | msgEncryptType | 1.3 |
第二步 · 在 CloudBase 创建集成
2.1 进入集成中心
路径:云开发平台 → 模板与集成 → 集成中心 → 小程序虚拟支付 → 创建集成。
2.2 填写集成信息
控制台向导需填写以下内容:
| 字段 | 说明 |
|---|---|
| 集成名称 | 自定义,例如 xpay-demo。决定云函数名前缀与回调域名子域 |
| 环境 | 集成资源所属的云开发环境 |
| 凭证 | 1.4 中的 offerId / appKey / appId / secret |
| 回调承接资源 | 指定回调转发到哪个云函数;不绑定则推送无处可转 |
| Token / EncodingAESKey / 加密方式 | 1.3 中的 token / msgSecret / msgEncryptType,须与微信后台保持一致 |
| 加解密方式 / 数据格式 | 明文 / 兼容 / 安全;XML / JSON,须与微信后台一致 |
| 事件订阅 | 勾选需要接收的 xpay_* 事件 |
2.3 自动生成云函数资源
提交后平台异步生成资源,约需 1–2 分钟:
| 资源 | 说明 |
|---|---|
| HTTP 云函数 | 名称形如 <集成名>-<随机串>,例如 xpay-demo-rwmx67sc |
| 环境变量 | 凭证、回调地址等由平台注入,代码中不出现明文 |
| 回调基础域名 | 形如 https://<集成标识>.integration-callback.tcloudbase.com |
| 回调路由 | 平台预置,不可修改路径 |
必须先创建集成、等资源就绪,再去微信后台提交推送配置。 微信在保存推送配置时会立即发起 GET 探测,若集成尚未就绪,探测必然失败。
第三步 · 配置微信后台消息推送
- 进入 MP 后台 → 虚拟支付 → 基本配置 → 基础配置 → 发货推送配置。
- 填写集成中心提供的回调 URL。
- 填写 Token 与 EncodingAESKey(与集成中心表单完全一致)。
- 选择与集成中心一致的数据格式(JSON / XML)与加密方式(明文 / 兼容 / 安全模式)。
- 保存并确认 GET 探测通过(网关会原样返回
echostr)。
配置完成后可在集成中心查看回调状态,或通过云函数日志确认推送已到达 /xpay/callback。
第四步 · 了解 xpay-common 云函数
创建集成时,平台已把表单中的凭证与回调配置作为环境变量注入云函数,运行时通过 process.env 读取。如需修改配置,进入「集成中心 → 对应集成 → 编辑」,保存后平台将自动重新部署(约 1–2 分钟)。
4.1 源码结构
xpay-common/
├── index.js 云函数入口
├── app.js Express 装配(同时支持 JSON 与 XML)
├── scf_bootstrap SCF 自定义运行时启动脚本
├── cloudbaserc.json CloudBase 部署配置(含环境变量占位符)
├── config/config.js 读取 process.env
├── routes/xpay.js 路由表
├── controllers/xpayController.js 下单 / 查单 / 回调应答
├── services/
│ ├── xpayService.js 下单 + 事件分发 + 幂等
│ ├── signService.js 双签名(paySig + signature)
│ └── strategies/xpayStrategy.js /xpay/* 服务端接口
├── utils/
│ ├── wechatApi.js code2Session / access_token
│ ├── xml.js XML 推送解析
│ └── cloudbaseAuth.js 读取平台注入的 x-wx-openid / x-wx-appid
└── tests/ 单元测试
4.2 对外路由
| 路由 | 鉴权 | 说明 |
|---|---|---|
POST /xpay/create_order | 上游网关(必须带 code) | 返回 payData 给 wx.requestVirtualPayment |
POST /xpay/query_order | 上游网关(必须带 code + x-wx-openid) | 查单,推送丢失时兜底补发货 |
POST /xpay/callback | 网关代验签;自建回调时由函数自验签解密 | 所有 xpay_* 事件统一入口 |
云函数本身不挂鉴权中间件,信任前提是「只经 callHTTPFunction 或集成中心网关调用」。如直接暴露公网,请在上游(API 网关 / 云托管)自行做身份校验。
4.3 响应结构
下单与查单接口返回统一结构:
{
"code": 0,
"msg": "success",
"data": {
"signData": "...",
"mode": "short_series_goods",
"paySig": "...",
"signature": "..."
}
}
code 为 0 表示成功;data 即调起收银台所需的参数。
第五步 · 小程序端接入
5.1 初始化云开发
// app.js
const ENV_ID = 'your-env-id' // ⚠️ 替换为你的云开发环境 ID
const FN_NAME = 'xpay-demo-xxxxxx' // ⚠️ 替换为集成中心生成的云函数名
App({
globalData: { envId: ENV_ID, fnName: FN_NAME },
onLaunch() {
wx.cloud.init({ env: ENV_ID, traceUser: true })
},
})
callHTTPFunction无需开启 CloudBase Auth 身份源、无需安装@cloudbase/js-sdk、无需构建 npm。
5.2 封装调用工具
// utils/xpay.js
function callXpay(path, data) {
const { envId, fnName } = getApp().globalData
return new Promise((resolve, reject) => {
wx.cloud.callHTTPFunction({
name: fnName,
config: { env: envId },
method: 'POST',
header: { 'Content-Type': 'application/json' },
path, // 例如 /xpay/create_order
data,
success: (res) => {
// 响应体可能是字符串,也可能是已解析对象,统一处理
const body = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
resolve(body)
},
fail: (err) => reject(new Error(err.errMsg || '请求失败')),
})
})
}
module.exports = { callXpay }
5.3 发起支付
// pages/index/index.js
const { callXpay } = require('../../utils/xpay')
Page({
data: { paying: false },
async onTapPay() {
if (this.data.paying) return
this.setData({ paying: true })
try {
// 1. 取登录凭证 code
const { code } = await wx.login()
// 2. 服务端生成 signData + 双签名(不会真正下单)
const res = await callXpay('/xpay/create_order', {
code,
productId: 'product_001', // 道具 ID,需与 MP 后台一致
goodsPrice: 100, // 单位:分(100 = 1 元)
buyQuantity: 1,
})
if (res.code !== 0) {
wx.showToast({ title: res.msg || '下单失败', icon: 'error' })
return
}
// 3. 客户端拉起收银台(安卓走微信支付,iOS 走 Apple IAP)
const { signData, mode, paySig, signature } = res.data
await wx.requestVirtualPayment({ signData, mode, paySig, signature })
wx.showToast({ title: '支付成功', icon: 'success' })
// 4. 兜底查单(应对推送延迟,最终以服务端回调为准)
const q = await callXpay('/xpay/query_order', {
code,
outTradeNo: signData.outTradeNo,
})
if (q.code === 0 && q.data?.paid) {
// 已支付:若业务尚未收到回调,可在此触发补发货
}
} catch (err) {
if (err?.errMsg?.includes('cancel')) return // 用户取消不算失败
console.error('[xpay] 支付失败:', err)
wx.showToast({ title: '支付失败', icon: 'error' })
} finally {
this.setData({ paying: false })
}
},
})
实现要点:
signData必须原样透传:任何JSON.parse→stringify导致键顺序变化,都会让签名失效。- 前端成功回调不能作为发货依据:用户异常退出、网络中断都会导致前端回调丢失,发货以服务端推送为准,查单仅作兜底。
goodsPrice单位为分,且需与 MP 后台发布的道具价格一致。- 基础库版本:
callHTTPFunction要求 ≥ 3.15.2,模拟器无法完成真实支付,请用真机验证。
第六步 · 实现发货与订单逻辑
模板中的 services/xpayService.js 已标注 TODO(业务侧),以下三处需要你自行实现:
| 位置 | 模板现状 | 生产环境必须替换 |
|---|---|---|
| 幂等校验 | 进程内 Set(有容量上限) | 数据库唯一索引(冷启动会丢内存状态) |
| 发货逻辑 | 仅更新内存订单状态 | 对接订单表、道具 / 代币余额 |
| iOS 退款问询 | 未发货即放过 | 查持久化订单表 + 真实风控规则,否则已发货订单有资损风险 |
6.1 回调事件
| Event | 处理 |
|---|---|
xpay_goods_deliver_notify | 核心:幂等发货 |
xpay_coin_pay_notify | 更新代币余额 |
xpay_refund_notify | 更新订单 + 回收道具 |
xpay_complaint_notify | 记录,人工跟进 |
xpay_wxpay_callback_notify | 记录风控,按需申诉 |
xpay_subscribe_ios_refund_query_notify | 3 秒内应答退款决策 |
6.2 发货所需字段
回调返回值中已带 goods,业务侧无需再解析推送结构:
{
"ok": true,
"eventType": "xpay_goods_deliver_notify",
"biz": "deliver",
"outTradeNo": "ORDER20261020001",
"wxOrderId": "WXORDER...",
"openid": "o_xxx",
"goods": {
"productId": "goods_001",
"quantity": 2,
"attach": "uid=123"
},
"goodsValid": true,
"missingFields": []
}
只拿到
outTradeNo是发不了货的——必须知道「发哪件、发几个」,即goods.productId与goods.quantity。
6.3 幂等与应答
- 幂等键使用平台单号
wxOrderId(推送中的WeChatPayInfo.MchOrderNo);缺失时回退商家单号outTradeNo。用商家单号去重,会在「用户取消后重新下单复用同一单号」时误判为重复而漏发货。 - 执行顺序必须是「查重 → 处理 → 落标记」。先落标记再处理,一旦处理失败,重推会被当作重复事件忽略,导致永久丢单。
- 错误分级应答:结构性错误(缺
Event)返回{"ErrCode":0,"ErrMsg":"success"}避免无效重推;业务异常返回非 0 触发微信重推。全返回 0 等于静默丢单。 - 商品信息不全时不要一律返回成功:缺
productId应标记异常订单(重推也补不上);quantity缺失可按 1 兜底并标记goodsValid: false;quantity非法(0 / 小数 / 非数字)应抛错触发重推,绝不能静默改成 1,否则「买了 5 件只发 1 件」属于无感知资损。 - 建议配置定时触发器每 5 分钟调用一次
/xpay/query_order,对已支付但未发货的订单补发货。
常见问题 FAQ
Q1:报 41004 appsecret missing
secret 为空或未注入。
排查:查看函数启动日志中的环境变量清单(只打印长度不打印值),确认 secret 有长度;走集成中心时不要单独执行 tcb fn deploy 覆盖平台注入的变量。
Q2:报 41002 appid missing
appId 为空或未注入,排查方式同上。
Q3:回调返回 404 或收不到推送
按顺序排查:
- 微信后台的推送 URL 是否指向集成中心回调域名。
- 集成中心的「回调承接资源」是否已绑定到目标云函数。
- 数据格式(JSON / XML)与加密方式是否与微信后台一致——格式不一致会导致解析出空对象,进而无路由可匹配而返回 404。
- 上线顺序是否正确:先创建集成并等待就绪,再提交微信后台配置。
Q4:回调验签失败
- Token 与 EncodingAESKey 必须与微信后台逐字符一致。
- 安全模式下校验的是
msg_signature(签名串包含Encrypt),不是signature,这是最高频的错误。 - 采用集成中心托管(
system模式)时,验签解密由网关完成,业务侧不要重复验签。
Q5:signData 签名无效
签名必须基于原样字符串。任何解析后重新序列化、或对字段做 trim / 补空格,都会改变待签内容。模板的 signService.js 已强制只接收字符串,自行改造时请保持这一约束。
Q6:iOS 支付失败 / 金额受限
- iOS 走 Apple IAP,最低 1 元,低于该金额的档位无法支付。
- Apple 支付不支持沙箱,沙箱仅在开发版 / 体验版可用,且真机预览可能被
PAYMENT_ILLEGAL_IN_SANDBOX拦截。 - 模板固定使用现网(
env=0),不提供沙箱切换。
Q7:下单报 COIN_OR_PRODUCT_ID_CREATED_IN_RECENTLY
道具刚创建或刚发布,微信侧尚未完成同步。等待几分钟至半小时后重试。
Q8:query_order 查不到订单
query_order 只能查现金单,查不了代币单;代币单请通过用户余额接口与幂等重试兜底。此外必须校验订单归属(openid 与订单的对应关系),否则他人可用自己的 code + 别人的单号越权查单。
Q9:能否修改函数名或回调路径
不能。云函数名中的随机串与回调域名子域、注入的环境变量相互绑定,改名即断回调;回调路由由平台预置,不要修改路径。
Q10:删除集成后云函数还在吗
删除集成不等于删除云函数。删除集成会释放回调域名与回调路由(不可恢复),但云函数与环境变量会保留,需要手动清理。
附录 · 环境变量速查
集成中心创建集成时会自动注入下列变量,用户无需手动维护;此处列出仅供排查与扩展开发参考。
| 变量 | 来源 | 说明 |
|---|---|---|
appId | 集成中心注入 | 小程序 AppID |
secret | 集成中心注入 | 小程序 AppSecret |
offerId | 集成中心注入 | 虚拟支付商户号 |
appKey | 集成中心注入 | 支付签名密钥 |
token | 集成中心注入 | 消息推送 Token |
msgSecret | 集成中心注入 | EncodingAESKey(43 位) |
notifyUrlCallbackType | 集成中心注入 | system = 网关代验签;未声明 = 本函数自行验签 |
msgEncryptType | 集成中心注入 | plaintext / aes / compatible,未传时默认 aes |
cloudbaserc.json 的 functions[].envVariables 是整体覆盖语义。若凭证为空就单独执行 tcb fn deploy,会把平台上已有的值覆盖为空串(表现为 41004 appsecret missing)。走集成中心托管时,请通过「编辑集成」触发部署,或先补全配置再部署。
延伸阅读
| 主题 | 链接 |
|---|---|
| 示例工程(云函数模板 + 小程序 Demo) | https://github.com/TencentCloudBase/awesome-cloudbase-examples/tree/master/integration/cloudbase-virtualpay |
wx.requestVirtualPayment API | https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestVirtualPayment.html |
wx.cloud.callHTTPFunction | https://developers.weixin.qq.com/miniprogram/dev/wxcloudservice/wxcloud/HTTPCloudFunction.html |
| 微信小程序支付集成(实物商品) | 微信小程序支付集成指南 |