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

企业微信外部联系人同步的 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 架构,企业微信外部联系人变更可被精准捕获,避免全量同步开销,保障下游系统数据一致性与实时性。

相关推荐
树码小子1 小时前
开启 IDEA 中的断言功能
android·java·intellij-idea
梦想的旅途21 小时前
企业微信 API 二次开发:AI 智能体接入外部群架构
人工智能·企业微信
这个DBA有点耶2 小时前
从DBA到数据架构师(五):数据架构演进中的技术债务管理
数据库·程序人生·云原生·架构·dba·数据库管理员
沪上企服通2 小时前
高端财税的技术切面:从“可审计级旧账重建“看企业税务合规中台的架构演进
架构
Android-Flutter2 小时前
android 动画详解
android·kotlin
dogstarhuang3 小时前
大模型 API 停服怎么办:用 API 网关实现多模型统一接入与可切换架构
人工智能·后端·架构·大模型·api·数字化转型·ai应用
梦想的旅途23 小时前
企业微信 API 二次开发:外部群消息异步队列
企业微信
过江龙8474 小时前
Java架构师的AI转型之路(中):Agent与编排体系实战
架构
ziyun6668885 小时前
Docker 容器化 Web 服务架构搭建与自动化运维项目
运维·docker·架构
技术传感器5 小时前
让 Hermes 自动完成一个软件需求:从 Issue 到 PR 的 AI 开发流水线
人工智能·架构·aigc·需求分析·issue