# Skip Sync 行为修复说明

## 问题分析

### 1. **绑定失败的根本原因**

**问题现象**：
- Company: 1/39 绑定
- Supplier: 0/65202 绑定（完全没绑定！）
- 导致依赖supplier的业务（contract, material等）也绑不上
- 远程异常创建了74条数据

**根本原因有两个**：

#### 原因1：skip_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 的新语义
