Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见. Co-authored-by: Cursor <cursoragent@cursor.com>
8.5 KiB
跨库数据同步中间件
支持方言 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 |
推荐配置:
- 通道:
remote.driver=postgres+ DSN;方向local_to_remote;策略lww_source。 - 表白名单须 UUID TEXT/UUID PK + FK 闭包(保存时校验)。
- 客户端显式
local_dbsync(见开通说明);未开通用户零感。 - 怀疑漏数时点 同步修复(对账)(有最小间隔限流)。
单服务器 / 多服务器(同一套 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 取行补齐 |
POST /api/v1/agent/sync/channels/{id}/pull
POST /api/v1/agent/sync/channels/{id}/bootstrap # 强制 mode=bootstrap
{
"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 示例
postgres://sync_user:pass@127.0.0.1:5432/app_online?sslmode=disable
联调临时 SQLite A(不推荐生产):
file:E:/path/to/remote.db?_pragma=foreign_keys(1)
勿用反斜杠;平台会自动规范化并补 busy_timeout。
控制台用法
- 数据同步 → 新建通道(remote 选 postgres)→ 测试 → 保存 → 启动
- 默认策略选 源端覆盖;方向首期用 本地 → 线上
- 点 同步修复 做主键对账(勿连续狂点,有限流)
- LWW 明细在 平台工作台(超管),不在公司同步页
- 多服务器:为每台库再新建一条通道,把对应
channel_id发给客户端
开通与迁移:见 数据同步-开通说明.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 示例
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": "..."
}