工作笔记 - 微信消息发送和处理

概述

最近在搞微信消息发送接收方面的一些测试和开发,遇到和解决和很多问题,觉得有必要总结和记录一下,遂有了本文。

本文还有一个姊妹篇,探讨微信消息接收的问题:

《工作笔记-微信消息接收机制与实现》

微信消息类型

所有的开发者,遇到微信消息发送,阅读相关技术资料的时候,可能都会有所迷惑。在微信中,消息作为一个基础能力,提供了很多类型和选项,来适应不同的业务需求和场景。但他的技术文档中好像没有比较清晰的表述应当如何根据不同的场景和需求来正确的理解和使用这些功能。

因为从经验来看,信息的发送系统,典型的比如手机短信,它的发送基本上除了内容长度之外,是没有什么其他的限制的,只要知道对方的手机号码就可以发送,电子邮件系统也是类似。而认真研究下来,显然微信信息发送的方式更加灵活多样,应用场景更复杂,加之微信消息本身的发送和接收对用户而言都是没有成本的,体验也好于一般的消息系统,对于业务影响特别是运营方面的就更多,也值得更加深入的了解和更好的开展应用。

基于以上情况,笔者在这里根据自己的理解,先来尝试梳理一下。笔者认为,基于程序和系统,向微信公众号用户主动发送的消息,其实可以大致分为三类。就是模板消息、群发消息和客服消息。根据这些不同类型的消息,我们应当可以感觉到,微信的设计定位,并不是简单的传统的技术平台,而是一个生态和内容体系。因为它不仅仅定义技术的标准,还试图定义业务的规范和内容的标准。

模板消息 Template Messages

模板消息,就是基于模板的消息,也就是说这个消息的格式和内容是收到严格的限制的。有点像格式合同,你大体上只能填空,不能随意定义和扩展内容。通常用于一些范式化的应用场景,最常见的就是验证码消息。

原来的消息系统如电子邮件或者短信,显然都是没有所谓模板的。而微信提供模板消息,应该很多情况是基于合规的要求,需要限制和管理用户发送消息的场景和内容。但又不好限制发送的频次,就提出这个模式。所以在使用方面,对第三方应用而言,发送和使用模板消息的频率限制反而是最小的。

先来看看一个标准模板消息的结构(最终发送到WXAPI的数据):

js 复制代码
{
  template_id: "sometemplateid",
  data: {
    thing52: { value: "某某公司", },
    thing4:  { value: "标题内容", },
    thing6:  { value: "日期内容", },
  },
  url: "可选链接",
  touser: "openid",
}

简单说明如下:

  • template_id: 告知微信系统要使用的模板id,配置模板后可以得到这个信息
  • data: 实际数据内容的内容,字段名称,根据配置模板时确定
  • url: 可选的消息链接,如果有这个记录,微信会在消息卡片中显示"详情",点击后进行跳转,否则点击无效
  • touser: 消息发送目标用户的openid

这里要批评一下,在微信公众平台中,这些表述方式其实比较晦涩难以理解,其实简单的给个示例(比如一个JSON对象),开发者很容易就可以理解了,不需要写那么多废话。而且,笔者还发现,并不是模板中所有字段都是可以使用的,比如下面这个模板,笔者尝试后只发现它只能使用keyword这几个字段,first和remark其实是不能使用的。所以开发者需要自己尝试模板最终使用的效果。

模板消息的发送效果大致如下:

可以看到,它会基于内容,来填写模板中的不同字段中的内容。虽然看起来可能有点奇怪,但实际上这些字段都是文本 信息,其内容并没有限定(当然有一些显示和长度的限制)。另外,如果带有连接,模板消息可以显示"查看详情",点击后可以跳转,这就提供了很大的灵活性。

群发消息 Mass Messages

另一种比较实用,但容易被忽视的消息类型,是群发消息。虽然名字叫做群发消息,但笔者感觉,实际上这个功能更像限定受众的文稿发布。

我们来看看群发消息的数据结构:

js 复制代码
{
    touser  : openids,
    msgtype : "text", 
    text: { content },
    send_ignore_reprint : 0
};

在这里touser必须是一个数组(多个openid),其他的方式,就像一个微信文稿的定义。实际上,它除了文本消息之外,也可以发送任意微信支持的文稿类型。本文只以文本类型作为示例讨论微信消息发送的技术原理,读者如果有兴趣可以自行研究其他类型的消息。

