用云函数给微信小程序发订阅消息
一句话定义:小程序前端通过
wx.requestSubscribeMessage取得用户授权后把订阅记录落库,服务端用cloud.openapi.subscribeMessage.send在合适的时机给单个用户发模板消息,完成「订单发货」「评论回复」这类业务通知。预计耗时:40 分钟 | 难度:进阶
适用场景
- 适用:微信小程序 + 独立 Cloudbase 环境,需要给已登录用户发模板消息
- 适用:已经在公众平台申请到至少一个订阅消息模板,拿到
templateId - 不适用:服务通知(那是公众号的能力,API 不同)
- 不适用:用
wx.cloud的微信·云开发体系(其官方文档另一套)
环境要求
| 依赖 | 版本 |
|---|---|
| Node.js(云函数运行时) | ≥ 16.13 |
wx-server-sdk | latest(用于云调用 OpenAPI) |
@cloudbase/node-sdk | 3.18.1 |
@cloudbase/cli | latest |
另外需要:
- 已完成 add-auth-wechat-miniprogram,小程序里有可用的登录态
- 已完成 add-database-wechat-miniprogram,知道怎么读写云数据库
- 在 微信公众平台 → 功能 → 订阅消息 申请到至少一个模板,记下
templateId和模板里的字段名(比如thing1time2amount3) - 在 Cloudbase 控制台 → 环境设置 → 微信公众平台关联,把当前小程序 AppID 和当前 env 关联起来。这一步是「云调用」能跑通的前提
「云调用」指的是云函数直接调微信开放 API,不需要业务代码自己换 access_token。Cloudbase 在请求微信侧时会用关联好的小程序身份,所以这一步必须先做。
第一步:确认模板字段映射
到 公众平台 → 订阅消息 → 我的模板,点开你那个模板,记下:
templateId,形如bV-cBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- 模板内容里出现的字段名,例如
{{thing1.DATA}}{{time2.DATA}}{{amount3.DATA}},后面data字段必须严格按这些字段名传
字段类型有限制:
| 字段名前缀 | 内容长度 / 格式 |
|---|---|
thing | ≤ 20 字 |
time | 「2024 年 1 月 1 日 14:00」这种格式 |
amount | 数字 + 货币符号,如 ¥100 |
phrase | ≤ 5 字 |
name | 姓名,≤ 10 字 |
写入时超长会被微信侧直接拒,错误码 47003。
第二步:小程序前端弹授权 + 落库
miniprogram/pages/order/order.js:
import { db } from '../../libs/cloudbase';
const SUBSCRIBE_TPL = 'bV-cBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
Page({
async onSubmitOrder() {
// 1. 弹授权(必须在用户点击事件回调里调)
const res = await new Promise((resolve, reject) => {
wx.requestSubscribeMessage({
tmplIds: [SUBSCRIBE_TPL],
success: resolve,
fail: reject,
});
});
if (res[SUBSCRIBE_TPL] !== 'accept') {
// 用户拒绝或本次未授权,业务可继续,但不发订阅消息
return;
}
// 2. 把这次授权落库,记录可用次数(一次性订阅每次授权 = 1 次发送额度)
await db.collection('subscriptions').add({
templateId: SUBSCRIBE_TPL,
remainingQuota: 1,
acceptedAt: db.serverDate(),
// _openid 由 SDK 自动写入
});
// 3. 后续业务流程(下单、付款……)
},
});
四个易疏漏点:
wx.requestSubscribeMessage必须在「用户点击」的同步回调里调,不能放在onLoad或异步await之后,否则会直接报「请求被拦截」- 用户在系统弹窗里勾「总是保持以上选择」之后,下次再调就不会弹窗,直接返回
accept,业务无感 - 一次性订阅消息每授权一次只能发一条,所以前端授权后要立刻落库 +1 条额度,服务端发送时要 -1
- 长期订阅(政府/医疗/交通等类目)是另一种额度规则,授权一次后服务端可在 30 天内重复发,适合公共服务
第三步:云函数发消息
新建云函数 cloudfunctions/notifyOrder/index.js:
const cloud = require('wx-server-sdk');
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
});
const db = cloud.database();
const _ = db.command;
exports.main = async (event) => {
const { openid, templateId, page, data } = event;
// 1. 从 subscriptions 集合里取一次「未消费的额度」
const queryRes = await db
.collection('subscriptions')
.where({
_openid: openid,
templateId,
remainingQuota: _.gt(0),
})
.orderBy('acceptedAt', 'asc')
.limit(1)
.get();
if (queryRes.data.length === 0) {
return { ok: false, error: 'NO_QUOTA', message: '用户没有可用的订阅授权' };
}
const quotaDoc = queryRes.data[0];
// 2. 调云调用发消息
try {
const sendRes = await cloud.openapi.subscribeMessage.send({
touser: openid,
templateId,
page: page || 'pages/index/index',
data,
miniprogramState: 'formal', // formal / trial / developer
lang: 'zh_CN',
});
// 3. 发送成功才扣额度
await db.collection('subscriptions').doc(quotaDoc._id).update({
remainingQuota: _.inc(-1),
lastUsedAt: db.serverDate(),
});
return { ok: true, sendRes };
} catch (err) {
return {
ok: false,
error: 'SEND_FAILED',
errcode: err.errCode,
errmsg: err.errMsg,
};
}
};
注意点:
- 这个云函数用的是
wx-server-sdk,不是 add-auth 里那个@cloudbase/node-sdk。wx-server-sdk才有cloud.openapi,而它要求云函数本身和小程序通过「微信公众平台关联」关联起来 templateId在调用参数里是templateId(驼峰),不是template_id(下划线)。微信 REST API 里是下划线,云调用层做了一次驼峰映射miniprogramState控制跳转的小程序版本:formal正式版、trial体验版、developer开发版,联调期用developer- 触达失败不要扣额度,先 try/catch 兜住,只有 send 成功的分支才
inc(-1)
第四步:前端调用云函数
回到小程序里,业务发生时调:
import app from '../../libs/cloudbase';
async function notifyOrderShipped(orderId) {
const callable = app.callFunction({
name: 'notifyOrder',
data: {
openid: app.auth().currentUser.customUserId, // 这个就是 openid
templateId: 'bV-cBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
page: `pages/order/detail?id=${orderId}`,
data: {
thing1: { value: '订单已发货' },
time2: { value: '2024 年 5 月 1 日 14:00' },
amount3: { value: '¥199' },
},
},
});
const res = await callable;
console.log('[notifyOrder]', res.result);
}
运行验证
- 在公众平台开发设置里,把当前小程序的
AppID关联到 Cloudbase 环境 - 在小程序里点击触发
wx.requestSubscribeMessage,系统弹窗 → 选「允许」 - Cloudbase 控制台 → 数据库 → subscriptions,应该多一条
_openid等于当前用户 openid 的记录,remainingQuota为 1 - 调一次
notifyOrder云函数,微信里应该能收到模板消息 - 再去看
subscriptions那条记录,remainingQuota应变为 0,lastUsedAt有值
常见错误
| 错误码 | 原因 | 修复 |
|---|---|---|
43101 | 用户没订阅这个模板,或上次的「一次性订阅」已经被消费过 | 正常业务路径,前端先 requestSubscribeMessage 拿授权后再发 |
47003 | data 里某个字段超长,或字段名和模板对不上 | 比对模板里的字段名(thing1 time2 等等),严格按 { value: '...' } 结构传,长字段截到限制以内 |
40037 | templateId 不存在或已删除 | 重新去公众平台拿一次最新的 templateId |
41030 | page 路径不存在 | 路径必须是已发布版本里有的页面,联调期把 miniprogramState 设成 developer 用开发版本 |
云函数报 cloud.openapi is not a function | 用了 @cloudbase/node-sdk 而不是 wx-server-sdk | 切到 wx-server-sdk,且小程序和当前 env 已经在控制台关联 |
错误码定义参考 error-code,微信侧错误码看 微信开放文档。
一次性 vs 长期订阅
| 类型 | 触发 | 额度规则 |
|---|---|---|
| 一次性订阅 | 任何模板,默认走这条 | 用户每授权 1 次 = 服务端可发 1 条;再发一次需要再次授权 |
| 长期订阅 | 必须申请「公共服务」类目模板,主体一般要求政府 / 医疗 / 交通 / 学校 | 用户授权 1 次,服务端 30 天内可重复发,不消耗额度 |
| 设备订阅 | IoT 设备类小程序 | 类似长期订阅,但绑定设备 |
商业类小程序大概率只能用一次性订阅,所以业务上要做的是「多埋几个授权入口」,提高单次会话里的授权数量。