# 跨库数据同步中间件 支持方言 **Postgres(生产线上库 A 推荐)** / MySQL / SQLite。 **形态 B(本机 agent)**:终端 agent 经平台 **push** 写线上 A;平台**不直连用户本机 SQLite**。 冲突策略默认 **自动 LWW(源端覆盖)**;落败写入**平台超级管理员**覆盖日志。公司管理员**无冲突台**。 ## 权限与隔离 | 项 | 说明 | |----|------| | 谁可配通道 | 公司**顶级权限(管理员)**,「数据同步」 | | Agent push / 拉白名单 | JWT 含「数据同步」(人类管理员或智能体凭证) | | LWW 覆盖审计 | **仅平台超级管理员**;公司 top → 403 | | 数据隔离 | 通道带 `tenant_id`;公司 A 看不到公司 B | ## 推荐场景(松离线 B→A,A=Postgres) | 端 | 角色 | |----|------| | **B** | 本机正式库(UUID 主键);开通且白名单表本地可见 | | **A** | 线上 **Postgres**(通道 `remote`);agent 经平台 push 幂等写入 | | **Agent** | 读本机 outbox → `POST /api/v1/agent/sync/channels/:id/push` | 推荐配置: 1. 通道:`remote.driver=postgres` + DSN;方向 **`local_to_remote`**;策略 **`lww_source`**。 2. 表白名单须 UUID TEXT/UUID PK + FK 闭包(保存时校验)。 3. 客户端显式 `local_dbsync`(见开通说明);未开通用户零感。 4. 怀疑漏数时点 **同步修复(对账)**(有最小间隔限流)。 ### 单服务器 / 多服务器(同一套 API) | 部署 | 配置 | |------|------| | 单服务器 | 一条通道指向该 Postgres;一个 `channel_id` | | 多服务器 | **每库一条通道**(不同 remote DSN);客户端按库选择 `channel_id` / Binding | 协议相同,均为「经智建 push」,无需第二套接口。 如何保证**不漏、不多**: | 手段 | 防什么 | |------|--------| | 触发器 / 本机 outbox → agent push | 漏 | | `WithApplying` / 远端应用不回写 outbox | 多(回声) | | 目标 meta 同 version 跳过 | 多(重复投递) | | LWW + 超管覆盖日志 | 并发同 PK | | 主键对账 reconcile | 漏(存量差) | ## 能力一览 | 项 | 说明 | |----|------| | 方言 | 生产 A:`postgres`;兼容 `mysql`;`sqlite` 仅联调临时 A(不宜多 writer) | | 方向 | 本地→线上(推荐)/ 线上→本地 / 双向 | | 冲突 | 默认 `lww_source`;`lww_target`;`queue` 仅调试(租户不可见) | | Agent | `GET .../agent/sync/.../whitelist`;`POST .../push`(管理员/智能体「数据同步」,或登录用户 + 本人 Binding) | | Binding | `GET/POST /api/v1/admin/sync/bindings`(同上鉴权;普通用户仅本人) | | 对账 | `POST .../reconcile`(默认最少间隔 300s;仍需「数据同步」) | | 审计 | `GET /api/v1/platform/dbsync/lww-overrides`(超管) | 持久化:`data/dbsync/channels.json`、`bindings.json`、`lww_overrides.json`(及遗留 `conflicts.json`)。 ## 用户自助同步(库级三态) 终端用户可在客户端对名下库选择仅本地 / 仅线上 / 同步;选 **同步** 后用**登录 JWT**(无需单独「数据同步」管理员权)调 agent API。 | 角色 | 范围 | |------|------| | 「数据同步」管理员 / 智能体 | 租户内通道;push 不强制 Binding | | 普通登录用户 | 仅本人 Binding:`channel_id` + `online_db_id`;push **须带** `online_db_id` | ### 表白名单策略(Z4,已冻结) | 项 | 行为 | |----|------| | **权限源** | 登录 JWT + 本人 Binding 的 `online_db_id`(不是通道 `tables`) | | **push** | 接受 Binding 库内**任意表**;表不存在时**自动建表**(TEXT 列 + PK) | | **通道 tables** | 仅历史兼容 / whitelist 接口展示;**不拒收** | 管理员不必再为业务表维护白名单。宇恒默认 `YXD_SYNC_ENFORCE_WHITELIST=0`。 ### 下行 / 仅线上(Z5) 形态 B:平台**不直连本机 B**,只读线上 A,把行返回给本机 agent 写入 B(建议 `WithApplying` 防回声)。 | mode | 用途 | |------|------| | `bootstrap` | 全量分页灌库(「仅线上」首启 / 本地空库) | | `pks` | 只列 A 上主键,客户端 diff 后再 `rows` | | `rows` | 按 `row_pks` 取行补齐 | ```http POST /api/v1/agent/sync/channels/{id}/pull POST /api/v1/agent/sync/channels/{id}/bootstrap # 强制 mode=bootstrap ``` ```json { "mode": "bootstrap", "table": "orders", "after_pk": "", "limit": 200, "online_db_id": "用户自助必填" } ``` 循环直到 `result.has_more=false`。单页默认 200、上限 500。 ## Agent push 幂等与重复投递 - **正确性**:同 `(table, row_pk, version)` 已落地 → `result.skipped=true` 且 HTTP 2xx;线上 A **不双行**。 - **允许**客户端重复 push(例如多 agent / 双 drain);去重是**客户端优化**(进程内 drain 锁),不是平台必做。 - 通道 stats:`pushed_applied` / `pushed_skipped` 可观察重推噪声。 - remote 暂不可达:HTTP **503** + `retryable: true`(agent 可稍后重试,勿换 UUID)。 - **Postgres A** 适合正常并发;若联调仍用 SQLite 作 A,请客户端**按库串行 drain**。 - 平台对 remote 连接做**池化复用 + 通道启动预热**,避免每次 push 冷开 SQLite 触发超时。 ## 请求日志怎么查 | 来源 | 位置 / 接口 | 内容 | |------|-------------|------| | Platform HTTP | `.runtime/logs/platform.err.log`(go-zero) | 路径、耗时、状态码;超时曾见 `slowcall(3000ms)` | | Platform dbsync | 同文件,关键字 `dbsync push` / `dbsync pull` | channel、table、pk、user、outcome、dur、`req=`(X-Request-Id) | | Gateway | `.runtime/logs/gateway.out.log` | sync 路径与 ≥500:`gateway POST /api/... status=... req=` | | 审计 API | `GET /api/v1/audit/logs`(需「查看审计」) | `action` 含 `dbsync.push` / `dbsync.pull` / `*.error`,detail JSON 带 `req_id` | 联调时用响应头 / 请求头 `X-Request-Id` 可在三处日志对齐同一笔请求。 ## 线上 Postgres DSN 示例 ```text postgres://sync_user:pass@127.0.0.1:5432/app_online?sslmode=disable ``` 联调临时 SQLite A(不推荐生产): ```text file:E:/path/to/remote.db?_pragma=foreign_keys(1) ``` 勿用反斜杠;平台会自动规范化并补 `busy_timeout`。 ## 控制台用法 1. **数据同步** → 新建通道(remote 选 **postgres**)→ 测试 → 保存 → 启动 2. 默认策略选 **源端覆盖**;方向首期用 **本地 → 线上** 3. 点 **同步修复** 做主键对账(勿连续狂点,有限流) 4. LWW 明细在 **平台工作台**(超管),不在公司同步页 5. 多服务器:为每台库再新建一条通道,把对应 `channel_id` 发给客户端 开通与迁移:见 [数据同步-开通说明.md](./数据同步-开通说明.md)、[数据同步-迁移手册.md](./数据同步-迁移手册.md)。 ## 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`(写通道 local,偏形态 A) | | GET/POST | `/api/v1/admin/sync/bindings` | | GET/POST | `/api/v1/admin/sync/conflicts*` → **403**(已迁超管审计) | ### 本机 Agent / 用户自助 鉴权:Bearer = 「数据同步」**或** 已登录人类 JWT(后者须先 `POST .../bindings` 且 push 带本人 `online_db_id`)。 | 方法 | 路径 | |------|------| | GET | `/api/v1/agent/sync/channels/{id}/whitelist` | | POST | `/api/v1/agent/sync/channels/{id}/push` | | POST | `/api/v1/agent/sync/channels/{id}/push/batch` | | POST | `/api/v1/agent/sync/channels/{id}/pull` | | POST | `/api/v1/agent/sync/channels/{id}/bootstrap` | | GET/POST | `/api/v1/admin/sync/bindings`(用户自助亦可) | ### 平台超级管理员 | 方法 | 路径 | |------|------| | GET | `/api/v1/platform/dbsync/lww-overrides` | | POST | `/api/v1/platform/dbsync/lww-overrides/{id}/rollback` | ### push 示例 ```json POST /api/v1/agent/sync/channels/{id}/push { "table": "orders", "op": "insert", "row_pk": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "row": { "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "title": "x" }, "version": 1710000000000000000, "client_outbox_id": "..." } ``` ## 相关 - [同步表约定.md](./同步表约定.md) - [数据同步-开通说明.md](./数据同步-开通说明.md) - [数据同步-迁移手册.md](./数据同步-迁移手册.md)