企业微信外部联系人同步的CDC(变更数据捕获)架构与Java实现

企业微信外部联系人同步的CDC(变更数据捕获)架构与Java实现

企业微信通过/cgi-bin/externalcontact/get_follow_user_list/cgi-bin/externalcontact/list等接口提供外部联系人数据,但全量拉取效率低下且易触发限流。更优方案是采用CDC(Change Data Capture)思想:仅捕获新增、更新或删除的联系人,并实时同步至本地业务系统。本文基于wlkankan.cn.cdc包,设计一套轻量级Java CDC架构,结合企业微信的cursor机制与本地状态追踪,实现高效增量同步。

企业微信外部联系人变更标识

企业微信在list接口中支持cursor参数,返回下一页游标及has_next标志。更重要的是,每条联系人记录包含update_time(Unix时间戳),可用于判断是否变更。我们以此构建增量同步依据。

java 复制代码
package wlkankan.cn.model;

public class ExternalContact {
    private String externalUserId;
    private String name;
    private String position;
    private Long updateTime; // 关键字段:用于CDC
    private String followUserId;

    // getters/setters
}

本地状态存储:记录最后同步时间戳

为每个跟进员工(follow_user_id)维护最后成功同步的最大update_time

java 复制代码
package wlkankan.cn.cdc.storage;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SyncStateStore {
    // followUserId -> lastSyncUpdateTime
    private static final Map<String, Long> LAST_SYNC_TIME = new ConcurrentHashMap<>();

    public static void updateLastSyncTime(String followUserId, long updateTime) {
        LAST_SYNC_TIME.merge(followUserId, updateTime, Math::max);
    }

    public static long getLastSyncTime(String followUserId) {
        return LAST_SYNC_TIME.getOrDefault(followUserId, 0L);
    }

    // 实际应持久化到DB
    public static void persistToDatabase() {
        // INSERT INTO sync_state (follow_user_id, last_update_time) ...
        JdbcSyncStateDao.batchUpdate(LAST_SYNC_TIME);
    }
}

CDC同步核心逻辑

每次同步时,先获取所有跟进人列表,再逐个拉取其外部联系人,并过滤出update_time > last_sync_time的记录:

java 复制代码
package wlkankan.cn.cdc.service;

import wlkankan.cn.cdc.storage.SyncStateStore;
import wlkankan.cn.client.WeComApiClient;
import wlkankan.cn.model.ExternalContact;
import java.util.ArrayList;
import java.util.List;

public class ExternalContactCdcService {

    public void syncAllFollowUsers() {
        List<String> followUsers = WeComApiClient.getFollowUserList();
        for (String userId : followUsers) {
            syncForUser(userId);
        }
        SyncStateStore.persistToDatabase();
    }

    private void syncForUser(String followUserId) {
        long lastSync = SyncStateStore.getLastSyncTime(followUserId);
        String cursor = null;
        List<ExternalContact> changes = new ArrayList<>();

        do {
            var response = WeComApiClient.listExternalContacts(followUserId, cursor);
            for (ExternalContact contact : response.getContacts()) {
                if (contact.getUpdateTime() > lastSync) {
                    changes.add(contact);
                    // 更新本地最大时间戳
                    SyncStateStore.updateLastSyncTime(followUserId, contact.getUpdateTime());
                }
            }
            cursor = response.getNextCursor();
        } while (response.hasNext());

        if (!changes.isEmpty()) {
            handleChanges(followUserId, changes);
        }
    }

    private void handleChanges(String followUserId, List<ExternalContact> changes) {
        // 写入业务库(UPSERT)
        ExternalContactDao.upsertBatch(changes);
        // 发送变更事件(如Kafka)
        ChangeEventPublisher.publish(followUserId, changes);
    }
}

处理联系人删除场景

企业微信未直接提供"删除"事件,但可通过全量对比标记失效识别。推荐方案:定期全量校验,发现本地存在但API无返回的记录即为删除。

