first commit

This commit is contained in:
strepsiades
2026-03-09 16:31:42 +08:00
commit 4a9c444b10
486 changed files with 52479 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# Root docs migration note
本目录为从项目根目录 `docs/` 迁移过来的文档快照(全量复制,不跳过)。
目的:
- 保留原始设计背景与历史规范,便于在 `sync_state_machine` 目录内就地查阅;
- 与当前 `sync_state_machine/docs/` 的“现行实现文档”并存。
使用建议:
-`sync_state_machine/docs/` 下现行文档(如 `state_machine.md``sync_flow.md``architecture.md`)作为运行时真义;
- 本目录文档作为历史参考来源,遇到冲突时以现行实现与状态机配置为准:
- `sync_state_machine/config/node_state_machine.yaml`
- `sync_state_machine/sync_system/strategy.py`
- `sync_state_machine/engine/semantics.py`
@@ -0,0 +1,706 @@
# API DataSource 架构设计
## 架构总览
```
┌─────────────────────────────────────────────────────────────┐
│ Pipeline │
│ (同步流程编排) │
└───────────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ ApiDataSource │
│ (数据源协调层) │
│ │
│ - 持有 ApiClient 实例 │
│ - 注册和管理 Handler │
│ - 实现 load_all / execute_batch │
│ - 注入 api_client 到 Handler │
└───────────────────────────┬─────────────────────────────────┘
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Handler │ │ Handler │ │ Handler │
│ Project │ │ Contract │ │ Material │
│ │ │ │ │ │
│ 业务逻辑层 │ │ 业务逻辑层 │ │ 业务逻辑层 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└───────────────────┼───────────────────┘
│ 使用 self.api_client
┌──────────────┐
│ ApiClient │
│ (HTTP 通信层) │
│ │
│ - 签名生成 │
│ - HTTP 请求 │
│ - Push Log │
└──────────────┘
```
**调用流程**
1. Pipeline 创建 ApiDataSource,传入 base_url, uid, secret
2. ApiDataSource 内部创建 ApiClient 实例
3. ApiDataSource 注册各业务 Handler,并注入 api_client
4. Handler 使用 `self.api_client.get/post/put()` 发送请求
5. ApiClient 自动处理签名、push_log 查询、错误处理
**优势**
-**职责清晰**ApiClient(通信)、DataSource(协调)、Handler(业务)
-**避免循环依赖**Handler → ApiClient(单向依赖)
-**易于测试**ApiClient 可 mockHandler 独立测试
-**易于扩展**:新业务只需实现 Handler
## 1. 问题分析:旧方案的复杂性
旧方案 `sync_system/datasource/api` 存在的问题:
1. **过度抽象**:引入了 `ApiDataSource``Repository``ResponseHandle``Cache` 多层封装,每个业务需要独立 Repository 类,Protocol 定义繁琐
2. **状态管理混乱**:缓存策略不清晰,ResponseHandle 封装异步结果但使用困难,缺少统一的 push_log 查询机制
3. **依赖关系困难**Repository 之间互相传递引用,难以从 collection 获取已有数据
4. **API 路由分散**:每个 Repository 手动实现多个 update 方法,缺少自动路由机制
## 2. 新方案设计原则
- **简单直接**Handler 直接调用 HTTP API,不过度封装
- **状态透明**DataSource 统一管理 session、签名、push_log 查询
- **依赖清晰**:通过 collection 获取依赖数据,使用 context 存储额外信息
- **智能路由**:根据数据字段自动选择 API endpoint
## 3. 核心组件职责
### 3.1 ApiClient - HTTP 通信层
**职责**
- 封装所有 HTTP 通信逻辑
- 管理 httpx.AsyncClient,维护连接池和 session
- 生成请求签名(timestamp, nonce, sign
- 统一请求入口,自动注入 uid
- 批量 Push Log 查询
**核心方法**
```python
class ApiClient:
def __init__(self, base_url: str, uid: str, secret: str):
self._client = httpx.AsyncClient()
self._base_url = base_url
self._uid = uid
self._secret = secret
async def request(self, method: str, url: str, **kwargs) -> httpx.Response
"""统一请求入口,自动签名"""
async def get(self, url: str, params: dict = None) -> httpx.Response
"""GET 请求"""
async def post(self, url: str, data: dict) -> httpx.Response
"""POST 请求,data 中已包含 push_id/biz_id(由 Handler 生成)"""
async def put(self, url: str, data: dict) -> httpx.Response
"""PUT 请求,data 中已包含 push_id/biz_id"""
async def delete(self, url: str, data: dict) -> httpx.Response
"""DELETE 请求,data 中已包含 push_id/biz_id"""
async def get_push_logs(self, push_ids: List[str]) -> Dict[str, dict]
"""批量查询 push_log 状态,返回 {push_id: push_log_data}
注意:单个查询也传单元素列表
当前实现:用 for 循环模拟,后续等后端实现批量接口
"""
def _generate_signature(self, params: dict) -> dict
"""生成签名参数(timestamp, nonce, sign"""
```
### 3.2 ApiDataSource - 数据源协调层
**职责**
- 持有 ApiClient 实例,提供给 Handler 使用
- 实现 DataSource 接口(load_all, sync_all
- 协调多个 Handler 的执行顺序(通过 sync_all
- Handler 内部负责:生成 push_id/biz_id、调用 poll 轮询、提取 created_id
- 统一异常处理和日志记录
**核心属性和方法**
```python
class ApiDataSource(DataSource):
def __init__(self, base_url: str, uid: str, secret: str):
self.client = ApiClient(base_url, uid, secret)
self._handlers: Dict[str, BaseApiHandler] = {}
def register_handler(self, node_type: str, handler: BaseApiHandler):
"""注册业务 Handler"""
handler.api_client = self.client # 注入 client
self._handlers[node_type] = handler
async def load_all(self, collection: DataCollection, order: List[str]) -> None:
"""调用基类的 load_all,按顺序加载数据"""
async def sync_all(self, collection: DataCollection, order: List[str]) -> None:
"""调用基类的 sync_all,按顺序执行同步"""
```
### 3.3 BaseApiHandler - 业务逻辑基类
**职责**
- 实现 Handler 接口(load, create, update, delete, poll
- 使用 self.api_client 发送 HTTP 请求
- 生成 push_id 和 biz_id(业务标识)
- 从 collection 获取依赖节点数据
- 管理节点的 context(存储 project_id, contract_id 等)
- 实现 poll 方法,调用 api_client.get_push_logs 批量查询
- 提取 created_id(因为不同业务的 id 字段名和位置可能不同)
- 返回 TaskResultSUCCESS/FAILED/IN_PROGRESS
**核心方法**
```python
class BaseApiHandler(NodeHandler):
def __init__(self):
self.api_client: ApiClient = None # 由 DataSource 注入
self.collection: DataCollection = None # 由 DataSource 注入
async def load(self, collection: DataCollection) -> List[dict]
async def create(self, node: SyncNode) -> TaskResult
async def update(self, node: SyncNode) -> TaskResult
async def delete(self, node: SyncNode) -> TaskResult
async def poll(self, node: SyncNode, task_id: str) -> TaskResult
def extract_created_id(self, push_log: dict) -> str
"""从 push_log 提取 created_id,子类可重写"""
# 重试逻辑(可选,子类可重写)
async def _execute_with_retry(self, operation, max_retries: int = 3) -> Any
"""通用重试逻辑,子类可自定义重试策略"""
```
**重试策略说明**
- **重试在 Handler 内部**create/update/delete 内部可调用 _execute_with_retry
- **返回给 DataSource**:最终只返回 SUCCESS/FAILED/IN_PROGRESS
- **灵活控制**:不同业务可重写 _execute_with_retry 实现自定义策略
- **默认策略**:网络错误/5xx 重试 3 次,4xx 不重试
**未实现 API 的处理**
- 部分业务可能没有实现 create 或 delete API
- 此时 Handler 应在对应方法中直接返回 FAILED 状态,并在 error 中说明原因
- 示例:`return TaskResult(status=TaskStatus.FAILED, error="Create API not implemented for this business")`
### 3.4 具体业务 Handler
**职责**
- 实现具体业务的 load 逻辑(调用 self.api_client.get()
- 创建节点时设置 context
- 重写 API endpoint 选择逻辑(可选)
- 定制 created_id 提取逻辑(可选)
每个业务 Handler 位于 `sync_system_new/domain/{业务}/api_handler.py`
## 4. HTTP 请求状态管理
### 4.0 状态系统概述
**通用状态枚举**(适用于所有 DataSource):
```python
class TaskStatus(Enum):
"""Handler 返回的任务执行状态"""
SUCCESS = "success" # 同步完成,立即成功
FAILED = "failed" # 同步失败
IN_PROGRESS = "in_progress" # 异步任务进行中
class SyncStatus(Enum):
"""SyncNode 的同步执行状态"""
PENDING = "PENDING"
IN_PROGRESS = "IN_PROGRESS"
SUCCESS = "SUCCESS"
FAILED = "FAILED"
SKIPPED = "SKIPPED"
PRECONDITION_FAILED = "PRECONDITION_FAILED"
```
**状态映射规则**
- TaskStatus 是 Handler 执行操作后的返回值
- SyncStatus 是 SyncNode 的最终状态,由 DataSource 根据 TaskStatus 更新
- 普通 DataSourceFileDataSource)和 API DataSource 使用相同的状态枚举
- 区别在于:**API DataSource 的网络操作如何转化为 TaskStatus**
**重要说明:Load 阶段 vs 执行阶段的状态**
- **Load 阶段**Handler.load()):从 API 拉取数据创建 SyncNode
- 此时节点状态为 `PENDING`(等待后续 bind/create/update
- GET 请求失败不影响节点创建,只是该节点不存在于 collection
- **执行阶段**Handler.create/update/delete):执行同步操作
- DataSource 在调用前设置 `node.status = IN_PROGRESS`
- Handler 返回 TaskResultDataSource 据此更新 node.status
- POST/PUT/DELETE 成功后才设置为 SUCCESS
**核心映射逻辑**(在 DataSource 中):
```python
# 执行前:DataSource 设置状态
node.status = SyncStatus.IN_PROGRESS
# 调用 HandlerHandler 内部可能重试多次)
result = await handler.create(node)
# 执行后:DataSource 根据最终结果更新状态
if result.status == TaskStatus.SUCCESS:
node.status = SyncStatus.SUCCESS
node.data_id = result.data_id
elif result.status == TaskStatus.FAILED:
node.status = SyncStatus.FAILED
node.error = result.error
elif result.status == TaskStatus.IN_PROGRESS:
# 记录 task_id 等待轮询
async_tasks[node.node_id] = result.task_id
```
**重要**:Handler 负责重试,最终只返回确定的结果(SUCCESS/FAILED/IN_PROGRESS
### 4.1 请求类型与状态流转
**API DataSource 的特殊性**
- **Load 阶段**GET 请求拉取数据,创建 PENDING 节点(失败则不创建节点)
- **执行阶段**POST/PUT/DELETE 先设置 IN_PROGRESS,通过 poll() 轮询变为 SUCCESS/FAILED
#### Load 阶段 - 数据拉取
**目的**:从 API 获取数据,创建 SyncNode 放入 collection
| 阶段 | HTTP 操作 | 成功行为 | 失败行为 | 节点状态 |
|------|----------|---------|---------|---------|
| 1. 调用 load() | GET /api/list | 返回数据列表 | 返回空列表或抛异常 | - |
| 2. 创建节点 | - | 为每条数据创建 SyncNode | 不创建节点 | PENDING |
**代码示例**
```python
async def load(self, collection: DataCollection) -> List[dict]:
"""加载数据 - GET 阶段不涉及状态转换"""
try:
response = await self.api_client.get("project/list")
if response.status_code == 200:
data_list = response.json().get("list", [])
# 返回数据,由 DataSource 创建 PENDING 节点
return data_list
else:
# 失败:返回空列表,不创建节点
logger.error(f"Load failed: {response.status_code}")
return []
except Exception as e:
logger.error(f"Load exception: {e}")
return [] # 或者抛出异常,由调用方处理
```
#### Mutation 请求(POST/PUT/DELETE - 执行阶段
**状态变化时机**
1. **执行前**DataSource 设置 `node.status = IN_PROGRESS`
2. **Handler 执行**:内部可能重试,最终返回 SUCCESS/FAILED/IN_PROGRESS
3. **轮询完成**poll() 返回 SUCCESS/FAILED
| 阶段 | Push Log | TaskStatus | SyncStatus | 说明 |
|------|---------|-----------|-----------|------|
| 执行前 | - | - | PENDING → IN_PROGRESS | DataSource 设置 |
| 提交 | pending | IN_PROGRESS | IN_PROGRESS | 任务已提交 |
| 轮询 | processing | IN_PROGRESS | IN_PROGRESS | 后端处理中 |
| 成功 | success | SUCCESS | SUCCESS | 同步完成 |
| 失败 | failed | FAILED | FAILED | 同步失败 |
**示例**
```python
async def create(self, node: SyncNode) -> TaskResult:
"""Handler 内部可重试,最终返回确定结果"""
response = await self.api_client.post(url, node.data)
if response.status_code == 202:
return TaskResult.in_progress(task_id=response.json()["push_id"])
else:
return TaskResult.failed(error=f"HTTP {response.status_code}")
async def poll(self, node: SyncNode, task_id: str) -> TaskResult:
push_logs = await self.api_client.get_push_logs([task_id])
status = push_logs.get(task_id, {}).get("status")
if status == "success":
return TaskResult.success(data_id=self.extract_created_id(push_logs[task_id]))
elif status == "failed":
return TaskResult.failed(error="Backend processing failed")
else:
return TaskResult.in_progress(task_id=task_id)
```
### 4.2 状态转换完整流程
```
Load 阶段:
Handler.load() → 返回数据列表 → DataSource 创建 PENDING 节点
执行阶段(同步操作):
DataSource: node.status = IN_PROGRESS
→ Handler.create() → TaskResult.success/failed()
→ DataSource: node.status = SUCCESS/FAILED
执行阶段(异步操作):
DataSource: node.status = IN_PROGRESS
→ Handler.create() → TaskResult.in_progress(push_id)
→ DataSource 轮询: Handler.poll(push_id)
→ TaskResult.success/failed()
→ DataSource: node.status = SUCCESS/FAILED
```
**关键点**
- Handler 负责重试,DataSource 只处理最终结果
- 不同业务可在 BaseApiHandler._execute_with_retry 中自定义重试策略
- FileDataSource 和 ApiDataSource 使用相同的状态枚举和转换逻辑
### 4.3 Push Log 状态机
```
pending → processing → success/failed
```
- 轮询间隔:0.5秒
- 超时:30秒
- 成功:提取 created_id
- 失败:获取 error_message
### 4.4 状态总结表
**Load 阶段(数据拉取)**
| HTTP 操作 | 响应码 | 行为 | 创建节点 | 节点状态 | 说明 |
|---------|-------|-----|---------|---------|------|
| GET /list | 200 | 返回数据列表 | ✅ 是 | PENDING | 等待后续 bind/create/update |
| GET /list | 4xx/5xx | 返回空列表 | ❌ 否 | - | 不创建节点 |
**执行阶段(POST/PUT/DELETE**
| HTTP 操作 | 响应码 | Push Log | TaskStatus | SyncStatus | 说明 |
|---------|-------|---------|-----------|-----------|------|
| POST/PUT | 202 | pending | IN_PROGRESS | IN_PROGRESS | 异步开始 |
| - | - | processing | IN_PROGRESS | IN_PROGRESS | 异步处理中 |
| - | - | success | SUCCESS | SUCCESS | 异步成功 |
| - | - | failed | FAILED | FAILED | 异步失败 |
| - | - | timeout | FAILED | FAILED | 轮询超时 |
## 5. Context 机制
### 5.1 Context 的作用
Context 是 SyncNode 的字典字段,用于存储业务数据之外的上下文信息,例如:
- `project_id`: 当前对象所属的项目ID
- `contract_id`: 合同变更所属的合同ID
- `parent_id`: 父对象ID
- 其他 API 调用需要但 schema 中没有的信息
### 5.2 Context 使用流程
**场景:加载合同数据**
1. **Handler.load()** 从 self._collection 获取所有 project
2. 遍历每个 project,调用 `GET contract/list?project_id=xxx`
3. 为每个返回的 contract 数据添加临时标记 `_context = {"project_id": xxx}`
4. **Handler.create_node()** 创建 SyncNode 时,提取 `_context` 并设置到 `node.context`
5. **Handler.update()** 更新时,从 `node.context["project_id"]` 获取 project_id 并添加到请求数据
### 5.3 Context 传递规则
- 父 → 子:从依赖的父节点继承 context
- 横向传递:同级节点不共享 context
- 更新时使用:从 context 提取必要信息添加到请求 payload
## 6. 智能 API 路由
### 6.1 路由策略
根据节点数据的字段,自动选择正确的 API endpoint
**方案1:字段匹配**
- 检查 node.data 中的字段集合
- 匹配预定义的 endpoint → required_fields 映射表
- 选择第一个匹配的 endpoint
**方案2Schema 类型**
- 根据 node.schema 的类型
- 查找 schema_type → endpoint 映射表
**默认规则**
- CREATE: `{node_type}`
- UPDATE: 子类重写 `_select_update_endpoint()`
- DELETE: `{node_type}`
### 6.2 Project 示例
Project 有多个更新接口:
- `project/key_constraints` - 需要字段:key_constraints, key_constraints_remark
- `project/complete_investment` - 需要字段:complete_investment
- `project/dynamic_investment` - 需要字段:dynamic_investment
- `project/plan_construction` - 需要字段:plan_construction_capacity, plan_construction_date
- ... 等
Handler 定义映射表,根据实际数据字段自动选择。
## 7. 依赖数据获取
### 7.1 获取模式
Handler 通过 `collection.filter(node_type="xxx")` 获取依赖数据:
**加载顺序**(按依赖关系):
1. Project(无依赖)
2. Contract(依赖 Project
3. Contract Change(依赖 Project + Contract
### 7.2 数据流
```
Pipeline
→ 按拓扑序加载
→ ProjectHandler.load()
→ GET project/list
→ 创建 ProjectSyncNode
→ ContractHandler.load()
→ collection.filter(node_type="project")
→ 遍历 projectGET contract/list?project_id=xxx
→ 创建 ContractSyncNode,设置 context["project_id"]
→ ContractChangeHandler.load()
→ collection.filter(node_type="contract")
→ 遍历 contract,从 context 获取 project_id
→ GET contract_change/list?project_id=xxx&contract_id=yyy
→ 创建节点,设置 context
```
## 8. 实现计划
### Phase 1: HTTP 通信层
- 实现 **ApiClient**
- HTTP Client 封装(httpx.AsyncClient
- 签名生成(_generate_signature
- 统一请求方法(get/post/put/delete
- **批量 Push Log 查询**get_push_logs,单个也传列表)
- ~~wait_for_push_log~~(不实现,由 DataSource 调用 poll
### Phase 2: DataSource 协调层
- 实现 **ApiDataSource**
- 持有 ApiClient 实例
- Handler 注册和管理
- 实现 DataSource 接口(load_all, execute_batch
- 注入 api_client 到 Handler
- 实现轮询逻辑(_poll_async_tasks
### Phase 3: Handler 基类
- 实现 **BaseApiHandler**
- load/create/update/delete/poll 方法
- 使用 self.api_client 发送请求
- **extract_created_id**(子类可重写)
- **_execute_with_retry**(可选,子类可重写重试策略)
- Context 管理
### Phase 4: 业务验证
- 实现 **ProjectApiHandler**(无依赖,简单场景)
- 实现 loadGET project/list
- 实现 createPOST project
- 实现 poll:查询 push_log
- 实现 **ContractApiHandler**(依赖 Project,验证 context
- 实现 load:遍历 projectGET contract/list?project_id=xxx
- 设置 context["project_id"]
- 编写测试验证设计
### Phase 5: 完善功能
- 批量 Push Log 查询优化(等待后端实现批量接口)
- 超时控制(可配置)
- 请求日志和调试
### Phase 6: 推广
- 实现剩余业务 Handler
- 性能优化(并发控制)
- 文档和示例
## 9. 与现有系统集成
**调用关系**
```
Pipeline
ApiDataSource (持有 ApiClient)
BaseApiHandler (使用 api_client)
ApiClient (发送 HTTP 请求)
```
**职责分离**
- **ApiClient**: 纯粹的 HTTP 通信,无业务逻辑
- **ApiDataSource**: 协调 Handler,管理执行流程
- **BaseApiHandler**: 业务逻辑,调用 ApiClient 完成网络操作
- **具体 Handler**: 实现特定业务的 load/create/update/delete
**优点**
- ApiClient 和 Handler 解耦,避免循环依赖
- Handler 通过 self.api_client 访问 HTTP 功能
- DataSource 统一管理 ApiClient 生命周期
- Handler 内部灵活控制重试策略
## 10. 关键优势
1. **简化架构**:去掉 Repository 层,代码量减少 50%
2. **职责清晰**
- ApiClientHTTP 通信
- DataSource:协调和轮询
- Handler:业务逻辑和重试
3. **重试灵活**:Handler 内部实现,不同业务可自定义策略
4. **状态清晰**Load 阶段创建 PENDING 节点,执行阶段明确状态转换
5. **批量优化**Push Log 批量查询,减少 HTTP 请求
6. **易于扩展**:新业务只需实现 Handler,复用 Client 和 DataSource
7. **易于测试**ApiClient 可 mockHandler 独立测试
## 11. 设计改进总结
| 改进点 | 初始设计 | 最终设计 | 理由 |
|-------|---------|---------|------|
| Push Log 查询 | get_push_log(id) | get_push_logs([ids]) | 批量查询 |
| 轮询逻辑 | wait_for_push_log 在 Client | 移除,DataSource 调用 poll | 职责清晰 |
| created_id 提取 | 在 Client | 在 Handler 基类 | 业务差异 |
| 重试逻辑 | 在 DataSource | **在 Handler** | 业务灵活性 |
| Load 状态 | GET 成功 → SUCCESS | GET 成功 → PENDING | Load ≠ 同步完成 |
| 执行状态 | 不明确 | 执行前设置 IN_PROGRESS | 状态清晰 |
7. **易于测试**ApiClient 可以 mockHandler 单独测试业务逻辑
## 12. 使用示例
### 12.1 基本使用流程
```python
# 1. 创建 DataSource
datasource = ApiDataSource(
base_url="https://api.example.com",
uid="user123",
secret="secret_key"
)
# 2. 初始化(必须调用)
await datasource.initialize()
# 3. 注册 Handler
datasource.register_handler(ProjectApiHandler())
datasource.register_handler(ContractApiHandler())
# 4. 创建 Collection
collection = DataCollection()
# 5. 加载数据(自动处理异常,不会抛出)
await datasource.load_all(collection, order=["project", "contract"])
# 6. 执行同步(自动处理异常,不会抛出)
await datasource.sync_all(collection, order=["project", "contract"])
# 7. 关闭连接(必须调用)
await datasource.close()
```
### 12.2 异常处理说明
**设计原则**
-**load_all 和 sync_all 不会抛出异常**
-**Handler 的 create/update/delete 捕获所有 HTTP 异常**
-**所有错误转换为状态(SyncStatus.FAILED)和错误信息(node.error**
**为什么不需要 try-finally**
```python
# ❌ 不需要这样做
try:
await datasource.load_all(collection, order=["project"])
finally:
await datasource.close()
# ✅ 直接调用即可
await datasource.initialize()
await datasource.load_all(collection, order=["project"]) # 内部已处理异常
await datasource.sync_all(collection, order=["project"]) # 内部已处理异常
await datasource.close()
```
**异常处理机制**
1. **load_all 异常处理**
```python
# 加载某个类型失败,记录错误但继续处理其他类型
for node_type in order:
try:
raw_items = await handler.load(collection)
# ... 创建节点
except Exception as e:
print(f"Error loading {node_type}: {e}")
# 继续处理下一个类型
```
2. **sync_all 异常处理**
```python
# Handler 返回的异常已被捕获转为 TaskResult
for node in nodes:
try:
result = await handler.create(node) # Handler 内部捕获异常
# 根据 result.status 更新节点状态
except Exception as e:
# DataSource 的最后保护
node.status = SyncStatus.FAILED
node.error = str(e)
```
3. **Handler 异常处理**
```python
async def create(self, node: SyncNode) -> TaskResult:
try:
# 生成参数
push_id = self._generate_push_id()
data = {..., "push_id": push_id, "biz_id": self._generate_biz_id(node)}
# 调用 API
response = await self.api_client.post("/api/v2/project", data)
# 返回成功
return TaskResult(status=TaskStatus.SUCCESS, data_id=response['data']['id'])
except httpx.HTTPStatusError as e:
# HTTP 错误(4xx, 5xx
return TaskResult(status=TaskStatus.FAILED, error=f"HTTP {e.response.status_code}: {e}")
except httpx.NetworkError as e:
# 网络错误
return TaskResult(status=TaskStatus.FAILED, error=f"Network error: {e}")
except Exception as e:
# 其他异常
return TaskResult(status=TaskStatus.FAILED, error=f"Unexpected error: {e}")
```
### 12.3 Pipeline 集成示例
```python
async def sync_pipeline(datasource: ApiDataSource):
"""同步流程编排"""
# 初始化
await datasource.initialize()
# 创建 Collection
collection = DataCollection()
# 执行同步流程(所有错误都被内部处理)
await datasource.load_all(collection, order=["project", "contract", "material"])
await datasource.sync_all(collection, order=["project", "contract", "material"])
# 检查结果
failed_nodes = collection.filter(status=SyncStatus.FAILED)
if failed_nodes:
print(f"Failed nodes: {len(failed_nodes)}")
for node in failed_nodes:
print(f" - {node.node_type}:{node.node_id}: {node.error}")
# 清理
await datasource.close()
```
**关键点**
- ✅ 不需要 try-except 包裹 load_all/sync_all
- ✅ 通过检查节点状态判断成功/失败
- ✅ 错误信息保存在 node.error 中
- ✅ Pipeline 专注于流程编排,不处理异常细节
@@ -0,0 +1,518 @@
# DataSource + Handler 架构设计 V2
## 核心原则
### **职责分离**
```text
Strategy (业务逻辑层)
↓ 设置 action, 完成 ID 映射
DataSource (状态管理 + 编排层)
↓ 调用 Handler, 管理状态流转
Handler (纯执行层)
↓ 调用后端 API, 返回结果
Backend API
```
### **关键设计决策**
1. **ID 映射在 Strategy 完成**
- `Strategy.create()` / `Strategy.update()` 时,已将依赖字段的本地 ID 替换为远程 ID
- DataSource 拿到的 `node.data` 已经是映射好的数据
- Handler 直接使用 `node.data`,不需要关心 ID 映射
2. **状态管理在 DataSource**
- 所有 `node.status` 的转换由 DataSource 负责
- Handler 只返回 `TaskResult`(成功/失败/进行中)
- DataSource 根据 `TaskResult` 更新 `node.status`
3. **Handler 只负责执行**
- 调用后端 APIload/create/update/delete
- 返回执行结果(TaskResult
- 提供异步任务轮询(poll_tasks
- 提供数据质量检查(validate)
---
## Handler 接口设计
### **核心方法**
```python
class NodeHandler(Protocol[T]):
# ===== 数据加载 =====
async def load(self, collection: Optional[DataCollection] = None) -> List[Dict[str, Any]]:
"""
从后端加载全部数据
Args:
collection: Collection 上下文(可选,用于依赖查询)
Returns:
原始数据列表
示例:
# Contract Handler(依赖 Project
async def load(self, collection=None):
# 从 collection 获取 project 列表
if collection:
projects = collection.filter(node_type="project")
project_ids = [p.data_id for p in projects if p.data_id]
else:
project_ids = []
# 只加载这些项目的合同
response = await self.api.get_contracts(project_ids=project_ids)
return response["data"]
"""
# ===== 数据操作 =====
async def create(self, node: SyncNode[T]) -> TaskResult:
"""
创建节点
注意:node.data 已经过 ID 映射,直接使用即可
Returns:
TaskResult:
- SUCCESS: 同步完成,返回 data_id
- IN_PROGRESS: 异步任务,返回 task_id
- FAILED: 失败,返回 error
示例:
# 同步接口
async def create(self, node):
try:
response = await self.api.create_contract(node.data)
return TaskResult.success(data_id=response["id"])
except Exception as e:
return TaskResult.failed(error=str(e))
# 异步接口
async def create(self, node):
try:
response = await self.api.submit_contract_async(node.data)
return TaskResult.in_progress(task_id=response["task_id"])
except Exception as e:
return TaskResult.failed(error=str(e))
"""
async def update(self, node: SyncNode[T]) -> TaskResult:
"""更新节点(同 create"""
async def delete(self, node: SyncNode[T]) -> TaskResult:
"""删除节点(同 create"""
# ===== 异步任务轮询 =====
async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]:
"""
批量轮询异步任务状态
Args:
task_ids: 任务 ID 列表
Returns:
{task_id: TaskResult} 字典
示例:
async def poll_tasks(self, task_ids):
response = await self.api.batch_query_tasks(task_ids)
results = {}
for task in response["tasks"]:
if task["status"] == "completed":
results[task["id"]] = TaskResult.success(
data_id=task["result"]["contract_id"]
)
elif task["status"] == "failed":
results[task["id"]] = TaskResult.failed(
error=task["error"]
)
else: # running
results[task["id"]] = TaskResult.in_progress(
task_id=task["id"]
)
return results
"""
# ===== 数据质量检查 =====
async def validate(self, data: Dict[str, Any]) -> ValidationResult:
"""
数据质量检查
Args:
data: 原始数据
Returns:
ValidationResult(是否有效 + 错误列表)
示例:
async def validate(self, data):
errors = []
if not data.get("code"):
errors.append("合同编码为空")
if not data.get("project_id"):
errors.append("项目 ID 为空")
return ValidationResult(
is_valid=len(errors) == 0,
errors=errors
)
"""
# ===== 辅助方法 =====
def extract_id(self, data: Dict[str, Any]) -> str:
"""从原始数据提取 ID"""
def create_node(self, data: Dict[str, Any]) -> SyncNode[T]:
"""从原始数据创建 SyncNode"""
```
### **TaskResult 设计**
```python
class TaskStatus(Enum):
SUCCESS = "success" # 同步完成
FAILED = "failed" # 失败
IN_PROGRESS = "in_progress" # 异步任务进行中
class TaskResult:
def __init__(
self,
status: TaskStatus,
data_id: Optional[str] = None, # 成功时返回的 ID
task_id: Optional[str] = None, # 异步任务 ID
error: Optional[str] = None, # 失败原因
metadata: Optional[Dict] = None # 其他元数据
):
...
@classmethod
def success(cls, data_id: Optional[str] = None) -> TaskResult:
"""创建成功结果"""
@classmethod
def failed(cls, error: str) -> TaskResult:
"""创建失败结果"""
@classmethod
def in_progress(cls, task_id: str) -> TaskResult:
"""创建进行中结果"""
```
---
## DataSource 接口设计
### **接口方法**
```python
class BaseDataSource(ABC):
# ===== Handler 管理 =====
def register_handler(self, handler: NodeHandler) -> None:
"""注册 Handler"""
def get_handler(self, node_type: str) -> NodeHandler:
"""获取 Handler"""
# ===== 数据加载 =====
async def load_all(
self,
order: List[str]
) -> None:
"""
按依赖顺序加载所有数据
注意:调用前需先 set_collection()
Args:
order: 节点类型顺序(如 ["project", "contract", "construction"]
工作流程:
1. 按顺序遍历每个节点类型
2. 调用 Handler.load()
3. Handler 可以从 self._collection 查询依赖节点
4. 使用 Handler.create_node() 创建 SyncNode
5. 添加到 Collection
"""
# ===== 同步执行 =====
async def sync_all(
self,
order: List[str]
) -> None:
"""
按依赖顺序执行同步
流程:
1. 按顺序遍历每个节点类型
2. 筛选需要同步的节点(status=PENDING, action!=NONE
3. 调用 Handler 的 create/update/delete
4. 根据 TaskResult 更新节点状态
5. 处理异步任务轮询
"""
# ===== 统计信息 =====
def get_sync_summary(self, collection: DataCollection) -> Dict[str, Any]:
"""获取同步统计信息"""
```
### **状态流转逻辑**
```python
async def _sync_nodes(self, handler, nodes):
async_tasks = {} # {node_id: task_id}
for node in nodes:
# 1. 跳过不需要处理的节点
if node.action == SyncAction.NONE:
continue
# 2. 跳过异常状态节点
if node.binding_status in [DEPENDENCY_ERROR, ABNORMAL, WARNING]:
node.status = SyncStatus.SKIPPED
continue
# 3. 只处理 PENDING 节点
if node.status != SyncStatus.PENDING:
continue
# 4. 执行操作
try:
node.status = SyncStatus.IN_PROGRESS
if node.action == SyncAction.CREATE:
result = await handler.create(node)
elif node.action == SyncAction.UPDATE:
result = await handler.update(node)
elif node.action == SyncAction.DELETE:
result = await handler.delete(node)
# 5. 处理结果
if result.status == TaskStatus.SUCCESS:
node.status = SyncStatus.SUCCESS
if result.data_id:
node.data_id = result.data_id # 回填 ID
elif result.status == TaskStatus.FAILED:
node.status = SyncStatus.FAILED
node.error = result.error
elif result.status == TaskStatus.IN_PROGRESS:
async_tasks[node.node_id] = result.task_id
except Exception as e:
node.status = SyncStatus.FAILED
node.error = str(e)
# 6. 轮询异步任务
if async_tasks:
await self._poll_async_tasks(handler, nodes, async_tasks)
```
### **异步任务轮询**
```python
async def _poll_async_tasks(self, handler, nodes, async_tasks):
node_map = {n.node_id: n for n in nodes}
max_retries = poll_max_retries # 默认轮询 1 次,可配置
retry_count = 0
while async_tasks and retry_count < max_retries:
# 批量查询
task_ids = list(async_tasks.values())
results = await handler.poll_tasks(task_ids)
# 处理结果
for node_id, task_id in list(async_tasks.items()):
if task_id not in results:
continue
result = results[task_id]
node = node_map[node_id]
if result.status == TaskStatus.SUCCESS:
node.status = SyncStatus.SUCCESS
if result.data_id:
node.data_id = result.data_id
async_tasks.pop(node_id)
elif result.status == TaskStatus.FAILED:
node.status = SyncStatus.FAILED
node.error = result.error
async_tasks.pop(node_id)
# 还有未完成任务,等待后重试
if async_tasks:
await asyncio.sleep(2)
retry_count += 1
# 超时未完成的任务标记为失败
for node_id in async_tasks:
node = node_map[node_id]
node.status = SyncStatus.FAILED
node.error = "Async task timeout"
```
---
## 完整流程示例
### **Pipeline 编排**
```python
# Stage 0: 初始化
collection = DataCollection()
datasource = ApiDataSource(api_client)
# 注册 Handler
datasource.register_handler(ProjectNodeHandler(api_client))
datasource.register_handler(ContractNodeHandler(api_client))
# Stage 1: 加载数据
datasource.set_collection(collection)
await datasource.load_all(
order=["project", "contract"] # 依赖顺序
)
# Stage 2: Strategy 执行
for strategy in [project_strategy, contract_strategy]:
await strategy.bind() # 设置 binding_status
await strategy.create() # 设置 action=CREATE + ID 映射
await strategy.update() # 设置 action=UPDATE + ID 映射
await strategy.pre_sync_check()
# Stage 3: 物理同步
await datasource.sync_all(
order=["project", "contract"]
)
# Stage 4: 检查结果
summary = datasource.get_sync_summary(collection)
print(f"Success: {summary['success']}, Failed: {summary['failed']}")
```
### **Contract Handler 实现**
```python
class ContractNodeHandler(BaseNodeHandler[ContractResponse]):
def __init__(self, api_client):
super().__init__(
node_type="contract",
node_class=ContractSyncNode,
schema=ContractResponse
)
self.api = api_client
async def load(self):
"""加载合同(依赖 Project"""
if self._collection:
projects = self._collection.filter(node_type="project")
project_ids = [p.data_id for p in projects if p.data_id]
else:
project_ids = []
response = await self.api.get_contracts(project_ids=project_ids)
return response["data"]
async def create(self, node):
"""创建合同(异步接口)"""
try:
# node.data 已经过 ID 映射,直接使用
response = await self.api.create_contract_async(node.data)
if response.get("async"):
return TaskResult.in_progress(task_id=response["task_id"])
else:
return TaskResult.success(data_id=response["id"])
except Exception as e:
return TaskResult.failed(error=str(e))
async def update(self, node):
"""更新合同(同步接口)"""
try:
response = await self.api.update_contract(node.data_id, node.data)
return TaskResult.success()
except Exception as e:
return TaskResult.failed(error=str(e))
async def poll_tasks(self, task_ids):
"""轮询异步任务"""
response = await self.api.batch_query_tasks(task_ids)
results = {}
for task in response["tasks"]:
if task["status"] == "completed":
results[task["id"]] = TaskResult.success(
data_id=task["result"]["contract_id"]
)
elif task["status"] == "failed":
results[task["id"]] = TaskResult.failed(error=task["error"])
else:
results[task["id"]] = TaskResult.in_progress(task_id=task["id"])
return results
async def validate(self, data):
"""数据质量检查"""
errors = []
if not data.get("code"):
errors.append("合同编码为空")
if not data.get("project_id"):
errors.append("项目 ID 为空")
return ValidationResult(is_valid=len(errors) == 0, errors=errors)
def extract_id(self, data):
return data["id"]
```
---
## 核心优势
### **1. 职责清晰**
- **Strategy**: 业务逻辑 + ID 映射
- **DataSource**: 状态管理 + 编排
- **Handler**: 纯执行(后端 API 调用)
### **2. ID 映射前置**
- ID 映射在 Strategy 完成,DataSource 和 Handler 无需关心
- `node.data` 已经是映射好的数据,直接使用
### **3. 状态统一管理**
- 所有状态流转由 DataSource 负责
- Handler 只返回结果,不改变节点状态
### **4. 异步任务支持**
- Handler 返回 `TaskResult.in_progress(task_id)`
- DataSource 自动轮询直到完成
### **5. 数据质量保障**
- Handler 提供 `validate()` 方法
- DataSource 在加载时检查数据质量
### **6. 易于测试**
- Handler 可以 Mock(返回假 TaskResult
- DataSource 可以单独测试状态流转逻辑
- Strategy 可以单独测试 ID 映射逻辑
---
## 与现有架构的兼容性
完全符合 [架构分层设计.md](./架构分层设计.md) 的四层模型:
1. **Common 层**: SyncNode, DataCollection, BindingManager
2. **Sync System 层**: SyncStrategy(负责 bind/create/update + ID 映射)
3. **DataSource 层**: BaseDataSource + NodeHandler(状态管理 + 执行)
4. **Pipeline 层**: 流程编排 + 依赖顺序
符合 [状态定义规范.md](./状态定义规范.md) 的状态流转逻辑。
@@ -0,0 +1,150 @@
# node_id / data_id / 持久化与生命周期说明
本文档说明同步系统中 `node_id``data_id` 的职责、生成时机、持久化行为,以及同步流程中“创建/绑定/回填”的完整生命周期。目标是避免“重复生成 node_id 导致绑定失效”的问题,并明确各层职责。
---
## 1. 基础概念
- **data_id**
- 业务主键(来自数据源/远端系统的资源 ID)。
- 在加载阶段通常已存在;在 CREATE 场景下初始为空,成功后由 DataSource 回填。
- **node_id**
- 同步系统内部的**稳定标识符**(绑定与持久化的主键)。
- 用于绑定表、持久化节点、依赖追踪等。
- 设计目标:**跨同步周期稳定**,不能随每次加载而变化。
---
## 2. 核心结论(必须遵守)
1. **node_id 一旦建立,不应再被数据源“重新生成”。**
- 绑定关系与持久化状态依赖 `node_id`;若每次加载都重新生成,会导致历史绑定失效。
2. **加载阶段应先恢复 Collection(持久化节点),再加载 DataSource 数据。**
- 数据源加载出的 `data_id` 应优先匹配已持久化的节点;匹配到则复用旧 `node_id`
3. **CREATE 场景允许临时/新建 node_id**,但必须通过后续回填与绑定更新保持一致性。
---
## 3. 正确的初始化/加载顺序
### 推荐流程(Pipeline 前半段)
1. **持久化恢复阶段**
- 必须同时恢复 `node_id``data_id`(以及状态字段),否则无法建立稳定的映射关系。
- Collection 可能为空,这属于合法情况。
2. **数据源加载阶段(DataSource.load_all**
- 对每一条业务数据(含 `data_id`):
- **若 Collection 内已存在该 `data_id` 对应节点**:将数据写回原节点(保留旧 `node_id`)。
- **若不存在**:由 **Collection** 生成新的 `node_id`,并创建新节点。
> 这样可避免“第二次同步时重建节点,导致旧绑定丢失”。
---
## 4. node_id 的来源与稳定性
### A. 持久化恢复阶段
- **恢复时必须包含 `node_id``data_id`**
- 两者成对恢复,才能保证后续 load 阶段按 `data_id` 命中旧节点。
### B. 数据源加载阶段
- **优先复用持久化 node_id**
- 通过 `data_id` 在 Collection 中查找并更新已有节点。
- **仅在未知 data_id 时新建 node_id**
- 新节点 `node_id` 必须由 **Collection** 生成,而非 DataSource/Handler 直接拼接。
### C. 创建阶段(Strategy.create
- 新建“目标端”节点时,**必须有 node_id**
- 用于写入 Collection、记录状态、建立临时绑定。
- 成功后回填 `data_id`**node_id 不变**。
---
## 5. data_id 的来源与回填
本节按阶段拆分说明 `data_id` 的来源与写回。
### A. 持久化恢复阶段
- `data_id` 从持久化数据中恢复(与 `node_id` 一起恢复)。
### B. 数据源加载阶段
- `data_id` 来自数据源原始数据(`id` / `_id`)。
- 若命中已有节点,则仅更新数据与状态,不改变 `node_id`
### C. 同步创建与轮询阶段
- CREATE 提交后,DataSource 从响应/轮询结果提取 `biz_id`
- 成功时回填到 `node.data_id`,并回写 `node.data` 的主键字段(仅 `id` / `_id`)。
### D. 持久化阶段
- 将更新后的 `node_id` / `data_id` / 状态字段持久化,供下次同步恢复使用。
---
## 6. 绑定与 node_id 的关系
- 绑定表的 key 为 `node_id`,绑定记录是 `local_node_id <-> remote_node_id`
- 因此:**node_id 必须稳定**,才能保证绑定长期有效。
- 绑定可在创建前建立“临时关系”,但成功后不应更换 node_id。
---
## 7. 同步周期中的典型场景
### 场景 1:第一次同步(无持久化)
- Collection 为空 → DataSource load 新节点 → 生成新 `node_id`
- Strategy bind / create / update 执行。
- 持久化后,下次同步可复用 `node_id`
### 场景 2:第二次同步(已有持久化)
- 先恢复 Collection(已有 `node_id`)。
- DataSource load 时按 `data_id` 匹配旧节点并更新数据。
- 绑定关系仍有效。
### 场景 3:创建新数据(CREATE)
- Strategy 产生目标端节点(`node_id` 新生成)。
- DataSource 执行创建,回填 `data_id`
- 绑定记录更新为真实 `data_id`(但 node_id 保持不变)。
---
## 8. 关键规则总结
- **node_id 是“同步系统内的稳定 ID”,必须持久化与复用。**
- **data_id 是“业务系统的资源 ID”,由数据源加载或创建后回填。**
- **加载顺序:先恢复持久化节点 → 再 load 数据源。**
- **遇到已存在 data_id:复用 node_id;否则创建新节点。**
- **CREATE 成功后只更新 data_id,不更换 node_id。**
---
## 9. 相关文件参考
- 绑定与状态定义:见 [docs/状态定义规范.md](状态定义规范.md)
- 编排流程:见 [docs/编排流程规范.md](编排流程规范.md)
- BaseDataSource:见 [sync_system_new/datasource/datasource.py](../sync_system_new/datasource/datasource.py)
- BaseApiHandler:见 [sync_system_new/datasource/api/handler.py](../sync_system_new/datasource/api/handler.py)
---
## 10. 明确规则与建议实现
- **DataSource.load_all 必须先按 `data_id` 命中已有节点并更新**,未命中时再由 Collection 创建新节点并分配新的 `node_id`
- **推荐实现方式**:先 `find_by_data_id`,命中走通用更新;未命中再创建新节点(无需强制 `upsert_by_data_id` 接口)。
- **创建流程中的临时绑定/成功更新规则**:
- CREATE 节点创建时:`binding_status = NORMAL``action = CREATE``status = PENDING``data_id = ""`
- 执行提交前置为 `status = IN_PROGRESS`(异步)或保持 `PENDING`(同步接口待执行)。
- 成功回填后:`status = SUCCESS``data_id` 回填并写回主键字段,绑定记录保持 `node_id` 不变。
- 过程中出现错误:`status = FAILED``error` 记录原因。
- **异常节点持久化/恢复策略**
- 持久化时保留异常节点与错误信息。
- 恢复时是否保留/清理异常节点应通过策略配置控制(默认建议保留以便人工修复)。
@@ -0,0 +1,232 @@
# Skip Sync 行为修复说明
## 问题分析
### 1. **绑定失败的根本原因**
**问题现象**
- Company: 1/39 绑定
- Supplier: 0/65202 绑定(完全没绑定!)
- 导致依赖supplier的业务(contract, material等)也绑不上
- 远程异常创建了74条数据
**根本原因有两个**
#### 原因1skip_sync 跳过了整个策略(包括 bind)
```python
# 旧逻辑:skip_sync=True 时跳过整个策略
if getattr(strategy, 'skip_sync', False):
continue # ← 跳过了 bind/create/update 全部!
```
修复后:
```python
# 新逻辑:bind() 始终执行,skip_sync 只跳过 create/update/sync
await strategy.bind() # ← 始终执行
if getattr(strategy, 'skip_sync', False):
continue # ← 只跳过 create/update
```
#### 原因2:自动绑定ID解析失败没有设置DEPENDENCY_ERROR
```python
# 旧逻辑:ID解析失败只是跳过
if not resolved_key_dict:
continue # ← 节点保持 MISSING,会被误创建!
```
修复后:
```python
# 新逻辑:ID解析失败设置DEPENDENCY_ERROR
if not resolved_key_dict:
node.set_binding_status(BindingStatus.DEPENDENCY_ERROR, "业务键中的ID字段解析失败")
continue # ← 节点为 DEPENDENCY_ERROR,不会被创建
```
### 2. **状态与创建的关系**
| 状态 | 来源 | 允许创建 | 说明 |
|------|------|:---:|------|
| **UNCHECKED** | 加载后初始状态 | ❌ | bind() 未执行 |
| **NORMAL** | 有绑定记录 | ❌ | 已绑定 |
| **MISSING** | 无绑定记录 | ✅ | 真正的孤儿 |
| **WARNING** | 业务键字段缺失 | ❌ | 数据不完整 |
| **DEPENDENCY_ERROR** | 依赖不满足 | ❌ | 父节点未就绪 |
| **ABNORMAL** | 数据损坏 | ❌ | 需人工处理 |
**关键原则**
- `create()` 只查找 `binding_status == MISSING` 的节点
- 如果 bind() 没执行,状态是 UNCHECKED → 不会创建
- 如果 bind() 执行了但依赖不满足 → DEPENDENCY_ERROR → 不会创建
### 3. **Supplier 默认行为变更**
**旧行为**
- `default_skip_sync = True` → 默认跳过同步
- 需要在调用层设置 `force_sync_types=["supplier"]`
**新行为**
- `default_skip_sync` 已移除(使用基类默认值 False)
- Supplier 默认会执行完整同步(bind/create/update
- 如需跳过,在调用层配置 `skip_sync_types=["supplier"]`
## 解决方案
### 修改 1:分离 bind() 和 skip_sync
**新逻辑**
```python
# sync_system_new/pipeline/full_sync_pipeline.py
# Step 1a: 绑定(始终执行)
await strategy.bind()
# Step 1b: 检查是否跳过 create/update/sync
if getattr(strategy, 'skip_sync', False):
print(f"⏭️ Skipping create/update/sync for {node_type}")
# 继续执行 pre_sync_check,但跳过 create/update/sync
if hasattr(strategy, "pre_sync_check"):
ok = await strategy.pre_sync_check()
...
continue
# Step 1c: 正常的 create/update
await strategy.create()
await strategy.update()
```
**效果**
-**bind() 始终执行**(建立绑定关系)
-**skip_sync=True 只跳过 create/update/sync**(避免昂贵操作)
-**pre_sync_check 仍然执行**(依赖检查)
### 修改 2:添加 force_sync_types 配置
**配置示例**
```python
# tests/run_full_sync_pipeline_simple.py
CONFIG = {
# 第一次运行:从 API 加载 + 绑定 + 创建
"force_sync_types": ["supplier"],
"wipe_persistence_on_start": True,
# 第二次运行:从持久化加载 + 跳过同步
# "handler_configs": {"supplier": {"load_mode": "persisted_only"}},
# "skip_sync_types": ["supplier"],
# "wipe_persistence_on_start": False,
}
```
**优先级**
1. **force_sync_types**(最高)→ `skip_sync = False`
2. **skip_sync_types**`skip_sync = True`
3. **策略默认值** (default_skip_sync)
### 修改 3:添加测试日志保存
**功能**
```python
# tests/run_full_sync_pipeline_simple.py
log_path = log_dir / f"simple_pipeline_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
sys.stdout = Tee(original_stdout, log_file)
```
**输出位置**
```
test_results/simple_pipeline_20260204_162345.log
```
## 使用指南
### Phase 1: 首次运行(API + 绑定 + 创建)
```python
CONFIG = {
# 不需要特殊配置,所有类型默认都会同步
"wipe_persistence_on_start": True, # 清空数据库
}
```
**预期结果**
- Supplier: 从API加载60000条 → 自动绑定(credit_code) → 拉取到本地 → 持久化
- Company: 自动绑定 → 拉取
- Contract等: 通过supplier_id/project_id绑定 → 创建
### Phase 2: 后续运行(持久化 + 跳过同步)
```python
CONFIG = {
"handler_configs": {
"supplier": {"load_mode": "persisted_only"} # 从DB加载
},
"skip_sync_types": ["supplier"], # 跳过create/update(但仍执行bind
"wipe_persistence_on_start": False, # 保留数据
}
```
**预期结果**
- Supplier: 从DB加载(带绑定关系) → 执行bind(验证状态) → 跳过create/update
- 其他业务: 正常同步
## 测试验证
### 1. 删除远程异常创建的数据
```bash
# TODO: 通过API删除远程的74条测试数据
```
### 2. 清空数据库重新测试
```bash
rm _runtime/test_simple.db
python scripts/legacy/run_full_sync_pipeline_simple.py
```
### 3. 验证绑定统计
**期望输出**
```
Local Summary:
company: 39/39 bindings ← 全部绑定
supplier: 60000+/65202 bindings ← 大量绑定
contract: XX/34 bindings ← 通过supplier_id绑定
Remote Summary:
company: 39/39 bindings
supplier: 60000+/60000 bindings
contract: XX/155 bindings (XX creates)
```
### 4. 检查日志文件
```bash
cat test_results/simple_pipeline_*.log | grep "Skipping"
```
**期望**
- 第一次运行:**不应该有** "Skipping" 输出
- 第二次运行:`⏭️ Skipping create/update/sync for supplier`
## 风险评估
### 影响范围
- ✅ 修复了 supplier/company 绑定失败问题
- ✅ 防止未绑定节点异常创建
- ✅ 保持了两阶段工作流(首次API+绑定,后续持久化)
- ⚠️ 可能影响其他使用 skip_sync 的场景(需要测试)
### 向后兼容性
- ✅ 原始脚本 (run_full_sync_pipeline_api.py) 不受影响
- ✅ 没有 skip_sync 属性的策略行为不变
- ✅ 新增的 force_sync_types 是可选参数
## 待办事项
- [ ] 测试所有业务类型的绑定统计
- [ ] 验证依赖关系链(project → contract → material → detail
- [ ] 检查 construction_task 为什么没绑定(0/32
- [ ] 测试第二次运行的持久化加载逻辑
- [ ] 更新文档说明 skip_sync 的新语义
@@ -0,0 +1,72 @@
# 业务流程与依赖关系
本文档描述推送系统涉及的业务实体及其依赖关系,用于指导同步顺序编排。
---
## 业务实体分类
### 1. 基础数据(仅拉取)
- **用户(User)**:拉取集团管理员、区域公司管理员及特定用户信息,用于跨平台穿透。其他用户需手动创建。
- **公司(Company)**:仅拉取,不推送。
- **供应商(Supplier)**:仅拉取,不推送。
### 2. 项目及扩展数据
**项目(Project**
- **绑定方式**:必须手动绑定(根节点)
- **同步操作**:允许更新和拉取
- **依赖项**:公司、附件(Attachment
**项目扩展业务**
- 通过 `(project_id, key)``(project_id, code)` 自动绑定
- 允许创建、更新、拉取
- 依赖项目
### 3. 合同及子业务
**合同(Contract**
- **绑定方式**:通过业务键自动绑定(需与 ECP 平台数据交叉检验)
- **同步操作**:允许拉取、创建、更新、删除
- **依赖项**:项目
**合同子业务**(施工任务、物资、结算、力能)
- **绑定方式**:通过 `(contract_id, code)` 自动绑定
- **同步操作**:允许拉取、创建、更新、删除
- **特殊规则**:首次绑定后,项目侧不会同步创建远程已有的数据
- **依赖项**:合同
**合同子业务明细**(施工明细、物资明细、结算明细、力能明细)
- **绑定方式**:通过 `(parent_id, date)` 自动绑定
- **同步操作**:允许拉取、创建、更新、删除
- **特殊规则**:首次绑定后,项目侧不会同步创建远程已有的数据
- **依赖项**:对应的合同子业务
---
## 依赖关系图
```
Company (基础)
Project (手动绑定)
Contract (自动绑定)
Contract Sub-Business (施工/物资/结算/力能)
Details (明细)
```
---
## 同步顺序原则
1. **依赖优先**:父节点必须先于子节点同步
2. **绑定传递**:子节点的业务键构建依赖父节点的 ID 映射
3. **故障隔离**:父节点同步失败时,子节点标记为 `DEPENDENCY_ERROR` 并跳过
详细状态定义参见:[sync_system_new/sync_system/STATE_DEFINITIONS.md](../sync_system_new/sync_system/STATE_DEFINITIONS.md)
详细架构设计参见:[sync_system_new/architecture_layers.md](../sync_system_new/architecture_layers.md)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,152 @@
# 节点状态持久化与人工介入规范
> **代码实现**`sync_system_new/common/state_reset.py`
---
## 1. 持久化规则
### 1.1 持久化目标
| 目标 | 说明 |
|------|------|
| **可恢复性** | 系统重启后能从上次状态继续 |
| **可观测性** | 失败和异常状态要保留,供人工查看 |
| **可追溯性** | 失败信息持久化,不自动重试,需人工确认 |
| **绑定稳定性** | 手工绑定一旦建立,永不自动改变 |
### 1.2 分阶段重置设计
状态重置分为两个阶段执行:
#### 阶段1:加载时重置(Collection.load_from_persistence
| 字段 | 性质 | 持久化 | 阶段1重置 |
|------|------|--------|-----------|
| `node_id` | 恒定标识 | ✅ 必须 | ❌ 不重置 |
| `data_id` | 业务主键 | ✅ 必须 | ❌ 不重置 |
| `data` | 业务数据 | ✅ 必须 | ✅ **重置**(每次重新取数) |
| `origin_data` | 原始快照 | ✅ 必须 | ✅ **重置**(每次重新取数) |
| `depend_ids` | 依赖列表 | ❌ **不持久化** | ✅ **初始化为[]**(bind 阶段从 data 实时计算) |
| `binding_status` | 绑定状态 | ✅ 必须 | ✅ **重置为 UNCHECKED** |
| `action` | 同步意图 | ✅ 保存 | ❌ **保留**(用于阶段2判断) |
| `status` | 执行状态 | ✅ 保存 | ❌ **保留**(全部保留,不重置) |
| `error` | 错误信息 | ✅ 保存 | ✅ **重置为None**(清空上次错误) |
| `context` | 扩展上下文 | ✅ 保存 | ❌ 不重置 |
#### 阶段2:数据加载后清理(Pipeline._cleanup_create_failed_zombies
在数据加载完成后、策略执行前,执行CREATE失败节点清理:
1. **检查条件**
- 本地节点 `action == CREATE`(上次尝试创建)
- 本地节点 `status == FAILED`(创建失败)
2. **清理动作**
- **删除本地失败节点**(从 collection 中移除)
- 如果有绑定记录:
- 解除绑定关系
- 删除远程 collection 中的僵尸节点
3. **最后重置 action**
- 所有剩余节点的 `action` 重置为 NONE
- 准备进入策略阶段
4. **下次同步的自动恢复**
- 被删除的节点会从数据源重新加载
- 如果远程实际已创建成功(网络波动导致未获取到结果)→ 绑定阶段会自动绑定上
- 如果远程实际也失败了 → 重新尝试创建
5. **UPDATE 阶段保护**
- UPDATE 操作会跳过所有 `status == FAILED` 的节点
- 防止 UPDATE 阶段覆盖 CREATE/UPDATE 失败的状态和错误信息
- 确保失败节点的 action 和 error 信息保持不变,直到下次同步清理
> **说明**
> - `data` / `origin_data`:每次运行从数据源重新获取,保证数据最新
> - `depend_ids`**不持久化**,每次 bind 阶段从 data 的业务字段(如 contract_id, project_id)实时提取
> - `binding_status`:持久化用于审计,加载时重置为 `UNCHECKED`,由 bind() 重新判定
> - `error`:**阶段1清空**,避免上次错误影响本次运行,新错误会在各阶段重新生成
> - `action`**阶段1保留**用于识别 CREATE 失败的节点,**阶段2清理后重置**
> - `status`**阶段1全部保留**(包括 FAILED、PRECONDITION_FAILED 等),不自动重置
> - **CREATE 失败节点会被自动删除并重新加载**,UPDATE/DELETE 失败保留需人工处理
> - **UPDATE 操作会跳过 FAILED 节点**,避免覆盖失败状态和错误信息
### 1.3 绑定记录 vs binding_status
| 概念 | 存储位置 | 持久性 | 如何改变 |
|------|---------|--------|---------|
| **绑定记录** | `bindings` 表 | **永久保留** | 只能手工 bind/unbind,或 CREATE 失败自动清理 |
| **binding_status** | `nodes` 表 | 保存但可重判 | 每次 bind() 重新判定 |
### 1.4 CREATE 失败的自动恢复
当 CREATE 操作失败时,会产生"僵尸绑定":
- 绑定记录已建立(local_id → remote_node_id
- 但远程节点的 `data_id` 为空(API 未返回真实 ID
**下次同步时的自动恢复流程**
1. 阶段1加载:保留 `action=CREATE`
2. 阶段2清理:检测到僵尸绑定,自动清理
3. 本地节点恢复为 MISSING 状态
4. 策略阶段重新判定,再次尝试 CREATE
---
## 2. 人工介入规范
### 2.1 介入时机
| 时机 | 场景 | 操作 |
|------|------|------|
| **运行前** | 查看上次失败记录 | 查看持久化的 error,决定是否修复后重试 |
| **bind() 后** | 出现 ABNORMAL/WARNING | 检查数据一致性,手工修复或确认 |
| **bind() 后** | 1:N / N:N 映射 | 手工绑定指定远程记录 |
| **sync() 后** | 批量失败 | 分析失败原因,决定重试策略 |
### 2.2 binding_status 与介入需求
| binding_status | 含义 | 需要介入 | 说明 |
|----------------|------|---------|------|
| `UNCHECKED` | 未检查 | ❌ | 等待 bind() 判定 |
| `NORMAL` | 绑定正常 | ❌ | 自动处理 |
| `MISSING` | 无绑定记录 | ❌ | 自动创建 |
| `WARNING` | 本地有绑定但远程无数据 | ⚠️ 可选 | 可能是远程删除,需确认 |
| `ABNORMAL` | 状态不一致 | ✅ **必须** | 需人工排查 |
| `DEPENDENCY_ERROR` | 依赖未就绪 | ❌ | 等依赖解决后自动重试 |
### 2.3 人工绑定操作
```python
# 手工绑定(1:N 场景)
binding_manager.bind(node_id="local_xxx", data_id="remote_xxx")
# 手工解绑
binding_manager.unbind(node_id="local_xxx")
```
绑定记录一旦建立,永不自动改变,只能手工操作。
### 2.4 职责分工
| 职责 | 系统自动 | 人工介入 |
|------|---------|---------|
| 1:1 自动绑定 | ✅ | - |
| 1:N / N:N 绑定 | ❌ | ✅ |
| NORMAL 节点同步 | ✅ | - |
| MISSING 节点创建 | ✅ | - |
| CREATE 失败重试 | ✅(自动清理僵尸) | ⚠️(需确认后手动重置) |
| UPDATE 失败重试 | ❌(保留FAILED状态) | ✅(需确认后手动重置) |
| ABNORMAL 处理 | ❌ | ✅ |
| WARNING 确认 | ❌ | ✅(可选) |
---
## 3. 总结
1. **分阶段重置**
- 阶段1(加载时):只重置 binding_status**保留 action/status/error**
- 阶段2(数据加载后):清理 CREATE 失败僵尸,然后重置 action
2. **失败状态持久化**FAILED 状态和 error 信息保留,不自动重试
3. **绑定记录**:永久保留,除非 CREATE 失败自动清理或手工操作
4. **人工介入**:失败需确认后手动重置,ABNORMAL/WARNING 需人工处理,1:N 绑定需手工指定
@@ -0,0 +1,112 @@
# 架构分层设计:四层模型
本架构旨在将同步系统的**数据容器**、**同步逻辑**、**执行层**与**编排流程**完全解耦,提供极简且类型安全的开发体验。
---
## 1. 架构概览
系统分为四层,依赖关系严格从上到下:
1. **通用层 (Common)**: 定义纯数据结构(Data Container)和基础工具。
2. **同步系统层 (Sync System)**: 定义同步行为接口(SyncStrategy)与业务规则。
3. **数据源层 (DataSource)**: 实现数据的加载与持久化,属于盲目执行层。
4. **编排层 (Pipeline)**: 应用层逻辑,硬编码同步顺序,协调 ID 映射与错误传播。
---
## 2. 通用层 (Layer 0: Common)
本层不包含业务逻辑,仅定义数据契约。
- **SyncNode[T]**: 核心状态机容器。
- `origin_data`: 原始数据(对应 `data_id`)。
- `data`: 计算后的最新目标数据。
- `data_id`: 业务主键ID。加载时必定有值,CREATE 节点初始为空字符串 `""`(布尔值为 False,可通过校验)。
- `action`: 同步意图 (NONE, CREATE, UPDATE, DELETE)。
- `status`: 执行状态 (PENDING, IN_PROGRESS, SUCCESS, FAILED, SKIPPED, PRECONDITION_FAILED)。
- 详见 [状态定义规范 § 5.A](./状态定义规范.md#5-执行同步状态转换-physical-sync-state-transitions)
- `binding_status`: 绑定状态 (UNCHECKED, NORMAL, MISSING, DEPENDENCY_ERROR, ABNORMAL, WARNING)。
- 详见 [状态定义规范 § 1](./状态定义规范.md#1-核心状态判定表-core-binding-matrix)
- `error`: 异常详情。
- **DataCollection**: 节点的集合,提供类型安全的查询和过滤方法。
- **BindingManager**:
- **ID 映射**: 维护 `local_node_id` <-> `remote_node_id` 的双向映射。
- **绑定键**: 使用 `node_id` 作为绑定记录的键,**一致不变**。CREATE 成功后只更新节点的 `data_id`,绑定记录不变。
- **持久化**: 将映射关系持久化到存储(如 JSON/数据库)。
- **Enums**: `SyncAction`, `SyncStatus`, `BindingStatus` 等。
---
## 3. 同步系统层 (Layer 1: Sync System)
核心业务逻辑层。通过实现 `SyncStrategy` 接口来定义特定实体的同步逻辑。
- **SyncStrategy[T] 接口**:
- `bind()`:
- 职责:判定 `binding_status` (NORMAL/MISSING/ABNORMAL/WARNING/DEPENDENCY_ERROR)
- 详见 [状态定义规范 § 1-3](./状态定义规范.md)
- `create()`:
- 职责:处理孤儿节点(MISSING),在对方侧创建新节点并设置 `action=CREATE`
- **ID 替换**: 创建新节点时,复制数据并将依赖字段的本地 ID 替换为远程 ID
- **内联检查**: 通过 `IDResolver.resolve_and_validate()` 验证依赖 ID 解析
- 详见 [状态定义规范 § 4.C](./状态定义规范.md)
- `update()`:
- 职责:处理差异节点(NORMAL 且有 diff),设置 `action=UPDATE`
- **ID 替换**: 准备更新数据时,替换数据中的依赖 ID
- **内联检查**: 验证目标节点 data_id 完整性,失败则设置 `PRECONDITION_FAILED`
- **FAILED 保护**: 自动跳过 `status=FAILED` 的节点
- 详见 [状态定义规范 § 4.A](./状态定义规范.md)
---
## 4. 数据源层 (Layer 2: DataSource)
作为各个后端的适配器,负责数据加载与持久化。
- **职责**:
- `load_nodes(node_type)`: 从后端加载并返回 `SyncNode` 集合,自动添加到 `DataCollection`
- `save_nodes(nodes)`:
- 遍历节点,仅处理 `status == PENDING``action != NONE` 的节点。
- 根据 `action` 执行相应的 CREATE/UPDATE/DELETE 操作。
- 执行成功后,更新节点的 `status` 和相关信息(如新生成的 remote_id)。
---
## 5. 编排层 (Layer 3: Pipeline)
最高层级,负责流程编排。
- **职责**:
- **显式顺序**: 直接在代码中按顺序调用同步逻辑(例如:先同步项目,再同步合同)。
- **故障传播**: 若前置实体某节点同步失败(`FAILED`),编排器在处理后续依赖节点时可将其标记为 `DEPENDENCY_ERROR`
- **生命周期管理**: 初始化 `BindingManager``DataCollection`,同步结束后调用持久化方法。
---
## 6. ID 映射与对齐:核心流程
为了解决本地数据引用本地 ID 与远程数据引用远程 ID 的冲突,系统采用以下映射逻辑:
### 绑定阶段 (Binding)
- **时机**: `Strategy.bind`
- **目标**: 在 `BindingManager` 中确定 local_id ↔ remote_id 映射
- **手段**: 业务键匹配(如 Code 匹配)、历史记录恢复等
### 执行阶段 (Execution)
- **时机**: `Strategy.create()` / `Strategy.update()` 阶段
- **目标**: 在创建新节点或准备更新时,将依赖字段的本地 ID 替换为远程 ID
- **手段**:
- `create()`: 创建对方节点时,复制数据并替换所有依赖 ID
- `update()`: 准备更新数据时,替换数据中的依赖 ID
- **验证**: `_pre_commit_check` 在 create/update 最后检查,确保所有依赖节点都已有远程 ID
**注意**: ID 替换在 Strategy 的 create/update 阶段完成,DataSource 只负责盲目执行已准备好的数据。
---
## 7. 关键设计原则
- **删除类型依赖配置**: 依赖关系通过 `Pipeline` 中的代码调用顺序自然表达。
- **无状态 DataSource**: 数据源不了解绑定逻辑,只看到 `action``status``binding_status`
- **BindingManager 为核心**: 负责 ID 空间映射和持久化。
- **配置驱动**: 通过 `StrategyConfig` 和预设配置(ConfigPresets)灵活控制同步行为。
@@ -0,0 +1,342 @@
# 同步系统状态定义 (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 后 | 业务主键IDCREATE 前为 "" |
| `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 后设置
```
@@ -0,0 +1,498 @@
# 状态机形式化规范
本文档用 **Guard-Event-Action (GEA)** 形式严格定义节点状态机。
所有转换以代码实现为准,模糊之处在文档中显式标注。
> **代码基线**
> - 枚举: `sync_system_new/common/types.py`
> - 决策引擎: `sync_system_new/sync_system/decision.py`
> - 重置策略: `sync_system_new/common/state_reset.py`
> - 同步策略: `sync_system_new/sync_system/strategy.py`
> - 物理同步: `sync_system_new/datasource/datasource.py`
> **配置基线(状态机真源)**
> - `sync_system_new/state_machine/node_state_machine.yaml`
### 2026-02 对齐说明(重构前必读)
以下语义以 `node_state_machine.yaml` 为准,用于修正文档中易混淆点:
1. **E12a 为什么从 S01 出发?**
- `S01` 只约束**源节点**是 NORMAL/NONE/PENDING 且有 `data_id`
- `E12a` 的 guard 是 `target_has_data_id=false`,检查对象是**目标节点**。
- 所以“源节点在 S01”与“E12a 触发前置失败”并不冲突。
2. **S01 会不会触发创建?**
- 当前配置中,create 的入口状态是 `S02(MISSING)`,不是 `S01`
- `S01` 进入的是 update_prepareE12/E13/E14)。
3. **N:0 + create_enabled=true 的当前行为**
- 自动绑定分支 `E08f``S02 -> S01`,并 `emit spawn_target_node_state: S06`
- create_prepare 分支 `E09b` 也可在检查成功后 `S02 -> S01 + spawn S06`
- 即:源节点不会停在“等待创建结果”的专用状态;创建执行结果体现在目标创建节点链路 `S06 -> S10/S11/S12/S13`
4. **旧章节中的事件编号偏差**
- 本文后续仍有部分“E11a/E11b/…”旧编号描述,已与当前 YAML 不一致。
- 进行代码重构时,优先依据 YAML 的 `E08/E09/E10/E12/E13/E14/E15..E20`
---
## §1 状态空间定义
### §1.1 状态维度
节点携带 **三个独立的状态字段****两个辅助字段**
| 字段 | 枚举 | 值域 | 语义 |
|------|------|------|------|
| **B** `binding_status` | `BindingStatus` | `UNCHECKED` · `NORMAL` · `MISSING` · `ABNORMAL` · `WARNING` · `DEPENDENCY_ERROR` | 绑定健康度 |
| **A** `action` | `SyncAction` | `NONE` · `CREATE` · `UPDATE` · `DELETE` | 待执行动作 |
| **S** `status` | `SyncStatus` | `PENDING` · `IN_PROGRESS` · `SUCCESS` · `FAILED` · `SKIPPED` · `PRECONDITION_FAILED` | 执行结果 |
辅助字段(影响 Guard 但不是状态维度):
| 字段 | 类型 | 语义 |
|------|------|------|
| `data_id` | `str` | 业务主键(空串 `""` = 尚未从远程获取) |
| `error` | `str \| None` | 最近一次错误信息 |
理论组合 = 6 × 4 × 6 = **144**,实际可达 ≈ **23** 种。
### §1.2 可达状态枚举
以下列出所有在正常流程中可以到达的 `(binding_status, action, status)` 三元组。
| # | binding_status | action | status | 语义 | 产生阶段 |
|---|---|---|---|------|---------|
| R1 | UNCHECKED | NONE | PENDING | 首次加载 | 数据加载 |
| R2 | UNCHECKED | NONE | SUCCESS | 从持久化恢复(上轮成功)后重置 | 阶段1/2重置 |
| R3 | UNCHECKED | NONE | FAILED | 从持久化恢复(上轮失败)后重置 | 阶段1/2重置 |
| R4 | UNCHECKED | NONE | SKIPPED | 从持久化恢复(上轮跳过)后重置 | 阶段1/2重置 |
| R5 | UNCHECKED | NONE | PRECONDITION_FAILED | 从持久化恢复(上轮前置失败)后重置 | 阶段1/2重置 |
| R6 | NORMAL | NONE | PENDING | 绑定正常,无需操作(首次) | bind.p1 |
| R7 | NORMAL | NONE | SUCCESS | 绑定正常,无需操作(历史成功) | bind.p1 |
| R8 | NORMAL | NONE | FAILED | 绑定正常但上次失败,被 FAILED 保护 | bind.p1 |
| R9 | MISSING | NONE | PENDING | 孤儿,等待自动绑定或创建(首次) | bind.p1 |
| R10 | MISSING | NONE | SUCCESS | 孤儿(历史成功但绑定丢失?) | bind.p1 |
| R11 | ABNORMAL | NONE | * | 数据损坏,需人工 | bind.p1 |
| R12 | WARNING | NONE | * | 多候选/对方损坏,需人工 | bind.p1/p3 |
| R13 | DEPENDENCY_ERROR | NONE | PENDING | 依赖不满足 | bind.p2/p3/create |
| R14 | NORMAL | CREATE | PENDING | 准备创建(目标节点) | create |
| R15 | NORMAL | CREATE | IN_PROGRESS | 异步创建进行中 | sync |
| R16 | NORMAL | CREATE | SUCCESS | 创建成功 | sync |
| R17 | NORMAL | CREATE | FAILED | 创建失败 | sync |
| R18 | NORMAL | UPDATE | PENDING | 准备更新(目标节点) | update |
| R19 | NORMAL | UPDATE | IN_PROGRESS | 异步更新进行中 | sync |
| R20 | NORMAL | UPDATE | SUCCESS | 更新成功 | sync |
| R21 | NORMAL | UPDATE | FAILED | 更新失败 | sync |
| R22 | NORMAL | UPDATE | PRECONDITION_FAILED | 更新前置条件失败 | update |
| R23 | NORMAL | CREATE | SKIPPED | 创建被 Handler 跳过 | sync |
> **不可达组合(示例)**
> - `(MISSING, UPDATE, *)` — MISSING 节点不可能被安排 UPDATE
> - `(ABNORMAL, CREATE, *)` — ABNORMAL 节点不可能被安排 CREATE
> - `(*, DELETE, *)` — DELETE 尚未实现
> - `(UNCHECKED, CREATE, *)` — UNCHECKED 是瞬态,在 bind 之前不会有 CREATE
> - `(DEPENDENCY_ERROR, UPDATE, *)` — DEPENDENCY_ERROR 节点不可能进入 update 流程
### §1.3 辅助字段约束
| 对应状态 | binding_status | action | status | data_id 约束 | error 约束 |
|---------|---|---|---|-------------|-----------|
| R14 | NORMAL | CREATE | PENDING | `""` (待远程返回) | None |
| R16 | NORMAL | CREATE | SUCCESS | 非空 (远程回填) | None |
| R17 | NORMAL | CREATE | FAILED | `""` (未回填) | 非空 |
| R22 | NORMAL | UPDATE | PRECONDITION_FAILED | * | 非空 |
| R13 | DEPENDENCY_ERROR | NONE | PENDING | * | 非空 |
---
## §2 不变量 (Invariants)
以下约束在**任何事件执行后**都必须成立。违反即为 Bug。
```
INV-1 B = UNCHECKED ⟹ A = NONE
(UNCHECKED 是瞬态,在 bind 之前不允许有动作)
INV-2 B ∈ {ABNORMAL, WARNING, DEPENDENCY_ERROR} ⟹ A = NONE
(阻断态节点不允许执行任何同步动作)
INV-3 B = MISSING ⟹ A ∈ {NONE, CREATE}
(MISSING 节点只能等待创建或保持不动)
INV-4 A = CREATE ⟹ B = NORMAL
(CREATE 动作只出现在目标节点上,其 binding_status 已被设为 NORMAL)
INV-5 A = UPDATE ⟹ B = NORMAL
(UPDATE 动作只对 NORMAL 节点执行)
INV-6 S = IN_PROGRESS ⟹ A ∈ {CREATE, UPDATE, DELETE}
(只有提交过 API 的节点才可能处于 IN_PROGRESS)
INV-7 S = PRECONDITION_FAILED ⟹ A = UPDATE
(PRECONDITION_FAILED 只在 update 阶段产生)
INV-8 A = CREATE ∧ S = SUCCESS ⟹ data_id ≠ ""
(创建成功必须回填 data_id)
INV-9 S = FAILED ⟹ error ≠ None
(失败必须有错误信息)
INV-10 B = DEPENDENCY_ERROR ⟹ error ≠ None
(依赖阻断必须有错误信息)
```
---
## §3 事件与转换
### §3.1 事件总览图
```
┌─────────────────────────────────────────────────────┐
│ 节点生命周期事件流 │
└─────────────────────────────────────────────────────┘
持久化恢复 策略层 (Strategy) 执行层 (DataSource)
═══════════ ══════════════════════════════════ ══════════════════════════
┌──────────────────────────────┐
┌──────┐ │ bind() │ ┌──────────────────────┐
│E01 │──────────▶│ E04 ─┐ │ │ sync_all() │
│RESET1│ │ E05 ├─ phase1: core_state │ │ │
└──┬───┘ │ E06 ─┘ │ │ E12 SYNC_OK │
│ │ E07 ── phase2: dep_check │ ┌───▶│ E13 SYNC_FAIL │
┌──▼───┐ │ E08 ─┐ │ │ │ E14 SYNC_ASYNC │
│E02 │ │ E09 ├─ phase3: auto_bind │ │ │ E15 SYNC_TIMEOUT │
│ZOMBIE│ │ E10 ─┘ │ │ │ E16 HANDLER_SKIP │
└──┬───┘ └──────────────────────────────┘ │ │ E17 HANDLER_DOWN │
│ ┌──────────────────────────────┐ │ └──────────────────────┘
┌──▼───┐ │ create() / update() │ │
│E03 │──────────▶│ E11a CREATE_OK │────┘
│RESET2│ │ E11b CREATE_DEP_FAIL │
└──────┘ │ E11c UPDATE_DIFF │
│ E11d UPDATE_PRECOND_FAIL │
│ E11e UPDATE_SKIP_FAILED │
└──────────────────────────────┘
```
### §3.2 状态转换详解
本节按 Pipeline 执行阶段组织。每个阶段包含:
1. **状态转换图**:状态(R 编号)为节点,事件(E 编号)为有向边
2. **转换表**:每个事件的精确规格——起始状态、判断条件、结束状态、副作用
---
#### 🔄 阶段 0:重置
**状态转换图:**
```
╔═══════════════════════════════════════╗
║ 任意持久化终态 (R6 ~ R23) ║
╚═══════════════════╤═══════════════════╝
│ E01
┌────────────────────────────────────────┐ E02
│ 瞬态: UNCHECKED │────────────▶ [节点删除 + 解除绑定]
│ (action 保留, status 保留, error=None) │
└────────────────────┬──────────────────┘
│ E03 (存活节点)
╔══════════════════════════════════════════════════════════════╗
║ [R1] UNCHECKED, NONE, PENDING ← 首次加载 ║
║ [R2] UNCHECKED, NONE, SUCCESS ← 上轮成功 ║
║ [R3] UNCHECKED, NONE, FAILED ← 上轮失败 ║
║ [R4] UNCHECKED, NONE, SKIPPED ← 上轮跳过 ║
║ [R5] UNCHECKED, NONE, PRECONDITION_FAILED ← 上轮前置 ║
╚══════════════════════════════════════════════════════════════╝
```
**转换表:**
| 事件 | 起始状态 | 判断条件 (Guard) | 结束状态 | 副作用 |
|------|---------|-----------------|---------|--------|
| **E01**: PHASE1_RESET | R6~R23 (任意持久化终态) | 节点从持久化加载 | 瞬态 (UNCHECKED, action 保留, status 保留) | error → None; data/origin_data → None |
| **E02**: ZOMBIE_KILL | 瞬态 | action = CREATE ∧ (status = FAILED data_id = "") | **节点删除** | 解除绑定关系; 对 local/remote 两侧都扫描 |
| **E03**: ACTION_RESET | 瞬态 (存活节点) | E02 完成后所有存活节点 | R1 / R2 / R3 / R4 / R5 (按原 status 分) | action → NONE |
> **代码**: `NodeStateReset.get_phase1_reset_defaults()` · `cleanup_create_failed_zombies()` · `reset_action_for_nodes()`
---
#### 🔗 阶段 1Bind
Bind 由三个子阶段串行执行。后一阶段只处理前一阶段的遗留 MISSING 节点。
**状态转换图:**
```
[R1~R5] UNCHECKED, NONE, *
│ ══════════ Phase 1: 核心状态判定 ══════════
├── E04 (有绑定, 双方数据 VALID) ───────────▶ [R6/R7/R8] NORMAL ──────▶ 出口 ✅
├── E04 (有绑定, 自身数据缺失/损坏) ────────▶ [R11] ABNORMAL ─────────▶ 出口 🔒
├── E04 (有绑定, 对方数据缺失/损坏) ────────▶ [R12] WARNING ──────────▶ 出口 🔒
└── E05 (无绑定记录) ───────────────────────▶ [R9] MISSING
══════════ Phase 2: 依赖检查 ══════════ │
├── E06 (data = None) ──────────────────────▶ [R11] ABNORMAL ──────▶ 出口 🔒
├── E07 (依赖节点不满足) ───────────────────▶ [R13] DEP_ERROR ─────▶ 出口 🔒
│ (无依赖配置 或 依赖全部满足 → 继续 ▼)
══════════ Phase 3: 自动绑定 ══════════
├── E08 (auto_bind 关闭) ──────────────────▶ [R12] WARNING ────────▶ 出口 🔒
├── E09 (去噪后 1:1 匹配成功) ─────────────▶ [R6] NORMAL ─────────▶ 出口 ✅
├── E10a (去噪后非 1:1, 多候选) ───────────▶ [R12] WARNING ────────▶ 出口 🔒
├── E10b (业务键字段缺失) ─────────────────▶ [R12] WARNING ────────▶ 出口 🔒
├── E10c (业务键中 ID 字段解析失败) ───────▶ [R13] DEP_ERROR ─────▶ 出口 🔒
└── (1:0 无远程匹配)
├── create_enabled = True → [R9] MISSING ──────────────▶ 进入 create
└── create_enabled = False → [R12] WARNING ─────────────▶ 出口 🔒
```
**转换表:**
| 事件 | 起始状态 | 判断条件 (Guard) | 结束状态 | 副作用 |
|------|---------|-----------------|---------|--------|
| **E04**: BIND_PAIRED | R1~R5 | 有绑定记录 ∧ 双方数据 VALID | R6 (PENDING) / R7 (SUCCESS) / R8 (FAILED) — status 保持 | 远程节点同步设置 binding_status |
| **E04**: BIND_PAIRED (自身异常) | R1~R5 | 有绑定记录 ∧ 本方数据缺失或损坏 | R11 (ABNORMAL) | 对方节点设为 WARNING |
| **E04**: BIND_PAIRED (对方异常) | R1~R5 | 有绑定记录 ∧ 对方数据缺失或损坏 | R12 (WARNING) | 对方节点设为 ABNORMAL |
| **E05**: BIND_ORPHAN | R1~R5 | 无绑定记录 | R9 (MISSING, NONE, status 保持) | — |
| **E06**: DATA_NULL | R9 | binding_status = MISSING ∧ data = None | R11 (ABNORMAL) | error = "数据损坏: 节点数据为空或不存在" |
| **E07**: DEP_CHECK_FAIL | R9 | depend_fields 配置非空 ∧ ∃依赖节点: 不存在 ∨ status ≠ NORMAL data_id 为空 | R13 (DEPENDENCY_ERROR) | error = "依赖项不满足: ..."; depend_ids 填充 |
| **E08**: AUTOBIND_DISABLED | R9 | auto_bind = False 或 auto_bind_fields 为空 | R12 (WARNING) | — |
| **E09**: AUTOBIND_OK | R9 | 去噪后 local:remote = 1:1 ∧ 双方都是 MISSING | R6 (NORMAL) | 建立绑定记录; 双方都设为 NORMAL |
| **E10a**: AUTOBIND_AMBIGUOUS | R9 | 去噪后非 1:1 (多候选) | R12 (WARNING) | error += "存在多个候选节点" |
| **E10b**: AUTOBIND_KEY_MISSING | R9 | auto_bind_fields 中有字段不存在于 data | R12 (WARNING) | error += "缺少业务键字段: ..." |
| **E10c**: AUTOBIND_ID_FAIL | R9 | 业务键包含 ID 字段 ∧ ID 解析失败 | R13 (DEPENDENCY_ERROR) | error = "业务键中的ID字段解析失败: ..." |
| **E10d**: ORPHAN_CREATE_DISABLED | R9 | 去噪后 1:0 ∧ create_enabled = False | R12 (WARNING) | error += "孤儿且不允许自动创建" |
> **E04 核心状态矩阵** (完整查表):
>
> | | 远程: VALID | 远程: ABSENT | 远程: INVALID |
> |---|---|---|---|
> | **本地: VALID** | 本:NORMAL 远:NORMAL | 本:WARNING 远:ABNORMAL | 本:WARNING 远:ABNORMAL |
> | **本地: ABSENT** | 本:ABNORMAL 远:WARNING | 本:ABNORMAL 远:ABNORMAL | — |
> | **本地: INVALID** | 本:ABNORMAL 远:WARNING | — | 本:ABNORMAL 远:ABNORMAL |
>
> 不在矩阵中的组合 → 默认 (ABNORMAL, ABNORMAL)
> **代码**: `SyncDecisionEngine.determine_core_status()` · `strategy_ops/bind_ops.py::phase_core_state()` · `strategy_ops/bind_ops.py::check_dependencies_for_nodes()` · `strategy_ops/bind_ops.py::phase_auto_binding()`
---
#### ⚡ 阶段 2Create / Update
**状态转换图:**
```
═══════════════ Create ═══════════════
[R9] MISSING, NONE, PENDING (源节点, data ≠ None)
├── E11a (ID 解析成功) ────▶ 源节点: [R9] → [R6] NORMAL
│ 新建目标节点: → [R14] NORMAL, CREATE, PENDING
│ + 建立绑定记录, 目标 data_id = ""
└── E11b (ID 解析失败) ────▶ 源节点: [R9] → [R13] DEPENDENCY_ERROR
═══════════════ Update ═══════════════
[R6/R7] NORMAL, NONE, PENDING/SUCCESS (源节点, status ≠ FAILED)
├── E11c (差异检测通过) ────▶ 目标节点 → [R18] NORMAL, UPDATE, PENDING
├── E11d.1 (目标 data_id="") ▶ 目标节点 → [R22] status = PRECONDITION_FAILED
├── E11d.2 (ID 解析失败) ───▶ 目标节点 → [R22] action = UPDATE, status = PRECOND_FAIL
└── (无差异) ──────────────▶ 保持不变
═══════════════ FAILED 保护 ═══════════════
[R8] NORMAL, NONE, FAILED (源节点)
└── E11e ──────────────────▶ 跳过,保持 [R8] 不变
```
**转换表:**
| 事件 | 起始状态 (源节点) | 判断条件 (Guard) | 结束状态 | 副作用 |
|------|---------|-----------------|---------|--------|
| **E11a**: CREATE_OK | R9 (MISSING) | data ≠ None ∧ IDResolver.resolve_and_validate() = OK | 源 → R6 (NORMAL); **新建**目标 → R14 | 建立绑定; 新节点 data = 已替换 ID 的副本, data_id = "" |
| **E11b**: CREATE_DEP_FAIL | R9 (MISSING) | data ≠ None ∧ IDResolver.resolve_and_validate() = FAIL | R13 (DEPENDENCY_ERROR) | error = "创建时依赖 ID 未解析: ..." |
| **E11c**: UPDATE_DIFF | R6/R7 (NORMAL, status≠FAILED) | 目标存在 ∧ 目标 data_id ≠ "" ∧ ID 解析 OK ∧ _needs_update() = True | 目标 → R18 (UPDATE, PENDING) | 目标: data 替换, error = None |
| **E11d**: UPDATE_PRECOND_FAIL | R6/R7 (NORMAL, status≠FAILED) | 目标 data_id = "" (d1) 或 ID 解析失败 (d2) | 目标 → R22 (UPDATE, PRECONDITION_FAILED) | error = "目标缺少 data_id" 或 "依赖 ID 未解析" |
| **E11e**: UPDATE_SKIP_FAILED | R8 (NORMAL, FAILED) | status = FAILED | R8 不变 | 保护上次的 error 和 FAILED 状态,不做任何修改 |
> **代码**: `strategy_ops/create_ops.py::add_create_action()` · `strategy_ops/update_ops.py::add_update_action()`
---
#### 📡 阶段 3:Sync(物理同步)
**状态转换图:**
```
[R14] NORMAL, CREATE, PENDING [R18] NORMAL, UPDATE, PENDING
│ │
└───────────────┬───────────────────┘
├── E12 (API 成功) ──────▶ [R16] CREATE,SUCCESS / [R20] UPDATE,SUCCESS
├── E13 (API 失败) ──────▶ [R17] CREATE,FAILED / [R21] UPDATE,FAILED
├── E16 (Handler 跳过) ──▶ [R23] CREATE, SKIPPED
├── E17 (Handler 降级) ──▶ [R6] NORMAL, NONE, PENDING (仅 UPDATE)
└── E14 (异步提交) ──────▶ [R15] CREATE,IN_PROGRESS / [R19] UPDATE,INP
├── (轮询成功) ──▶ [R16] / [R20] SUCCESS
└── E15 (超时) ──▶ [R17] / [R21] FAILED
```
**转换表:**
| 事件 | 起始状态 | 判断条件 (Guard) | 结束状态 | 副作用 |
|------|---------|-----------------|---------|--------|
| **E12**: SYNC_OK | R14 或 R18 | Handler API 返回 SUCCESS | R16 (CREATE) 或 R20 (UPDATE) | error → None; data_id 回填 (CREATE); origin_data 更新 (UPDATE) |
| **E13**: SYNC_FAIL | R14 或 R18 | Handler API 返回 FAILED | R17 (CREATE) 或 R21 (UPDATE) | error = Handler 返回的错误信息 |
| **E14**: SYNC_ASYNC | R14 或 R18 | Handler API 返回 IN_PROGRESS | R15 (CREATE) 或 R19 (UPDATE) | 记录 task_id, 进入轮询队列 |
| **E15**: SYNC_TIMEOUT | R15 或 R19 | 轮询次数 > max_retries | R17 (CREATE) 或 R21 (UPDATE) | error = "Async task timeout after N seconds" |
| **E16**: HANDLER_SKIP | R14 或 R18 | Handler API 返回 SKIPPED | R23 (CREATE,SKIPPED) | error → None |
| **E17**: HANDLER_DOWNGRADE | R18 (仅 UPDATE) | Handler 运行时发现无实际变更字段 | R6 (NORMAL, NONE, PENDING) | action: UPDATE → NONE; 不提交 API; 统计列从 update 移入 none |
> **代码**: `BaseDataSource._handle_task_result()` · `_poll_async_tasks()` · `domain/*/api_handler.py`
---
## §4 终态分类与死胡同
### §4.1 终态分类
| 分类 | 状态 | (binding_status, action, status) | 语义 | 下一轮行为 |
|------|------|------|------|---------|
| ✅ 成功 | R16 | (NORMAL, CREATE, SUCCESS) | 创建成功 | →R2→bind→R7→update 检查 |
| ✅ 成功 | R20 | (NORMAL, UPDATE, SUCCESS) | 更新成功 | →R2→bind→R7→update 检查 |
| ❌ 失败 | R17 | (NORMAL, CREATE, FAILED) | 创建失败 | E02 zombie 判定→删除节点→对方回到 R9 重建 |
| ❌ 失败 | R21 | (NORMAL, UPDATE, FAILED) | 更新失败 | →R3→bind→R8→FAILED 保护(E11e)→保持 |
| 🔒 阻断 | R11 | (ABNORMAL, NONE, \*) | 数据损坏 | 每轮重新 bind 检查; 需人工修复数据 |
| 🔒 阻断 | R12 | (WARNING, NONE, \*) | 多候选/对方异常 | 每轮重新 bind 检查; 需人工绑定或修复 |
| 🔒 阻断 | R13 | (DEPENDENCY_ERROR, NONE, PENDING) | 依赖不满足 | 每轮重新检查依赖; 依赖修复后自动恢复 |
| ⏭ 跳过 | R23 | (NORMAL, CREATE, SKIPPED) | Handler 跳过创建 | →R4→bind→R6→update 检查(无绑定则再 create |
| 🔄 异步 | R15 | (NORMAL, CREATE, IN\_PROGRESS) | 创建异步中 | 轮询直到 SUCCESS 或超时→FAILED |
| 🔄 异步 | R19 | (NORMAL, UPDATE, IN\_PROGRESS) | 更新异步中 | 轮询直到 SUCCESS 或超时→FAILED |
| 🔄 前置失败 | R22 | (NORMAL, UPDATE, PRECONDITION\_FAILED) | 前置条件不满足 | →R5→bind→R6/R7→update 重试 |
### §4.2 恢复路径
```
R17 (CREATE, FAILED):
持久化 → 下轮 E01 → 瞬态(UNCHECKED, CREATE, FAILED)
→ E02: action=CREATE ∧ (status=FAILED data_id="") → 满足 zombie 条件
→ 删除节点 + 解除绑定
→ 对方节点回到 R9 (MISSING) → 重新进入 create 流程
R21 (UPDATE, FAILED):
持久化 → 下轮 E01 → 瞬态(UNCHECKED, UPDATE, FAILED)
→ E02: action=UPDATE ≠ CREATE → 非 zombie,存活
→ E03: action → NONE → R3 (UNCHECKED, NONE, FAILED)
→ bind → R8 (NORMAL, NONE, FAILED)
→ E11e: FAILED 保护 → 跳过 update → 保持 R8
⚠ 需人工清除 FAILED 才能恢复
R22 (UPDATE, PRECONDITION_FAILED):
持久化 → 下轮 E01 → 瞬态(UNCHECKED, UPDATE, PRECONDITION_FAILED)
→ E02: action=UPDATE ≠ CREATE → 非 zombie,存活
→ E03: action → NONE → R5 (UNCHECKED, NONE, PRECONDITION_FAILED)
→ bind → R6/R7 (NORMAL) → update 流程重试
R11 (ABNORMAL):
每轮 E01 重置 → bind 重新检查
→ 若数据被人工修复 → E04 → R6/R7/R8 (NORMAL)
→ 若数据仍损坏 → 再次 R11
R12 (WARNING):
每轮 E01 重置 → bind 重新检查
→ 若人工建立了绑定 → E04 → R6/R7 (NORMAL)
→ 若对方数据被修复 → E04 → R6/R7 (NORMAL)
R13 (DEPENDENCY_ERROR):
每轮 E01 重置 → bind 重新检查依赖
→ 依赖节点达到 NORMAL 且 data_id 非空后
→ E07 通过 → 继续 auto_bind 或 create 流程
```
### §4.3 FAILED 保护机制详解
FAILED 保护确保失败节点不会被后续流程静默覆盖,保留 error 信息供人工调查。
```
进入条件:
节点经过 bind 后为 R8 (NORMAL, NONE, FAILED)
即: 绑定关系正常,但上一轮执行失败
保护行为:
`strategy_ops/update_ops.py::add_update_action()` 中:
if source.status == SyncStatus.FAILED:
skip (→ 事件 E11e)
结果: R8 原样保持 → 持久化 → 下一轮依然 R8
解除条件 (需人工介入):
1. 清除 error 字段 (→ None)
2. 将 status 改为 PENDING 或 SUCCESS
3. 下轮 bind 后 → R6 或 R7 → 正常进入 update 流程
```
---
## §5 完备性分析
### §5.1 状态 × 事件矩阵
行 = 当前状态,列 = 可能触发的事件。`→Rx` = 转换到目标状态,`—` = 不可能触发。
| 状态 | E01 | E02 | E03 | E04/E05 | E06~E10 | E11a/b | E11c/d/e | E12~E17 |
|------|-----|-----|-----|---------|---------|--------|----------|---------|
| R1~R5 | — | — | — | →R6~R13 | — | — | — | — |
| R6/R7 | →瞬态 | — | — | — | — | — | →R18/R22 | — |
| R8 | →瞬态 | — | — | — | — | — | →R8(skip) | — |
| R9 | →瞬态 | — | — | — | →R6/R11~R13 | →R6+R14/R13 | — | — |
| R11~R13 | →瞬态 | — | — | — | — | — | — | — |
| R14 | →瞬态 | — | — | — | — | — | — | →R15~R17/R23 |
| R15 | →瞬态 | — | — | — | — | — | — | →R16/R17 |
| R18 | →瞬态 | — | — | — | — | — | — | →R6/R19~R21/R23 |
| R19 | →瞬态 | — | — | — | — | — | — | →R20/R21 |
| R16/R17 | →瞬态 | — | — | — | — | — | — | — |
| R20~R23 | →瞬态 | — | — | — | — | — | — | — |
> **读表方式**: 找到当前状态行和事件列组的交叉点,即可得到目标状态。`—` 表示该状态不可能触发该事件。
### §5.2 已知 GAP
| GAP | 描述 | 风险 | 建议 |
|-----|------|------|------|
| GAP-1 | `set_binding_status()` / `set_action()` / `set_status()` 是纯 setter,不验证转换合法性 | 中 | 加入前置断言:检查 (当前状态, 新状态) 是否匹配某条已知转换 |
| GAP-2 | INV-1 ~ INV-10 未在代码中 assert | 中 | 每次 setter 调用后或阶段结束时断言所有不变量 |
| GAP-3 | R22 (PRECONDITION\_FAILED) 节点下一轮行为路径需验证 | 低 | 已补充恢复路径: R22→E01→E03→R5→bind→R6/R7→update 重试 |
| GAP-4 | Handler 降级 (E17) 后 status 残留为 PENDING 但 action 已清零 | 低 | 可考虑降级时同时重置 status 为 SUCCESS(当前保持 PENDING |
| GAP-5 | 物理同步后缺少整体一致性检查 (post-sync invariant check) | 低 | 在 `sync_all()` 结束后遍历所有节点验证 INV-1 ~ INV-10 |
---
## §6 引用
| 文档 | 路径 |
|------|------|
| 枚举定义 | `sync_system_new/common/types.py` |
| 决策引擎 | `sync_system_new/sync_system/decision.py` |
| 重置策略 | `sync_system_new/common/state_reset.py` |
| 同步策略 | `sync_system_new/sync_system/strategy.py` |
| 物理同步 | `sync_system_new/datasource/datasource.py` |
| ID 解析 | `sync_system_new/sync_system/resolve_ids.py` |
| 节点生命周期(叙事) | [节点生命周期状态转移](./节点生命周期状态转移.md) |
@@ -0,0 +1,534 @@
# 同步编排流程规范 (Pipeline Specification)
本文档定义了完整的同步编排流程,包括各阶段的职责、节点状态转换和检查点。
> **详细的状态定义和转换规则,请参考**: [STATE_DEFINITIONS.md](../sync_system/STATE_DEFINITIONS.md)
---
## 流程概览
同步系统的完整执行流程分为 5 个阶段:
```
┌─────────────────────────────────────────────────────────────┐
│ Stage 0: 初始化 (Initialization) │
├─────────────────────────────────────────────────────────────┤
│ - 创建 Collection (Local/Remote) │
│ - 创建 BindingManager │
│ - 注册 SyncStrategy (各业务类型) │
│ - 创建 DataSource (Local/Remote) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Stage 1: 数据加载 (Data Loading) │
├─────────────────────────────────────────────────────────────┤
│ 节点状态: binding_status=UNCHECKED, action=NONE, status=PENDING │
│ - Persistence.load_nodes() → 从持久化恢复 │
│ - DataSource.load_all() → 从数据源加载 │
│ - BindingManager.load_bindings() → 加载绑定关系 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Stage 2 & 3: 策略执行与同步 (按 node_type 顺序) │
├─────────────────────────────────────────────────────────────┤
│ For each node_type in sync_order: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 2.1 bind() → 绑定判定 │ │
│ │ 2.2 create() → 设置 CREATE action │ │
│ │ └─ 内联 ID 解析 + 前置检查 │ │
│ │ 2.3 Commit Creates → 立即同步 CREATE 节点 │ │
│ │ └─ DataSource.sync_all(poll_async=True) │ │
│ │ 2.4 update() → 设置 UPDATE action │ │
│ │ └─ 内联 ID 解析 + 前置检查 │ │
│ │ 2.5 Commit Updates → 立即同步 UPDATE 节点 │ │
│ │ └─ DataSource.sync_all(poll_async=True) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 说明: │
│ - 每个 node_type 完整同步后才处理下一个(保证依赖顺序) │
│ - commit 操作内部包含异步轮询,无需独立轮询阶段 │
│ - sync_all() 自动过滤 action=CREATE/UPDATE 的节点 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Stage 4: 同步结果报告 (Summary Report) │
├─────────────────────────────────────────────────────────────┤
│ - 打印同步结果摘要 │
│ * 节点统计(按 action/status 分类) │
│ * 绑定关系统计 │
│ * 异常节点检查(data_id 缺失、绑定缺失等) │
│ - 收集统计信息(成功/失败/残留问题) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Stage 5: 状态持久化 (State Persistence) │
├─────────────────────────────────────────────────────────────┤
│ - Collection.persist() → 保存节点状态 │
│ - BindingManager.persist() → 保存绑定关系 │
└─────────────────────────────────────────────────────────────┘
【流程完成】
```
---
## 各阶段节点状态
### Stage 0: 初始化
**输出**: 所有组件实例化完成。
---
### Stage 1: 数据加载
**节点状态**:
```python
binding_status = BindingStatus.UNCHECKED # 未判定
action = SyncAction.NONE # 无动作
status = SyncStatus.PENDING # 待处理
data_id = <从数据源加载> # 已有业务ID
origin_data = <从数据源加载> # 原始数据快照
data = <从数据源加载> # 当前数据
depend_ids = <从数据源加载> # 依赖节点列表
```
**关键点**:
- `data_id` 在加载时**必定有值**(来自数据源)
- 只有待创建的新节点 `data_id` 才可能为 ""(空字符串)
- 从持久化恢复时应重置所有 `action``NONE`
- `binding_status` 初始值是 `UNCHECKED`,不是 `None`
---
### Stage 2.1: Bind (状态判定)
**状态转换**: `binding_status: UNCHECKED → NORMAL/MISSING/ABNORMAL/WARNING/DEPENDENCY_ERROR`
**可能的结果状态**:
| 结果状态 | 含义 | 占比 |
|---------|------|-----|
| **NORMAL** | 有绑定,数据健康 | 大部分 |
| **MISSING** | 无绑定,孤儿节点 | 少量 |
| **ABNORMAL** | 有绑定,自身数据损坏 | 极少 |
| **WARNING** | 有绑定,对方数据损坏 | 极少 |
| **DEPENDENCY_ERROR** | 依赖未满足 | 少量 |
**详细规则**: 参考 [STATE_DEFINITIONS.md § 1-3](../sync_system/STATE_DEFINITIONS.md)
**补充约束**:
- 若依赖节点 `binding_status = NORMAL``data_id` 为空(含 `status = FAILED`),视为依赖阻断。
- 只要依赖节点已回填 `data_id`,即使其 `status = FAILED`,后续类型仍允许进入 Create/Update。
- 自动绑定只在**业务键完整且唯一**时才尝试:
- 若业务键字段缺失(如 `serial_number = null`),设置 `binding_status = WARNING`,并在 `node.error` 记录原因:`Auto-bind skipped: missing fields ...`
- 若同一业务键出现多对多/多对一,设置 `binding_status = WARNING`,并在 `node.error` 记录原因:`Auto-bind skipped: ambiguous candidates`
- 以上情况仅记录 **Warning**,不进行自动绑定。
---
### Stage 2.2: Create (孤儿处理)
**状态转换**: `action: NONE → CREATE` (对 MISSING 节点)
**补充约束**:
- `binding_status = WARNING` 的节点不会进入 CREATE`create()` 只查找 MISSING 节点)
- 创建时会通过 `IDResolver.resolve_and_validate()` 二次验证依赖 ID,失败则设置 `DEPENDENCY_ERROR`
**副作用**:
- 在对方 Collection 创建新节点
- 新节点的 `binding_status = NORMAL, action = CREATE, data_id = ""`
- 建立绑定记录
- 原 MISSING 节点也变为 `NORMAL`
**示例**:
```
Before:
local_node: binding_status=MISSING, action=NONE
After Create:
local_node: binding_status=NORMAL, action=NONE
remote_node (新建): binding_status=NORMAL, action=CREATE, data_id=""
绑定记录: local_id ↔ remote_temp_id
```
---
### Stage 2.3: Update (差异同步)
**状态转换**: `action: NONE → UPDATE` (对 NORMAL 且有差异的节点)
**前提条件**:
- `binding_status = NORMAL`
- `status != FAILED`(保护上次失败的状态和错误信息)
- `compute_diff()` 判定有差异
- 配置的 `update_direction` 决定哪一方节点设置 UPDATE
**内联检查**
- 目标节点 `data_id` 为空 → `status = PRECONDITION_FAILED`
- `IDResolver.resolve_and_validate()` 失败 → `status = PRECONDITION_FAILED`
- Handler 校验后无实际变更字段 → `action` 降级为 `NONE`(不提交)
---
### Stage 2 & 3: 策略执行与同步
**执行顺序**: 按 `sync_order` 顺序逐个处理 node_type
**单个 node_type 的完整流程**:
```
For node_type in sync_order:
┌─────────────────────────────────────────────┐
│ 2.1 Bind - 绑定判定 │
│ - 设置 binding_status: NORMAL/MISSING/... │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 2.2 Create - 创建策略 │
│ - 处理 MISSING 节点,设置 action=CREATE │
│ - 内联 ID 解析验证 (resolve_and_validate) │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 2.3 Commit Creates - 立即同步 CREATE │
│ - DataSource.sync_all(poll_async=True) │
│ - 自动过滤 action=CREATE 的节点 │
│ - 内部包含异步轮询(IN_PROGRESS→SUCCESS)│
│ - ⭐ 执行 data_id 回填和绑定更新 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 2.4 Update - 更新策略 │
│ - 处理差异节点,设置 action=UPDATE │
│ - 内联 ID 解析验证 + data_id 检查 │
│ - FAILED 节点自动跳过 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 2.5 Commit Updates - 立即同步 UPDATE │
│ - DataSource.sync_all(poll_async=True) │
│ - 自动过滤 action=UPDATE 的节点 │
│ - 内部包含异步轮询(IN_PROGRESS→SUCCESS)│
└─────────────────────────────────────────────┘
下一个 node_type
```
**关键特性**:
1. **按类型顺序**: 每个 node_type 完全同步后才处理下一个(保证依赖顺序)
2. **立即提交**: create/update 后立即同步,不等待所有策略执行完
3. **内置轮询**: `sync_all(poll_async=True)` 内部自动轮询异步任务
4. **data_id 回填**: CREATE 成功后立即回填 data_id,下游依赖可用
**同步状态转换**:
**同步接口(一步式)**:
```
action=CREATE, API 成功 → status=SUCCESS, data_id=<远程ID>
action=CREATE, API 失败 → status=FAILED, error=<错误信息>
action=UPDATE, API 成功 → status=SUCCESS
action=UPDATE, API 失败 → status=FAILED, error=<错误信息>
action=NONE → status=PENDING (不执行)
```
**异步接口(两步式,内置轮询)**:
```
action=CREATE, 提交成功 → status=IN_PROGRESS, task_id=<任务ID>
status=IN_PROGRESS, 轮询中... → status=IN_PROGRESS (保持)
status=IN_PROGRESS, 轮询成功 → status=SUCCESS, data_id=<远程ID>
status=IN_PROGRESS, 轮询失败 → status=FAILED, error=<错误信息>
status=IN_PROGRESS, 超时 → status=FAILED, error="Timeout"
```
**关键副作用**(由 DataSource 在 sync_all 中执行):
- CREATE 成功: `data_id` 从 "" 更新为远程分配的 ID,并回写 `data` 主键字段
- CREATE 成功: 绑定记录从临时ID更新为真实ID
- UPDATE 成功: `origin_data` 更新为当前 `data`
- DELETE 成功: 移除绑定记录
**skip_sync 处理**:
- 如果 `strategy.skip_sync = True`,跳过 create/update/commit 步骤
- bind 步骤始终执行(用于状态判定)
**详细规则**: 参考 [STATE_DEFINITIONS.md § 5](../sync_system/STATE_DEFINITIONS.md)
---
### Stage 4: 同步结果报告
**目标**: 打印同步结果摘要,帮助用户了解同步情况
**报告内容**:
1. **节点统计**:按 node_type 统计各种 action/status 的节点数量
2. **绑定关系**:每个 node_type 的绑定记录数
3. **异常检查**:扫描并报告异常情况:
- CREATE 成功但 data_id 为空(DataSource 回填失败)
- CREATE 成功但绑定记录缺失
- UPDATE/CREATE 失败的节点及错误原因
**输出示例**:
```
================================================================================
📊 Local Collection Summary
================================================================================
Node Type Bindings Create Update Delete None Total
--------------------------------------------------------------------------------
project 5/5 0(0/0/0) 2(2/0/0) 0(0/0/0) 3 5(5/0/0)
contract 10/12 2(2/0/0) 5(5/0/0) 0(0/0/0) 5 12(12/0/0)
```
**注意**: 此阶段不修改任何状态,仅作为信息展示。
---
### Stage 5: 状态持久化
**持久化内容**:
- `binding_status`, `action`, `status` 的最终值
- `data_id` (可能在 CREATE 后更新)
- `error` (如果失败)
- 绑定关系的完整映射
---
## 错误处理策略
| 阶段 | 失败类型 | 处理策略 |
|------|---------|---------|
| Stage 0 | 初始化失败 | **终止流程**,抛出异常 |
| Stage 1 | 数据加载失败 | **终止流程**,抛出异常 |
| Stage 2 & 3 | 代码异常 | **终止流程**,抛出异常 |
| Stage 2: create/update 内联检查 | 依赖ID未解析、data_id缺失 | **继续流程**,节点标记为 DEPENDENCY_ERROR 或 PRECONDITION_FAILED |
| Stage 2 & 3 | API 调用失败 | **继续流程**,标记节点 FAILED |
| Stage 4 | - | 总是继续,仅作为报告 |
| Stage 5 | 持久化失败 | **记录错误**,不影响内存状态 |
---
## 节点状态完整时间线
```
┌──────────────┬─────────────┬────────┬────────┬─────────┐
│ 阶段 │ binding_ │ action │ status │ data_id │
│ │ status │ │ │ │
├──────────────┼─────────────┼────────┼────────┼─────────┤
│ 数据加载 │ UNCHECKED │ NONE │PENDING │ 已加载 │
│ Bind │ NORMAL/... │ NONE │PENDING │ 已加载 │
│ Create │ NORMAL │ CREATE │PENDING │ ""* │
│ Commit Create│ NORMAL │ CREATE │SUCCESS │ 远程ID** │
│ Update │ NORMAL │ UPDATE │PENDING │ 已加载 │
│ Commit Update│ NORMAL │ UPDATE │SUCCESS │ 已加载 │
└──────────────┴─────────────┴────────┴────────┴─────────┘
* 新创建的节点 data_id 初始为 ""
** CREATE 成功后 data_id 立即更新为远程分配的ID,下游依赖可用
```
---
## 配置驱动的流程控制
### 跳过某些阶段
```python
pipeline_config = {
"skip_bind": False, # 是否跳过 bind
"skip_create": False, # 是否跳过 create
"skip_update": False, # 是否跳过 update
"skip_post_check": False, # 是否跳过同步后检查
"fail_on_warning": False, # Pre-Check 时是否将 WARNING 视为失败
}
```
> **注意**`skip_pre_check` 已移除,原 `_pre_commit_check` 逻辑已内联到 create/update 方法中。
```
### 策略预设应用
```python
# 首次同步:启用自动绑定,双向创建
for strategy in strategies:
strategy.apply_preset("first_sync")
# 日常同步:仅推送更新
for strategy in strategies:
strategy.apply_preset("push_only")
```
---
## 幂等性保证
**要求**: 同一个 Collection 的多次同步应该是幂等的。
**设计保证**:
1. **Bind 阶段**: 基于绑定记录判定,已绑定节点不会重复绑定
2. **Create 阶段**: 已有绑定的节点 (NORMAL) 不会重复创建
3. **Update 阶段**: 基于 `compute_diff()` 判定,无差异则不执行
4. **Sync 阶段**: `action=NONE` 的节点跳过执行
**前提条件**:
- 绑定记录正确持久化
- 节点的 `binding_status` 正确保存和加载
- `compute_diff()` 逻辑准确
---
## 监控和日志
### 关键指标
| 指标 | 含义 | 阶段 |
|------|------|-----|
| `nodes_loaded` | 加载的节点总数 | Stage 1 |
| `nodes_normal` | NORMAL 状态节点数 | Stage 2.1 |
| `nodes_missing` | MISSING 状态节点数 | Stage 2.1 |
| `nodes_abnormal` | ABNORMAL+WARNING 节点数 | Stage 2.1 |
| `nodes_dep_error` | DEPENDENCY_ERROR 节点数 | Stage 2.1 |
| `actions_create` | CREATE 动作节点数 | Stage 2.2 |
| `actions_update` | UPDATE 动作节点数 | Stage 2.3 |
| `sync_success` | 同步成功节点数 | Stage 3 |
| `sync_failed` | 同步失败节点数 | Stage 3 |
| `success_rate` | 成功率 (success/total) | Stage 4 |
### 日志级别
```
INFO: 流程阶段切换、统计信息
DEBUG: 节点级别的详细操作
WARN: 检查发现的警告、同步失败
ERROR: 检查发现的错误、致命异常
```
---
## 参考文档
- [STATE_DEFINITIONS.md](../sync_system/STATE_DEFINITIONS.md) - 详细的状态定义和转换规则
- [CHECK_SPEC.md](./CHECK_SPEC.md) - 检查点的实现细节和代码示例(可选)
# 1. 持久化 Collection
await local_collection.persist()
await remote_collection.persist()
# 2. 持久化 Binding
await binding_manager.persist()
```
**持久化内容**:
- 节点的 `binding_status`, `action`, `status`, `error`
- 节点的 `data_id`(可能在 CREATE 后更新)
- 绑定关系的完整映射
---
## 错误处理策略
### 各阶段的失败处理
| 阶段 | 失败原因示例 | 处理策略 |
|------|------------|---------|
| Stage 1: 数据加载 | 网络错误、文件缺失 | **终止流程**,抛出异常 |
| Stage 2.1: Bind | 代码bug | **终止流程**,抛出异常 |
| Stage 2.2-2.3: Create/Update | 代码bug | **终止流程**,抛出异常 |
| Stage 2: create/update 内联检查 | 依赖ID未解析、data_id缺失 | **继续流程**,节点标记为 DEPENDENCY_ERROR 或 PRECONDITION_FAILED |
| Stage 3: Physical Sync | API错误、权限不足 | **继续流程**,标记节点为 FAILED |
| Stage 4: Post-Sync Check | 部分节点同步失败 | **继续流程**,打印警告 |
| Stage 5: Persistence | 磁盘满、权限错误 | **记录错误**,但不影响内存状态 |
### 部分成功的处理
在 Stage 3 中,允许部分节点成功、部分失败:
- 成功的节点: `status = SUCCESS`
- 失败的节点: `status = FAILED`, `error = "错误信息"`
Post-Sync Check 会统计成功率并给出警告。
---
## 配置驱动的流程控制
### 跳过某些阶段
通过配置控制哪些阶段执行:
```python
pipeline_config = {
"skip_bind": False, # 是否跳过 bind
"skip_create": False, # 是否跳过 create
"skip_update": False, # 是否跳过 update
"skip_post_check": False, # 是否跳过同步后检查
"fail_on_warning": False, # 是否将 WARNING 也视为失败
}
```
### 策略预设应用
在 Stage 2 之前,可以应用策略预设:
```python
# 首次同步:启用自动绑定,双向创建
for strategy in strategies:
strategy.apply_preset("first_sync")
# 日常同步:仅推送更新
for strategy in strategies:
strategy.apply_preset("push_only")
```
---
## 监控和日志
### 关键指标
在各阶段收集以下指标:
| 指标 | 含义 |
|------|------|
| `nodes_loaded` | 加载的节点总数 |
| `nodes_bound` | 成功绑定的节点数 |
| `nodes_missing` | MISSING 状态的节点数 |
| `nodes_abnormal` | ABNORMAL/WARNING 状态的节点数 |
| `nodes_created` | CREATE 动作的节点数 |
| `nodes_updated` | UPDATE 动作的节点数 |
| `sync_success` | 同步成功的节点数 |
| `sync_failed` | 同步失败的节点数 |
| `errors_count` | 检查发现的 ERROR 数量 |
| `warnings_count` | 检查发现的 WARNING 数量 |
### 日志级别
```
INFO: 流程阶段切换、统计信息
DEBUG: 节点级别的详细操作
WARN: 检查发现的警告
ERROR: 检查发现的错误、异常
```
---
## 幂等性保证
**要求**: 同一个 Collection 的多次同步应该是幂等的。
**设计保证**:
1. **Bind 阶段**: 基于绑定记录判定状态,已绑定的节点不会重复绑定
2. **Create 阶段**: 已有绑定的节点(NORMAL)不会重复创建
3. **Update 阶段**: 基于 `compute_diff()` 判定,无差异则不执行
4. **Sync 阶段**: DataSource 根据 `action` 执行,`action=NONE` 的节点跳过
**前提条件**:
- Binding 记录正确持久化
- 节点的 `binding_status` 正确保存和加载
- `compute_diff()` 逻辑准确
---
## 下一步
详细的检查规范和状态转换规则,请参考:
- [CHECK_SPEC.md](./CHECK_SPEC.md) - 检查点详细规范
- [STATE_TRANSITIONS.md](./STATE_TRANSITIONS.md) - 状态转换表
@@ -0,0 +1,492 @@
# 节点生命周期状态转移 (Node Lifecycle State Transitions)
本文档从一个**单节点视角**出发,完整描述一个 `SyncNode` 从诞生到持久化的全部状态变化。
> **代码映射**
> - 持久化重置:`sync_system_new/common/state_reset.py` → `NodeStateReset`
> - 绑定判定:`sync_system_new/sync_system/decision.py` → `SyncDecisionEngine`
> - 策略执行:`sync_system_new/sync_system/strategy.py` → `DefaultSyncStrategy`
> - ID 解析:`sync_system_new/sync_system/resolve_ids.py` → `IDResolver`
> - 物理同步:`sync_system_new/datasource/datasource.py` → `BaseDataSource`
> - Handler 降级:`sync_system_new/domain/*/api_handler.py`
---
## 状态字段一览
| 字段 | 类型 | 含义 | 谁负责设置 |
|------|------|------|-----------|
| `binding_status` | BindingStatus | 绑定健康状态 | Strategy.bind() |
| `action` | SyncAction | 待执行动作 | Strategy.create() / update() |
| `status` | SyncStatus | 执行结果状态 | DataSource.sync_all() |
| `data_id` | str | 业务主键ID | 数据加载 / DataSource 回填 |
| `error` | str \| None | 错误信息 | 各阶段 |
---
## 完整生命周期
### 第一次运行(无持久化数据)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 节点尚不存在 │
└─────────────────────────────────────────────────────────────────────────────┘
数据源加载 (Stage 1)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 节点诞生 │
│ │
│ binding_status = UNCHECKED ← Collection 创建节点时的初始值 │
│ action = NONE │
│ status = PENDING │
│ data_id = "abc-123" ← 从数据源获取的业务主键 │
│ data = {...} ← 从数据源获取的业务数据 │
│ origin_data = {...} ← 同上,原始快照 │
│ depend_ids = [] ← 空,待 bind 阶段填充 │
│ error = None │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
### 第二次及后续运行(有持久化数据)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 从持久化恢复(原始值) │
│ │
│ binding_status = NORMAL (上次值) │
│ action = UPDATE (上次值) │
│ status = SUCCESS (上次值) │
│ data_id = "abc-123" │
│ error = None (上次值) │
└─────────────────────────────────────────────────────────────────────────────┘
阶段1:加载时重置 (NodeStateReset)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 阶段1重置后 │
│ │
│ binding_status = UNCHECKED ← 重置(每次重新判定) │
│ action = UPDATE ← 保留!(用于阶段2识别 CREATE 失败) │
│ status = SUCCESS ← 保留!(不自动重置) │
│ data_id = "abc-123" ← 不变 │
│ data = None ← 重置(等待数据源覆盖) │
│ origin_data = None ← 重置(等待数据源覆盖) │
│ error = None ← 清空(避免上次错误干扰) │
└─────────────────────────────────────────────────────────────────────────────┘
阶段2CREATE 僵尸清理
(只影响 action=CREATE 且 status=FAILED 或 data_id="" 的节点)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 阶段2清理后(正常节点) │
│ │
│ action = NONE ← 全部重置为 NONE,准备进入策略阶段 │
│ 其他字段不变 │
│ │
│ (若为 CREATE 僵尸节点:直接从 Collection 删除 + 解除绑定) │
└─────────────────────────────────────────────────────────────────────────────┘
数据源加载覆盖 (Stage 1 后半)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 数据源加载后 │
│ │
│ data = {...} ← 数据源最新数据覆盖 │
│ origin_data = {...} ← 数据源最新数据覆盖 │
│ 其他字段不变 │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Bind 阶段状态转移 (Stage 2.1)
Bind 分三个子阶段顺序执行,每个子阶段只修改 `binding_status`
### Phase 1: 核心状态判定
> **代码**`strategy_ops/bind_ops.py::phase_core_state()` → `SyncDecisionEngine.determine_core_status()`
```
输入:binding_status = UNCHECKED
┌─ 有绑定记录?
┌─────┤
│ YES │
│ └─── 本地数据 + 远程数据 都健康?
│ │
│ ┌────┴────┐
│ │ YES │ NO
│ ▼ ▼
│ NORMAL WARNING / ABNORMAL
│ NO
└──────────────► MISSING
```
| 输入条件 | binding_status 输出 |
|---------|-------------------|
| 有绑定,双方数据健康 | **NORMAL** |
| 有绑定,自身数据损坏 | **ABNORMAL** |
| 有绑定,对方数据损坏 | **WARNING** |
| 无绑定记录 | **MISSING** |
### Phase 2: 依赖检查
> **代码**`strategy_ops/bind_ops.py::phase_dependency_check()` → `strategy_ops/bind_ops.py::check_dependencies_for_nodes()`
**仅处理 `binding_status = MISSING` 的节点**,其他状态跳过。
```
输入:binding_status = MISSING
┌─ 有配置 depend_fields
┌─────┤
│ NO └──► 保持 MISSING(无依赖,直接通过)
│ YES ──► 逐一检查依赖节点:
│ │
│ ┌────┴────────────────────────┐
│ │ 全部依赖节点: │
│ │ - 存在? ✅ │
│ │ - binding_status=NORMAL ✅ │
│ │ - data_id 非空? ✅ │
│ └────┬────────────────────────┘
│ │
│ ┌────┴────┐
│ │ 全通过 │ 任一失败
│ ▼ ▼
│ MISSING DEPENDENCY_ERROR
│ + error = "依赖项不满足: ..."
│ + depend_ids 已填充
```
**副作用**:无论检查是否通过,都会填充 `node.depend_ids`
### Phase 3: 自动绑定
> **代码**`strategy_ops/bind_ops.py::phase_auto_binding()`
**仅处理 `binding_status = MISSING` 且依赖已通过的节点**
```
输入:binding_status = MISSING
auto_bind = False?
└──► MISSING → WARNING"跳过自动绑定,无法确认是否需要创建")
auto_bind = True:
├─ 业务键字段缺失?
│ └──► MISSING → WARNING"缺少业务键字段: ..."
├─ 业务键中 ID 字段解析失败?
│ └──► MISSING → DEPENDENCY_ERROR"业务键中的ID字段解析失败")
├─ 去噪后 1:1 匹配?
│ └──► MISSING → NORMAL(执行自动绑定,建立绑定记录)
├─ N:0(无远程匹配)?
│ ├──► create_enabled = True → MISSING → NORMAL,并 spawn 目标 CREATE 节点(S06
│ └──► create_enabled = False → MISSING → WARNING(孤儿且不允许自动创建,需人工处理)
└─ N:M / 多对多 / 多对一?
└──► MISSING → WARNING"存在多个候选节点,无法自动绑定")
```
### Bind 阶段结束后可能的状态
| binding_status | 后续可执行的操作 |
|---------------|----------------|
| **NORMAL** | UPDATE(如有差异)|
| **MISSING** | CREATE |
| **ABNORMAL** | 无(需人工) |
| **WARNING** | 无(阻止自动操作) |
| **DEPENDENCY_ERROR** | 无(等待依赖修复) |
---
## Create 阶段状态转移 (Stage 2.2)
> **代码**`create()` → `strategy_ops/create_ops.py::add_create_action()`
> **对齐说明(与状态机配置一致)**:
> - create_prepare 的入口仍是源节点 `binding_status = MISSING`。
> - 但当前配置下,`N:0 + create_enabled=true` 可以在自动绑定阶段直接把源节点转为 `NORMAL`,并 `emit spawn_target_node_state=S06`。
> - 因此“触发创建”的入口有两条:
> 1) auto_bind 分支(E08f)直接 spawn
> 2) create_prepare 分支(E09b)在 create 检查成功后 spawn。
**仅处理 `binding_status = MISSING` 的节点**
```
输入:源节点 binding_status = MISSING, action = NONE
┌─ 源节点数据为空?
│ └──► 跳过,记录日志
├─ IDResolver.resolve_and_validate() 失败?
│ └──► binding_status → DEPENDENCY_ERROR
│ error = "创建时依赖 ID 未解析: ..."
└─ 成功 ──► 在目标 Collection 创建新节点
├─ 源节点:
│ binding_status → NORMAL"从本地创建到远程"
│ action 不变(仍为 NONE
└─ 新建目标节点:
binding_status = NORMAL
action = CREATE
status = PENDING
data_id = ""(待远程返回)
data = 已替换 ID 的数据副本
└─ 同时建立绑定记录:local_id ↔ remote_id
```
---
## Commit Creates — 物理同步 (Stage 2.3)
> **代码**`DataSource.sync_all()` → Handler.create()
```
输入:action = CREATE, status = PENDING
┌─ Handler 执行 API 调用
├─ 同步 API 成功(200/201
│ └──► status → SUCCESS
│ data_id = "remote-xyz"(远程分配的 ID 回填)
│ data["id"] = "remote-xyz"(主键字段回写)
├─ 异步 API 提交成功(202)
│ └──► status → IN_PROGRESS
│ context.task_id = "task-xxx"
│ │
│ └─ 轮询结果:
│ ├─ 成功 → status → SUCCESS, data_id 回填
│ ├─ 失败 → status → FAILED, error = "..."
│ └─ 超时 → status → FAILED, error = "Timeout"
└─ API 失败(4xx/5xx
└──► status → FAILED
error = "远程操作失败: ..."
```
---
## Update 阶段状态转移 (Stage 2.4)
> **代码**`update()` → `strategy_ops/update_ops.py::add_update_action()`
> **对齐说明**:图里的 `E12a` 从 `S01` 出发并不矛盾。`S01` 约束的是“源节点”有 `data_id`;
> `E12a` 检查的是“目标节点”是否有 `data_id`guard: `target_has_data_id=false`),两者不是同一个对象。
**过滤条件**`binding_status = NORMAL``status != FAILED`
```
输入:源节点 binding_status = NORMAL, action = NONE
┌─ status == FAILED
│ └──► 跳过(保护上次失败的状态和错误信息)
├─ 源节点数据为空?
│ └──► 跳过
├─ 通过绑定找不到目标节点?
│ └──► 跳过,记录日志
├─ 目标节点 data_id 为空?
│ └──► 目标节点 status → PRECONDITION_FAILED
"目标节点缺少 data_id"
├─ IDResolver.resolve_and_validate() 失败?
│ └──► 目标节点 action → UPDATE
│ 目标节点 status → PRECONDITION_FAILED
│ 目标节点 error = "依赖 ID 未解析: ..."
├─ _needs_update() 判定无差异?
│ └──► 跳过(不设置 action)
└─ 有差异 ──► 更新目标节点:
action → UPDATE
status → PENDING
data = 已替换 ID 的数据副本
error = None
```
---
## Commit Updates — 物理同步 (Stage 2.5)
> **代码**`DataSource.sync_all()` → Handler.update()
```
输入:action = UPDATE, status = PENDING
┌─ Handler 构建 API 请求
├─ Handler 发现无实际变更字段
│ └──► action → NONEHandler 降级,不提交 API
├─ 同步 API 成功(200)
│ └──► status → SUCCESS
├─ 异步 API 提交成功(202)
│ └──► status → IN_PROGRESS → (轮询)→ SUCCESS / FAILED
└─ API 失败
└──► status → FAILED
error = "远程操作失败: ..."
```
---
## 持久化阶段 (Stage 5)
```
节点当前状态原样写入存储。
持久化字段:
✅ node_id, data_id, data, origin_data
✅ binding_status, action, status, error, context
❌ depend_ids(不持久化,下次 bind 时从 data 实时计算)
```
---
## 全景时间线表
下表展示**一个节点在典型场景中的状态变化**:
### 场景 A:首次创建(本地 → 远程)
| 阶段 | binding_status | action | status | data_id | error |
|------|---------------|--------|--------|---------|-------|
| 数据加载 | UNCHECKED | NONE | PENDING | "abc-123" | None |
| bind phase1 | **MISSING** | NONE | PENDING | "abc-123" | None |
| bind phase2 | MISSING | NONE | PENDING | "abc-123" | None |
| bind phase3 | MISSING | NONE | PENDING | "abc-123" | None |
| create | **NORMAL** | NONE | PENDING | "abc-123" | None |
| _(新建远程节点)_ | NORMAL | **CREATE** | PENDING | **""** | None |
| commit create | NORMAL | CREATE | **SUCCESS** | **"remote-456"** | None |
| 持久化 | NORMAL | CREATE | SUCCESS | "remote-456" | None |
### 场景 B:日常更新
| 阶段 | binding_status | action | status | data_id | error |
|------|---------------|--------|--------|---------|-------|
| 持久化恢复 | NORMAL→**UNCHECKED** | CREATE→保留 | SUCCESS→保留 | "abc-123" | 清空 |
| 阶段2 action重置 | UNCHECKED | **NONE** | SUCCESS | "abc-123" | None |
| 数据源覆盖 | UNCHECKED | NONE | SUCCESS | "abc-123" | None |
| bind phase1 | **NORMAL** | NONE | SUCCESS | "abc-123" | None |
| update 检测差异 | NORMAL | NONE | SUCCESS | "abc-123" | None |
| _(目标节点)_ | NORMAL | **UPDATE** | **PENDING** | "remote-456" | None |
| commit update | NORMAL | UPDATE | **SUCCESS** | "remote-456" | None |
### 场景 C:依赖阻断
| 阶段 | binding_status | action | status | data_id | error |
|------|---------------|--------|--------|---------|-------|
| 数据加载 | UNCHECKED | NONE | PENDING | "contract-789" | None |
| bind phase1 | **MISSING** | NONE | PENDING | "contract-789" | None |
| bind phase2 | **DEPENDENCY_ERROR** | NONE | PENDING | "contract-789" | "依赖项不满足: project_id: 状态=dep_err" |
| _(后续阶段不处理)_ | | | | | |
| 持久化 | DEPENDENCY_ERROR | NONE | PENDING | "contract-789" | "依赖项不满足..." |
### 场景 D:CREATE 失败 → 下次自动恢复
| 阶段 | binding_status | action | status | data_id | error |
|------|---------------|--------|--------|---------|-------|
| **第 1 次运行** | | | | | |
| create | NORMAL | CREATE | PENDING | "" | None |
| commit create 失败 | NORMAL | CREATE | **FAILED** | "" | "远程操作失败: 500" |
| 持久化 | NORMAL | CREATE | FAILED | "" | "远程操作失败: 500" |
| **第 2 次运行** | | | | | |
| 阶段1恢复 | **UNCHECKED** | CREATE | FAILED | "" | **None** |
| 阶段2僵尸清理 | _(节点被删除,绑定被解除)_ | | | | |
| 数据源重新加载 | UNCHECKED | NONE | PENDING | "" → 新 node_id | None |
| bind → create | 重新走首次创建流程 | | | | |
### 场景 E:UPDATE 失败 → 保留供人工处理
| 阶段 | binding_status | action | status | data_id | error |
|------|---------------|--------|--------|---------|-------|
| **第 1 次运行** | | | | | |
| update | NORMAL | UPDATE | PENDING | "remote-456" | None |
| commit update 失败 | NORMAL | UPDATE | **FAILED** | "remote-456" | "403 Forbidden" |
| 持久化 | NORMAL | UPDATE | FAILED | "remote-456" | "403 Forbidden" |
| **第 2 次运行** | | | | | |
| 阶段1恢复 | UNCHECKED | UPDATE | FAILED | "remote-456" | None |
| 阶段2 action重置 | UNCHECKED | **NONE** | **FAILED** | "remote-456" | None |
| bind | NORMAL | NONE | FAILED | "remote-456" | None |
| update 跳过 | _(status=FAILED,自动跳过)_ | | | | |
| 持久化 | NORMAL | NONE | FAILED | "remote-456" | None |
| _(需人工重置 status 后才能重新更新)_ | | | | | |
---
## 状态转换合法性约束
### binding_status 允许的转换
```
UNCHECKED → NORMAL (核心判定: 有绑定且健康)
UNCHECKED → MISSING (核心判定: 无绑定记录)
UNCHECKED → ABNORMAL (核心判定: 自身数据损坏)
UNCHECKED → WARNING (核心判定: 对方数据损坏)
MISSING → DEPENDENCY_ERROR (依赖检查: 父节点不满足)
MISSING → WARNING (自动绑定: 业务键缺失 / 多候选 / auto_bind=False)
MISSING → DEPENDENCY_ERROR (自动绑定: ID 字段解析失败)
MISSING → NORMAL (自动绑定: 1:1 成功)
MISSING → NORMAL (create: 源节点创建成功后)
MISSING → DEPENDENCY_ERROR (create: ID 解析失败)
```
> **注意**:当前 `SyncNode.set_binding_status()` 不做转换合法性校验【未实现】。
### action 允许的转换
```
NONE → CREATE (create: 新建目标节点)
NONE → UPDATE (update: 检测到差异)
UPDATE → NONE (Handler 降级: 实际无变更字段)
```
### status 允许的转换
```
PENDING → IN_PROGRESS (DataSource: 异步提交成功)
PENDING → SUCCESS (DataSource: 同步执行成功)
PENDING → FAILED (DataSource: 执行失败)
PENDING → SKIPPED (DataSource: 被跳过)
(常见于 CREATE/UPDATEHandler 返回 SKIPPED(无可执行操作/业务条件不满足),对应状态机里的 E19→S13 或 E20→S14
PENDING → PRECONDITION_FAILED (Strategy: 前置条件不满足)
IN_PROGRESS → SUCCESS (DataSource: 异步轮询成功)
IN_PROGRESS → FAILED (DataSource: 异步轮询失败/超时)
```
> **注意**:当前 `SyncNode.set_status()` 不做转换合法性校验【未实现】。
---
## 参考文档
- [状态定义规范](./状态定义规范.md) — 状态枚举和判定规则
- [持久化与人工介入规范](./持久化与人工介入规范.md) — 持久化重置策略
- [编排流程规范](./编排流程规范.md) — Pipeline 阶段定义
- [架构分层设计](./架构分层设计.md) — 各层职责
- [node_id_data_id_lifecycle](./node_id_data_id_lifecycle.md) — ID 生命周期