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

105 lines
7.6 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.
## Bug 调研报告:`BusinessObject.content_type/object_id` 为空导致状态流转记录不可追溯
### 背景与结论摘要
- **问题**`stateflow.BusinessObject``content_type_id` / `object_id` 允许为空,导致部分流程实例无法追溯到真实业务对象;相关 `StateFlowRecord` / 参数记录即使存在,也无法再关联回订单/任务等业务数据。
- **结论**`object_id=NULL` 不是“默认 process 能自动补齐”的字段;它只能在创建/绑定 `BusinessObject` 时显式写入。历史/旁路创建路径只写了 `process/name`,且 services 层此前允许在未绑定的 `BusinessObject` 上继续写日志,最终放大成数据不可追溯问题。
- **处理目标**:在 **API + services 层永久性杜绝新增未绑定 BusinessObject**,并提供历史数据回填/隔离方案。
### 现象与影响
- **现象**
- `BusinessObject.content_type_id IS NULL``BusinessObject.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 = 16``id <= 15``PrintingJob` 已不存在
- 但仍存在多条 `BusinessObject.name = PrintingJob-1..15``content_type_id/object_id = NULL`
- 说明这些 `BusinessObject` 是“创建时就未绑定”,且后续对应业务对象被清理/重建GFK 不会级联清理 BusinessObject后遗留为孤儿
### 根因分析(为什么会产生 NULL
- **Schema 层允许为空(放行)**
- `stateflow/migrations/0012_alter_businessobject_content_type_and_more.py``content_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_type``object_id` **均为必填且不可为空**
- `object_id` 必须能在 `content_type` 指向的模型中查到真实对象(避免“悬空绑定”)
#### 2) Services 层:禁止在未绑定 BusinessObject 上推进/克隆(永久杜绝新增日志污染)
- **推进**`stateflow/services.py::advance_to_next_state`
-`content_type_id/object_id` 为空:直接返回失败(禁止推进)
-`content_object``None`(悬空绑定):直接返回失败(禁止推进)
- **克隆**`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_type``object_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 必须是已绑定状态
- 批量推进/单条推进在遇到历史脏数据时应给出明确错误或被自愈修复(取决于调用方策略)