NEW

免费试用已开放

立即开始

API 网关 · 长连接

企业微信协议消息回调怎么接

更新于 2026-08-167 分钟

回调本身不难,难的是接进来两套。官方回调和网关侧的事件回调在不少项目里同时存在,团队按两条链路各写一遍,然后在业务层被两份长得完全不一样的事件反复折磨。这篇讲清楚差异到底在哪一层,以及怎么用一层很薄的归一化把它们收成一套;网关一侧以 wecomapi 的事件回调为例。

差异的根在视角,不在报文格式

官方回调站在企业的位置上看:谁被加进了通讯录、哪个应用收到了消息、哪条审批走完了。它回答的是「这家企业里发生了什么」,观察者是一个被授权的应用。

网关侧的回调站在账号的位置上看:这个账号收到了谁的消息、它所在的群多了个人、它的登录态变了。它回答的是「这个账号看到了什么」,观察者是一个具体的实例。

后面所有差异都从这一条派生 —— 身份体系为什么对不上、事件粒度为什么不同、两边的事件能不能合并去重,根都在这里。先纠结报文格式的团队,会在归一化那一步反复返工,因为他们试图用字段映射解决一个视角问题。

一个快速判断:必须站在企业管理员的位置才观察得到的事,多半只在官方那一侧有;某个具体账号在会话里看到的事,在账号侧才拿得完整。归属想不清楚的事件,先别急着做双源合并。

四处会改变代码结构的差异

两侧的差异真要列可以列很长,但影响代码结构的只有四处。前两处影响接入层的代码量,后两处影响数据模型 —— 前两处写错改一天,后两处设计错改一个季度。

  • 配置位置与验证握手:一个在企业管理后台配,一个在服务方控制台配,两边保存回调地址时都会先来一次验证请求,回应不正确就存不上。这一步纯操作,但它决定了你要维护几个对外入口、域名和证书挂在几个地方。
  • 报文形态:一侧通常带一层加解密封装,要先解开再解析;另一侧多是可直接读的结构化报文加签名校验。解析层没法共用,验签也是两套 —— 别试图写一个「通用验签函数」,那只会让两套逻辑纠缠在一起。
  • 订阅粒度:能订阅哪些类型、能不能按类型细订,两侧不一样。在噪声大的那一侧做全量订阅,最先撑不住的是你的队列,而不是业务代码。
  • 身份维度:同一个人在两侧的标识不保证是同一个值。这是双源并存里最贵的一处,后面单独讲。

还有一处不算差异但必须统一:响应口径按最严的那一侧定 —— 先快速 ACK 再异步处理,两侧共用这一条,别因为某一侧看起来更宽容就在它的分支里同步跑业务。接 wecomapi 的事件回调时也一样,按它自己的形态老实解析,不要在解析阶段就往另一侧的结构上靠,否则两侧的解析器会同时变形,改哪一侧都得动两个地方。

归一化层:先定内部事件,再写两个 mapper

归一化不是把两份报文合并成一个更大的对象。合出来的只会是两边字段的并集,业务代码还是要不停判断「这个字段这次有没有」,等于什么都没解决。做法是先定义一份你自己的事件结构,两侧各写一个 mapper 往里填,填不满的槽位留空并记录原因。

五个槽位就够跑大部分业务:来源标识、事件类型(你自己的枚举,不是对方的字符串)、业务发生时刻(对方给的那个时刻,不是你收到的时刻)、身份三元组(账号、会话、对端)、原始报文的存储引用。第五个存的是引用不是原文,原文单独落对象存储,事件表才不会被撑爆。

mapper 里只做搬运和枚举映射,不要做业务判断。一旦里面出现「如果是重点客户就……」,两侧会各长出一套业务逻辑,半年后没人敢动它们中的任何一个。

示意:两侧各一个 mapper,业务只认内部事件javascript
// 示意逻辑,两侧的精确字段与签名算法以各自线上文档为准
const mappers = {
  official: (raw) => ({ source: "official", ...readOfficial(raw) }),
  gateway:  (raw) => ({ source: "gateway",  ...readGateway(raw)  }),
};

async function onEvent(source, raw) {
  const evt = mappers[source](raw);   // 只搬运,不判断业务
  evt.rawRef = await blob.put(raw);   // 原文存引用,不塞进事件表
  await queue.push(evt);              // ACK 之后的事都挂在这条线上
}

// 业务侧只面对内部事件,回写也只有一个出口
await http.post("https://manager.wecomapi.com/message/sendText", {
  guid:    evt.accountId,
  toId:    evt.peerId,
  content: reply,
});

