Appearance
云函数订阅消息发放
说明
这里仅讨论批量发放模板消息的场景,单次发放不纳入考虑
分为「用户模板消息订阅记录」和「模板消息发放任务」两个概念:用户订阅成功生成订阅记录,运营方可根据订阅记录的标签来生成模板消息发放任务
核心是如何将「订阅记录」整合并生成「订阅消息发放任务」以及如何执行模板消息发放任务
核心实现逻辑
用户在完成模板消息订阅时,可根据需要生成订阅记录(subscribe_log),并为此记录打上 tags
后续当运营需要发送模板消息时,可筛选出带有指定 tags 的订阅记录批量生成模板消息发送任务,定时/手动执行发送任务来给用户发送订阅消息
工具集合
获取订阅记录 getSubscribeLogs
根据上述定义,用户在客户端订阅模板消息后,可根据场景需要创建订阅记录,即 subscribe_log,详细可看 @ifanrx/uni-mp
在云函数端可通过 getSubscribeLogs 来获取这些订阅记录
ts
interface GetSubscribeLogs {
(payload: GetSubscribeLogsOptions) => Promise<{subscribeLogIds: string[], userIds: UserId[]}>
}
interface GetSubscribeLogsOptions {
// 使用 arrayContains 查询
tags: string[]
templateId?: string
tradeUnionId?: string
tradeUnionHierarchy?: string[]
}
TIP
需注意使用 tags 查询指定订阅记录,否则容易因为数据量过大导致云函数超时或内存溢出
同时 getSubscribeLogs 只会筛选出 status 为 idle 的订阅记录
getSubscribeLogs 会自动根据 created_by 过滤掉同一个用户的订阅记录,最多只保留一条记录
getSubscribeLogs 查询订阅记录的条件如下:
ts
query.arrayContains('tags', options.tags).compare('status', '=', 'idle')
if (options.templateId) query.compare('template_id', '=', options.templateId)
if (options.tradeUnionId)
query.compare('trade_union_id', '=', options.tradeUnionId)
if (options.tradeUnionHierarchy) {
query.in('trade_union_hierarchy', options.tradeUnionHierarchy)
}
example
js
// 获取用户 id 以及订阅记录 id
const {userIds, subscribeLogIds} = getSubscribeLogs({
tags: ['act100-xxx'],
templateId: 'xxxxx',
tradeUnionId: 'xxxxx',
})
批量创建模板消息发送任务 createSubscribeTasks
当运营准备发送模板消息时,可通过 createSubscribeTasks 批量创建模板消息发送任务
ts
// 通过该云函数创建 subscribe_task
interface CreateSubscribeTasks {
(payload: CreateSubscribeTasksOptions) => Promise<SubscribeTask[]>
}
interface CreateSubscribeTasksOptions {
/**
* 每次执行任务时发送的模板消息数量
*/
limit: number
/**
* 当前批次任务的标识,同一批次的任务 key 相同,后续通过 executeSubscribeTask 来执行任务
*/
key: string
userIds: number[]
/**
* 非必填,不传入时不操作 subscribe_log
* 填入时需确保顺序与 user_ids 保持对应关系
*/
subscribeLogIds?: string[]
/** 用于识别该任务将使用哪个模板执行任务 */
templateId?: string
/**
* wx.sendSubscribeMessage 所需要的配置
* 会自动填入 recipient_type 和 user_list
* 需要手动填入 template_id、keywords、page 等内容
* keywords 的填入规则可查看 SubscribeKeywordsBuilder
*/
messageConfig: object
}
createSubscribeTasks 支持在两种场景下使用:
- 有订阅记录存在,同时指定每个发送任务的 user_ids 和 subscribe_log_ids,如:在某个地市活动开始之前,给该地市下订阅了某个模板消息的用户发送订阅消息
- 不存在订阅记录,仅指定每个发送任务的 user_ids,如:活动结束后,给所有参与了活动的用户发送活动抽奖结果通知
example
在活动开始前对指定人群发送开始通知
js
// 收集订阅了带有 'act100-xxx' 标签,且工会 id 是 'xxxxx' 的用户
const {userIds, subscribeLogIds} = await getSubscribeLogs({
tags: ['act100-xxx'],
tradeUnionId: 'xxxxx',
})
await createSubscribeTasks({
// 这批模板消息发送任务的 key 定义为 'task_key'
key: 'task_key',
// 每次执行模板消息发送任务时,最多给 1000 位用户发送消息
limit: 1000,
userIds,
subscribeLogIds,
template_id: 'xxx',
// 模板消息配置
messageConfig: {
keywords: {
thing3: 'xxx',
time1: 'xxx',
phrase2: 'xxx',
},
template_id: 'xxx',
page: 'pages/index/index',
},
})
活动抽奖结果通知
js
// 获取中奖用户列表
const luckyLotteryLogs = await io.lotteryLog.find({
query: io.query.compare('is_lucky', '=', true),
limit: 1000,
})
await createSubscribeTasks({
// 这批模板消息发送任务的 key 定义为 'task_key'
key: 'task_key',
// 每次执行模板消息发送任务时,最多给 1000 位用户发送消息
limit: 1000,
userIds: luckyLotteryLogs.map(({created_by}) => created_by),
template_id: 'xxx',
// 模板消息配置
messageConfig: {
keywords: {
thing3: 'xxx',
time1: 'xxx',
phrase2: 'xxx',
},
template_id: 'xxx',
page: 'pages/index/index',
},
})
执行发送任务 executeSubscribeTask
用于执行由 createSubscribeTasks 生成的任务
ts
interface ExecuteSubscribeTask {
(payload: ExecuteSubscribeTaskOptions): Promise<SubscribeTask>
}
interface ExecuteSubscribeTaskOptions {
/**
* 与 createSubscribeTasks 的 key 相同
*/
key: string
/**
* 支持覆盖 subscribe_task 的 messageConfig,传入后会覆盖 subscribe_task 的 messageConfig
*/
messageConfig: object
}
会根据 key 对任务进行筛选,且只会筛选状态为 pending 的 subscribe_task,只执行最新一条 subscribe_task
在创建执行 executeSubscribeTask 的云函数后,可通过触发器定时任务来执行该云函数,也可手动执行
example
js
// 使用创建任务时定义的 messageConfig 发送
await executeSubscribeTask({key: 'task_key'})
// 使用最新的 messageConfig 发送,会同步更新到 subscribe_task 内
await executeSubscribeTask({
key: 'task_key',
messageConfig: {
keywords: {
thing3: 'xxx',
time1: 'xxx',
phrase2: 'xxx',
},
template_id: 'xxx',
page: 'pages/index/index',
},
})
订阅消息参数构造器 SubscribeKeywordsBuilder
用于生成订阅消息的 keywords
会根据每个参数类型的字数限制自动对文本进行裁剪,以及对日期做格式化等
example
链式调用生成 keywords 实例
js
const keywords = new SubscribeKeywordsBuilder()
.addKeyword(
'thing2',
'这是一段很长很长很长很长很长很长很长很长很长很长很长很长很长的文字'
)
.addKeyword(
'phrase4',
'这是一段很长很长很长很长很长很长很长很长很长很长很长的文字'
)
.addKeyword('thing3', '标题五个字')
.addKeyword('date2', '2024-01-01', '2024-02-01')
.export()
console.log(keywords) // {"thing2":{"value":"这是一段很长很长很长很长很长很长很长很⋯"},"phrase4":{"value":"这是一段⋯"},"thing3":{"value":"标题五个字"},"date2":{"value":"2024-01-01 00:00~2024-02-01 00:00"}}
使用初始值生成 keywords 实例
js
const keywords = new SubscribeKeywordsBuilder({
thing3: '123',
date2: ['2024-01-01', '2024-02-01'],
phrase: '456',
}).export()
console.log(keywords) // {"thing3":{"value":"123"},"date2":{"value":"2024-01-01 00:00~2024-02-01 00:00"},"phrase":{"value":"456"}}
订阅消息发放 sendSubscribeMessage
对 BaaS.wechat.sendSubscribeMessage 进行二次封装,主要是对 keywords 的传参进行优化
example
直接传入 SubscribeKeywordsBuilder 实例
js
sendSubscribeMessage({
keywords: new SubscribeKeywordsBuilder()
.addKeyword('thing3', '123')
.addKeyword('date2', '2024-01-01', '2024-02-01'),
template_id: 'xxx',
page: 'pages/index/index',
})
sendSubscribeMessage({
keywords: new SubscribeKeywordsBuilder({
thing3: '123',
date2: ['2024-01-01', '2024-02-01'],
phrase: '456',
}),
template_id: 'xxx',
page: 'pages/index/index',
})
使用普通对象,可以是与 SubscribeKeywordsBuilder 初始值相同的格式,内部会进行预处理
js
// 更推荐使用这种方法
sendSubscribeMessage({
keywords: {
thing3: '123',
date2: ['2024-01-01', '2024-02-01'],
phrase: '456',
},
template_id: 'xxx',
page: 'pages/index/index',
})
// 支持使用 BaaS.wechat.sendSubscribeMessage 支持的 keywords 格式
sendSubscribeMessage({
keywords: {
thing3: {value: '123'},
date2: {value: '2024-01-01 00:00~2024-02-01 00:00'},
phrase: {value: '456'},
},
template_id: 'xxx',
page: 'pages/index/index',
})
附录
subscribe_log schema
Click me to view the code
json
{
"name": "subscribe_log",
"description": null,
"row_read_perm": ["user:{created_by}"],
"row_write_perm": [],
"write_perm": ["user:*"],
"schema": {
"fields": [
{
"name": "id",
"type": "id",
"description": "id"
},
{
"acl": {
"clientVisible": false,
"clientReadOnly": true,
"creatorVisible": false
},
"name": "status",
"type": "string",
"default": "idle",
"constraints": {
"rules": [
{
"type": "choices",
"value": ["idle", "pending", "succeeded", "failed"]
}
],
"required": true
},
"description": ""
},
{
"acl": {
"clientVisible": false,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "tags",
"type": "array",
"items": {
"type": "string"
},
"constraints": {
"rules": [],
"required": true
},
"description": ""
},
{
"acl": {
"clientVisible": false,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "template_id",
"type": "string",
"constraints": {
"rules": [],
"required": true
},
"description": ""
},
{
"acl": {
"clientVisible": false,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "trade_union_id",
"type": "string",
"constraints": {
"rules": [],
"required": false
},
"description": ""
},
{
"acl": {
"clientVisible": false,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "trade_union_hierarchy",
"type": "array",
"items": {
"type": "string"
},
"constraints": {
"rules": [],
"required": false
},
"description": ""
},
{
"acl": {
"clientVisible": false,
"clientReadOnly": true,
"creatorVisible": false
},
"name": "subscribe_task",
"type": "string",
"constraints": {
"rules": [],
"required": false
},
"description": ""
},
{
"acl": {
"clientVisible": false,
"clientReadOnly": true,
"creatorVisible": false
},
"name": "failure_reason",
"type": "string",
"constraints": {
"rules": [],
"required": false
},
"description": ""
},
{
"name": "created_by",
"type": "integer",
"description": "created_by"
},
{
"name": "created_at",
"type": "integer",
"description": "created_at"
},
{
"name": "updated_at",
"type": "integer",
"description": "updated_at"
}
]
}
}
subscribe_task schema
Click me to view the code
json
{
"name": "subscribe_task",
"description": null,
"row_read_perm": ["user:{created_by}"],
"row_write_perm": [],
"write_perm": [],
"schema": {
"fields": [
{
"name": "id",
"type": "id",
"description": "id"
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "user_ids",
"type": "array",
"items": {
"type": "number"
},
"constraints": {
"rules": [],
"required": true
},
"description": ""
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "subscribe_log_ids",
"type": "array",
"items": {
"type": "string"
},
"constraints": {
"rules": [],
"required": false
},
"description": ""
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "template_id",
"type": "string",
"constraints": {
"rules": [],
"required": false
},
"description": ""
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "key",
"type": "string",
"constraints": {
"rules": [],
"required": true
},
"description": ""
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "status",
"type": "string",
"default": "pending",
"constraints": {
"rules": [
{
"type": "choices",
"value": ["pending", "succeeded", "failed"]
}
],
"required": true
},
"description": ""
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "message_config",
"type": "object",
"constraints": {
"rules": [],
"required": false
},
"description": ""
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "failure_reason",
"type": "string",
"constraints": {
"rules": [],
"required": false
},
"description": ""
},
{
"name": "created_by",
"type": "integer",
"description": "created_by"
},
{
"name": "created_at",
"type": "integer",
"description": "created_at"
},
{
"name": "updated_at",
"type": "integer",
"description": "updated_at"
}
]
}
}