数据库的表数据同步到飞书多维表格,如何避免重复同步呢,也就是后面只需要增量同步

38 阅读5分钟

一句话结论(先给答案)

避免重复同步 = 一定要有「唯一标识 + 同步状态」

最常见、最稳妥的做法是:
用数据库里的唯一业务 ID(如 requestId)作为飞书多维表的唯一键,只同步“还没同步过 / 有更新的记录”


一、你现在为什么一定会重复?

你当前逻辑是:

SELECT * FROM zhyl003 
WHERE sqrq >= DATE_SUB(NOW(),INTERVAL 3 MONTH)
ORDER BY sqrq DESC

然后:

client.bitable.v1.appTableRecord.batchCreate(...)

⚠️ 问题在于:

  • ❌ 每次都是 batchCreate(只新增)
  • ❌ 没判断「这条数据之前有没有同步过」
  • ❌ 飞书多维表 不会自动去重

👉 所以每跑一次,就全量新增一次


二、推荐方案一(最简单、最实用)✅

👉「数据库侧打同步标记」(我最推荐你用)

1️⃣ 思路

在你的 数据库表 zhyl003 中新增 2 个字段:【注意不能用于视图,最后面有针对视图的解决方案】

ALTER TABLE zhyl003
ADD COLUMN feishu_sync_flag TINYINT DEFAULT 0 COMMENT '是否已同步飞书',
ADD COLUMN feishu_sync_time DATETIME NULL COMMENT '同步到飞书时间';

2️⃣ 查询时只查「未同步的数据」

const [rows] = await connection.execute(`
  SELECT * 
  FROM zhyl003
  WHERE feishu_sync_flag = 0
    AND sqrq >= DATE_SUB(NOW(), INTERVAL 3 MONTH)
  ORDER BY sqrq DESC
`);

3️⃣ 同步成功后,回写数据库

在你这里👇

const res = await reqAddTRecord(tenant_access_token, salesFormData);

后面加:

if (res.code === 0) {
    const ids = rows.map(r => r.requestId);

    await connection.execute(
        `UPDATE zhyl003 
         SET feishu_sync_flag = 1,
             feishu_sync_time = NOW()
         WHERE requestId IN (${ids.map(() => '?').join(',')})`,
        ids
    );
}

✅ 效果:

  • 第一次:全量同步
  • 第二次:0 条数据
  • 新增数据 → 只同步新增的

这是 企业里 80% 的同步系统用的方案


三、方案二(更专业):用「业务唯一键」在飞书侧去重

1️⃣ 选一个“绝对唯一”的字段

你代码里已经有一个完美的唯一键

requestId // 流程ID

👉 这是 OA 流程 ID,本身就是唯一的


2️⃣ 在飞书多维表中新增一列

  • 字段名:流程ID
  • 类型:文本
  • 要求:唯一(人工约束)

3️⃣ 同步前,先查飞书已有记录

步骤:

  1. 调用 飞书多维表查询接口
  2. 拿到已有的 流程ID 集合
  3. DB 查询结果里 filter 掉已存在的

伪代码:

const existedIds = await getFeishuRequestIds(); // Set

const newRows = rows.filter(
    row => !existedIds.has(row.requestId)
);

然后只对 newRowsbatchCreate


⚠️ 这个方案的问题

  • 多一次飞书 API 调用
  • 数据量大时性能差
  • 代码复杂度明显上升

👉 不如方案一稳


四、方案三(进阶):支持“增量 + 更新”

如果你未来要支持:

  • DB 里数据被修改
  • 飞书里的对应行也要更新

那就用 时间戳方案

1️⃣ 数据库必须有 update_time

update_time DATETIME

2️⃣ 本地保存「上次同步时间」

可以存在:

  • 数据库表
  • Redis
  • 本地文件

例如:

lastSyncTime = '2025-01-01 00:00:00';

3️⃣ 查询:

SELECT *
FROM zhyl003
WHERE update_time > ?

4️⃣ 飞书侧逻辑

  • 有记录 → update
  • 没记录 → create

