【订阅消息】uniapp云开发给订阅用户发送微信小程序订阅消息(实战 + 踩坑)

3,116 阅读9分钟

前言

最近用uniapp云开发搞了个小程序,大部分功能都已经实现了,结果卡死在了订阅消息这个功能上,由于订阅是前端实现,发送是后端实现,而uniapp云开发里面又单独封装了一个公共模块供开发者使用,所以导致文档看起来就很乱,不知道该看哪个了。

经过反复研究和沸点JYM给的思路,算是将这个功能完成了,接下来就把实现过程和踩过的坑记录一下,也给大家一个参考。

这里只讲解uniapp云开发中的公共模块uni-subscribemsg的使用,主要结合云开发去实现,不使用其他后端。

uni-subscribemsg 公共模块

这个公共模块将微信公众号模板消息、微信小程序订阅消息都封装起来了,可以很方便的使用云函数/云对象去操作发送。

点击进入 uni-subscribemsg 公共模块 插件地址

按照一般的思路,我们会在云函数中直接请求微信服务端接口

POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN

// 在云函数中请求微信服务端API(发送订阅消息)
const sendMsg = await uniCloud.httpclient.request(
    "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + access_token, {
    method: "POST",
    ...
});

通过引入公共模块之后,就可以直接使用公共模块中的方法去发送订阅消息

// 引入uni-subscribemsg公共模块
const UniSubscribemsg = require('uni-subscribemsg');
// 初始化实例
let uniSubscribemsg = new UniSubscribemsg({
	dcloudAppid: "你项目的dcloudAppid",
	provider: "weixin-mp",
});
// 发送订阅消息
let res = await uniSubscribemsg.sendSubscribeMessage({
	touser: "用户openid",
	template_id: "消息模板id",
	page: "pages/index/index", // 小程序页面地址
	miniprogram_state: "developer", // 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版
	lang: "zh_CN",
	data: {
		name1: {
			value: "张三"
		},
		time2: {
			value: "2023-12-21 15:30:20"
		}
	}
});

基础部分已说明,公共模块中的代码会在云函数/云对象中进行说明

获取公共模块中的参数

dcloudAppid: "你项目的dcloudAppid"

此参数在uniCloud后台获取 点击进入uniCloud后台

image.png

  • 列表中的Appid即为此参数的值

template_id: "消息模板id"

此参数在小程序后台获取 点击进入小程序后台

image.png
  • 侧边栏选择订阅消息

image.png

  • 在我的模板中点击右侧 选用 按钮

image.png

  • 右侧搜索 签到 就会出现类似的订阅模板了,直接选用你想要的就可以了
  • 你可以搜索其他关键词,和你的小程序类目相关即可

image.png

  • 添加好模板之后,在模板列表中就会出现一个模板ID,复制下来,粘贴到发送消息的参数位置

touser: "用户openid"

此参数在用户登录时获取,可以参考我写的这篇文章。 使用 Uniapp + UniCloud 云开发微信小程序获取用户信息

建表

根据自己的需求建立用户表签到表等,下面只介绍消息订阅表

image.png

  • 在项目 - uniCloud - database上右键,新建 DB Schema
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
    "bsonType": "object",
    "required": [],
    "permission": {
        "read": false,
        "create": false,
        "update": false,
        "delete": false
    },
    "properties": {
        "_id": {
            "description": "ID,系统自动生成"
        },
        "user_id": {
            "title": "关联用户表中的id,订阅者",
            "bsonType": "string",
            "trim": "both"
        },
        "openid": {
            "title": "微信openid",
            "bsonType": "string",
            "trim": "both"
        },
        "state": {
            "title": "订阅状态",
            "bsonType": "int",
            "trim": "both",
            "defaultValue": 1,
            "enum": [{
                    "text": "正常",
                    "value": 1
                }
            ],
            "description": "过滤掉非正常的用户,方便发送订阅消息"
        },
        "type": {
            "title": "类型",
            "bsonType": "int",
            "trim": "both",
            "enum": [{
                    "text": "签到",
                    "value": 1
                }
            ]
        }
    }
}
  • 具体字段可以根据自己的需要去修改

image.png

  • 新建完成后,右键即可上传至云空间了

image.png

  • 云空间中有了这张表之后,就表示创建完成了。

用户触发订阅消息

  • 当用户签到完成后,弹出订阅框(如果用户没有勾选“总是保持以上选择,不再询问”,则每次签到都会弹出)
  • 用户订阅成功之后,将订阅状态存入表中(上面新建的用户订阅表),方便发送时获取openid
