小白也能学会的nodejs配置微信公众号后台

181 阅读7分钟

不得不说,上一次正经写文章已经是一年以前的事情了,期间虽然折腾了不少有意思的东西,但一直提不起兴趣记录下来,不得不说这实在是一件高付出低回报的活,每次写文章都需要收集很多错误日志和解决问题的过程,往往一写就是好几个小时,然后看的人也就几个,别说点赞数了,点进来阅读的人都不多,每次看到自己花费心力写出来的东西不被大家认可,就感觉这件事失去了意义,毕竟不是天赋型选手,对于这件事也谈不上热爱。

上周因为公司要搞微信登录,想起来许久以前弄过这一套流程,应该当时写了文章,于是时隔许久又回到掘金翻看过去的记录,发现之前遇到的问题这次依然存在,节省了不少试错的时间,又简单翻看了一下后台记录,发现有不少新人注册账号就为了给我说一声谢谢,还是让人挺感动的,也让我萌发了记录这个过程的念头。

不管弄多少次,找到微信公众号接口的报错原因都能让人抓狂,因为是中途决定要写这篇文章的,很多错误日志我没做保留,但是我会尽可能的把一些容易犯错的地方记录下来,希望能给到大家一些帮助,如果遇到了一些文章中没有提到的报错,请详细对照微信公众号官方文档,找到对应的接口部分,按照文档提到的数据结构一个字母一个字母的核对,这篇文档写的很笼统,而且很多地方莫名其妙的和其他类似功能不一样,如果抱着扫一眼就能接上的心态去看那大概率是要出问题的。

1、流程分析

整个流程大体上可以分为 配置公众号 -> 配置关注/菜单功能 -> 其他花里胡哨的功能,微信公众号的功能看上去很多,但实际上常用的就这几步,这篇文章里我会主要讲解 菜单/消息回复 和 关注微信号实现登录跳转 两个功能,其他关于门店卡券之类的功能因为暂时没用到,所以不涉及,如果希望了解这方面内容的话可以换篇文章浏览。

由于每次调用接口都需要给当前IP添加白名单,而修改白名单在正式环境又需要扫码验证,整个调试流程非常麻烦,在这里建议各位申请一个测试公众号 image.png 测试公众号中对应的接口配置和JS安全域名修改需要注意的地方和配置公众号接口一致,参考后文就可以实现了。

为方便后文描述,在这里我总结一下后续可能会遇到的一些类型:

export class WeChatEventObj {
    // 开发者微信号
    ToUserName: string
    // 发送方帐号(一个OpenID)
    FromUserName: string
    // 消息创建时间 (整型)	
    CreateTime: string
    // 消息类型,event
    MsgType: string
    // 事件类型,subscribe(订阅)、unsubscribe(取消订阅)
    Event: string
    // 扫描带参数的二维码返回 事件 KEY 值,qrscene_为前缀,后面为二维码的参数值
    EventKey?: string
    // 二维码的ticket,可用来换取二维码图片
    Ticket?: string
    // 文本消息内容
    Content?: string
}
    async doEvent(eventObj: WeChatEventObj) {
        console.log(eventObj);
        switch (eventObj.Event) {
            // 订阅公众号
            case "subscribe":
                return await this.doSubscribe(eventObj);
            // 取关公众号
            case "unsubscribe":
                return await this.doUnsubscribe(eventObj);
            // 扫二维码
            case "SCAN":
                return await this.doScan(eventObj);
            case "CLICK":
                return await this.doMenu(eventObj);
            default:
                break;
        }
    }

2、配置公众号接口

首先调通微信公众号接口并不困难,我们需要做的第一步就是在 设置与开发-> 基本配置 -> 服务器配置 中开启服务器配置,这个过程我在由SSL证书引发的微信公众平台token验证失败问题 一文中有比较详细的介绍,在这里就不做赘述了,但是不得不提的是,在URL一栏中填写的内容不仅仅是对应的服务器域名,需要精确到对应的是哪个校验接口,这一点很好理解,但是容易被忽略,且这里用的是GET接口,后续接收公众号事件用的是POST接口。

具体写法如下:

  /**
   * 微信校验接口,供微信验证使用
   * 注意验证的时候需要带上当前controll的前缀
   * @param loginInfo
   * @returns
  */
  @HttpCode(200)
  @Get("verify")
  verify(@Query() param: any) {
    if (checkEcho(param)) {
      return param.echostr;
    } else {
      return false;
    }
  }
import { createHash } from "crypto";

