# 同步问题分析（2026-03-05）

## 背景

- 同步方式：先用 `working_local_datasource/filtered` 初始化远程数据库，再基于 `run_profiles/jsonl_to_api.yaml` 执行 `jsonl -> api` 同步。
- 本文基于日志 `test_results/sync_state_machine/simple_pipeline_sm_20260305_102926.log` 与状态库 `test_results/sync_state_machine/simple_pipeline_sm.db`。

## 总体结果

- Pipeline 统计：`loaded=311, synced=204, failed=8, post_check_passed=True, mismatch_types=4`
- 不一致类型：`company`, `construction_task`, `material`, `contract_settlement`
- 其中失败（`failed=8`）全部集中在 `contract_settlement` 的 `UPDATE` 阶段。

## 自动绑定问题

> 注：本轮日志已降噪，自动绑定明细多数为 debug 级；以下结论来自 sqlite 状态库查询。

### 非 normal 绑定状态

1. `local.construction_task_detail` 1 条 `dep_err`
   - 错误：依赖 `parent_id -> construction_task` 未找到
   - 现象：`depends=[parent_id:construction_task data_id=... -> not_found]`
   - 结论：这是明确的依赖绑定失败（上游父节点缺失或 data_id 不可达）。

2. `local.project_detail_data` 2 条 `warning`
   - 错误：`未匹配到候选节点且不允许创建`
   - 结论：策略配置不允许创建，且自动匹配未命中，属于预期拦截，不是执行异常。

除上述 3 条外，主要业务类型绑定状态均为 `normal`。

## 更新后仍不一致（按类型）

### 1) company（1/1 不一致）

- 差异字段：`city_name`, `province_name`, `parent_name`
- 模式：本地有值，远端为 `None`
- 结论：远端 list 返回字段缺失/置空，导致 post-check 一定失败。

### 2) construction_task（12/32 不一致）

- 典型残留字段：
  - `status`
  - `complete_quantity`, `complete_rate`
  - `actual_complete_date`, `statistic_date`, `delay_days`
  - `plan_node`（特别是 `is_audit` 值）
- 模式：远端回读为 `None`/`0.0`/默认值，而本地有业务值。
- 结论：该类字段主要受后端计算/回写策略影响，当前 update API 无法完全收敛到本地口径。

### 3) material（3/20 不一致）

- 典型残留字段：
  - `complete_quantity`, `complete_rate`
  - `actual_start_date`, `status`, `delay_days`
  - `plan_node.is_audit`
- 模式与 construction_task 一致：部分字段被后端默认化或不回写。
- 结论：属于“可更新字段集合与回读字段集合不一致”导致的残留差异。

### 4) contract_settlement（34/34 不一致，最严重）

- 全量普遍差异：`contract_type`（本地 `None`，远端有值如 `DP/GP/...`）
- 伴随差异：`complete_price`, `contract_price`, `actual_start_date`, `plan_complete_date`, `plan_node`, `status` 等
- 执行结果（remote）：`UPDATE SUCCESS=1, FAILED=8, SKIPPED=25`
- 8 条失败统一报错：`API error: 计划节点时间必须 晚于计划开始时间`
- 结论：
  - 一部分差异来自字段语义不一致（如 `contract_type`）
  - 一部分差异来自计划节点 API 校验失败，导致更新无法落库
  - 因此该类型短期内无法靠重跑自动收敛

## 根因归类

### A. 后端返回口径与本地模型口径不一致

- 典型：`company` 的名称类字段、`construction/material` 的状态/累计/统计字段。

### B. 更新 schema/API 覆盖范围有限

- 某些字段在 response 中存在，但不在 update schema 或后端 update 逻辑中生效。

### C. 计划节点约束导致更新失败

- `contract_settlement` 明确出现计划节点时间校验失败，直接阻断更新。

### D. 依赖链局部断裂

- `construction_task_detail` 存在 parent 依赖找不到，导致绑定 `dep_err`。

## 建议优先级（不改代码版）

1. **先处理 contract_settlement 的计划节点校验失败**
   - 这是唯一明确执行失败来源（8 条），修复后可显著降低不一致。

