销售在企微里聊得热火朝天,CRM 里一片空白——管理层看客户跟进过程全靠开会问。把企微消息实时灌进 CRM,链路就三步:接住、去重、对上人。看着简单,每一层都有能坑你一整天的细节。
第一步:接住——验签、快返回、异步处理
平台把消息事件 POST 到你配置的回调地址,接收器只干三件事:验签、去重、入队,然后立刻返回 200:
@app.route("/webhook", methods=["POST"]) def on_message(): raw = request.get_data() # 原始 body,验签别用解析后的 sig = request.headers.get("X-Eyun-Signature", "") expected = hmac.new(WEBHOOK_SECRET, raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expected): return "bad signature", 401 # 验不过直接丢 delivery_id = request.headers.get("X-Eyun-Delivery") if not redis.set(f"dl:{delivery_id}", 1, nx=True, ex=86400): return "ok" # 重推的重复事件,跳过 queue.enqueue(sync_to_crm, request.get_json()) # 入队异步处理 return "ok" # 5秒内必须返回两个不能省的点:验签必须用原始请求体算 HMAC-SHA256,先解析再重新序列化算出来的签名必挂;处理逻辑必须异步——同步在回调里写库调 CRM API,一旦超过响应窗口,平台判定失败会不断重推,雪球越滚越大。
第二步:去重——两层防线
回调是至少一次投递语义:网络抖动、服务重启、超时,都会导致同一条消息被推多次。去重要做两层:
防线 | 依据 | 拦什么 |
|---|---|---|
第一层(内存/Redis) |
| 平台重推的同一事件,24小时窗口内只处理一次 |
第二层(数据库) | 消息 | 兜底——Redis 清了、队列重放了,落库也不会出重复行 |
只做第一层,Redis 一重启历史重复全回来了;只做第二层,同一条消息会先进队列被消费两遍,浪费下游算力。两层都要。
第三步:对上人——externalUserId 映射 CRM 客户
消息落到 CRM 前,得回答"这是谁的聊天"。映射关系建在中间表里:
CREATE TABLE wecom_contact_map ( external_userid BIGINT PRIMARY KEY, -- 企微侧 ID,int64 crm_customer_id BIGINT NOT NULL, -- CRM 客户主键 owner_userid BIGINT, -- 归属销售 UNIQUE KEY uk_customer (crm_customer_id, external_userid) );注意一个客户可能被多个销售添加,产生多个externalUserId——externalUserId是关系 ID 不是用户 ID,映射表要容忍一个 CRM 客户对应多条企微关系。收到消息后按externalUserId查表拿到 CRM 客户,消息挂到对应客户的跟进时间线上。
落库与补偿
Worker 从队列取出消息后 upsert 进消息表(msgid唯一键),同时更新 CRM 客户的"最后跟进时间"。字段尽量原样保留——fromUin、content、conversationId、time都存,后续做质检、画像才有原料。
实时链路再稳也有漏的时候:服务发版重启的几分钟、回调地址迁移的窗口期。补偿机制用message/getHistoryMessageList按会话拉历史消息补漏,每天对一次账,把msgid对不上的窗口补齐。实时 + 补偿双通道,消息同步才敢说完整。
上线前自检清单
验签用原始 body,验不过返回 401
接收器 5 秒内返回 200,业务全异步
两层去重都就位(Delivery ID + msgid 唯一索引)
映射表支持一个客户多条企微关系
每日 getHistoryMessageList 对账补偿
回调事件字典和消息体字段在 Eyun 开发文档里有完整说明,实例凭证在Eyun 官网开通后即可对接。这套链路跑通,CRM 里的客户跟进记录从"销售手动填"变成"系统自动记",管理层看到的过程数据才是真的。