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

相关推荐
shangjian0076 分钟前
OpenClaw学习笔记-01-架构篇
笔记·学习·架构
code 小楊10 分钟前
深度解析RAG系统与AI Agent:原理、架构及协同落地
人工智能·架构
彭波39612 分钟前
听歌软件下载!全网音乐随便听!手机电脑+电视端!音乐播放器推荐
android·智能手机·音频·开源软件·娱乐·软件需求
碳基硅坊17 分钟前
OpenClaw接入企业微信
人工智能·企业微信·openclaw
江澎涌18 分钟前
鸿蒙动态导入实战
android·typescript·harmonyos
lifewange19 分钟前
SQL中的聚合函数有哪些
android·数据库·sql
无忧智库19 分钟前
破局与重构:大型集团财务共享业财一体化的数字基因革命(PPT)
大数据·架构
NPE~31 分钟前
[App逆向]环境搭建上篇——抓取apk https包
android·教程·逆向·android逆向·逆向分析
weixin1997010801635 分钟前
《淘宝双11同款:基于 Sentinel 的微服务流量防卫兵实战》
微服务·架构·sentinel
Shining059641 分钟前
AI 编译器系列(五)《拓展 Triton 深度学习编译器——DLCompiler》
人工智能·深度学习·学习·其他·架构·ai编译器·infinitensor