企业微信外部联系人同步的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模块实现了对企业微信外部联系人变更的精准捕获,避免全量拉取开销,同时兼顾新增、更新与删除场景,保障本地数据与企业微信端最终一致。