1
0
forked from erp-dev/erp
Files
erpnew/docs/bug-fix/business_object_object_id_null.md

7.6 KiB
Raw Permalink Blame History

Bug 调研报告:BusinessObject.content_type/object_id 为空导致状态流转记录不可追溯

背景与结论摘要

  • 问题stateflow.BusinessObjectcontent_type_id / object_id 允许为空,导致部分流程实例无法追溯到真实业务对象;相关 StateFlowRecord / 参数记录即使存在,也无法再关联回订单/任务等业务数据。
  • 结论object_id=NULL 不是“默认 process 能自动补齐”的字段;它只能在创建/绑定 BusinessObject 时显式写入。历史/旁路创建路径只写了 process/name,且 services 层此前允许在未绑定的 BusinessObject 上继续写日志,最终放大成数据不可追溯问题。
  • 处理目标:在 API + services 层永久性杜绝新增未绑定 BusinessObject,并提供历史数据回填/隔离方案。

现象与影响

  • 现象
    • BusinessObject.content_type_id IS NULLBusinessObject.object_id IS NULL
    • StateFlowRecord.business_object_id 通常不为空,但其指向的 BusinessObject 可能未绑定业务对象 → 日志“有记录但无归属”
  • 影响
    • 无法通过 BusinessObject.content_object 找到关联订单/任务GenericForeignKey 失效)
    • 依赖“从流程实例回溯业务对象”的接口/审计/参数回显/报表逻辑可能崩溃或产生错误结果
    • 一旦继续在未绑定对象上推进流程,会持续产生日志与参数记录,扩大脏数据

数据证据测试库抽样2025-12-17

注:以下数字来自本次调研时的测试库,仅用于说明问题形态与证据链;生产环境请用同样的统计口径核对。

  • 表级统计BusinessObject
    • BusinessObject.total = 119
    • content_type_id IS NULL = 70
    • object_id IS NULL = 65
    • content_type_id IS NULL AND object_id IS NULL = 65(典型“完全未绑定”)
    • content_type_id IS NULL AND object_id IS NOT NULL = 5(“半绑定”异常,通常来自不完整回填/旧逻辑)
  • 时间分布(出现明显分界)
    • 2025-11-18 ~ 2025-12-09新增的 BusinessObject 几乎全部为未绑定
    • 2025-12-10 之后:新增 BusinessObject 基本为已绑定
    • 这与“历史旁路/旧版本逻辑 → 后续修复上线”的典型形态一致
  • 关键反证(排除‘当前 printing 正确路径’)
    • 当前库中 PrintingJob.min_id = 16id <= 15PrintingJob 已不存在
    • 但仍存在多条 BusinessObject.name = PrintingJob-1..15content_type_id/object_id = NULL
    • 说明这些 BusinessObject 是“创建时就未绑定”,且后续对应业务对象被清理/重建GFK 不会级联清理 BusinessObject后遗留为孤儿

