# 跨库数据同步中间件 支持 **SQLite ↔ MySQL ↔ Postgres**,不要求两端同一种数据库。变更经 **outbox 队列** 近实时投递;冲突进 **冲突队列**。 ## 权限与隔离 | 项 | 说明 | |----|------| | 谁可配 | 仅公司**顶级权限(管理员)**,权限名「数据同步」 | | 谁不可 | 编辑 / 只读、智能体账号(即使有「发布模块」) | | 数据隔离 | 通道与冲突带 `tenant_id`;公司 A 看不到公司 B 的通道/DSN | | 多服务器 | 同一公司可建多条通道,分别填 B、C 等库的 DSN | ## 典型场景:A / B / C | 端 | 角色 | |----|------| | **A** | 线上库(用户增删改) | | **B** | 本地库(本机业务 + 接收 C) | | **C** | 额外数据源(Excel / API / 导入),只写入 **B** | 推荐配置: 1. 建一条通道:`local` = B,`remote` = A,**方向 `bidirectional`**,冲突策略 `queue`(或 LWW)。 2. C 的数据用 **ingest API**(或业务直接写 B)写入本地;触发器进 outbox,再推到 A。 3. A 上用户改的数据经 outbox 拉回 B。 4. 怀疑漏数时点 **对账**,或等双向通道约每分钟自动对账。 如何保证**不漏、不多**: | 手段 | 防什么 | |------|--------| | 表触发器 → `_ajz_sync_outbox` | 漏(本地/线上变更必入队) | | 应用远端时 `WithApplying`(触发器不写 outbox) | 多(A↔B 回声环) | | 目标 meta 版本相等则跳过 | 多(重复投递) | | 目标版本更新 → 冲突队列 / LWW | 并发改同一行 | | 主键对账 reconcile | 漏(存量差、触发器未装前的行) | | C→B upsert 同主键 | 多(重复灌入) | ``` C ──ingest/写库──► B (local) ◄──bidirectional outbox──► A (remote) ``` ## 能力 | 项 | 说明 | |----|------| | 方言 | `sqlite` / `mysql` / `postgres` | | 实时性 | 表触发器写 `_ajz_sync_outbox`,worker 默认每 500ms 拉取 | | 方向 | 本地→线上 / 线上→本地 / **双向**(A↔B 场景用这个) | | 冲突 | `queue`(入队)/ `lww_source` / `lww_target` | | 对账 | `POST .../reconcile`;双向运行中约每分钟自动一次 | | 外部源 | `POST .../ingest`:C → B,再同步到 A | | 配置 | 控制台「数据同步」页;可改线上 DSN | 配置与冲突持久化:`data/dbsync/channels.json`、`conflicts.json`(Docker:`.runtime/dbsync`)。 ## 控制台用法 1. 登录 → **数据同步** → **新建通道** 2. 本地 B:如 `sqlite` + `file:./data/local.db`,表名逗号分隔 3. 线上 A:`mysql` + `user:pass@tcp(host:3306)/db?parseTime=true` 4. 方向选 **双向** → **测试连接** → **保存** → **启动** 5. 需要补漏时点 **对账**;C 数据走业务写 B 或调用 ingest API ## API(需公司顶级权限「数据同步」/ 管理员) | 方法 | 路径 | |------|------| | GET/POST | `/api/v1/admin/sync/channels` | | GET/PUT/DELETE | `/api/v1/admin/sync/channels/{id}` | | POST | `/api/v1/admin/sync/test` | | POST | `/api/v1/admin/sync/channels/{id}/prepare\|start\|stop` | | POST | `/api/v1/admin/sync/channels/{id}/reconcile` | | POST | `/api/v1/admin/sync/channels/{id}/ingest` | | GET | `/api/v1/admin/sync/conflicts` | | POST | `/api/v1/admin/sync/conflicts/{id}/resolve` | ### ingest 示例 ```json POST /api/v1/admin/sync/channels/{id}/ingest { "table": "article", "source": "excel", "rows": [ { "id": "c-001", "title": "来自 C" } ] } ``` 按主键 upsert 写入本地 B,触发器入 outbox,worker 再推到线上 A。 ## 注意 - 两端业务表结构需兼容(同名列);主键默认 `id`,可用 `pk_columns` 覆盖。 - MySQL 需账号有建触发器权限。 - 密钥在 DSN 中;列表页会打码显示。 - 「实时」为亚秒级轮询 + 触发器,非 MySQL binlog CDC;同机延迟通常 <1s。 - 对账按**主键集合**补缺行,不做逐字段内容 diff;同 PK 内容冲突仍靠版本 / 冲突队列。