Files
ecm_sync_system/docs/sync_issue_analysis_20260305.md
strepsiades 4a9c444b10 first commit
2026-03-09 16:31:42 +08:00

159 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 同步问题分析(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) company1/1 不一致)
- 差异字段:`city_name`, `province_name`, `parent_name`
- 模式:本地有值,远端为 `None`
- 结论:远端 list 返回字段缺失/置空,导致 post-check 一定失败。
### 2) construction_task12/32 不一致)
- 典型残留字段:
- `status`
- `complete_quantity`, `complete_rate`
- `actual_complete_date`, `statistic_date`, `delay_days`
- `plan_node`(特别是 `is_audit` 值)
- 模式:远端回读为 `None`/`0.0`/默认值,而本地有业务值。
- 结论:该类字段主要受后端计算/回写策略影响,当前 update API 无法完全收敛到本地口径。
### 3) material3/20 不一致)
- 典型残留字段:
- `complete_quantity`, `complete_rate`
- `actual_start_date`, `status`, `delay_days`
- `plan_node.is_audit`
- 模式与 construction_task 一致:部分字段被后端默认化或不回写。
- 结论:属于“可更新字段集合与回读字段集合不一致”导致的残留差异。
### 4) contract_settlement34/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` 是否可命中公司映射。