export function checkEcho(param: any): boolean {
    const signature = param.signature;
    const timestamp = param.timestamp;
    const nonce = param.nonce;
    // let echostr = param.echostr.toString();
    // token就是我们配置时的token
    const token = "对应服务器配置中的令牌";
    // 将token、timestamp、nonce三个参数进行字典序排序
    const str = [token, timestamp, nonce].sort().join('');
    // 将三个参数字符串拼接成一个字符串进行sha1加密
    const shaStr = createHash('sha1').update(str).digest('hex');
    // 将加密后的字符串与signature对比、如果相同、标识该请求来源于微信、我们原样返回echostr参数
    return shaStr === signature;
}

3、微信扫码关注微信号登录

不知道各位有没有做过类似微信登录的功能,其实流程相对来说有点类似,但有一些区别,大体上来说分为三部:获取二维码 -> 接收用户扫码信息 -> 轮询登录结果

获取二维码

先放代码:

    // 获取临时二维码
    async getTempQR(sceneStr: string) {
        // https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
        // 获取 ticket
        const ticket = await httpPost("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + await this.getAccessToken(),
            {
                "expire_seconds": 60,
                "action_name": "QR_STR_SCENE",
                "action_info": {
                    "scene": { "scene_str": sceneStr }
                }
            });
        // 通过 ticket 拼接临时二维码
        return {
            tmpurl: `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${ticket.data.ticket}`,
            url: ticket.data.url,
            expire_seconds: ticket.data.expire_seconds
        }
    }

其实代码很简单,为什么我要特地放出来,首先scene这个参数中,其实有两种类型,一种是 scene_id,一种是scene_str,这两种类型的区别在于一种只能使用有范围的数字,且不能以0开头,一种是可以使用字符串,说实话,我是没想到可能会有哪些场景使用到数字这种类型,如果有这种需要,请注意选择code类型。

其中code会在用户扫码时,以EventKey的形式传递过来,这一点在后续流程中会提到。

用户扫码/用户关注

这两者在微信回调时是有区别的,当用户未关注扫码时,会跳转到微信号的关注页,此时点关注按钮,微信会给后台回调一条类型为subscribeEventKey字段值为二维码场景值的信息,在这里,你可以获取到用户的openId,可以在这一步做注册账号、获取用户token等操作,并将生成的token放入缓存待获取,这里需要注意的是,扫码关注时发往后台的消息中scene_str会带有qrscene_前缀,如果需要匹配二维码,需要注意将这部分裁掉。

用户直接关注的流程和用户扫码两者的区别主要是在于调用类型,当用户已经是关注状态扫码,后台收到的回调类型为SCAN,EventKey中没有qrscene_前缀,但如果想统一处理的话,直接用replace方法也是可以的sceneStr.replace("qrscene_", "")

轮询结果

简单来说就是前端通过获取二维码时传入的scene_str调用后端接口查询是否登录成功,这里由于查询可能会比较频繁,不建议将登录结果存入数据库而是放入缓存,从缓存中查询完成后将该条消息从缓存中删除。

4、被动回复信息

如果前面的流程都顺利的话,到这一步也问题不大,如果遇到了拿不到回调内容的问题,参考这篇文章:nestjs开发微信后台获取body回调参数为空

在这里,我主要讲一下自定义菜单栏的触发方式。根据官方文档可以得知,菜单的响应动作类型,view表示网页类型,click表示点击类型,miniprogram表示小程序类型,在这里我直接贴一下官网给出的示例:

https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Personalized_menu_interface.html#0

{
    "button": [
        {
            "type": "click", 
            "name": "今日歌曲", 
            "key": "V1001_TODAY_MUSIC"
        }, 
        {
            "name": "菜单", 
            "sub_button": [
                {
                    "type": "view", 
                    "name": "搜索", 
                    "url": "http://www.soso.com/"
                }, 
                {
                    "type": "miniprogram", 
                    "name": "wxa", 
                    "url": "http://mp.weixin.qq.com", 
                    "appid": "wx286b93c14bbf93aa", 
                    "pagepath": "pages/lunar/index"
                }, 
                {
                    "type": "click", 
                    "name": "赞一下我们", 
                    "key": "V1001_GOOD"
                }
            ]
        }
    ], 
    "matchrule": {
        "tag_id": "2", 
        "sex": "1", 
        "country": "中国", 
        "province": "广东", 
        "city": "广州", 
        "client_platform_type": "2", 
        "language": "zh_CN"
    }
}

看上去你只需要照着这个样式写,然后放上去就完成了,但实际上我在这个菜单栏上遇到的问题整整处理了1天。