从笔者的使用经验而言,群发消息好像并没有什么特别的限制。笔者曾经使用消息群发模式,以40个openid为一批,持续发送了17000余条群发消息,也没有遇到什么异常。

笔者在后面研究发送技术的时候,发现群发消息其实也是有限制的,只不过这个限制相对宽松,应该可以满足绝大部分的需求(下图)。

群发消息的使用效果大致如下:

这个消息就更像短信了,就是一段简单的文本信息,没有格式方面的限制。而且在无法显示全文的情况下,微信程序会自动裁剪,并显示"全文"的提示,点击后可以查看全文。在很多情况下,群发消息的使用比模板消息更加方便,不用定义模板,不用构造数据结构,内容更加灵活多变,也不用准备链接信息。唯一的问题是,群发消息不能完全根据发送目标进行定制化,比如为每个用户都准备不同的内容;而相对而言,模板消息是可以做到这一点的。

客服消息 Customer Messages

客服消息,也是微信消息提供的一个重要的消息能力。笔者认为可能更确切的应该称为服务消息。

笔者的理解,虽然从技术操作方面,客服消息的发送和群发消息其实是一样的,包括它也支撑各种丰富的内容。但它的发送是有前置条件和场景的,并不能随意发送。这些场景和条件如下:

  • 用户发送消息, 5条/48小时
  • 点击自定义菜单, 3条/1分钟
  • 关注公众号, 3条/1分钟
  • 扫描二维码, 3条/1分钟

以用户发送消息为例,就是说用户需要主动向公众号发送消息之后,应用系统,才能在48小时内,向这个用户回复和发送5条服务消息。其他的场景也是相同的机制。

笔者简单实验了一下,如果超过这些限制或者场景,可能的错误信息包括:

js 复制代码
{
  errcode: 45015,
  errmsg: "response out of time limit or subscription is canceled rid: 68b14e83-7123710b-414d9cee",
}

客服消息的发送效果,基本上和群发消息一致,这里就不展示了。

实现和实践

在我们简单的讨论和理解了不同种类的微信消息之后,就可以进入实际操作的环节了。

发送配置

基于安全设计的考虑,虽然微信发送使用标准的HTTP协议和请求响应的模式,但并不是在任何地方都可以发起这个请求的。对于发送程序,基本上有三个限制,请求IP地址限制、调用接口次数和AccessToken验证。

请求IP限制,是指要发送消息的主机,其公网地址必须在微信公众号的白名单里面,这个在公众管理界面中进行配置。位置在"设置与开发-开发接口管理-基本配置-IP白名单"板块(下图)。开发者需要获得发送消息主机的公网地址,并将其配置在这个白名单中,可以支持多个IP地址。

另外,发送消息,作为微信API接口调用的操作,对于调用的次数和频度,也是有一定的限制的,当然一般情况这个限制比较宽松,不会影响实际业务。这些信息,可以在"设置与开发-开发接口管理-接口权限"板块查看权限和限制(图)。

AccessToken验证的问题,在后面由更深入的讨论。

发送请求和响应

从技术处理的角度而言,这些微信消息(我们可以称为下行消息,从系统到用户)的发送方式和流程都是一样的。

它们都是使用HttpPost方法请求一个消息发送的接口,这样,请求操作可以用任意的HTTP兼容的客户端和技术体系。为了保证安全,请求这些接口的时候,URL需要包括accessToken作为参数;具体的请求内容是JSON字符串;响应内容也是JSON字符串,而且响应内容的格式是统一的。

我们可以通过JS语言的示例代码来理解这个机制,如下(以最简单的群发消息为例):

js 复制代码
// 消息发送接口
const url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=__TOKEN__";

// 发送对象 
const openids = [id1, id2, ...];

// 发送内容
constent = "尊敬的客户.....";


// 请求数据体
const qdata =  {
    touser  : openids,
    msgtype : "text", // "mpnews",
    text: { content },
    send_ignore_reprint : 0
};

// 接口请求
const res = await fetch(url, {
    method: "POST",
    body: JSON.stringify(qdata);
})

// 正常响应,获取内容
if (res.ok) result = res.json();


// 正常响应结果示例
{
  errcode: 0,
  errmsg: "ok",
  msgid: xxx
}

请求的地址是由微信平台提供的,不同的功能可能使用不同的路径。url中,token的问题我们在后面讨论。请求的方法为POST,内容是字符串化的JSON对象。

响应方面,也是JSON对象,但具有相对一致的结构。一般正常响应是,errcode为0,否则这个信息是一个错误代码,并且在errmsg属性中有相关错误信息。msgid是当前微信系统为当前这个消息产生的id,有问题可以方便在后期跟踪或者检查。

