|
|
@@ -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,并用显式版本脚本固化数据库约束。
|