2. **明确“应比对字段白名单”与“只读/回写字段”**
   - 对 `construction_task/material/company` 区分“业务必须一致字段”与“后端派生/可忽略字段”。

3. **修复依赖断裂样本**
   - 针对 `construction_task_detail` 的 `parent_id` 失配，核对父节点是否在过滤阶段被裁掉或 data_id 不一致。

4. **针对 `project_detail_data` 的 warning 决策**
   - 确认是否允许创建；若业务上应存在，需调整策略配置。

---

如果需要，我可以基于本文再拆成一份“可执行排障清单”（按 SQL/API/配置三条线），每条给出具体核对命令与验收标准。

## 外部后端实现核对结论（只分析，不改代码）

以下结论来自对 `GroupSideProjectManagementSystem` 后端 `third-v2` 路由、schema、operation、service 的实现阅读。

### 1) company 名称字段为何会空

- `/company/list` 并非直接返回库里的 `city_name/province_name/parent_name`，而是服务层在查询后“二次计算”这三个字段。
- 计算依赖两张映射：
  - 行政区映射（`province/city code -> name`）
  - 公司映射（`parent_id -> parent_name`）
- 如果编码不在映射中、或 `parent_id` 未命中，最终会返回 `None`/空字符串。
- 因此该类差异并不一定是“推送失败”，更像是“回读时依赖映射补全失败或口径不一致”。

### 2) construction_task/material 的 plan_node 差异为何顽固

- 在 `third-v2` 的 `PlanUpdate` schema 中，存在 `model_validator`：会先自动清理边界节点（等于 `plan_start_date` 或 `plan_complete_date` 的首尾节点）。
- 随后在 operation 的 `push(plan)` 中，又会把边界节点重新插回库里，并且统一写入 `is_audit=True`。
- 这意味着：
  - 请求入参和最终落库结构并不完全一致；
  - 如果本地源数据不含边界节点，或 `is_audit` 口径不同，post-check 很容易出现稳定残差。

### 3) contract_settlement 为何最严重（34/34 + 8条失败）

- 计划更新接口的业务规则是“开始时间与合同金额来自合同表”，不是来自推送体：
  - `plan_start_date` 强制取合同 `sign_date`
  - `contract_price` 强制取合同 `total_price`
- 即便推送体带了这两个值，也会在 operation 中被覆盖。
- 同时仍有严格校验：计划节点必须晚于开始时间、早于结束时间、数量递增等；不满足直接 400。
- 这正对应日志里的 8 条失败（计划节点时间校验）与大量更新后不一致（字段被后端覆盖到“合同口径”）。

### 4) list 回读与单条回读口径不一致（会放大不一致）

- 多个类型的 `get_by_id` 会裁掉 `plan_node` 首尾节点，但 `get_list_by_project_id` 通常返回主表原始节点（含首尾）。
- 同步系统做 post-check 用的是 list 拉取结果时，这种“单条/列表口径不一致”会放大 plan_node 相关差异。

### 5) status/complete_rate/delay_days 等字段为何容易残留差异

- 这些字段主要由明细驱动的主表同步逻辑计算并回写，不是 plan/base update 的直接输入字段。
- 因此“推送成功”并不等价于“这些派生字段立即与源数据一致”。

## 建议（仍不改代码）

1. **先按后端口径预处理结算计划数据**
   - 以合同 `sign_date/total_price` 作为 settlement 比对基准和推送基准；
   - 推送前先做节点时间边界校验，避免 400。

2. **对 plan_node 建立统一比较口径**
   - 在 post-check 里对 construction/material/settlement 统一做“边界节点归一化 + `is_audit` 口径归一化”后再比较。

3. **区分“业务真差异”与“后端派生差异”**
   - 对 `status/complete_rate/delay_days/actual_*` 这类派生字段设为可选比对或延迟比对。

4. **company 名称字段先做映射可用性核查**
   - 重点核对 `province/city` 编码是否在后端行政区字典可解析、`parent_id` 是否可命中公司映射。
