群发接口本身没什么难度,循环调发送,一天就能写完。难的是发完之后:运营问「到底发出去多少」,你答不上来;某一批失败了要补,补到一半发现有人收到了两条;昨天的活动提醒今天补发出去,客户来投诉。这三个问题都不在发送那一步,在它前后的口径、分片和补偿上。下面按 wecomapi 的接入方式,把这三段拆开讲。
「群发」是三件事,别用一套代码
需求文档里的「群发」,落到工程上至少是三种形态,送达语义和失败语义完全不同。
- 批量单聊:给 N 个客户各发一条。每条独立成功或失败、可以精确统计、频率约束按账号算。绝大多数群发需求最后落成的都是这一种。
- 批量群消息:给 N 个群各发一条。结构和上一种一样,但爆炸半径大得多 —— 单聊发错影响一个人,群里发错是一屋子人同时看见,而且撤不干净。它必须有独立的审核环节,不能和批量单聊共用一条发布流程。
- 批量改群公告:给 N 个群各更新一段常驻文本。它不是发消息,所以没有逐个接收方的送达记录,能拿到的只有「这个群的公告改成功了没有」;而且它是覆盖式的,改错了没有撤回这回事,只能再改一次,改的过程群里所有人都看得到。
判断在这里:前两种的统计单位是「一条消息对一个接收方」,第三种是「一次修改对一个群」。把三者混进同一张报表,数字永远对不上 —— 这是群发这块最常见的对不上账的来源。公告那一类要单独算成功率,也要单独定审核流程:它的失败形态不是「少发一条」,而是「一段错的文字在群里一直挂着」。
curl -X POST https://manager.wecomapi.com/message/sendText \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"guid": "7db8...",
"toId": "78813...",
"content": "Hello WeCom"
}'所以「群发接口怎么用」这个问题,真正的答案不在这段 curl 里。这段谁都会写,决定成败的是它外面那一圈:口径怎么定、投递项怎么分片、失败了怎么补。下面三节讲的就是这一圈。
提交成功不等于送达,先把口径定下来
运营问「发出去多少」,技术答「调用成功九千八百条」,这两句话说的不是一件事。中间至少有四个口径,每往下一层数字都会掉一截。
- 1已入队:任务拆出来的投递项总数。这是分母,也是唯一一个完全由你掌控的数。
- 2已提交:真的调用了发送接口并拿到明确的成功响应。
- 3已受理未确认:调用超时或响应丢失,你不知道对面收没收到。这一类必须单独占一列,既不能算进成功也不能算进失败 —— 并进任何一边,补偿逻辑都会做错决定。
- 4已放弃:超过时效窗口或重试耗尽,明确不再补。
这四个数加起来必须等于已入队。对不上就说明有投递项在某个环节被静默丢了,这比失败率高危险得多,因为它不触发任何告警。
四个状态的来源要分清楚:已入队来自你自己的任务拆分,已提交来自调用返回,中间态的收敛靠 wecomapi 的事件回调加事后对账。别指望一次调用返回就把状态定死,那是把整条链路的不确定性压进了一个瞬间。
还有一条:不要拿「发送成功率」当健康指标。群发任务的成功率天然会随对象状态波动 —— 名单里总有一部分人已经不再是你的客户了 —— 拿它做告警会天天误报,几周之后就没人看了。该盯的是「已受理未确认」的占比及其变化趋势,这个数一抬头说明链路在抖,而且它在成功率还完全正常的时候就会先动。
最后一个细节决定运营信不信你的报表:任务结束时要冻结一份统计快照,而不是每次打开报表现算。投递项的状态在任务结束后还会继续变,补偿会把一部分「已受理未确认」推成「已提交」,过期会把另一部分推成「已放弃」。实时算的结果每刷一次都不一样,运营看两回就不信了,然后回去自己数消息 —— 到那一步,这套统计做了等于没做。
分片键:账号分桶,桶内按接收方哈希
分片切在哪、分片作为一个调度实体要维护哪些状态、分片数为什么定了就不能改,站内讲批量任务调度的那篇写过,这里不重复;这一层的结论只有一句:桶要跟着约束的作用域走,频率约束和登录态都是账号维度的,所以先按发送账号分桶。
群发要在桶内再加一层:按接收方标识哈希成固定数量的片,而不是按投递项序号平均切。理由是同一个接收方的所有投递项必须落在同一片 —— 跨片就没有顺序,「群发一条紧跟一条个性化跟进」这种组合迟早会有人提,两条被不同 worker 并行处理,先后就乱了;去重同理,片内一次本地判断就够,跨片就得上分布式锁。按序号切还会顺手把限速点弄丢:每片混着不同账号的投递项,派发前算不出某个账号这一轮要承担多少,只能等被拒了再退,而那时候错误率已经打上去了。
失败补偿:三层,而且有保质期
补偿分三层,用错层是补偿反过来把事情搞砸的主要原因。
- 条级补偿:单条失败单条重发,适用于明确的临时性失败。前提是投递项有稳定的幂等键。
- 片级补偿:整片重跑,适用于一整片都失败的情况,比如账号异常或桶级限速卡死。前提是片内每条都幂等,否则重跑等于给已经成功的那些人再发一遍。
- 任务级补偿:换一个时间窗整体重来,适用于任务本身出了问题 —— 内容错了、圈选错了、时机不对。这一层的正确动作往往不是重发,是取消。
更重要的是那条容易被忽略的判断:群发的补偿有保质期。这是它和普通接口重试最本质的区别。一条订单通知晚十分钟送到还有价值,一条限时活动提醒晚八小时送到是负价值 —— 客户看到一个已经结束的活动,比没看到更糟,还会顺手把你屏蔽掉。
所以每个群发任务必须带一个失效时刻,而且过期判断要放在投递前的最后一刻做,不能在入队时做,入队时还没到窗口边界。落地上就是在调用 wecomapi 的发送接口之前加一行时刻校验,这一行能挡掉整个死信重放带来的事故。
过期的投递项直接置为「已放弃」并计入统计,不要进死信队列等人重放。死信队列里的群发项被人在第二天手动重放一遍,是这个模块最经典的事故形态。死信队列该放的是需要人判断的失败,比如内容被拒、账号状态异常;纯粹的时效过期不需要人判断,程序自己就该丢掉。
补偿的速率要比首次发送更保守。首次发送时账号是健康的,需要补偿本身就说明当时链路或账号出过问题,用原速率补很容易第二次撞在同一堵墙上。
发出去之前的最后一道闸
群发是少数几个「错了没法撤」的操作,所以提交前的校验值得做成独立一层,而不是散在业务代码里。至少四项。
- 1渲染校验:带变量的模板必须在提交前全量渲染一遍,任何一条渲染出未替换的占位符或空值,整个任务拦住不发。发出去一条称呼位置是空白的消息,比晚发一天贵得多。
- 2圈选复核:目标名单的数量、抽样内容,以及和上一次同类任务的名单重叠度。重叠度高说明你可能在反复打扰同一批人,而这批人正是最容易流失的。
- 3时间窗校验:可投递窗口必须显式指定,不指定就不允许提交。默认值在这里是危险的,因为默认值总会在某次半夜的重试里生效。
- 4抽样试发:先发给一个内部小名单,人看一眼再放开剩下的。这一步在流程上加不了五分钟,能挡掉绝大多数低级事故。
如果四项里只能做一项,做渲染校验。它是唯一一个纯技术就能百分之百拦住的问题,其余三项都依赖人,而人在赶活动上线的那天最不可靠。
再加一条不属于校验但属于同一层的东西:留痕。每个群发任务要记下发起人、复核人、内容版本和圈选条件的快照。出事之后第一个问题永远是「这条谁发的、名单怎么来的」,没有留痕就只能靠回忆,而回忆在这种场合从来对不上。这份记录也是后面做效果归因的唯一依据 —— 同一批人这个月收到过几次、分别是哪些内容,只有留痕能回答。
本文讲的是调度结构与补偿策略。群发相关接口的精确字段、形态差异与频率约束以 wecomapi 线上接口文档为准,示意代码只表达调用形态,不代表任何接口的实际行为。
常见问题
- 企业微信群发接口一次能发多少条?
- 这个数不该出现在你的代码里。可发送量随接口形态、账号状态与平台策略变化,硬编码之后失效时不会有人提醒你。工程上该固化的是结构:投递项按账号分桶、限速做在桶级、任务带失效时刻,量的上限只作为一处配置存在。具体约束以 wecomapi 文档和你自己账号的实际状态为准,并且在放量前用小批量先跑一轮。
- 群发失败的那些,直接重发一遍行吗?
- 先分三件事:这条失败是不是可重试的、这条内容现在还有没有意义、重发会不会造成重复。可重试的临时失败按条补;整片失败按片补,但片内每条必须幂等;内容或圈选错了应该取消而不是重发。另外补偿速率要比首次更保守,需要补偿本身就说明当时链路或账号出过问题。
- 提交成功了但客户说没收到,怎么排查?
- 先看这条投递项停在哪个状态。「已提交」和「已受理未确认」是两回事,后者说明调用超时或响应丢失,你并不知道对面收没收到,这一类在库里必须单独一列。定位单条时用调用时留下的请求标识去查,比翻日志快得多 —— 这个标识在联调阶段就该落进业务日志,而不是出事了再去加。
准备好动手了?
精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。
