多账号托管真正难的地方不是接上第二个号 —— 那只是多传一个实例标识而已。难的是第十个号掉线时,你的系统能不能赶在客户投诉之前知道;以及三条业务线共用一批账号跑了半年之后,你还能不能说清楚某条消息是哪个号发的、成本算在谁头上。这篇讲的就是这两件事的工程解法。示例按 wecomapi 的多实例接入方式写,换成自建协议层,模型是一样的。
先把「账号」这个词拆成两个
多账号系统里最早埋下的坑,是把「账号」当成一张表。它至少是两个东西。一个是运行时的账号实例:有登录态、有在线状态、有速率预算,生命周期由登录和掉线决定。另一个是业务意义上的席位:属于哪条业务线、谁在用、成本记给谁,生命周期由组织架构决定。
这两者的变化频率差了一个数量级。实例可能一天掉线重连三次,业务归属可能一年才调一次。把它们塞进同一张表,结果就是每次掉线都在更新一行同时携带业务字段的记录 —— 审计、对账、按业务线拉数据,三件事一起变难看。
先分表,再谈别的。在项目第一周做这件事的成本接近于零,跑了半年再补就是一次带历史数据的迁移。
实例隔离:哪些必须隔离,哪些不该隔离
隔离不是越彻底越好。隔错了地方就是重复建设,该隔的没隔才是事故来源。这条线可以划得很清楚。
- 必须隔离:登录态与凭证上下文 —— 一个实例的状态变化不能影响另一个。
- 必须隔离:事件订阅与回调路由 —— 事件在入口就得知道自己属于哪个实例。
- 必须隔离:速率预算与发送节奏 —— 这是最容易漏掉、后果最大的一项。
- 必须隔离:日志、审计与告警维度 —— 排障时你需要按实例过滤,而不是按时间翻。
- 不该隔离:业务规则、话术模板、知识库 —— 隔离它们等于让运营维护 N 套配置。
- 不该隔离:客户主档与标签体系 —— 恰恰相反,这层需要跨实例聚合,见后文。
- 不该隔离:队列、监控、发布流程等基础设施 —— 按实例拆基础设施是自找麻烦。
最常见的两个错误都出在第一组里。一个是共享速率预算:一批任务里有一个实例被限住,整批都在等,看上去像平台变慢了,其实是自己把不相干的账号绑在了同一根绳上。另一个是回调处理器不带实例维度:所有事件打到同一个地址是正常的,但如果入口不立刻把实例标识分流出去,下游只能靠会话去猜来源,排障时间直接翻倍。用 wecomapi 的事件回调拿消息时,实例标识就在事件体里,入口第一件事就是按它分流并写进日志上下文。
# 同一个端点、同一套鉴权;请求里的实例标识决定这条消息从哪个账号发出
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"
}'这说明「多账号」在调用层几乎是免费的。复杂度不在发送这一步,而在于谁来决定这次该用哪个实例、以及那个实例此刻能不能用。下面两节都是在回答后半句。
登录态是有状态资源,不是一行配置
扫码登录、会话保活这类事在网关式接入里由托管侧承担 —— wecomapi 把登录、保活与实例隔离收敛在平台内部,控制台有状态面板。但这不等于你的系统可以不管登录态:只要你在做调度,就必须为每个实例维护一个状态机,并且让业务链路真正去读它。
- 1待登录:实例已创建但未建立会话,不参与任何调度。
- 2在线:可正常收发,是调度池里唯一的合法状态。
- 3异常:短时不可用,通常无需人工介入,暂停调度并按退避探活。
- 4离线待重登:需要人重新扫码,重试没有意义,必须移出调度池并通知人。
- 5已停用:业务侧主动下线,不参与调度,也不该出现在告警里。
由此引出一条工程纪律:发送前查状态,不要靠发送失败来发现掉线。这两种做法在单账号时没差别,在几十个实例时差别巨大 —— 前者是一次内存判断,后者是把整批任务打到平台上再被逐条拒绝,既拖慢批次,又污染错误率指标,还让真正的异常淹没在噪声里。
状态要按实例逐个存,别只维护一个「几个号在线」的聚合数字。多账号系统里真正要回答的问题永远是「哪一个掉了、它归谁」,聚合数字回答不了,而调度池需要的恰好是逐个实例的答案。
掉线之后:多账号多出来的那两件事
掉线怎么分级、检测信号怎么组合、恢复之后的数据空洞怎么补,站内讲掉线自愈那篇已经拆得很细,这里不重复。判据只有一句:解决这个异常的动作里有没有人 —— 有人就立刻停止重试、把通知送到人,没人才轮到退避探活。下面只说多账号系统在这条链路上比单账号多出来的两件事。
第一件是挂起与重放的粒度。任务必须按实例挂起,不能把整个发送队列停掉 —— 后者会让一个号的问题拖住其余所有号,本质上和前面说的共享速率预算是同一个错误。恢复时也要按实例逐个放量,几个号同时恢复叠在一起就是一次自己造出来的尖峰。
第二件是告警要带得动身份。「有账号掉线了」在单账号系统里是全部信息,在三十个实例的系统里几乎等于什么都没说:值班的人还得再查一轮才知道该找谁、停的是哪条业务线。所以告警正文里必须能定位到具体实例和它的业务归属 —— 而这两样查不查得出来,取决于下一节那张表有没有建对。
多业务线分账的数据模型
前面几节最终都会落到同一张图上。下面是一个够用的最小模型,四组表,每一组都对应上面的一个判断。
-- 示意结构,用于说明维度关系,不是可直接执行的建表脚本
-- 1) 运行时实例:只放随登录态变化的东西
instance(instance_id, status, status_reason, last_ok_at, rate_budget)
-- 2) 业务席位:只放随组织变化的东西
seat(seat_id, biz_line, owner_team, purpose, cost_center)
-- 3) 实例与席位是多对多,且带生效区间
instance_seat(instance_id, seat_id, effective_from, effective_to)
-- 4) 客户身份聚合:同一个人在不同实例下是不同的外部身份
person(person_id, merged_from, updated_at)
person_identity(person_id, instance_id, external_id, first_seen_at)
-- 消息与会话表一律带 instance_id,并以它作为分片维度- 实例与席位为什么是多对多:几乎所有团队一开始都建成一对一,然后在第一次「这个号临时借给市场部用两周」时崩掉。多对多加生效区间的代价只是一张关联表,收益是任意时点都能算清一条消息该记在谁头上。
- 为什么要有 person 聚合层:同一个客户加了你三个账号,在这三个实例下就是三个互不相通的外部身份。直接拿外部身份当业务主键,客户档案会天然裂成三份,之后的标签、跟进记录、转化归因全是错的。聚合规则可以先做得很粗,但这一层必须一开始就存在 —— 补建聚合层等于重洗全部历史数据。
- 为什么消息表必须带实例维度:一是排障,没有它你回答不了「哪个号出的问题」;二是分片,多账号系统的数据量按实例数线性叠加,实例是最自然的分片键。
上面的表名与列名是为说明维度关系而虚构的,属于你自己库里的结构。平台侧的实例标识、状态语义与精确字段以 wecomapi 线上接口文档为准。
上线前的五个自查
这五个问题不需要读文档,对着自己的系统问一遍就行。任何一个答不上来,对应那一节就还没做完。
- 1随便挑一个实例,能不能在一分钟内说出它当前状态、上次成功调用时间、归属哪条业务线?
- 2把一个实例强制下线,批量任务是整批失败,还是只跳过它继续跑?
- 3一个需要重新扫码的实例,通知在几分钟内到人?到的是日志文件,还是值班的人?
- 4同一个客户在两个实例下的记录,在你库里是一条还是两条?
- 5任取一条已发出的消息,能不能追到具体实例和这次调用的请求标识?
常见问题
- 一个账号实例能同时服务多条业务线吗?
- 技术上可以,但成本与责任归属只能靠带生效区间的关联表来还原,不能靠拍脑袋分摊:同一个实例发出的消息,事后很难说清该记给哪条线。如果两条线都在意时延、且高峰重叠,分开实例在运维和对账上都更省事。
- 掉线恢复后要不要把积压的消息补发?
- 分类型,不能一刀切。客服回复、验证码这类强时效消息过期即丢弃并记录,补发造成的伤害往往大于不发;状态同步、标签写入这类幂等操作适合重放。恢复后无差别全量重放是多账号系统里最常见的二次事故。
- 多账号是不是要为每个账号准备一套密钥?
- 鉴权凭证和实例标识是两个维度:凭证回答「谁在调用」,实例标识回答「用哪个号发出」。实践中更常见的是按环境和团队拆分凭证,而不是按账号数量拆。具体的对应关系、权限粒度与轮换方式以控制台和 wecomapi 文档为准。
准备好动手了?
精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。
