一句话结论(先给答案)
避免重复同步 = 一定要有「唯一标识 + 同步状态」
最常见、最稳妥的做法是:
用数据库里的唯一业务 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️⃣ 同步前,先查飞书已有记录
步骤:
- 调用 飞书多维表查询接口
- 拿到已有的
流程ID集合 - DB 查询结果里
filter掉已存在的
伪代码:
const existedIds = await getFeishuRequestIds(); // Set
const newRows = rows.filter(
row => !existedIds.has(row.requestId)
);
然后只对 newRows 做 batchCreate
⚠️ 这个方案的问题
- 多一次飞书 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是视图,不能在它上面新增字段,也不能直接打同步标记,所以上面的方案实际上都不可行。
为什么视图不能用“打字段标记”方案?
你现在的结构是:
表 A
表 B
表 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();
}