微信云托管(二)--消息推送手记

微信云托管(二)--消息推送手记

众所周知,小程序如果没有相应的资质的话,是没有办法主动推送消息给用户的,想要从它的服务通知里面推送,资质认证和审核比较麻烦。另一种方法就是订阅消息,但是需要用户授权,授权一次,然后这边可以推送一次,如果想要一直可以主动推送,我们通常都是借公众号的模板消息。

正题之前,描述一下之间做出的一个弯路方案:

当前处理的是一个聊天工具,我需要点击发送的时候,用户有新的消息的时候,可以接收到提醒,设想让用户在点击发送的时候,勾上“总是授权”,然后把授权事件一直绑定在发送按钮上,这样每一次消息发送的时候,就等于自动授权了一次,那就可以接收下一次的订阅消息推送了。典线完成无限次的消息推送。

Image.png

正经的开始,开局一张草图,内容全靠编。

云托管的好处就是不需要维护accessToken,想要啥接口,直接调就行。

Image.png

前置准备

  • 需要一个认证后的公众号
  • 需要一个认证并备案过的小程序
  • 需要一个认证后的第三方开放平台
  • 公众号和小程序是同一个主体
  • 需要一个可用云托管环境
  • 记下公众号的appid,小程序的appid
  • 添加一个模板消息,记下模板消息的模板信息

开始配置

公众号:

添加模板消息,如果还没有开通的先进行开通,和设置类目。

Image.png

绑定小程序为关联小程序(不配的话,发送跳转到小程序的消息会失败)

小程序

开放平台

添加小程序到开放平台

添加公众号同一个开放平台

云托管

  1. 资源复用

如果公众号开的云托管,就把小程序添加到资源复用

如果小程序开的云托管,就把公众号添加到资源复用

重要的知识点:

如果没有资源复用的时候,服务调用微信开放接口,直接调用就行了。不需要

accessToken 和签名这些内容,直调。

但是如果资源复用了之后,调主账号的内容的时候,可以维护原来一样,直调。

调用复用的应用开发接口的时候,则需要在接口后面增加上from_appid的参数,不然后提示未授权。

例子说明:我现在的主账号是小程序注册的云托管,调用小程序的微信开放接口直接调用

比如https://api.weixin.qq.com/cgi-bin/message/subscribe/send

但是公众号是添加的资源复用,调用公众号的开放接口的时候,则需要这样

http://api.weixin.qq.com/cgi-bin/message/template/send?from_appid=公众号的appid

它的机制是不填默认是主应用的appid,小程序的appid调用公众号的接口,肯定不成功,但是官方不给你提示这个错误信息,只是返回未授权。

我这里是小程序开的账号,所以把公众号添加到资源复用。

Image.png

  1. 云调用接口配置

在云调用里面,把公众号需要调用的接口路径添加到配置里面。

前面两个小程序的订阅消息和获取手机号,添加后面两个就行了,获取用户信息和发送模板消息。

Image.png

  1. 配置消息推送功能

服务端写一个接口,接收公众号的事件推送,用户关注/取消关注等事件,会主动触发该接口,接口根据拿到的信息处理相应的逻辑就行了。

比如这里的这个接口的主要逻辑就是:

  • 判断事件类型是否是关注事件
  • 如果是关注事件,从接口里面获取用户的openId
  • 根据这个openId调用cgi-bin/user/info接口,获取用户的unionId
  • 根据unionId查询小程序的用户表,获取的用户信息,将公众号的openId更新进去

关注/取消关注事件 | 微信开放文档

模板消息 | 微信开放文档

Image.png

Image.png

然后就是撸两个接口

第一个更新用户公众号的openId到信息表。

当然也可以用其它的逻辑,按事件同步公众号的用户列表等

// 处理微信事件推送的接口
app.post('/api/v1/sendTemplateMessage', async (req, res) => {
    const msgType = req.body.Event;
    const openid = req.body.FromUserName;

    // 根据事件类型进行处理
    switch (msgType) {
        case 'subscribe':
            // 用户关注事件处理逻辑
            try {
                // 通过openid获取unionid
                const response = await axios.get(`http://api.weixin.qq.com/cgi-bin/user/info?openid=${openid}&lang=zh_CN&from_appid=你的公众号appid`);
                const unionid = response.data.unionid;

                if (unionid) {
                    // 查询Users表,检查是否存在该unionid
                    const user = await Users.findOne({ where: { unionId: unionid } });

                    if (user) {
                        // 更新publicId字段
                        user.publicId = openid;
                        await user.save();
                        console.log('用户信息已更新:', user);
                    } else {
                        console.log('未找到匹配的用户信息');
                    }
                } else {
                    console.log('未能获取到unionid');
                }
            } catch (error) {
                console.error('获取unionid或更新用户信息时出错:', error);
            }
            break;

        case 'unsubscribe':
            console.log('用户取消了关注');
            break;

        case 'SCAN':
            console.log('用户扫描了二维码');
            break;

        case 'LOCATION':
            console.log('用户上报了地理位置');
            break;

        case 'CLICK':
            console.log('用户点击了菜单');
            break;

        case 'VIEW':
            console.log('用户点击了跳转链接');
            break;

        default:
            console.log('未知事件类型:', msgType);
    }

    // 返回空响应以避免重试
    res.send('');
});

发送模板消息的事件

// 需要发送消息的时候,传入参数调用该事件,
async function sendTempMessage(openid,data) {
    // 构造小程序跳转链接
    const page = `pages/index/index`;
    const {time,content} = data

    // 发送模板消息
    try {
        const apiUrl = `http://api.weixin.qq.com/cgi-bin/message/template/send?from_appid=你的公众号appid`;
        const requestBody = {
            touser:openid,
            template_id:"你的消息模板ID",
            miniprogram:{
                appid:"你的消息要跳转的小程序appid",
                pagepath:page
            },
            data:{
                // 这里的内容根据你的模板消息的格式来
                "thing19": {
                    "value": content
                },
                "time04": {
                    "value": time
                }
            }
        }
        const response = await axios.post(apiUrl, requestBody);
        if (response.data.errcode === 0) {
            console.log('模板消息发送成功');
        } else {
            console.error('模板消息发送失败:', response.data);
            throw new Error('模板消息发送失败');
        }
    } catch (error) {
        console.error('模板消息发送失败:', error);
    }
} 

Image.png

忽略时间吧,时间我写死了测试的。

IMG_4561.heic