昨晚在复盘 星云API www.xingyapi.com 的底层高并发架构时,有个做医美私域中台的架构师找我倒苦水。

他们团队搞了个“高意向客户自动打标与分发”模块:只要客户在企微单聊里触发了特定关键词(比如“热玛吉”、“价格”),机器人就会自动给他打上“S级意向”的标签,并推给高级销售跟进。 这兄弟的业务逻辑写得极其“耿直”:收到聊天回调 -> 调“获取客户详情”API 查出当前所有标签 -> 在内存里把新标签 add 进数组 -> 调“编辑客户企业标签”API 把完整的数组写回去。

结果大促一波流,几千个客户同时进线咨询。这种“先查后写”的读写模型直接引发了灾难级的连锁反应: 第一,联系人查询接口极其沉重,瞬间把企微的 API 频控池打穿,大面积报 45009(频率超限); 第二,发生了严重的并发“脏写”——一线销售刚在手机上手动给客户打的“已邀约”标签,被机器人拿着一秒钟前查到的旧数据直接强行覆盖、抹除了!销售总监看着后台乱七八糟的客户画像,差点顺着网线过去打人。

在工业级的企微 SaaS 架构中,“联系人数据”是静态画像,“客户标签”是动态状态,把它们组合成业务模块,绝不是用简单的 CRUD 读写模型来硬凑的。今天咱们直接手撕一套“CQRS 读写分离 + 原子增量打标 + 标签驱动路由”的高阶组合管线。

第一关:CQRS 读写分离——斩断实时查询,构建异构画像底座

如果你去翻过底层的 开放文档,你会发现企微的“获取客户详情”接口是一个极其庞大的聚合型 API。它不仅返回客户的基础信息,还附带复杂的跟进人列表(follow_user)和全量标签树。拿它来做实时业务的前置判断,就是用大炮打蚊子。

工业级解法:抛弃主动查询,拥抱事件驱动的影子库。

你的主干业务链路绝对不允许主动发起联系人查询。必须通过监听 Webhook 中的 add_external_contact(添加好友)和 edit_external_contact(修改资料/标签)事件,来维护本地的“影子画像”。

在处理客户消息时,直接从 Redis 捞出预先拼装好的 富化上下文(Enriched Context)

Java

// 绝不调企微 API!微秒级从本地异构缓存拼装上下文
public EnrichedCustomerContext buildCustomerContext(String tenantId, String externalUserId) {
    // 1. 获取客户基础属性 (性别、名称等)
    CustomerBaseInfo baseInfo = redisTemplate.opsForHash().get("SCRM:Customer:Base:" + tenantId, externalUserId);
    
    // 2. 获取该客户身上的全量标签集合
    Set<String> tags = redisTemplate.opsForSet().members("SCRM:Customer:Tags:" + tenantId + ":" + externalUserId);
    
    return new EnrichedCustomerContext(baseInfo, tags);
}

把这种重度 IO 动作转化为内存里的 O(1) 提取,你的机器人模块才能在成千上万的并发咨询下稳如老狗。

第二关:原子化增量打标——防并发脏写的绝对防御

既然不让实时查接口,那我怎么更新标签?难道不怕覆盖掉销售刚刚手动打的标签吗?

这是企微底层 API 设计中最容易被新手忽略的黄金细节:企微的“编辑客户企业标签”接口,天然支持增量(Delta)修改,根本不需要你把旧标签传过去!

看看官方报文规范里的 add_tagremove_tag 字段。我们封装底层的打标动作时,必须采用“原子增减”的指令流,绝对禁止“全量覆盖”。

Java

/**
 * 工业级增量打标核心逻辑
 * 绝不先查后写,直接下发动作指令,依靠企微底层网关的原子性防止并发脏写!
 */
public void markCustomerTagAtomic(String tenantId, String userId, String externalUserId, List<String> newTagIds) {
    JSONObject payload = new JSONObject();
    payload.put("userid", userId);
    payload.put("external_userid", externalUserId);
    
    // 核心护城河:只传增量(差异量)!
    if (newTagIds != null && !newTagIds.isEmpty()) {
        payload.put("add_tag", newTagIds); 
    }

    try {
        wecomClient.markTag(payload);
        log.info("【原子打标】客户 {} 增量打标指令已下发,规避并发脏写", externalUserId);
    } catch (WeComApiException e) {
        // 捕获 45009 等异常,走 RabbitMQ 延迟队列进行指数退避重试
        mqProducer.sendDelayed("TOPIC_TAG_RETRY", payload, 5000);
    }
}

用了这种原子增量模型,哪怕你的大模型机器人在后台疯狂打“高意向”标签,同时一线销售在手机端手动打“价格敏感”标签,两者也绝对不会发生任何冲突,底层数据完美融合。

第三关:标签驱动业务(Tag-Driven Workflow)——把数据变成状态机

在工业级的企微 SaaS 架构中,最大的误区就是“打完标签就结束了”。联系人和标签的组合,本质上是用来驱动下游流水线(Workflow)的引擎。

我们要把 Webhook 回调接收器,改造成一个基于内存 Diff 的“标签状态机”。当收到 edit_external_contact 回调时,计算出到底是哪个标签被触发了,进而通过 MQ 扇出业务动作。

Java

@WeComRouter(msgType = "event", event = "change_external_contact")
public class CustomerTagWorkflowHandler implements IWeComMsgHandler {
    
    @Override
    public void handle(StandardMsgDTO msgDTO) {
        if (!"edit_external_contact".equals(msgDTO.getChangeType())) {
            return;
        }
        
        String externalUserId = msgDTO.getExternalUserId();
        
        // 1. 从 XML 密文中提取官方推送的最新的全量标签集合
        List<String> newTags = extractTags(msgDTO.getRawXml());
        
        // 2. 从本地 Redis 中拿到上一秒的旧标签基线
        List<String> oldTags = getLocalTags(externalUserId);
        
        // 3. 内存级双指针 Diff,算出究竟新增了哪个标签
        List<String> addedTags = new ArrayList<>(newTags);
        addedTags.removeAll(oldTags);
        
        // 4. 业务路由分发:如果命中核心业务标签,立刻触发转化管线!
        if (addedTags.contains(TAG_ID_VIP_S)) {
            log.info("【状态机路由】客户 {} 晋升 S 级,触发专属客服派单与发券机制", externalUserId);
            
            // 组装 Enriched Context 并将其扔进 Kafka 扇出,绝不在此阻塞线程
            EnrichedCustomerContext context = buildCustomerContext(msgDTO.getTenantId(), externalUserId);
            mqProducer.send("TOPIC_VIP_CONVERSION", context);
        }
        
        // 5. 将新标签刷入本地异构缓存,作为下一次比对的基线
        updateLocalTags(externalUserId, newTags);
    }
}

做企微的业务组合开发,本质上是在做一套高吞吐的流式计算系统。把联系人当做多租户下隔离的静态载体,把标签当做驱动状态机流转的事件引擎。用 CQRS 抹平 API 的限流恐惧,用增量原子指令解决并发脏写,你的机器人模块才能在海量流量下跑得丝滑顺畅。

这套“联系人+标签”的数据流转体系非常关键。你们在实际交付时,如果遇到销售离职、其名下客户被“在职继承”分配给新销售,企微底层大概率会把原销售打的“私有标签”清空。面对这种资产交接时的“标签蒸发”问题,你们的中台一般是怎么做标签数据继承和恢复的?

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