⚠️ 这是准 ETL 级别了,复杂度最高


五、视图解决方案

由于zhyl003是视图,不能在它上面新增字段,也不能直接打同步标记,所以上面的方案实际上都不可行。

为什么视图不能用“打字段标记”方案?

你现在的结构是:

AB
表 C
   ↓
  视图 zhyl003

视图的本质是:

  • 一个 SELECT 查询
  • 本身 不存数据
  • 除非是 可更新视图(你这种多表 join 基本不可能)

所以:

  • ❌ 不能 ALTER VIEW ADD COLUMN
  • ❌ 不能 UPDATE zhyl003 SET synced = 1

现在有 4 种「正确可行」的增量同步方案

我按 推荐程度 + 适配你现在情况 排序。


✅ 方案一(⭐⭐⭐⭐⭐ 强烈推荐):独立同步状态表(最通用)

👉 不改视图,不改原表
👉 专门建一个“同步记录表”

1️⃣ 新建一张表

CREATE TABLE zhyl003_sync_state (
  biz_id        NVARCHAR(100) PRIMARY KEY,  -- 视图里的唯一业务ID
  last_sync_at  DATETIME,
  sync_hash     NVARCHAR(64),
  synced       BIT DEFAULT 0
);

2️⃣ 同步逻辑(核心)

每次你从 zhyl003 查数据时:

SELECT v.*
FROM zhyl003 v
LEFT JOIN zhyl003_sync_state s
  ON v.biz_id = s.biz_id
WHERE
  s.biz_id IS NULL        -- 从未同步过
  OR v.update_time > s.last_sync_at;

3️⃣ 同步成功后,写状态表

INSERT INTO zhyl003_sync_state (biz_id, last_sync_at, synced)
VALUES (?, NOW(), 1)
ON DUPLICATE KEY UPDATE
  last_sync_at = NOW(),
  synced = 1;

⭐ 为什么这是最佳方案?

  • ✅ 不依赖视图是否可更新
  • ✅ 不污染业务表
  • ✅ 同步逻辑清晰、可回滚
  • ✅ 以后你换飞书 / 换系统都能复用

✅ 方案二(⭐⭐⭐⭐):按时间戳增量(前提:源表有更新时间)

如果你的视图 zhyl003 中:

  • update_time
  • 且是来自底层表

你可以:

SELECT *
FROM zhyl003
WHERE update_time > @last_sync_time;

同步完后:

last_sync_time = max(update_time)

⚠️ 注意:

  • 多表 join 时,update_time 要非常谨慎
  • 有可能漏数据(延迟更新)

⚠️ 方案三(⭐⭐):全量 + 去重(小数据量可用)

如果数据量不大(几千以内):

  • 每次全量查视图
  • 用业务唯一 key 去重

适合:

  • 低频同步
  • 简单脚本

不适合你现在这个「数据库 → 飞书多维表格」长期任务。


❌ 方案四(不推荐):在底层原表打标记

理论可行,但实际风险大:

  • 改动原业务表
  • 可能影响其他系统
  • 视图多表 join,标记逻辑复杂

如何把视图解决方案一嵌入代码中?

async function markRecordSynced(connection, bizId) {
    const sql = `
        INSERT INTO zhyl003_sync_state (biz_id, last_sync_at, synced)
        VALUES (?, NOW(), 1)
        ON DUPLICATE KEY UPDATE
          last_sync_at = NOW(),
          synced = 1
    `;
    await connection.execute(sql, [bizId]);
}
const connection = await mysql.createConnection(serverConfig.DbConfig);
try {
   // 查 zhyl003
   SELECT v.*
FROM zhyl003 v
LEFT JOIN zhyl003_sync_state s
  ON v.requestId = s.biz_id
WHERE s.biz_id IS NULL
  AND v.sqrq >= DATE_SUB(NOW(), INTERVAL 3 MONTH);
   // 写飞书
   // 写 sync_state
   markRecordSynced(connection, row.requestId)
} catch (e) {
   ...
} finally {
   await connection.end();
}