跳到主要内容

用云函数给微信小程序发订阅消息

一句话定义:小程序前端通过 wx.requestSubscribeMessage 取得用户授权后把订阅记录落库,服务端用 cloud.openapi.subscribeMessage.send 在合适的时机给单个用户发模板消息,完成「订单发货」「评论回复」这类业务通知。

预计耗时:40 分钟 | 难度:进阶

适用场景

  • 适用:微信小程序 + 独立 Cloudbase 环境,需要给已登录用户发模板消息
  • 适用:已经在公众平台申请到至少一个订阅消息模板,拿到 templateId
  • 不适用:服务通知(那是公众号的能力,API 不同)
  • 不适用:用 wx.cloud 的微信·云开发体系(其官方文档另一套)

环境要求

依赖版本
Node.js(云函数运行时)≥ 16.13
wx-server-sdklatest(用于云调用 OpenAPI)
@cloudbase/node-sdk3.18.1
@cloudbase/clilatest

另外需要:

  • 已完成 add-auth-wechat-miniprogram,小程序里有可用的登录态
  • 已完成 add-database-wechat-miniprogram,知道怎么读写云数据库
  • 微信公众平台 → 功能 → 订阅消息 申请到至少一个模板,记下 templateId 和模板里的字段名(比如 thing1 time2 amount3)
  • 在 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-sdkwx-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);
}

运行验证

  1. 在公众平台开发设置里,把当前小程序的 AppID 关联到 Cloudbase 环境
  2. 在小程序里点击触发 wx.requestSubscribeMessage,系统弹窗 → 选「允许」
  3. Cloudbase 控制台 → 数据库 → subscriptions,应该多一条 _openid 等于当前用户 openid 的记录,remainingQuota 为 1
  4. 调一次 notifyOrder 云函数,微信里应该能收到模板消息
  5. 再去看 subscriptions 那条记录,remainingQuota 应变为 0,lastUsedAt 有值

常见错误

错误码原因修复
43101用户没订阅这个模板,或上次的「一次性订阅」已经被消费过正常业务路径,前端先 requestSubscribeMessage 拿授权后再发
47003data 里某个字段超长,或字段名和模板对不上比对模板里的字段名(thing1 time2 等等),严格按 { value: '...' } 结构传,长字段截到限制以内
40037templateId 不存在或已删除重新去公众平台拿一次最新的 templateId
41030page 路径不存在路径必须是已发布版本里有的页面,联调期把 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 设备类小程序类似长期订阅,但绑定设备

商业类小程序大概率只能用一次性订阅,所以业务上要做的是「多埋几个授权入口」,提高单次会话里的授权数量。

相关文档

下一步