const _this = this
// 弹出签到提醒的订阅消息,让用户订阅
uni.requestSubscribeMessage({
    tmplIds: [""wb0SXQ86lR*****************liHZ7Y"],
    success: (res) => {
        // 判断是否订阅了
        if (res["wb0SXQ86lR*****************liHZ7Y"] === 'accept') {
            // 订阅之后,就得将此用户信息记录下来,方便后续发送订阅消息
            uniCloud.callFunction({
                name: 'a_subscribe_sign',
                data: {
                    action: 'setSubscribeUserInfo',
                    user_id: _this.user._id,
                    openid: _this.user.openid,
                    state: 1, // 正常
                    type: 1 // 签到
                },
                success: (res) => {
                    uni.showToast({
                        title: '订阅成功',
                        icon: 'success'
                    })
                },
                fail: () => {
                    uni.showToast({
                        title: '订阅失败',
                        icon: 'error',
                        duration: 2000
                    })
                }
            })
        }
    }
});
  • 用 * 号加密的部分是微信后台自己添加的订阅模板ID哦!
  • 其他参数根据自己的需求去获取即可。

云对象发消息

新建云对象

image.png

  • 在项目 - uniCloud - cloudfunctions目录上右键 - 新建云函数/云对象

image.png

  • 在弹出框里面选择云对象
  • 输入云对象名称
  • 点击 添加公共模块或扩展库依赖

image.png

  • 管理扩展库中选择 uni-subscribemsg公共模块

image.png

  • 新建完成后,就可以在目录中看到新建的云对象了
// 一个空的云对象代码
module.exports = {
    _before: function() {

    }
}

云对象定时发送

由于签到的订阅消息是在用户订阅之后,每天都会发送的,所以需要给一个定时任务,便于每天定时触发

注意:下面的操作均在DCloud控制台中完成

image.png

  • 根据图片中的步骤说明,找到对应的云对象,点击后面的 详情 按钮

image.png

  • 进入详情页后,找到 定时器触发 模块,点击 编辑 按钮

image.png

["0 0 18 * * *"]
  • 将上面这个数组复制进去即可
  • 表示 每天下午六点整 准时触发

如需修改定时器时间,可以点击查看定时器文档

定时任务方法

  • 在uniCloud中,给云对象内置了一个定时触发的方法
module.exports = {
	_timing: function () { 
		console.log('triggered by timing')
	}
}
  • _timing就是内置的定时触发方法

接下来,我们就可以把发送订阅消息的代码复制进来了,引入模块的内容放在module.exports外层。

此处的源码就是上面介绍公共模块时的代码,稍加修改即可。

// 云对象教程: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
// jsdoc语法提示教程:https://ask.dcloud.net.cn/docs/#//ask.dcloud.net.cn/article/129
// 引入uni-subscribemsg公共模块
const UniSubscribemsg = require('uni-subscribemsg');
// 初始化实例
let uniSubscribemsg = new UniSubscribemsg({
	dcloudAppid: "__U*******B9", // https://dev.dcloud.net.cn/pages/app/list
	provider: "weixin-mp",
});

// 这里是签到定时任务
// 会定时推送微信订阅消息给用户

module.exports = {
	_before: function() { // 通用预处理器
		
	},
	// 定时触发内置方法
	_timing: async function() {
		// 发送订阅消息
		let res = await uniSubscribemsg.sendSubscribeMessage({
			touser: "olhn80For3o23OrxC*********",	// 用户openid
			template_id: "wb0SXQ86lRoa*************liHZ7Y",	// 消息模板id
			page: "pages/login/login", // 小程序页面地址
			miniprogram_state: "trial", // 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版
			lang: "zh_CN",
			data: {	// 模板参数
				phrase6: {
					value: "未签到"
				},
				thing2: {
					value: "签到最高可获得20积分哦~"
				},
				time3: {
					value: "2019年10月1日 15:01:01"
				},
				thing5: {
					value: "点击去签到"
				}
			}
		});
	}
}

  • 源码中加了 * 号的都是需要自己定义的内容,均在上文中有讲解说明

订阅消息中的 data 对象中的内容,需要和订阅消息模板中的内容保持一致。(此处有不明白的,可以评论中提问)

此时就可以等待下午六点准时触发定时任务后下发订阅通知

image.png

云函数发消息

由于云对象中无法调用云函数(获取用户openid)的接口,所以放弃了使用云对象下发定时任务的方法

新建云函数

  • 和新建云对象是一样的,只是选项不同哦!
  • 新建云函数的时候同样需要引入公共模块

云函数定时发送

在云函数中,也有一种方法去判断定时任务,通过云函数自带的 context 字段

exports.main = async (event, context) => {
  let source = context.SOURCE // 当前云函数被何种方式调用
  // client   客户端callFunction方式调用
  // http     云函数url化方式调用
  // timing   定时触发器调用
  // server   由管理端调用,HBuilderX里上传并运行
  // function 由其他云函数callFunction调用
}
  • 会根据source最终的值去判断是否为定时任务触发的
let source = context.SOURCE // 云函数调用来源
// 判断来源是否为定时任务触发调用
if (source === 'timing') {
    // 这里面就是云对象中定时任务方法里面的代码,复制进来即可
}

动态获取用户openid

根据建表时定义的字段,我们可以将statetype作为查询参数

const db = uniCloud.database();
// 获取 `a-subscribe-user` 集合的引用(定于用户表)
const a_subscribe_sign = db.collection('a-subscribe-user');

// 循环判断客户端传递过来的 action
// 通过 action 判断请求对象
let result = {};
switch (event.action) {
    // setSubscribeUserInfo用户触发订阅时的方法,添加到订阅用户表中
    case 'setSubscribeUserInfo':
        const res_sign = await a_subscribe_sign.add({
            user_id: event.user_id,
            openid: event.openid,
            state: event.state,
            type: event.type
        })
        break;
    // getAllOpenId获取所有订阅的openid
    case 'getAllOpenId':
        // 获取所有正常用户的openid
        // 用于发送订阅消息
        const res_all_val = await a_subscribe_sign.where({
            state: 1,
            type: 1
        }).get()
        return res_all_val.data
}

将方法定义好之后,我们就可以在定时任务判断逻辑中引用了。

云函数完整代码

下面是云函数的所有代码,可以根据注释进行理解

'use strict';
exports.main = async (event, context) => {
	//event为客户端上传的参数
	// console.log('event : ', event)

	const db = uniCloud.database();
	// 获取 `a-subscribe-user` 集合的引用(定于用户表)
	const a_subscribe_sign = db.collection('a-subscribe-user');

	// 条件查询必备语法
	// const dbCmd = db.command

	// 循环判断客户端传递过来的 action
	// 通过 action 判断请求对象
	let result = {};
	switch (event.action) {
		case 'setSubscribeUserInfo':
			const res_sign = await a_subscribe_sign.add({
				user_id: event.user_id,
				openid: event.openid,
				state: event.state,
				type: event.type
			})
			break;
		case 'getAllOpenId':
			// 获取所有正常用户的openid
			// 用于发送订阅消息
			const res_all_val = await a_subscribe_sign.where({
				state: 1,
				type: 1
			}).get()

			return res_all_val.data
			break;
	}




	// 引入uni-subscribemsg公共模块
	const UniSubscribemsg = require('uni-subscribemsg');
	// 初始化实例
	let uniSubscribemsg = new UniSubscribemsg({
		dcloudAppid: "__UNI__C40B1B9", // https://dev.dcloud.net.cn/pages/app/list
		provider: "weixin-mp",
	});

	let source = context.SOURCE // 云函数调用来源

	// 判断来源是否为定时任务触发调用
	if (source === 'timing') {

		console.log('获取用户信息')

		let callFunctionResult = await uniCloud.callFunction({
			name: "a_subscribe_sign",
			data: {
				action: 'getAllOpenId'
			}
		})

		console.log('callFunctionResult*****', callFunctionResult.result)

		// 获取到所有数据后,循环拿出openid
		let openids = []
		for (let i in callFunctionResult.result) {
			openids.push(callFunctionResult.result[i].openid)
		}

		// 过滤掉重复的openid
		openids = [...new Set(openids)]

		// 获取时间,并格式化
		function formatDate(date, cut) {
			var date = new Date(date);
			var YY = date.getFullYear() + cut;
			var MM =
				(date.getMonth() + 1 < 10 ?
					"0" + (date.getMonth() + 1) :
					date.getMonth() + 1) + cut;
			var DD = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
			var hh =
				(date.getHours() < 10 ? "0" + date.getHours() : date.getHours()) + ":";
		 var mm =
				(date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes()) +
				":";
			var ss = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
			return YY + '年' + MM + '月' + DD + '日' + " " + hh + mm + ss;
		}

		console.log('我是定时任务!', openids)

		// 循环发送订阅消息(给所有获取到的openid)
		for (let i in openids) {
			// 发送订阅消息
			let res = await uniSubscribemsg.sendSubscribeMessage({
				touser: openids[i], // 用户openid
				template_id: "wb0SXQ86lRoaJO8j4DxheOKpgVUuw-iER-gDRliHZ7Y", // 消息模板id
				page: "pages/login/login", // 小程序页面地址
				miniprogram_state: "trial", // 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版
				lang: "zh_CN",
				data: { // 模板参数
					phrase6: {
						value: "未签到"
					},
					thing2: {
						value: "签到最高可获得20积分哦~"
					},
					time3: {
						value: formatDate()
					},
					thing5: {
						value: "点击去签到"
					}
				}
			});
			console.log('订阅消息打印一下****', res)
		}

	}

	//返回数据给客户端
	// return event
};

总结

由于微信小程序订阅消息的限制,无法一次性批量触发,只能通过循环获取openid的方式去给每个用户发送。

由于uniapp云对象的限制,有些逻辑只能放在云函数里面,如果有更好的云对象操作方法,可以在评论区讨论一下,毕竟云对象我觉得相对于云函数来说是比较简单的。