不得不说,上一次正经写文章已经是一年以前的事情了,期间虽然折腾了不少有意思的东西,但一直提不起兴趣记录下来,不得不说这实在是一件高付出低回报的活,每次写文章都需要收集很多错误日志和解决问题的过程,往往一写就是好几个小时,然后看的人也就几个,别说点赞数了,点进来阅读的人都不多,每次看到自己花费心力写出来的东西不被大家认可,就感觉这件事失去了意义,毕竟不是天赋型选手,对于这件事也谈不上热爱。
上周因为公司要搞微信登录,想起来许久以前弄过这一套流程,应该当时写了文章,于是时隔许久又回到掘金翻看过去的记录,发现之前遇到的问题这次依然存在,节省了不少试错的时间,又简单翻看了一下后台记录,发现有不少新人注册账号就为了给我说一声谢谢,还是让人挺感动的,也让我萌发了记录这个过程的念头。
不管弄多少次,找到微信公众号接口的报错原因都能让人抓狂,因为是中途决定要写这篇文章的,很多错误日志我没做保留,但是我会尽可能的把一些容易犯错的地方记录下来,希望能给到大家一些帮助,如果遇到了一些文章中没有提到的报错,请详细对照微信公众号官方文档,找到对应的接口部分,按照文档提到的数据结构一个字母一个字母的核对
,这篇文档写的很笼统,而且很多地方莫名其妙的和其他类似功能不一样,如果抱着扫一眼就能接上的心态去看那大概率是要出问题的。
1、流程分析
整个流程大体上可以分为 配置公众号 -> 配置关注/菜单功能 -> 其他花里胡哨的功能,微信公众号的功能看上去很多,但实际上常用的就这几步,这篇文章里我会主要讲解 菜单/消息回复 和 关注微信号实现登录跳转 两个功能,其他关于门店卡券之类的功能因为暂时没用到,所以不涉及,如果希望了解这方面内容的话可以换篇文章浏览。
由于每次调用接口都需要给当前IP添加白名单,而修改白名单在正式环境又需要扫码验证,整个调试流程非常麻烦,在这里建议各位申请一个测试公众号
测试公众号中对应的接口配置和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
}
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
的形式传递过来,这一点在后续流程中会提到。
用户扫码/用户关注
这两者在微信回调时是有区别的,当用户未关注扫码时,会跳转到微信号的关注页,此时点关注按钮,微信会给后台回调一条类型为subscribe
,EventKey
字段值为二维码场景值的信息,在这里,你可以获取到用户的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、花里胡哨的功能
即使已经做到这一步了,产品依旧不满足,她希望用户在关注时能够回复一条文字信息和一条图片信息。自己尝试过微信公众号开发的老哥应该知道,被动回复是需要用户发消息才能触发的,每个消息只能触发一个返回值,如果想回复多条消息,只能通过别的途径主动发送消息,目前我了解到的微信有一个客服系统,申请添加客服后,可以通过客服账号给用户主动发送消息,但尝试过之后遇到了报错,也是我这篇文章中保留下的唯一一个报错:
需要开启这个功能,上哪去开启,不知道,反正产品一定要,虽然用户可以点击下方菜单获取一张一模一样的图片,产品也一定要在用户关注的时候多发一张,她说微信公众号后台可以配置,但是由于要做关注微信号登录必须要关闭微信公众号自动回复功能。于是,还得是我来做,具体什么时候做好,什么时候会补充这方面的内容,不知道,可能是明天,也可能是明年吧。