要说清楚的是,这一层产出的还不是领域事件,它只保证「两个源长得一样」。把平台事件直接当领域事件用会引出另一类问题,站内讲事件驱动架构的那篇专门说这件事,两层别合并成一层做。

mapper 通常一侧几十行。它值钱的地方不在代码量,在于把「对方改了报文」的影响面锁在一个文件里 —— 没有这一层,同一次报文调整会散落在十几个处理器里,你只能靠全仓库搜索去找齐。

双源并存的三个坑

坑一:同一件事被推两次,但那不是重复投递

站内讲幂等的那篇处理的是同一个源把同一条事件重复投递,用事件唯一标识去重就够。双源的重复完全不同:两个源各自观察到了同一个客观事实,各推一次,事件标识天然不同,投递级幂等一条都拦不住。

这时候要在事实层面定键:动作类型 + 参与者 + 时间窗,落在同一个窗口里的算同一件事,先到的生效,后到的只更新补充字段。窗口宽度按两侧的实际时间差定,别按理想值定 —— 上线前先只记录不去重,跑一周看看这个差值的分布,再决定窗口开多大。

坑二:身份对不齐

两侧对同一个客户的标识不保证一致,所以你自己的库里必须有一份内部主键,外部标识只作为映射存在。更实际的问题是映射用什么锚点建立:可靠的锚点是一次真实交互 —— 同一条会话里两侧都观察到了同一次消息往来,这时候绑一次。不可靠的锚点是昵称、手机号、头像这类可变或缺失的信息,用它们做自动匹配,早晚会把两个客户合成一个,而这种错误发现得极晚、修起来要人工逐条拆。

落地上,用 wecomapi 的事件回调时推过来的事件带着来源实例,账号这一维是现成的,要你自己对齐的只有对端那一维;官方一侧则要先定位到同一时刻的对应事件,再把两个标识写进映射表。绑定只做一次并留下依据,之后所有查询都走内部主键,业务代码里不该再出现任何一侧的原始标识。

坑三:两侧的时间戳不能直接比大小

两侧的时钟基准、事件产生位置、投递延迟都不同,差几百毫秒到几秒都属正常。真正需要顺序的地方只有一处:同一个会话内的消息先后 —— 用会话内的序号,或者你自己接收时打的单调序号。跨源时间戳只能用来展示,不能用来判断因果;需要因果的地方,比如判断某条自动回复到底回应了哪条消息,用你在处理链路里自己写下的关系记录,别靠时间去推。

接入顺序

  1. 1先只接一侧,把「收到事件 → 异步处理 → 回写会话」这条闭环跑通。两侧同时接的项目,第一周都在查是哪一侧的问题。
  2. 2闭环稳定后再定内部事件结构。顺序反过来会失败:还没接过真实报文就设计出来的「统一模型」,一半槽位是想象的。
  3. 3接第二侧时只允许写 mapper,不允许改业务代码。如果发现必须改,说明内部结构漏了槽位,回上一步补,别在业务层打补丁。
  4. 4最后才做事实级去重。它依赖前面几步积累的真实时间差分布,提前做就是在猜。

这四步走完通常是两到三周,比两侧并行接慢不了多少,但它把风险按顺序摊开了:每一步出问题,你都清楚问题属于哪一步。并行接不是更快,是把所有问题堆到同一周一起爆发,而那一周你还分不清是哪一侧引起的。

本文讲的是两侧的差异分层与归一化位置。精确的事件类型、签名算法与字段定义以各自线上文档为准,网关一侧以 wecomapi 接口文档为准,示意代码只表达调用位置。

常见问题

只接一侧回调够不够?
多数项目够,而且建议先只接一侧。把要做的事列出来,看它们需要的是企业视角还是账号视角的事件 —— 只有两类需求同时存在、且都压在主流程上,才值得付双源的成本。为了「全一点」而接两侧,换来的是身份映射和事实级去重两笔长期开销。
两套回调能共用一个接收地址吗?
技术上可以,实践上建议分路径不分服务:同一个服务、两个路由,最外层就能靠路径判断来源,验签和解析各走各的。共用一个路由意味着要在解析前先猜来源,一旦某一侧报文结构调整,猜错的代价会分摊到所有事件上。
官方那侧有的事件,网关侧没有,怎么办?
先按视角判断这个事件本该属于谁,而不是按「谁更全」。企业级的组织与授权类事件本来就是官方视角的,账号级的会话事件在账号侧才完整。要确认某个事件是否覆盖,以 wecomapi 线上文档为准并在预发环境实测一次,别按二手描述估。

准备好动手了?

精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。

相关文章