# 松离线 + dbsync 方案 · 最终版（冻结）

> **效力**：本文为双方协商后的**唯一开工依据**。  
> 原方案稿 / 审评追加 / 决议草稿见同目录 `松离线-dbsync方案.md`（过程稿，不再改口径）。  
> 冻结日期：2026-07-31

---

## 0. 一句话

**技术**：松离线写本地 B（最终 UUID）+ 本机 agent 经 dbsync 幂等对齐线上 A；冲突自动 LWW，落败入**平台超级管理员**覆盖日志。  
**产品**：宇恒**默认零影响**；仅显式 `local_dbsync` + **表白名单** opt-in；M4 前不对全员切默认。

---

## 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
[开通 + 白名单表]
  业务写 ──► B（UUID）──► outbox ──► 本机 agent ──► A（ingest/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. 写 outbox；agent 异步推 A。  
4. agent 未装 / 停运：本地成功 + UI「待同步」；**不失败保存**（定案 R1）。  
5. delete：outbox `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**（定案 O4）；M2 可用现网派生 `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. 书面确认：R1–R4 全部按上文硬约束执行（已并入本文 §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 + dbsync；LWW 自动、超管可追溯；默认不影响宇恒现网。按 M0→M4 开工，本文冻结。**

---

## 宇恒追加-实施钉死项（摘录已并入 §16）

见过程稿全文；下列 §16 为智建复核后的**采纳与补钉**（不回退 H1–H6）。

---

## 16. 实施钉死项 · 智建复核结论（2026-07-31）

> 宇恒 N1–N6 **总体可采纳**，与最终版无冲突。下列为「写死值 + 防坑补钉」。

### 16.1 对 N1–N6 的定案

| # | 宇恒建议 | 智建结论 | 写死值 |
|---|----------|----------|--------|
| 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 宇恒反馈有没有问题？

**没有方向性问题。** N1–N6 都是在落实「不影响原有」，应进 M0/M1。  
需注意的不是反对项，而是**执行时的灰区**（见下补钉）。

### 16.3 仍不完善 · 建议补钉（P0/P1）

| # | 缺口 | 建议 | 优先级 |
|---|------|------|--------|
| P0-a | 白名单缓存过期 | 定 TTL（如 5–15 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 补全。**全员默认 / 宇恒灰度由对方与运营决策，本仓不改宇恒** |
