feat: ship loose-offline dbsync (validate, agent push, LWW audit)

Add UUID/FK channel checks, agent whitelist/push APIs, bindings, super-admin LWW audit with rollback, reconcile rate limits, and sync docs. Default customers stay opt-in; company conflict UI is removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-07-31 17:54:14 +08:00
parent 632057c857
commit 76cdcd760e
39 changed files with 3302 additions and 199 deletions

View File

@@ -0,0 +1,294 @@
# 松离线 + dbsync 方案 · 最终版(冻结)
> **效力**:本文为双方协商后的**唯一开工依据**。
> 原方案稿 / 审评追加 / 决议草稿见同目录 `松离线-dbsync方案.md`(过程稿,不再改口径)。
> 冻结日期2026-07-31
---
## 0. 一句话
**技术**:松离线写本地 B最终 UUID+ 本机 agent 经 dbsync 幂等对齐线上 A冲突自动 LWW落败入**平台超级管理员**覆盖日志。
**产品**:宇恒**默认零影响**;仅显式 `local_dbsync` + **表白名单** opt-inM4 前不对全员切默认。
---
## 1. 硬约束(不可回退)
| # | 约束 |
|---|------|
| H1 | 未开通同步的用户:保存 / 自增表 / 插件行为与现网一致(行为 diff = 0 |
| H2 | 默认**不得**自动升为 `local_dbsync` |
| H3 | agent 停运**不得**导致 `apply_write` / 保存失败;心跳只驱动 UI |
| H4 | 默认建表模板**保持自增**;仅「同步表模板」为 UUID |
| H5 | 旧 `online_primary` 客户**禁止静默关双写**;仅新客或显式迁移客评估切换 |
| H6 | 未开通用户**零同步文案**(无强制状态条、无「已自动合并」) |
---
## 2. 架构定案
| 项 | 定案 |
|----|------|
| 离线 | **松离线**:开通且白名单表 → 本地正式库落最终 UUID立刻可见 |
| 同步 | **接法① dbsync**(非 HTTP 双写主路径) |
| 通道形态 | **B 本机 sync agent**(平台不直连用户 SQLite |
| 首期方向 | **B→A**;双向另开,默认关 |
| 冲突 | **自动 LWW**;首期投递以 **outbox 单调 version** 为主,偏源(本机推上)时覆盖目标并记审计 |
| 落败追溯 | **LWW 自动覆盖日志****仅平台超级管理员**;公司 top **403**;建议 TTL 90 天 |
| 终端冲突 UI | **不做** |
| 形态 A / CRDT / 对账替代 UUID | **不做** |
```text
[开通 + 白名单表]
业务写 ──► BUUID──► outbox ──► 本机 agent ──► Aingest/upsert
UI仅本机 / 待同步 / 已同步(不影响保存成败)
[未开通]
现网路径不变local_only 或 online_primary
```
---
## 3. 模式三分(宇恒)
| 模式 | 含义 | 默认 |
|------|------|------|
| `local_only` | 只写本地 | 可作默认之一 |
| `online_primary` | 现网 HTTP 双写 / 离线 pending | **已上线客户保留** |
| `local_dbsync` | 松离线 + agent | **仅显式配置** |
- 未配置 / 未识别 → **不得**当成 `local_dbsync`
- **互斥粒度(定案 O6****同一张表**不可同时走 `online_primary` 双写与 `local_dbsync`**同一库**允许部分表白名单走 `local_dbsync`,其余表保持该库原模式写本地(不强制整库切模式)。
---
## 4. 白名单(定案 O1 / O2
| 项 | 定案 |
|----|------|
| 源 of truth | **智建通道配置** |
| 宇恒 | 本地缓存副本;拉取前 / 拉失败 → **视为未开通同步表**(走原写路径) |
| 谁可改 | **公司 top**(现「数据同步」权限);终端只读 |
| 闭包 | 父子外键须同进白名单,否则保存通道失败 |
| 入通道 | 非 `id TEXT` UUID → **拒绝** |
---
## 5. 写路径规则
### 5.1 未进白名单或未开 `local_dbsync`
与现网完全一致:可不传 `id`、允许自增;`online_primary` 语义不变。
### 5.2 已开 `local_dbsync` 且表在白名单
1.`id`**网关自动补 UUID**(规范化小写带连字符),响应带回 `id` / `inserted_id`;并记 auto_id 日志。
2. **始终写 B 正式表**;成功即返回成功。
3. 写 outboxagent 异步推 A。
4. agent 未装 / 停运:本地成功 + UI「待同步」**不失败保存**(定案 R1
5. deleteoutbox `delete` 或墓碑agent **按 outbox id 保序**;失败**禁止换新 UUID**。
**开通判定(写路径)**`mode=local_dbsync` **且** 白名单缓存命中该表;否则走 §5.1。
### 5.3 M1 无 agent 时「开通」含义(定案 O3
允许只落 B + 积压待同步;文案须写明 **「需 agent 才上云」**,不得暗示已上云。
---
## 6. 建表(定案 R2
| 模板 | 主键 |
|------|------|
| 默认 create双端 | **仍自增**(不动) |
| 同步表模板(双端新增) | `id TEXT` UUID + 建议 `updated_at` / 配合 outbox version |
禁止「全局默认改成 UUID」。
---
## 7. version / LWW定案 O5
| 项 | 定案 |
|----|------|
| 主序 | **outbox 单调 id / version** |
| 辅 | 客户端 `updated_at`(不对时**不挡保存** |
| 落败 | 写超管覆盖日志(两边 payload、胜出策略、tenant、表、pk、时间 |
| 弱提示「已自动合并」 | 默认**关**;仅开通用户可配置打开(定案 R4 |
---
## 8. 绑定与迁移
| 项 | 定案 |
|----|------|
| Binding API | **P1不挡 M2**(定案 O4M2 可用现网派生 `online_db_id` |
| 存量 `online_primary``local_dbsync` | **另附迁移手册**(定案 O7加白名单 → 装 agent → 观察 → 关 HTTP**禁止静默迁移**(定案 R5/H5 |
---
## 9. 本机 agent 最小集
- 可选装;未开通可不装。
-`_ajz_sync_outbox`(或等价),保序推 A复用/扩展智建 ingest
- 凭证:最小权限、按 `online_db_id` 隔离、可吊销outbox 敏感列脱敏或加密。
- 心跳 → UI only。
---
## 10. 智建平台交付
| 优先级 | 项 |
|--------|-----|
| **P0** | 入通道 UUID + 闭包校验ingest 认客户端 UUID 幂等;模式/开通说明(默认客户无感) |
| **P0** | M3超管 LWW 审计 API/存储与租户「数据同步」菜单分离TTL |
| **P1** | Binding 登记/查询;限流 reconcile设置页「同步修复」可选、不弹窗强打断 |
| **P1** | 表白名单 B→A / 双向开关(默认 B→A |
| **P2** | 超管按快照回滚单行 |
公司 top配通道、白名单、对账现权
平台超管:覆盖日志。
终端:无冲突台。
---
## 11. 分期与退出标准
| 阶段 | 目标 | 退出标准 |
|------|------|----------|
| **M0** | 同步表模板 + 入通道校验 + 模式三分文档 | 自增表无法误入通道 |
| **M1** | 宇恒旁路:`local_dbsync` + 白名单松离线写 B可无 agent | **未开通用户自动化回归 = 现网**;开通用户离线可见;文案不承诺上云 |
| **M2** | agent 最小 B→A + ingest 幂等 | 同 UUID 上云、不双行agent 停仍可本地保存 |
| **M3** | 超管 LWW 日志 + 限流 reconcile | top 403双入口覆盖可查 |
| **M4** | 扩白名单;**仅**评估新客或显式迁移客是否默认 `local_dbsync` | 旧客默认不变 |
**M4 前禁止全员切默认、禁止一刀切关 HTTP 双写。**
---
## 12. 验收清单(冻结)
### 不影响原有
- [ ] 未开 `local_dbsync`:现网回归通过(自增不传 id、原 `online_primary`)。
- [ ] 非白名单:不强制 UUID入通道拒绝。
- [ ]`local_dbsync` / 清白名单缓存:行为回原模式。
- [ ] 未开通:无强制 agent、无同步文案、保存不因同步失败。
- [ ] 同表未同时走双路径;关同步后无双写放大。
### 开通后(白名单)
- [ ] 离线写 B 可见;联网同 UUID 上 A幂等不双行。
- [ ] agent 停:可保存 + 待同步;恢复后追上。
- [ ] delete/重放保序;不换新 UUID。
- [ ] LWW 落败在超管日志;公司 top 不可见。
- [ ] 闭包不完整无法保存通道。
- [ ] M1 文案不暗示已上云。
---
## 13. 双方承诺(冻结)
### 宇恒
1. M0/M1模式三分 + 白名单进网关;旧分支零行为 diff附回归清单
2. agent 非安装强依赖;同步 UI 仅开通后出现。
3. M2 起对接 ingest不自造第二套写语义。
4. 合入门禁:未开通用户行为 diff ≠ 0 → 不予合入。
### 智建
1. M0入通道 UUID + 闭包校验。
2. M2 前ingest 幂等认客户端 UUID。
3. M3超管审计与租户控制台分离。
4. 发版说明写明:默认客户无感;`local_dbsync` 为增值开通。
5. 书面确认R1R4 全部按上文硬约束执行(已并入本文 §1/§5/§6/§7/§8
---
## 14. 对第二轮「待拍板」的最终答复
| 编号 | 决议 |
|------|------|
| R1 | **确认**:心跳只驱动 UI永不因 agent 拒绝保存 |
| R2 | **确认**:只加同步模板,默认建表仍自增 |
| R3 | **确认**:不一刀切关旧客 HTTP 双写 |
| R4 | **确认**:弱提示默认关,仅开通用户可配 |
| O1 | **通道为源**;宇恒缓存;无名单=未开通 |
| O2 | **公司 top 改白名单**;终端只读 |
| O3 | **M1 可无 agent**;文案「需 agent 才上云」 |
| O4 | **Binding 不挡 M2** |
| O5 | **outbox 单调 version 为主**;不对时不挡保存 |
| O6 | **同表互斥**(非同库整库互斥) |
| O7 | **迁移手册 + 禁止静默迁移** |
---
## 15. 最终口径
**旁路 opt-in 的松离线 + 本机 agent + dbsyncLWW 自动、超管可追溯;默认不影响宇恒现网。按 M0→M4 开工,本文冻结。**
---
## 宇恒追加-实施钉死项(摘录已并入 §16
见过程稿全文;下列 §16 为智建复核后的**采纳与补钉**(不回退 H1H6
---
## 16. 实施钉死项 · 智建复核结论2026-07-31
> 宇恒 N1N6 **总体可采纳**,与最终版无冲突。下列为「写死值 + 防坑补钉」。
### 16.1 对 N1N6 的定案
| # | 宇恒建议 | 智建结论 | 写死值 |
|---|----------|----------|--------|
| N1 | 无 id → 网关自动补 UUID | **采纳** | 自动生成并在响应带回 `id`(可用 `inserted_id` 作别名兼容);**同时打日志「auto_id」** 便于排障 |
| N2 | 开通判定 | **采纳并收紧** | **硬条件**`mode=local_dbsync` **且** 白名单缓存命中该表。租户「同步开通」标志若存在,作为**下发白名单的前提**(无标志则不下发名单),**不**再在写路径上增加第三道易漂移条件 |
| N3 | 按表分支 + 单测 | **采纳** | M1 必测:同库表白名单 / 非白名单分叉 |
| N4 | outbox未开通不建表 | **采纳并优选** | **优先与 permanent 同库** `_ajz_sync_outbox`(触发器简单);未开通库**不创建**该表;首次开通时迁移创建 |
| N5 | UUID 小写带连字符 | **采纳** | 双端入库前 `normalize`(小写 + 标准 8-4-4-4-12 |
| N6 | 旧字段保留、新字段仅开通附加 | **采纳** | 未开通响应形状与现网一致(可多不可少成功语义) |
### 16.2 宇恒反馈有没有问题?
**没有方向性问题。** N1N6 都是在落实「不影响原有」,应进 M0/M1。
需注意的不是反对项,而是**执行时的灰区**(见下补钉)。
### 16.3 仍不完善 · 建议补钉P0/P1
| # | 缺口 | 建议 | 优先级 |
|---|------|------|--------|
| P0-a | 白名单缓存过期 | 定 TTL如 515 min+ 开通/改名单后**主动推送或下次启动强制拉**;过期失败 → 未开通(已定),避免长期用过期「仍在名单」误走同步 | P0 |
| P0-b | 同表从 HTTP 双写迁入白名单 | 迁移手册写死:先停该表 `online_primary` 双写 → 再进白名单 / `local_dbsync`;禁止「边双写边进名单」 | P0并入 O7 手册) |
| P0-c | 未开通回归套件归属 | 宇恒维护基线用例;智建提供「通道拒绝非 UUID」用例M1 退出双方签字 | P0 |
| P1-a | M1 无 agent 积压 | outbox/待同步条数告警阈值(如 >N 提示装 agent**仍不挡保存** | P1 |
| P1-b | agent 凭证续期 | ingest Token 刷新与吊销流程写入 agent 最小集;过期只影响上云,不影响本地写 | P1 |
| P1-c | 多 `local_database_id` | 回归:同一 user 多库绑定不串 `online_db_id`;名单按库缓存 | P1 |
| P2-a | 自动补 UUID 掩盖调用方漏传 | 开通路径 metrics`auto_id_count`;超管/日志可查,不挡业务 | P2 |
### 16.4 不采纳 / 不回潮
- 不因「自动补 UUID」改为默认 `local_dbsync`
- 不恢复「写前 agent 心跳失败则拒绝保存」。
- 不把 LWW 审计开放给公司 top。
### 16.5 复核一句话
**宇恒钉死项应采纳;再补缓存 TTL、迁表白名单与双写互斥顺序、回归归属M0/M1 即可干净开工。**
---
## 17. 变更记录(落地)
| 日期 | 范围 | 说明 |
|------|------|------|
| 2026-07-31 | M0 智建 | 通道保存 / `PrepareChannel`UUID TEXT PK + FK 闭包校验(`platform/internal/dbsync/validate.go`);文档 `docs/同步表约定.md`SyncPage 表白名单提示 |
| 2026-07-31 | M2 智建 | `PushToRemote` + `/api/v1/agent/sync/.../push|whitelist`agent JWT 需「数据同步」);**宇恒本机 agent / 写网关由宇恒侧自行落地,智建不改对方仓库** |
| 2026-07-31 | 约定 | M1宇恒 `local_dbsync` 旁路)与 agent 消费 outbox以本文 + `docs/同步表约定.md` 为接口约定,不在智建仓改宇恒代码 |
| 2026-07-31 | M3 智建 | LWW 覆盖审计 `lww_overrides.json``GET /api/v1/platform/dbsync/lww-overrides`(仅超管);公司 conflicts API 403对账限流默认 `lww_source`SyncPage 去冲突台 |
| 2026-07-31 | M4 智建 | Binding API开通说明/O7 迁移手册;中间件文档 LWW 对齐SyncPage「同步修复」+ opt-in 文案apidef 补全。**全员默认 / 宇恒灰度由对方与运营决策,本仓不改宇恒** |
| 2026-07-31 | P2 智建 | 超管按 LWW 落败快照回滚单行:`POST /api/v1/platform/dbsync/lww-overrides/:id/rollback` + 平台工作台按钮 |
| 2026-07-31 | 收尾 智建 | OpenAPI 补 sync/agent/lwwyaml TTL/限流INTEGER PK 拒绝集成测;发版说明 |