根因分析(为什么会产生 NULL

  • Schema 层允许为空(放行)
    • stateflow/migrations/0012_alter_businessobject_content_type_and_more.pycontent_type/object_id 改为 null=True, blank=True
    • stateflow/models.py::BusinessObject 仍保持字段可空(历史兼容需要)
  • 创建入口存在“只创建流程实例、不绑定业务对象”的旁路(直接成因)
    • 旁路创建方式通常类似:BusinessObject.objects.create(name=..., process=...)(不传 content_type/object_id
    • 该模式在仓库测试代码中可见(反映团队历史习惯/旧实现可能性),且与库中 PrintingJob-* 未绑定样本吻合
  • services 层此前缺少硬防线(放大器)
    • 若允许在未绑定的 BusinessObject 上执行 advance_to_next_state,则会不断生成 StateFlowRecord / 参数记录,但永远无法追溯业务对象

为什么“默认 process”不能保证 object_id 非空

  • process_id 只决定流程模板,并不能推出“关联的业务对象是谁”
  • 业务对象 ID 必须来自具体实例(如 PlateOrder.id / PrintingJob.id),而这个 ID 只有在业务对象入库后才确定
  • 因此任何“先建 BusinessObject、后忘记补绑定”的路径都会产生 object_id=NULL,且 DB 允许该脏数据落库

处理方法(已落地)

1) API 层:禁止创建/更新未绑定 BusinessObject永久杜绝新增

  • 位置stateflow/serializers.py::BusinessObjectCreateUpdateSerializer
  • 规则
    • content_typeobject_id 均为必填且不可为空
    • object_id 必须能在 content_type 指向的模型中查到真实对象(避免“悬空绑定”)

2) Services 层:禁止在未绑定 BusinessObject 上推进/克隆(永久杜绝新增日志污染)

  • 推进stateflow/services.py::advance_to_next_state
    • content_type_id/object_id 为空:直接返回失败(禁止推进)
    • content_objectNone(悬空绑定):直接返回失败(禁止推进)
  • 克隆stateflow/services.py::clone_business_object
    • 若源对象未绑定:抛错禁止克隆
    • 克隆目标 new_object_id 必填,且目标对象必须存在(避免生成新的悬空绑定)

3) 历史数据修复:提供 printing 维度回填命令(修复“仍存在且可确定归属”的记录)

  • 命令stateflow/management/commands/repair_business_object_bindings.py
  • 用途:对 PrintingJob.business_object / PlateOrder.business_object 一对一绑定关系做一致性回填(content_type/object_id/name
  • 用法
    • 仅统计不落库:uv run manage.py repair_business_object_bindings --dry-run
    • 分类型:--only printingjob|plateorder|all
    • 分批:--limit N
  • 重要限制
    • BusinessObject 已成为孤儿(业务对象已被删除/不存在),则无法可靠回填,只能隔离/清理(见下节建议)

建议补齐的“永久性治理”(防止新旁路再次引入)

  • 收敛创建入口(强烈建议)
    • 统一通过“创建并绑定”的工厂函数/服务创建 BusinessObject禁止散落的 BusinessObject.objects.create(...)
    • 对 printing 等业务模型,创建时必须使用“先保存业务对象 → 再创建并绑定 BusinessObject”的顺序
  • 全仓扫描与 CI 约束
    • BusinessObject.objects.create( 做静态扫描,若未同时出现 content_typeobject_id 则在 CI 失败(或至少告警)
  • 线上监控/告警
    • 增加周期性检查:若发现“近 X 分钟/小时新增的 BusinessObject 存在 NULL 绑定”立刻告警Sentry/日志/钉钉均可)
    • 目的:让问题从“长期潜伏”变为“即时可见”
  • 可选DB 级兜底
    • 若允许:用触发器/约束保证“新写入必须非空”,同时保留历史空值(更强的最后一道防线)

历史孤儿数据建议处置

  • 识别
    • content_type_id/object_id 均为空的 BusinessObject
    • 或者 content_object is None 的悬空绑定
  • 处置选项(需业务确认):
    • 清理:删除孤儿 BusinessObject 及其 state_logs若这些日志对业务无意义且不会被引用
    • 隔离:保留数据但在查询/报表中排除,并记录“不可追溯原因”(便于审计)
    • 人工映射:若能通过其它字段(如外部单号、备注)恢复归属,可进行一次性人工回填

验证与回归

  • 新增防线验证点
    • 创建/更新 BusinessObject API缺失绑定应直接拒绝
    • 推进/克隆:未绑定/悬空绑定应被禁止(不再产生新的 StateFlowRecord
  • 建议回归覆盖
    • printing 创建 PlateOrder/PrintingJob 后,关联的 BusinessObject 必须是已绑定状态
    • 批量推进/单条推进在遇到历史脏数据时应给出明确错误或被自愈修复(取决于调用方策略)