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

7.8 KiB
Raw Permalink Blame History

同步问题分析(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_settlementUPDATE 阶段。

自动绑定问题

注:本轮日志已降噪,自动绑定明细多数为 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_detailparent_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-v2PlanUpdate schema 中,存在 model_validator:会先自动清理边界节点(等于 plan_start_dateplan_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 是否可命中公司映射。