企业微信外部联系人同步的 CDC(变更数据捕获)架构实践
在企业服务系统中,客户数据的实时同步是提升运营效率的关键环节。以企业微信为例,其外部联系人(External Contact)数据频繁变动,若采用定时全量拉取方式,不仅资源消耗大,且存在数据延迟。本文介绍一种基于 CDC(Change Data Capture)机制的外部联系人同步架构,并结合 Java 实现示例,展示如何高效捕获并同步变更。
CDC 架构设计概述
CDC 的核心思想是监听数据源的变更事件(如新增、更新、删除),仅处理变化部分。对于企业微信外部联系人同步场景,我们可将企业微信 API 视为"逻辑数据源",通过增量拉取配合本地状态比对,实现准实时变更捕获。
整体架构包括以下组件:
- 变更探测器(Change Detector) :定期调用企业微信
/externalcontact/list和/externalcontact/get接口获取最新外部联系人列表及详情。 - 状态存储(State Store):持久化上一次同步的外部联系人快照(如使用 MySQL 或 Redis)。
- 变更处理器(Change Processor):比对当前与历史快照,生成变更事件(CREATE/UPDATE/DELETE)。
- 下游同步器(Sync Handler) :将变更事件推送到业务系统(如 CRM)。

Java 实现:变更探测与状态比对
以下代码基于 wlkankan.cn.sync.cdc 包结构实现核心逻辑。
java
package wlkankan.cn.sync.cdc;
import wlkankan.cn.sync.model.ExternalContact;
import wlkankan.cn.sync.repo.ContactSnapshotRepository;
import wlkankan.cn.sync.client.WeComApiClient;
import java.util.*;
import java.util.stream.Collectors;
public class ExternalContactCdcService {
private final WeComApiClient weComClient;
private final ContactSnapshotRepository snapshotRepo;
public ExternalContactCdcService(WeComApiClient client, ContactSnapshotRepository repo) {
this.weComClient = client;
this.snapshotRepo = repo;
}
public List<ContactChangeEvent> detectChanges(String corpId, String accessToken) {
// 1. 获取当前外部联系人ID列表
List<String> currentIds = weComClient.listExternalContactIds(accessToken);
Set<String> currentIdSet = new HashSet<>(currentIds);
// 2. 获取上一次快照
Map<String, ExternalContact> lastSnapshot = snapshotRepo.loadSnapshot(corpId);
// 3. 初始化变更事件列表
List<ContactChangeEvent> events = new ArrayList<>();
// 4. 检测新增和更新
for (String id : currentIds) {
ExternalContact current = weComClient.getContactDetail(accessToken, id);
ExternalContact last = lastSnapshot.get(id);
if (last == null) {
events.add(new ContactChangeEvent(id, ChangeType.CREATE, current));
} else if (!last.equals(current)) {
events.add(new ContactChangeEvent(id, ChangeType.UPDATE, current));
}
}
// 5. 检测删除(存在于快照但不在当前列表中)
for (String id : lastSnapshot.keySet()) {
if (!currentIdSet.contains(id)) {
events.add(new ContactChangeEvent(id, ChangeType.DELETE, null));
}
}
// 6. 更新快照
Map<String, ExternalContact> newSnapshot = currentIds.stream()
.collect(Collectors.toMap(
id -> id,
id -> weComClient.getContactDetail(accessToken, id)
));
snapshotRepo.saveSnapshot(corpId, newSnapshot);
return events;
}
}
其中,ContactChangeEvent 定义如下:
java
package wlkankan.cn.sync.model;
public class ContactChangeEvent {
private String contactId;
private ChangeType type;
private ExternalContact data;
public ContactChangeEvent(String contactId, ChangeType type, ExternalContact data) {
this.contactId = contactId;
this.type = type;
this.data = data;
}
// getters and setters omitted
}
枚举 ChangeType:
java
package wlkankan.cn.sync.model;
public enum ChangeType {
CREATE, UPDATE, DELETE
}
状态存储实现示例
使用 MySQL 存储快照,表结构如下:
sql
CREATE TABLE external_contact_snapshot (
corp_id VARCHAR(64) NOT NULL,
contact_id VARCHAR(64) NOT NULL,
contact_json JSON NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (corp_id, contact_id)
);
对应的 Repository 实现:
java
package wlkankan.cn.sync.repo;
import wlkankan.cn.sync.model.ExternalContact;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashMap;
import java.util.Map;
public class JdbcContactSnapshotRepository implements ContactSnapshotRepository {
private final DataSource dataSource;
private final ObjectMapper objectMapper = new ObjectMapper();
public JdbcContactSnapshotRepository(DataSource ds) {
this.dataSource = ds;
}
@Override
public Map<String, ExternalContact> loadSnapshot(String corpId) {
Map<String, ExternalContact> snapshot = new HashMap<>();
String sql = "SELECT contact_id, contact_json FROM external_contact_snapshot WHERE corp_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, corpId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
String id = rs.getString("contact_id");
String json = rs.getString("contact_json");
ExternalContact contact = objectMapper.readValue(json, ExternalContact.class);
snapshot.put(id, contact);
}
} catch (Exception e) {
throw new RuntimeException("Failed to load snapshot", e);
}
return snapshot;
}
@Override
public void saveSnapshot(String corpId, Map<String, ExternalContact> snapshot) {
String deleteSql = "DELETE FROM external_contact_snapshot WHERE corp_id = ?";
String insertSql = "INSERT INTO external_contact_snapshot (corp_id, contact_id, contact_json) VALUES (?, ?, ?)";
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement delStmt = conn.prepareStatement(deleteSql)) {
delStmt.setString(1, corpId);
delStmt.executeUpdate();
}
try (PreparedStatement insStmt = conn.prepareStatement(insertSql)) {
for (Map.Entry<String, ExternalContact> entry : snapshot.entrySet()) {
insStmt.setString(1, corpId);
insStmt.setString(2, entry.getKey());
insStmt.setString(3, objectMapper.writeValueAsString(entry.getValue()));
insStmt.addBatch();
}
insStmt.executeBatch();
}
conn.commit();
} catch (Exception e) {
throw new RuntimeException("Failed to save snapshot", e);
}
}
}
调度与容错机制
建议使用 Quartz 或 Spring Scheduler 定期触发 detectChanges 方法,频率可根据业务容忍延迟设定(如每5分钟一次)。同时应记录每次同步的上下文(如 token 有效期、API 调用次数),并在失败时支持重试与告警。
通过上述 CDC 架构,企业微信外部联系人变更可被精准捕获,避免全量同步开销,保障下游系统数据一致性与实时性。