其他类型的消息发送,相关的请求路径如下:

  • 基础地址, API_ROOT: api.weixin.qq.com/cgi-bin/
  • 群发消息: API_ROOT+"message/mass/send"
  • 模板消息: API_ROOT+"message/template/send"
  • 客服消息: API_ROOT+"message/customer/send"
  • Token参数: "?access_token="+token

请求的内容,也稍有不同,但机制是一样的。内容方面前面已经讨论过了。

基本过程就是如此,下面我们展开讨论几个具体的技术细节,这些问题都是我们有很大机会会遇到的。

AccessToken

所有有关微信消息发送的操作,在请求URL中都需要带有当前有效的access_token。这个token可以通过微信提供的专门的token接口获取。获取的方式本身比较简单,就是使用appid和appsec作为POST参数请求一个接口,参考示例代码如下:

js 复制代码
const URL_TOKEN = API_ROOT+"token";

const TOKEN_REQ = {
  grant_type: "client_credential",
  appid : "someappid",
  secret: "someappsec"
},

const res = await fetch(URL_TOKEN, {
    method: "POST",
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(TOKEN_REQ)
}); 
...

// token结果:
{
   errcode: 0
   access_token: "xxxx",
   expires_in: "yyy"
}

代码中可以看到,使用这种方式获取的token,其实包括两个部分,token的内容,和过期时间(以秒计),一般为7200s。

获取token本身是很简单的,但在实际场景中使用token却可能遇到很多问题。其中最大的问题在于token刷新的问题。这个问题在很多场景中被误解和错用了。

很多简单的场景中,特别是开发环境中,有些开发者在每次发送消息前,都会先获取这个token,然后发送信息。这种使用方式其实是不对的。微信提供了过期时间,就是说,它是可以保证这个accessToken的稳定性的,在这个时间之前,都是有效的,不需要重复获取。每次发送都用新的token,在实际应用中,很容易超过微信的使用限制。

微信token还有另外一个设定是token的唯一性。对于每个公众号,在某一个时间,只有一个有效的token可以使用。这样,如果有多个应用都需要使用token,它们自己来更新,就会造成冲突和其他的应用不可用的情况。

所以,正确的做法,应当就是维护和使用一个相对稳定的token刷新机制。获取token后,存储在缓存中,后面的业务请求,都共享使用这个当前的token。然后在过期时间之前,有一个自动更新的机制,这样保证缓存中的token总是最新可用的。

而且,为了改善令牌刷新冲突的问题,微信还提供了"稳定Token"的接口,应当在生产系统中优先考虑使用。所谓稳定Token,就是它的过期时间是可以预期的,在某个时间段内无论谁来更新,得到的Token其实都是相同的,。这样就可以方便的支撑多个由于条件限制,不方便共享token的应用。

这些需要注意的问题和信息,都可以在微信公众平台的技术文档中找到:

developers.weixin.qq.com/doc/service...

这里笔者想要补充说明一下,就是笔者觉得这个访问令牌设计的机制,可能其实并不高明。起码直接将appsec放在请求参数中,和定期更新token,这些操作就有很多改善的余地。比如笔者在另一篇博文中讨论的OTP,可能就是一个更好的可以参考和借鉴的技术方案。当然,我们也可以将腾讯现在的选择归因于"技术债",这就不是本文要讨论的问题了。

模板消息的构造

针对微信模板消息高度格式化的特点,同时为了方便模板消息的构造和发送,笔者编写了一些工具方法,更方便在业务场景中使用。相关参考代码如下:

js 复制代码
// 模板消息数据填充
public tempData (tempName, dataList) {
    const template = this.WXCONFIG.template[tempName];

    let url,
    fields = template.fields,
    data = template.data; // current fix data
    
    let tempData = { template_id: template.id  };
    
    // set value by field
    fields.map((f,i)=>{
        if (f === "url") { 
            tempData.url  = dataList[i];
        } else {
            if (!data[f]) data[f] = {}; 
            data[f].value = dataList[i];
        }
    });

    // set data content
    tempData.data = data;

    return tempData;
}

// 使用方式
// 1 固定的配置信息
const tempConfig = {
    DQTX: { // 到期提醒
      id: "templateid...", // 模板id
      data   : { thing52 : { value : "某某公司"} }, // 可以有一些固定信息
      fields : "thing4,thing6,url".split(",") // 定义数据占位
    },
    JFTZ: {  // 预约成功提醒

    },
  }
}

