Pārlūkot izejas kodu

docs(s0): document CodeFirst BOM cleanup

Record the S0 legacy BOM CodeFirst startup warning, root cause, and recommended data governance path so the team can fix it safely.

Co-authored-by: Cursor <cursoragent@cursor.com>
skygu 2 nedēļas atpakaļ
vecāks
revīzija
e69ef59131

+ 1 - 0
doc/README.md

@@ -47,6 +47,7 @@
 | [Windows后端WinSW守护重启方案.md](./Windows后端WinSW守护重启方案.md) | Windows 服务器上使用 WinSW 守护后端服务、自动重启与日志落盘 |
 | [plan/S1/S1-订单管理-审批流程实施方案.md](./plan/S1/S1-订单管理-审批流程实施方案.md) | S1 订单审批实施方案 |
 | [plan/S1-S3业务逻辑测试与数据准备方案.md](./plan/S1-S3业务逻辑测试与数据准备方案.md) | S1-S3 完整业务逻辑测试 case 与测试数据准备方案 |
+| [plan/S0-CodeFirst启动异常与BOM历史数据治理方案.md](./plan/S0-CodeFirst启动异常与BOM历史数据治理方案.md) | S0 legacy BOM 表 CodeFirst 启动异常、根因与彻底治理步骤 |
 | [plan/S8/S8数据库处理优化方案.md](./plan/S8/S8数据库处理优化方案.md) | S8 调度、检测日志、通知扫描与数据库写入压力优化方案 |
 | [plan/新系统数据库简明介绍与S1数据流.md](./plan/新系统数据库简明介绍与S1数据流.md) | 新系统数据库简明说明,并以 S1 为例说明数据中台流转 |
 | [plan/数据库迁移/S1/S1-任务交接记忆.md](./plan/数据库迁移/S1/S1-任务交接记忆.md) | **S1 数据中台迁移**跨会话交接(当前进度、阻塞、验收 SQL) |

+ 270 - 0
doc/plan/S0-CodeFirst启动异常与BOM历史数据治理方案.md

