# 同步系统状态定义 (Final Spec) 本文档定义了同步系统的状态机逻辑。逻辑流转遵循以下顺序: 1. **核心状态判定** (Core State) 2. **依赖检查与ID解析** (Dependency) 3. **自动绑定/发现** (Discovery) 4. **策略执行** (Action) --- ## 0. 绑定状态枚举 (BindingStatus) | 状态值 | 含义 | 来源 | 允许创建 | | :--- | :--- | :--- | :---: | | **UNCHECKED** | 未执行绑定检查 | 加载后的初始状态 | ❌ | | **NORMAL** | 绑定正常 | 核心状态判定 | ❌ | | **MISSING** | 无绑定记录(孤儿) | 核心状态判定 | ✅ | | **WARNING** | 业务键不完整 | 自动绑定阶段 | ❌ | | **ABNORMAL** | 数据损坏 | 核心状态判定 | ❌ | | **DEPENDENCY_ERROR** | 依赖未就绪 | 依赖检查或自动绑定 | ❌ | **关键原则**: - **UNCHECKED 阻止创建**:加载后节点状态为 UNCHECKED,只有执行了 `bind()` 后才会转为其他状态 - **只有 MISSING 状态才能触发 CREATE**:`create()` 方法只查找 `binding_status == MISSING` 的节点 - **如果 bind() 被跳过**(如 skip_sync=True),节点保持 UNCHECKED,不会被创建 --- ## 1. 核心状态判定表 (Core Binding Matrix) **说明**: * **Binding Record Pair**: 数据库中的绑定记录 (必须成对出现,已排除单边绑定)。 * **Data Valid**: 指数据是否可读、Schema校验是否通过。 * **Warning vs Abnormal**: * `ABNORMAL`: 自身数据损坏。 * `WARNING`: 自身健康,但因为对方损坏导致**连接不可用**。 | Bind Pair (Record) | Local Data (Valid?) | Remote Data (Valid?) | -> | **Local Status** | **Remote Status** | **说明** | | :---: | :---: | :---: | :--- | :--- | :--- | :--- | | **YES** | **YES** | **YES** | -> | **NORMAL** | **NORMAL** | **稳态**。链路完整,数据健康,可进行 Diff/Update。 | | **YES** | **YES** | **NO** | -> | **WARNING** | **ABNORMAL** | **远程损坏**。本地虽好,但链路已断 (Link Degraded)。禁止同步。 | | **YES** | **NO** | **YES** | -> | **ABNORMAL** | **WARNING** | **本地损坏**。远程虽好,但链路已断。禁止同步。 | | **YES** | **NO** | **NO** | -> | **ABNORMAL** | **ABNORMAL** | **双向损坏**。 | | **NO** | **YES** | N/A | -> | **MISSING** | N/A | **本地孤儿**。进入“依赖与发现”流程。 | | **NO** | N/A | **YES** | -> | N/A | **MISSING** | **远程孤儿**。进入“依赖与发现”流程。 | --- ## 2. 依赖与业务键解析 (Dependency & ID Resolution) **适用范围**: 所有涉及外键依赖 (Foreign Key) 的节点。 **前置条件**: 核心状态 = `MISSING` (准备进行自动绑定或新建)。 在进行 `Auto-Bind` 或 `Create` 之前,必须先尝试构建**远程业务键**。 例如:本地 `Contract(proj_id="P1", code="C01")` 想找远程对象,必须先查询 `P1` 对应的远程 ID `RP_99`,转换成 `(RP_99, "C01")` 才能去远程搜索。 | 依赖项 (Dependency) | 依赖项绑定状态 | -> | **能否构建业务键** | **节点状态** | **说明** | | :--- | :--- | :--- | :--- | :--- | :--- | | **无依赖** | N/A | -> | ✅ Yes | **MISSING** | 根节点 (如 Project),直接进入下一阶段。 | | **有依赖** | **NORMAL** 且 `data_id` 非空(`status` 任意) | -> | ✅ Yes | **MISSING** | 依赖项已获得远程ID,可进入下一阶段。 | | **有依赖** | **NORMAL** 且 `data_id` 为空("",含 `status = FAILED`) | -> | ❌ No | **DEPENDENCY_ERROR** | 依赖项未回填ID,阻断绑定与同步。 | | **有依赖** | **MISSING / ABNORMAL** | -> | ❌ No | **DEPENDENCY_ERROR** | **依赖阻断**。父节点未同步或损坏,无法计算远程键,无法绑定,无法创建。 | | **有依赖** | **WARNING** | -> | ❌ No | **DEPENDENCY_ERROR** | 父节点状态不确定(如连接降级),禁止基于此建立新关系。 | > **处理策略**: 处于 `DEPENDENCY_ERROR` 状态的节点,其动作强制为 **SKIP** (或 PENDING)。必须先修复父节点。 > > **错误信息格式**: `DEPENDENCY_ERROR` 节点的 `error` 字段会包含详细的依赖失败信息: > - `dep_err: not_found: field_name(node_type)=value` - 依赖节点未找到 > - `dep_err: field_name: status=FAILED` - 依赖节点状态为 FAILED > - 多个依赖失败时用分号分隔:`dep_err: not_found: project_id(project)=P1; contract_id: status=FAILED` --- ## 3. 自动绑定状态处理 (Auto-Binding State Handling) 此逻辑遵循“去噪后 1:1”原则:只有在排除掉所有健康同步对后,若剩余节点形成绝对的 1:1 关系,才允许自动绑定。 **配置说明与触发机制**: - **初始化场景**: 通常在系统第一次初始化或首次接入新实体时,配置 `auto_bind = True` 来对齐历史数据。 - **后续同步**: 系统在日常同步中不建议开启全局自动绑定。自动绑定仅作为“发现”手段,一旦建立过关系的节点,其后续绑定操作通常仅在“自动创建”逻辑中伴随产生(详见第 4 节孤儿处理)。 - **执行前提**: 只有当节点状态为 `MISSING` 且依赖解析成功(`DEPENDENCY_ERROR = False`)时,才会尝试以下探测逻辑。 - **业务键完整性**: 若业务键字段缺失(如 `serial_number = null`),直接标记 `binding_status = WARNING`,并在 `node.error` 记录原因(`Auto-bind skipped: missing fields ...`),不参与自动绑定与孤儿创建判定。- **ID字段解析失败**: 若业务键包含ID字段(如 `supplier_id`)且依赖节点未绑定,ID解析失败,标记 `binding_status = DEPENDENCY_ERROR`,阻止孤儿创建。 **核心算法**: 1. **全集搜索**: 寻找本地/远端所有共享同一个业务键(Business Key)的记录。 2. **排除稳态对**: 识别并移除**成对且互绑**的 `NORMAL` 记录。 - 只有当本地 A 与远端 B 互为绑定对象,且状态皆为 `NORMAL` 时,才将这一对记录从待判定集合中剔除。 - 若出现单边 `NORMAL` 记录(即其配对项不在本次搜索范围内或状态不属于另一端的 `NORMAL`),则不予剔除,视为环境污染项。 3. **判定剩余**: 检查排除上述稳态对后的剩余集合(包含 `MISSING`, `ABNORMAL`, `WARNING` 及单边已占用的 `NORMAL` 记录)。 > **术语**: > - `L_remain` = 本地记录中,排除“成对稳态”后剩余的数量。 > - `R_remain` = 远端记录中,排除“成对稳态”后剩余的数量。 | `L_remain` | `R_remain` | -> | **动作** | **说明** | | :---: | :---: | :--- | :--- | :--- | | **1** | **1** | -> | **BIND (Link)** | **1对1 自动对齐**:仅当两端剩余记录皆为 `MISSING` 且绝对 1:1 时,执行自动绑定。 | | **1** | **1** | -> | **SKIP** | **带障不处理**:若剩余记录中包含 `ABNORMAL` 等异常状态,说明存在历史冲突,不执行自动动作。 | | **N** | **0** | -> | **CREATE** | **可判定缺失**:业务键完整且唯一,本地存在而远端不存在(N:0)时,才允许进入孤儿创建策略。 | | 其他 | 其他 | -> | **SKIP** | **非 1:1 不处理**:存在多对多、多对一或无候选情况,系统保持原状。 | **设计优势**: 1. **对故障极端敏感**:环境中存在任何 `ABNORMAL` 记录都会阻断自动绑定,强制人工介入处理异常。 2. **状态纯净**:失败时不产生任何中间态(如已废弃的 `AMBIGUOUS`),保持语义清晰。 3. **静默自愈**: 当外界清理了环境且满足 1:1 条件后,系统下次同步会自动完成绑定。 ### 3.1 空数据处理策略 **原则**:**空数据不推送**。 **空数据定义**: - 初始化时创建但未编辑的业务记录(如空合同、空施工任务) - 业务键字段为空或关键字段缺失的记录 **处理方式**: - 在自动绑定阶段,若业务键字段缺失,标记 `binding_status = WARNING`,记录原因 - 不参与自动绑定与孤儿创建判定 - 不推送到远程系统 **设计理由**: 1. **远程系统简化**:远程只需处理完整数据,无需兼容空数据校验逻辑 2. **避免垃圾数据**:空数据通常会被本地删除,若已推送则清理复杂 3. **数据质量保障**:远程系统只能看到本地已正确编辑保存的业务 **配置方式**(Strategy 层): ```python # 在 Strategy 配置中指定必填字段 default_config = StrategyConfig( auto_bind_fields=["contract_id", "task_type"], # 业务键字段 # 如果这些字段为空,节点将被标记为 WARNING 而非 MISSING ) ``` --- ## 4. 动作策略表 (Action Policy Matrix) 此表定义了在特定状态下,`execute` 阶段允许产生的 **意图行动 (`SyncAction`)**。 #### A. 稳态更新 (NORMAL) 适用于已有绑定关系且链路健康的节点。 | 状态 | 动作条件 | 执行动作 (`SyncAction`) | | :--- | :--- | :--- | | **NORMAL** | `compute_diff` 判定存在差异 | **UPDATE** | | **NORMAL** | `compute_diff` 判定无差异 | **NONE** | #### B. 异常处理 (阻断态) 处于这些状态的节点,引擎会自动中止后续的读写操作。 | 状态 | 说明 | 动作 (`SyncAction`) | | :--- | :--- | :--- | | **DEPENDENCY_ERROR** | 父节点缺失/异常同步导致阻断 | **NONE** | | **ABNORMAL / WARNING** | 数据损坏或链路降级,需人工修复 | **NONE** | #### C. 孤儿处理 (Create Only) 适用于最终探测状态为 **MISSING** 且无法/不满足自动绑定的节点。 **本地孤儿 (Local Orphan)**: 本地有,远程无。 | 策略配置 (`local_orphan_action`) | 对应的同步模式 | 动作 (`SyncAction`) | | :--- | :--- | :--- | | **"create_remote"** | Push (本地推远端) | **CREATE** | | **"delete_local"** | Pull (远端拉本地) | **报错 (未实现)** | | **"none"** | Any | **NONE** | **远程孤儿 (Remote Orphan)**: 远程有,本地无。 | 策略配置 (`remote_orphan_action`) | 对应的同步模式 | 动作 (`SyncAction`) | | :--- | :--- | :--- | | **"create_local"** | Pull (远端拉本地) | **CREATE** | | **"delete_remote"** | Push (本地推远端) | **报错 (未实现)** | | **"none"** | Any | **NONE** | --- ## 5. 执行同步状态转换 (Physical Sync State Transitions) 在策略执行完成、设置好 `action` 后,DataSource 层负责执行实际的同步操作并更新 `status` 字段。 ### A. 执行状态枚举 (SyncStatus) | 状态值 | 含义 | 适用场景 | | :--- | :--- | :--- | | **PENDING** | 待执行 | 初始状态、无动作的节点 | | **IN_PROGRESS** | 执行中 | 异步接口已提交,等待结果回调 | | **SUCCESS** | 执行成功 | 同步操作完成且成功 | | **FAILED** | 执行失败 | 同步操作失败,`error` 字段记录原因 | | **SKIPPED** | 已跳过 | 节点被主动跳过(如依赖未满足) | | **PRECONDITION_FAILED** | 前置条件失败 | create/update 内部验证发现问题 | ### B. 执行转换矩阵 #### 同步接口(一步式) 适用于同步 API(调用后立即返回成功/失败)。 | 输入: action | API 调用结果 | → | 输出: status | 副作用 | | :--- | :--- | :--- | :--- | :--- | | **CREATE** | 成功返回 (200/201) | → | **SUCCESS** | `data_id` 设置为远程返回的ID | | **CREATE** | 失败返回 (4xx/5xx) | → | **FAILED** | `error` 记录错误信息 | | **UPDATE** | 成功返回 (200) | → | **SUCCESS** | - | | **UPDATE** | 失败返回 (4xx/5xx) | → | **FAILED** | `error` 记录错误信息 | | **DELETE** | 成功返回 (200/204) | → | **SUCCESS** | 节点可能被移除 | | **DELETE** | 失败返回 (4xx/5xx) | → | **FAILED** | `error` 记录错误信息 | | **NONE** | - | → | **PENDING** | 不执行,保持原状 | #### 异步接口(两步式) 适用于异步 API(提交后需要轮询或回调获取结果)。 | 输入: action | API 提交结果 | → | 输出: status | 后续操作 | | :--- | :--- | :--- | :--- | :--- | | **CREATE** | 提交成功 (202 Accepted) | → | **IN_PROGRESS** | 记录 task_id,等待轮询 | | **CREATE** | 提交失败 (4xx/5xx) | → | **FAILED** | `error` 记录错误 | | **IN_PROGRESS** | 轮询返回成功 | → | **SUCCESS** | `data_id` 设置为远程ID | | **IN_PROGRESS** | 轮询返回失败 | → | **FAILED** | `error` 记录错误 | | **IN_PROGRESS** | 超时未完成 | → | **FAILED** | `error` 记录 "Timeout" | ### B+. Handler 降级(action 取消) > **代码实现**:各 `domain/*/api_handler.py` 中的 `update()` 方法。 当 Handler 在构建 API 请求数据时,发现实际无需更新的字段(如 schema 校验后所有字段与远程一致), Handler 会主动将节点的 `action` 从 `UPDATE` 降级为 `NONE`: ``` action=UPDATE → Handler 校验 → 无实际变更字段 → action=NONE(不提交 API) ``` 此机制确保 DataSource 不会向远程发送无意义的空更新请求。 ### C. 关键副作用 | 动作 | 成功时的副作用 | 说明 | | :--- | :--- | :--- | | **CREATE** | 临时绑定已建立 | 绑定关系可在提交前创建,用于推送与追踪;是否成功不影响该绑定存在 | | **CREATE** | 设置 `data_id` | 成功后从API响应中提取远程分配的ID,更新到节点 | | **CREATE** | 回写 `data` 主键 | DataSource 将远程返回的ID写回 `SyncNode.data`(如 `id`/`*_id` 字段) | | **CREATE** | 更新绑定记录 | 若使用临时ID,成功后替换为真实的远程ID | | **UPDATE** | 更新 `origin_data` | 将当前 `data` 保存为新的 `origin_data` | | **DELETE** | 移除绑定记录 | 删除成功后,清除对应的绑定关系 | ### D. 错误处理 | 错误类型 | 记录方式 | 重试策略 | | :--- | :--- | :--- | | **网络错误** | `error = "Network timeout"` | 可重试 | | **权限错误** | `error = "403 Forbidden"` | 不可重试,需人工介入 | | **数据校验错误** | `error = "Validation failed: ..."` | 不可重试,需修复数据 | | **依赖缺失** | `status = PRECONDITION_FAILED` | 不执行,等待依赖修复 | --- ## 6. 同步检查规范 (Sync Check Specification) 在执行同步前后,需要进行状态检查以确保操作的合法性和完整性。 ### A. 内联前置检查(create/update 阶段) > **实现说明**:原 `_pre_commit_check` 方法已移除(保留空壳兼容接口)。 > 所有有效检查已内联到 `strategy_ops/create_ops.py::add_create_action()` 和 `strategy_ops/update_ops.py::add_update_action()` 中。 **create() 阶段内联检查**: | 检查类别 | 检查条件 | 不通过时 | 说明 | | :--- | :--- | :--- | :--- | | **数据完整性** | `src_node.get_data()` 不为空 | 跳过该节点,记录日志 | MISSING 但数据为空 | | **依赖 ID 解析** | `id_resolver.resolve_and_validate()` 成功 | `binding_status → DEPENDENCY_ERROR` | 依赖字段无法解析到目标系统 ID | **update() 阶段内联检查**: | 检查类别 | 检查条件 | 不通过时 | 说明 | | :--- | :--- | :--- | :--- | | **FAILED 保护** | `src_node.status != FAILED` | 跳过该节点 | 不覆盖上次失败的状态和错误 | | **数据完整性** | `src_node.get_data()` 不为空 | 跳过该节点 | NORMAL 但数据为空 | | **目标节点存在** | 通过绑定找到目标节点 | 跳过该节点 | 绑定记录指向的节点不在集合中 | | **data_id 完整性** | `target_node.data_id` 非空 | `status → PRECONDITION_FAILED` | UPDATE 必须有业务主键 | | **依赖 ID 解析** | `id_resolver.resolve_and_validate()` 成功 | `status → PRECONDITION_FAILED` | 依赖字段无法解析到目标系统 ID | | **差异检测** | `_needs_update()` 返回 True | 跳过该节点 | 无差异则不更新 | > **注意**:以下检查项在当前实现中 **未覆盖**【未实现】: > - 状态合法性:`binding_status` 必须是已知状态 > - 动作合法性:`CREATE/UPDATE` 只能用于 `NORMAL` 节点 > - 绑定一致性:`NORMAL` 节点必须有绑定记录 ### B. 同步后检查 (Post-Sync Check) **目标**: 验证同步结果的完整性,发现残留问题。 > **实现说明**:当前 `_stage4_post_sync_check` 只打印摘要表格,不做异常检测。以下检查项均为【未实现】。 #### 检查项清单【未实现】 | 检查类别 | 检查条件 | 异常情况 | 严重程度 | | :--- | :--- | :--- | :--- | | **CREATE 完成性** | CREATE 节点应该有 `data_id` | `action = CREATE, status = SUCCESS` 但 `data_id = ""` | 🔴 ERROR | | **CREATE 绑定** | CREATE 节点应该有绑定记录 | CREATE 成功但无绑定 | 🔴 ERROR | | **UPDATE 完成性** | UPDATE 节点应该成功 | `action = UPDATE` 但 `status = FAILED` | ⚠️ WARNING | | **绑定保持** | UPDATE 节点的绑定应该保持 | UPDATE 后绑定记录丢失 | 🔴 ERROR | | **残留异常** | 不应有 ABNORMAL 节点 | 同步后仍有 `binding_status = ABNORMAL` | ⚠️ WARNING | | **残留依赖错误** | 不应有 DEPENDENCY_ERROR | 同步后仍有依赖阻断 | ⚠️ WARNING | #### 统计指标 | 指标 | 计算方式 | 用途 | | :--- | :--- | :--- | | **成功率** | `SUCCESS / (CREATE + UPDATE)` | 评估同步质量 | | **异常率** | `(ABNORMAL + WARNING) / total` | 发现数据质量问题 | | **阻断率** | `DEPENDENCY_ERROR / total` | 发现依赖关系问题 | #### 决策规则 | 检查结果 | ERROR 数量 | 决策 | | :--- | :--- | :--- | | ✅ 通过 | 0 | **继续持久化**,正常结束 | | ⚠️ 有警告 | 0, WARNING ≥1 | **继续持久化**,打印警告报告 | | ❌ 有错误 | ≥1 | **继续持久化**(已同步无法回滚),打印错误报告 | > **注意**: Post-Sync Check 即使有 ERROR 也不会终止流程,因为同步已经执行,无法回滚。检查目的是发现问题并记录,供后续修复。 --- ## 附录: 状态字段说明 ### SyncNode 核心字段 | 字段 | 类型 | 设置阶段 | 含义 | | :--- | :--- | :--- | :--- | | `binding_status` | BindingStatus | Bind 阶段 | 绑定健康状态 | | `action` | SyncAction | Create/Update 阶段 | 待执行动作 | | `status` | SyncStatus | Physical Sync 阶段 | 执行结果状态 | | `data_id` | str | 数据加载 / CREATE 后 | 业务主键ID(CREATE 前为 "") | | `depend_ids` | List[str] | 数据加载 | 依赖节点的 node_id 列表 | | `error` | str \| None | Physical Sync 阶段 | 错误信息(失败时) | ### 状态流转时序 ``` [数据加载] [Bind] [Create/Update] [Physical Sync] binding_status: UNCHECKED → NORMAL/MISSING/... → (保持) → (保持) action: NONE → NONE → CREATE/UPDATE → (保持) status: PENDING → PENDING → PENDING → SUCCESS/FAILED data_id: 从源加载 → (保持) → 可能为 "" → CREATE 后设置 ```