// 2 构造业务数据, 简单数组
let sdata = [
    "XX系统到期提醒",
    "2025-09-15",
    "https://somesite.com"
];

// template name in config, and datglist
let mdata = tempData("GZTX",sdata);
// 加入openid
mdata.openid = "someopenid";

现实情况中使用的时候,需要先根据配置的模板和业务的需求,定义好一个基础模板配置。包括固定的项目和内容,以及可变的部分;然后在实际生成模板消息的时候,按照顺序填充可变的业务数据,这是一个通用的模式,相比原来为每种模板消息都编写处理代码,大大简化了编程和操作。

openid

前面的分析中,可以看到,任何类型的消息发送,都需要知晓发送目标的openid。

在实际的业务系统中,这些openid并不能凭空生成,而是来源于前期的业务操作。最常见的就是在用户业务系统中注册或者登录的,想办法获取openid,然后和用户的账号进行关联。

然后在后续的业务操作中,实际的消息发送,都是基于业务需求和用户在业务系统中的标识的,这时就可以根据其关联关系,进行一个映射和转换,就满足了业务系统向业务用户发送消息的业务需求了。

批量发送和状态管理

让我们将抽象层级提高一下。对于大批量的业务消息的发送,很难想象一次性就可以将所有的消息,都正确无误的发送出去,就需要设计一个更好的管理机制。

笔者的构想,是使用数据结构和数据库表来计划和管理消息发送任务和管理消息发送状态。而且这个机制是抽象和通用的,适合任意类型和业务的消息发送管理。表的基本结构如下:

js 复制代码
// 发送任务表
jobs
jobid: int4 任务id
jobinfo: text 任务信息,如任务名称,说明,配置信息(如模板)
status: int2 任务状态

// 发送消息表
jobid: int4, 关联任务id
mtime: int 消息最后状态时间
openid: 发送目标的 
status: 发送状态
info: 失败信息

// 任务状态
0  - 初始,编辑
1  - 可执行发送
2  - 关闭/暂停

// 其中消息状态
0   - 初始,待发送
&1  - 已计划/发送中
&2  - 发送成功
&16 - 发送失败

简单的任务和消息状态管理流程如下:

  • 创建一个任务,分配一个id
  • 导入发送信息列表,主要是openid
  • 任务相关条目,状态初始都为0
  • 任务启动,发送程序从任务中取出一批未发送的条目,并将其状态设置为 ( | 1) (发送中)
  • 启动发送操作
  • 对于单一条目,如果发送失败,修改条目状态为 ( | 16 - &3)
  • 取出下一批处理,直到所有条目都处理完成
  • 管理员检查未发送的条目状态,处理错误
  • 将问题条目重置为初始状态,可以继续发送
  • 很长时间计划中状态的条目,设置为超时失败

这部分的具体操作,和微信消息发送本身关系不大,大多数都是相关的状态切换和数据库操作,这里只是顺带提一下消息发送过程管理的技术方案,具体就不再展开说明了。

小结

本文讨论了如何利用微信公众号提供的消息能力,使用程序控制发送各类消息的方法。包括这些消息发送的类型,使用场景和限制,基本技术原理,需要注意的问题和简单的示例代码等等,应当能够覆盖普通业务应用对于微信消息发送的场景和需求。

相关推荐
爱吃烤鸡翅的酸菜鱼3 小时前
【Spring】原理:Bean的作用域与生命周期
后端·spring
该用户已不存在3 小时前
macOS是开发的终极进化版吗?
前端·后端
计算机毕业设计木哥4 小时前
计算机毕设选题:基于Python+Django的B站数据分析系统的设计与实现【源码+文档+调试】
java·开发语言·后端·python·spark·django·课程设计
陈陈爱java4 小时前
Spring八股文
开发语言·javascript·数据库
歪歪1004 小时前
qt creator新手入门以及结合sql server数据库开发
c语言·开发语言·后端·qt·数据库开发
布列瑟农的星空4 小时前
大话设计模式——观察者模式和发布/订阅模式的区别
前端·后端·架构
@大迁世界4 小时前
用 popover=“hint“ 打造友好的 HTML 提示:一招让界面更“懂人”
开发语言·前端·javascript·css·html
Moonbit4 小时前
月报Vol.03: 新增Bitstring pattern支持,构造器模式匹配增强
后端·算法·github
烛阴4 小时前
【TS 设计模式完全指南】用工厂方法模式打造你的“对象生产线”
javascript·设计模式·typescript