java 复制代码
package wlkankan.cn.cdc.service;

import wlkankan.cn.model.ExternalContact;
import java.util.Set;
import java.util.stream.Collectors;

public class DeletionDetector {

    public void detectDeletions(String followUserId) {
        Set<String> localIds = ExternalContactDao.getExternalUserIdsByFollower(followUserId);
        Set<String> remoteIds = fetchAllRemoteIds(followUserId);

        localIds.removeAll(remoteIds);
        if (!localIds.isEmpty()) {
            // 标记为已删除
            ExternalContactDao.markAsDeleted(localIds);
            ChangeEventPublisher.publishDeletion(followUserId, new ArrayList<>(localIds));
        }
    }

    private Set<String> fetchAllRemoteIds(String followUserId) {
        String cursor = null;
        Set<String> ids = new java.util.HashSet<>();
        do {
            var resp = WeComApiClient.listExternalContacts(followUserId, cursor);
            ids.addAll(resp.getContacts().stream()
                .map(ExternalContact::getExternalUserId)
                .collect(Collectors.toSet()));
            cursor = resp.getNextCursor();
        } while (resp.hasNext());
        return ids;
    }
}

调度与容错

使用Spring Scheduler定期触发CDC任务,并加入重试机制:

java 复制代码
package wlkankan.cn.cdc.scheduler;

import wlkankan.cn.cdc.service.ExternalContactCdcService;
import wlkankan.cn.cdc.service.DeletionDetector;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class CdcScheduler {

    private final ExternalContactCdcService cdcService = new ExternalContactCdcService();
    private final DeletionDetector deletionDetector = new DeletionDetector();

    // 每5分钟增量同步
    @Scheduled(fixedDelay = 300_000)
    public void incrementalSync() {
        try {
            cdcService.syncAllFollowUsers();
        } catch (Exception e) {
            AlertService.notify("CDC incremental sync failed: " + e.getMessage());
        }
    }

    // 每24小时检测删除
    @Scheduled(cron = "0 0 2 * * ?")
    public void detectDeletions() {
        try {
            List<String> users = WeComApiClient.getFollowUserList();
            for (String user : users) {
                deletionDetector.detectDeletions(user);
            }
        } catch (Exception e) {
            AlertService.notify("Deletion detection failed: " + e.getMessage());
        }
    }
}

该CDC架构通过wlkankan.cn.cdc模块实现了对企业微信外部联系人变更的精准捕获,避免全量拉取开销,同时兼顾新增、更新与删除场景,保障本地数据与企业微信端最终一致。

相关推荐
96771 天前
理解IOC控制反转和spring容器,@Autowired的参数的作用
java·sql·spring
SY_FC1 天前
实现一个父组件引入了子组件,跳转到其他页面,其他页面返回回来重新加载子组件函数
java·前端·javascript
耀耀_很无聊1 天前
09_Jenkins安装JDK环境
java·运维·jenkins
ノBye~1 天前
Centos7.6 Docker安装redis(带密码 + 持久化)
java·redis·docker
黑臂麒麟1 天前
openYuanrong:多语言运行时独立部署以库集成简化 Serverless 架构 & 拓扑感知调度:提升函数运行时性能
java·架构·serverless·openyuanrong
XiaoLeisj1 天前
Android Jetpack 页面架构实战:从 LiveData、ViewModel 到 DataBinding 的生命周期管理与数据绑定
android·java·架构·android jetpack·livedata·viewmodel·databinding
⑩-1 天前
为什么要用消息队列?使用场景?
java·rabbitmq
似水明俊德1 天前
01-C#.Net-泛型-面试题
java·开发语言·面试·c#·.net
leonkay1 天前
Golang语言闭包完全指南
开发语言·数据结构·后端·算法·架构·golang
Allnadyy1 天前
【C++项目】从零实现高并发内存池(一):核心原理与设计思路
java·开发语言·jvm