首先,我需要实现的功能是当用户点击菜单栏中第一项时,给用户发送一条文本消息,这条其实很简单,但我一开始没搞清楚情况,type选择了text类型(来自愚蠢的人工智能编程),导致上传失败。其实所有的type都已经在这个文档里了,别指望自己独创一个不存在的类型来实现想要的功能。为什么说简单,因为当你选择type为click时,用户在点击后会给你的后台发送一条类型为CLICK的消息,同时EventKey字段对应的是你在key中填入的值,用于判断用户点击了哪个按钮。

返回文本这一块到这里就算解决了,但是其他的按钮需要我返回一张图片,这个需求也很简单,只要返回值中填入微信公众号后台上传的图片的MediaId就算完成了,对,所有的文章都这么说,但我有半天时间就耗在怎么获取这个MediaId上了。

现在我来告诉你正常应该怎么做。

首先,你无法从 内容与互动 -> 素材库 中获取任何有用的信息,除了看着这一堆屌图发呆以外无法有任何进展,如果你选择通过这篇文章点击前往发布,你会从地址栏中获取到一个material_id,这是你这篇文章的编号,没有任何卵用,可以F12打开调试模式刷新页面找到对应的接口,但接口中没有返回这个字段。目前已知的唯一方法,就是调用获取全部素材接口,从里面找到对应的那个图,在接口中,有这个图片的MediaId

https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html

    async getMaterials() {
        const access_token = await this.getAccessToken();
        const result = await httpPost(
            `https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=${access_token}`,
            JSON.stringify({
                "type": "image",
                "offset": 0,
                "count": 20
            }));

        return result.data;
    }

痛苦,太痛苦了,由于IP经常变动,每次调用接口就要找产品加微信白名单,如果可以的话,希望世界上不再有微信相关的需求。

在这里附上微信的三种常用回复格式:

// 创建微信文字回复内容
export function createTextMsg(eventObj: WeChatEventObj, text: string) {
    return create({
        xml: {
            //	接收方帐号(收到的OpenID)
            ToUserName: eventObj.FromUserName,
            //	开发者微信号
            FromUserName: eventObj.ToUserName,
            //	消息创建时间 (整型)
            CreateTime: new Date().getTime(),
            MsgType: 'text',
            Content: text
        },
    }).end({ prettyPrint: true });
}

// 创建微信图片回复内容
export function createImageMsg(eventObj: WeChatEventObj, mediaId: string) {
    return create({
        xml: {
            //	接收方帐号(收到的OpenID)
            ToUserName: eventObj.FromUserName,
            //	开发者微信号
            FromUserName: eventObj.ToUserName,
            //	消息创建时间 (整型)
            CreateTime: new Date().getTime(),
            MsgType: 'image',
            Image: {
                MediaId: mediaId
            }
        },
    }).end({ prettyPrint: true });
}

// 创建微信图文回复内容
export function createNewsMsg(eventObj: WeChatEventObj, content: { title: string, description: string, picUrl: string, url: string }) {
    return create({
        xml: {
            //	接收方帐号(收到的OpenID)
            ToUserName: eventObj.FromUserName,
            //	开发者微信号
            FromUserName: eventObj.ToUserName,
            //	消息创建时间 (整型)
            CreateTime: new Date().getTime(),
            MsgType: 'news',
            ArticleCount: 1,
            Articles: {
                item: {
                    // 文章标题
                    Title: content.title,
                    // 文章简介
                    Description: content.description,
                    // 文章展示图片链接
                    PicUrl: content.picUrl,
                    // 图文链接
                    Url: content.url
                }
            }
        },
    }).end({ prettyPrint: true });
}

4、花里胡哨的功能

即使已经做到这一步了,产品依旧不满足,她希望用户在关注时能够回复一条文字信息和一条图片信息。自己尝试过微信公众号开发的老哥应该知道,被动回复是需要用户发消息才能触发的,每个消息只能触发一个返回值,如果想回复多条消息,只能通过别的途径主动发送消息,目前我了解到的微信有一个客服系统,申请添加客服后,可以通过客服账号给用户主动发送消息,但尝试过之后遇到了报错,也是我这篇文章中保留下的唯一一个报错:

image.png

需要开启这个功能,上哪去开启,不知道,反正产品一定要,虽然用户可以点击下方菜单获取一张一模一样的图片,产品也一定要在用户关注的时候多发一张,她说微信公众号后台可以配置,但是由于要做关注微信号登录必须要关闭微信公众号自动回复功能。于是,还得是我来做,具体什么时候做好,什么时候会补充这方面的内容,不知道,可能是明天,也可能是明年吧。