@@ -0,0 +1,270 @@
+# S0 CodeFirst 启动异常与 BOM 历史数据治理方案
+
+## 结论摘要
+
+本问题不是 S2 数据中台扩展引入,也不是后端服务真正卡死。复测结果显示:
+
+- `dotnet run` 启动时,系统库 CodeFirst 可完成全部实体初始化。
+- 服务最终可访问,`http://localhost:5005` 返回 `200`。
+- S2/S3/S8 等调度任务能正常注册。
+- 启动日志中会出现 S0 legacy BOM 表结构对齐错误:
+
+```sql
+alter table `ProductStructureMaster`
+change column `ParentMaterialId` `ParentMaterialId` bigint NOT NULL
+```
+
+错误信息:
+
+```text
+SqlSugar.SqlSugarException: Invalid use of NULL value
+```
+
+当前配置 `TableSettings:ContinueInitTableOnEntityFailure = true`,所以该错误被跳过并继续启动,但会污染日志、拖慢启动,并暴露 S0 BOM 历史表存在数据质量或结构治理问题。
+
+## 问题现象
+
+启动日志关键片段:
+
+```text
+初始化表结构 Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing.AdoS0ProductStructureMaster
+
+[Sql]:alter table `ProductStructureMaster` change column `ParentMaterialId` `ParentMaterialId` bigint NOT NULL
+
+SqlSugar.SqlSugarException: Invalid use of NULL value
+
+已启用跳过单表失败,将继续初始化其余实体;库结构可能不完整。
+```
+
+随后服务仍继续启动:
+
+```text
+初始化表结构 ... 218/218
+初始化视图 ... 001/001
+AutoVersionUpdate 中间件结束
+The <trigger_s2_mdp_sync_transform> trigger ... successfully appended
+Schedule hosted service preload completed
+```
+
+## 初步根因
+
+实体 `AdoS0ProductStructureMaster` 映射旧系统表 `ProductStructureMaster`:
+
+```csharp
+[SugarTable("ProductStructureMaster", "标准BOM行维护(复刻 ProductStructureMaster)")]
+public class AdoS0ProductStructureMaster : ITenantIdFilter
+{
+    [SugarColumn(ColumnDescription = "父项物料", ColumnDataType = "bigint")]
+    public long ParentMaterialId { get; set; }
+}
+```
+
+C# 属性是非空 `long`,CodeFirst 会推导数据库列应为 `bigint NOT NULL`。但旧系统表中 `ParentMaterialId` 当前存在以下至少一种情况:
+
+- 列允许 NULL,且已有 NULL 数据。
+- 历史导入或旧业务流程未完整回填 `ParentMaterialId`。
+- 该表是 legacy 业务表,不适合由启动时 CodeFirst 自动做破坏性结构对齐。
+
+MySQL 在已有 NULL 数据时不允许直接把列改为 `NOT NULL`,因此报 `Invalid use of NULL value`。
+
+## 影响评估
+
+### 已确认不影响
+
+- 不影响 S2 数据中台和 S2 制造协同看板。
+- 不影响后端编译。
+- 不阻塞服务最终启动。
+- 不阻塞 `1.0.146` S2 更新脚本执行。
+
+### 实际影响
+
+- 启动日志出现 `fail` 级别错误,容易误判为启动失败。
+- 本地启动时间变长,排查时容易误判为 CodeFirst 卡死。
+- S0 标准 BOM 行维护相关数据可能存在父项物料 ID 缺失,后续列表、工序聚合、保存校验可能出现数据不完整风险。
+- 继续依赖启动时 CodeFirst 对齐 legacy 表,会反复尝试执行同一条失败 DDL。
+
+## 推荐修复策略
+
+推荐采用“三段式”彻底解法,不建议只吞异常。
+
+### 第一阶段:停止 legacy 表启动时自动结构对齐
+
+目标:先消除启动日志污染和反复失败 DDL。
+
+建议二选一:
+
+1. 将 `AdoS0ProductStructureMaster`、`AdoS0ProductStructureOp` 这类复刻旧系统表从 `Admin.NET.Plugin.AiDOP/Startup.cs` 的开发环境 `db.CodeFirst.InitTables(...)` 列表中移除。
+2. 或对确认由旧系统维护结构的实体加 `[IgnoreTable]`,避免 CodeFirst 管理表结构。
+
+推荐优先采用方案 1:只从开发环境 demo CodeFirst 列表移除该表,运行时查询和仓储映射仍保留,改动范围更小。
+
+不建议继续让这些 legacy 表由 CodeFirst 在启动时自动 `ALTER TABLE`,结构变更应走版本脚本和人工评审。
+
+### 第二阶段:S0 BOM 历史数据治理
+
+目标:清理 `ProductStructureMaster` 中父项/子项物料 ID 缺失的问题。
+
+先只读核验:
+
+```sql
+SELECT
+  COUNT(*) AS total_rows,
+  SUM(ParentMaterialId IS NULL) AS null_parent_material_id,
+  SUM(ComponentMaterialId IS NULL) AS null_component_material_id,
+  SUM(NULLIF(TRIM(ParentItem), '') IS NULL) AS empty_parent_item,
+  SUM(NULLIF(TRIM(ComponentItem), '') IS NULL) AS empty_component_item
+FROM ProductStructureMaster;
+```
+
+查看问题样本:
+
+```sql
+SELECT
+  RecID,
+  ParentItem,
+  ComponentItem,
+  ParentMaterialId,
+  ComponentMaterialId,
+  tenant_id
+FROM ProductStructureMaster
+WHERE ParentMaterialId IS NULL
+   OR ComponentMaterialId IS NULL
+LIMIT 50;
+```
+
+按物料编码尝试匹配物料主数据。团队需先确认物料主表以哪个表为准,例如 `ItemMaster` 或当前 S0 物料主数据实体对应表。以下 SQL 是模板,执行前需替换物料主表和字段名:
+
+```sql
+SELECT
+  p.RecID,
+  p.ParentItem,
+  parent.Id AS matched_parent_id,
+  p.ComponentItem,
+  component.Id AS matched_component_id
+FROM ProductStructureMaster p
+LEFT JOIN ItemMaster parent
+  ON parent.ItemNum = p.ParentItem
+LEFT JOIN ItemMaster component
+  ON component.ItemNum = p.ComponentItem
+WHERE p.ParentMaterialId IS NULL
+   OR p.ComponentMaterialId IS NULL
+LIMIT 200;
+```
+
+回填前必须输出无法匹配清单:
+
+```sql
+SELECT
+  p.RecID,
+  p.ParentItem,
+  p.ComponentItem,
+  p.ParentMaterialId,
+  p.ComponentMaterialId
+FROM ProductStructureMaster p
+LEFT JOIN ItemMaster parent
+  ON parent.ItemNum = p.ParentItem
+LEFT JOIN ItemMaster component
+  ON component.ItemNum = p.ComponentItem
+WHERE (p.ParentMaterialId IS NULL AND parent.Id IS NULL)
+   OR (p.ComponentMaterialId IS NULL AND component.Id IS NULL);
+```
+
+回填模板:
+
+```sql
+UPDATE ProductStructureMaster p
+LEFT JOIN ItemMaster parent
+  ON parent.ItemNum = p.ParentItem
+LEFT JOIN ItemMaster component
+  ON component.ItemNum = p.ComponentItem
+SET
+  p.ParentMaterialId = COALESCE(p.ParentMaterialId, parent.Id),
+  p.ComponentMaterialId = COALESCE(p.ComponentMaterialId, component.Id)
+WHERE p.ParentMaterialId IS NULL
+   OR p.ComponentMaterialId IS NULL;
+```
+
+执行后复核:
+
+```sql
+SELECT
+  COUNT(*) AS remaining_bad_rows
+FROM ProductStructureMaster
+WHERE ParentMaterialId IS NULL
+   OR ComponentMaterialId IS NULL;
+```
+
+注意:不要把 NULL 直接填为 `0`。`0` 会绕过数据库非空约束,但会制造无效外键语义,后续业务排查更困难。
+
+### 第三阶段:显式版本脚本固化约束
+
+目标:数据治理完成后,用版本脚本明确固化结构,而不是依赖启动时 CodeFirst。
+
+前提:
+
+- `ParentMaterialId` 无 NULL。
+- `ComponentMaterialId` 无 NULL。
+- 业务确认所有 BOM 行均可追溯到有效物料主数据。
+- 已完成备份或在预发环境验证。
+
+约束脚本模板:
+
+```sql
+ALTER TABLE ProductStructureMaster
+  MODIFY COLUMN ParentMaterialId BIGINT NOT NULL;
+
+ALTER TABLE ProductStructureMaster
+  MODIFY COLUMN ComponentMaterialId BIGINT NOT NULL;
+```
+
+如 `CompanyRefId`、`FactoryRefId` 也存在同类问题,应按同样流程先核验、再回填、最后改约束。
+
+## 不推荐方案
+
+### 不推荐直接把实体字段改为 `long?`
+
+原因:
+
+- 控制器和 DTO 当前将父项/子项物料作为必填业务字段。
+- 改 nullable 会把历史脏数据继续带入列表、保存、工序聚合逻辑。
+- 后续需要在多处补防御判断,问题会从启动阶段扩散到业务运行阶段。
+
+只有在业务确认旧表允许“无父项物料 ID”的 BOM 行长期存在时,才考虑将实体和 DTO 一并改 nullable,并同步调整前端和校验规则。
+
+### 不推荐继续只依赖 `ContinueInitTableOnEntityFailure`
+
+原因:
+
+- 只能让服务继续启动,不能解决表结构和数据质量问题。
+- 每次启动仍会尝试失败 DDL。
+- 日志中持续出现 `fail`,容易掩盖真正的新故障。
+
+## 团队明日执行建议
+
+1. 确认 `ProductStructureMaster` 是否由旧系统/外部导入维护结构。
+2. 若是 legacy 表,先从 AiDOP 开发环境 CodeFirst 初始化列表移除。
+3. 在数据库只读执行 NULL 统计 SQL。
+4. 确认物料主表映射口径:`ParentItem` / `ComponentItem` 应匹配哪个物料表、哪个主键。
+5. 输出无法匹配清单,交业务确认。
+6. 对可匹配数据执行回填。
+7. 在预发环境执行 NOT NULL 约束脚本。
+8. 重新启动后端,确认不再出现 `Invalid use of NULL value`。
+9. 回归 S0 标准 BOM 行维护列表、详情、新增、编辑、工序聚合。
+
+## 验收清单
+
+- 后端启动日志不再出现:
+  - `ProductStructureMaster.ParentMaterialId`
+  - `Invalid use of NULL value`
+  - `alter table ProductStructureMaster ... NOT NULL`
+- `http://localhost:5005` 可访问。
+- S0 标准 BOM 行维护页面可打开。
+- S0 BOM 搜索、分页、详情、新增、编辑通过。
+- S2/S3/S8 调度注册日志正常。
+- `dotnet build` 通过。
+
+## 当前建议结论
+
+短期:移除 S0 legacy BOM 表的启动时 CodeFirst 对齐,消除启动噪音。
+
+长期:通过数据治理修复 `ProductStructureMaster` 中缺失的物料 ID,并用显式版本脚本固化数据库约束。