159 lines
7.8 KiB
Markdown
159 lines
7.8 KiB
Markdown
# 同步问题分析(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` 是否可命中公司映射。
|