-- 1.0.132.sql -- S0-2026-05-28|物料替代 ItemSubstituteDetail 数据治理导入 -- -- 业务背景: -- 前端「物料替代」页面(/aidop/s0/manufacturing/material-substitution) -- 后端 AdoS0ItemSubstituteDetailsController 只查 ItemSubstituteDetail 表(当前 0 行)。 -- 导入数据落在 ic_bom / ic_bom_child / ic_substitute_group_detail 三张 legacy 表。 -- 本脚本将三表 JOIN 后排除原料自身行,导入到 ItemSubstituteDetail。 -- -- 拍板口径: -- ParentItem = ic_bom.item_number -- ItemNum = ic_bom_child.item_number -- SubstituteItem = ic_substitute_group_detail.icitem_number -- SubstituteQty = ic_substitute_group_detail.replace_qty -- SubstituteType = '替代' -- Domain = '8010' (基于 factory_id 字符串化兜底,非正式业务域;后续如业务给正式 Domain,另开批次修正) -- tenant_id = ic_bom_child.tenant_id -- 排除原料自身 : g.icitem_number <> c.item_number -- -- 预期导入行数:1147(已在阶段 2 + 阶段 3 各跑一次只读校验确认) -- -- 操作流程: -- 1) 执行段 1【执行前校验】,5 项指标必须命中预期 -- 2) 执行段 2【INSERT SELECT】(含 NOT EXISTS 幂等保护) -- 3) 执行段 3【执行后对账】,本批次行数应 = 1147 -- 4) 如需回滚,先跑段 4 的 SELECT 预览,再人工取消注释 DELETE -- =========================================================================== -- 段 1:执行前校验(只读) -- 所有 5 项必须全绿才允许执行段 2 -- =========================================================================== -- 校验 1.1:预计导入行数(应 = 1147) SELECT COUNT(*) AS expected_insert_rows FROM ic_bom_child c INNER JOIN ic_bom b ON b.Id = c.bom_id AND b.IsDeleted = 0 INNER JOIN ic_substitute_group_detail g ON g.substitute_code = c.substitute_code WHERE c.IsDeleted = 0 AND NULLIF(TRIM(c.substitute_code),'') IS NOT NULL AND NULLIF(TRIM(g.icitem_number),'') IS NOT NULL AND g.icitem_number <> c.item_number; -- 校验 1.2:空值检查(5 个字段都应 = 0) SELECT SUM(b.item_number IS NULL OR TRIM(b.item_number) = '') AS empty_parent, SUM(c.item_number IS NULL OR TRIM(c.item_number) = '') AS empty_itemnum, SUM(g.icitem_number IS NULL OR TRIM(g.icitem_number) = '') AS empty_substitute, SUM(g.replace_qty IS NULL) AS null_qty, SUM(c.tenant_id IS NULL OR c.tenant_id = 0) AS empty_tenant FROM ic_bom_child c INNER JOIN ic_bom b ON b.Id = c.bom_id AND b.IsDeleted = 0 INNER JOIN ic_substitute_group_detail g ON g.substitute_code = c.substitute_code WHERE c.IsDeleted = 0 AND NULLIF(TRIM(c.substitute_code),'') IS NOT NULL AND g.icitem_number <> c.item_number; -- 校验 1.3:三元组重复(total = distinct = 1147;dup = 0) SELECT total_rows, distinct_triples, total_rows - distinct_triples AS dup_rows FROM ( SELECT COUNT(*) AS total_rows, COUNT(DISTINCT CONCAT_WS('|', c.tenant_id, '8010', b.item_number, c.item_number, g.icitem_number)) AS distinct_triples FROM ic_bom_child c INNER JOIN ic_bom b ON b.Id = c.bom_id AND b.IsDeleted = 0 INNER JOIN ic_substitute_group_detail g ON g.substitute_code = c.substitute_code WHERE c.IsDeleted = 0 AND NULLIF(TRIM(c.substitute_code),'') IS NOT NULL AND g.icitem_number <> c.item_number ) t; -- 校验 1.4:目标表当前行数(应 = 0;若 > 0,需评估 NOT EXISTS 的去重效果) SELECT COUNT(*) AS target_existing_rows FROM ItemSubstituteDetail; -- 校验 1.5:tenant_id 三表一致性(应只出现 797403760988229 一行) SELECT c.tenant_id AS child_tenant, b.tenant_id AS bom_tenant, g.tenant_id AS group_tenant, COUNT(*) AS row_cnt FROM ic_bom_child c INNER JOIN ic_bom b ON b.Id = c.bom_id INNER JOIN ic_substitute_group_detail g ON g.substitute_code = c.substitute_code WHERE c.IsDeleted = 0 AND NULLIF(TRIM(c.substitute_code),'') IS NOT NULL GROUP BY c.tenant_id, b.tenant_id, g.tenant_id; -- =========================================================================== -- 段 2:INSERT SELECT(数据导入) -- 含 NOT EXISTS 幂等保护:重复执行不会产生重复数据 -- =========================================================================== INSERT INTO ItemSubstituteDetail (ParentItem, ItemNum, SubstituteItem, SubstituteQty, SubstituteType, Remark, Domain, CreateUser, CreateTime, tenant_id) SELECT b.item_number AS ParentItem, c.item_number AS ItemNum, g.icitem_number AS SubstituteItem, g.replace_qty AS SubstituteQty, '替代' AS SubstituteType, CONCAT('migrated from ic_bom_child Id=', c.Id, ', substitute_code=', c.substitute_code) AS Remark, '8010' AS Domain, 'data-migration' AS CreateUser, NOW() AS CreateTime, c.tenant_id AS tenant_id FROM ic_bom_child c INNER JOIN ic_bom b ON b.Id = c.bom_id AND b.IsDeleted = 0 INNER JOIN ic_substitute_group_detail g ON g.substitute_code = c.substitute_code WHERE c.IsDeleted = 0 AND NULLIF(TRIM(c.substitute_code),'') IS NOT NULL AND NULLIF(TRIM(g.icitem_number),'') IS NOT NULL AND g.icitem_number <> c.item_number AND NOT EXISTS ( SELECT 1 FROM ItemSubstituteDetail x WHERE x.tenant_id = c.tenant_id AND x.Domain = '8010' AND x.ParentItem = b.item_number AND x.ItemNum = c.item_number AND x.SubstituteItem = g.icitem_number ); -- =========================================================================== -- 段 3:执行后对账(只读) -- =========================================================================== -- 对账 3.1:本批次导入行数(应 = 1147) SELECT COUNT(*) AS migrated_rows FROM ItemSubstituteDetail WHERE CreateUser = 'data-migration' AND Remark LIKE 'migrated from ic_bom_child Id=%, substitute_code=%' AND Domain = '8010' AND tenant_id = 797403760988229; -- 对账 3.2:ItemSubstituteDetail 总行数(应 = 1147,假设目标表初始为 0) SELECT COUNT(*) AS total_rows FROM ItemSubstituteDetail; -- 对账 3.3:按 Domain / SubstituteType / tenant_id 分组 SELECT Domain, SubstituteType, tenant_id, COUNT(*) AS row_cnt FROM ItemSubstituteDetail GROUP BY Domain, SubstituteType, tenant_id ORDER BY row_cnt DESC; -- =========================================================================== -- 段 4:回滚 SQL 草案 -- 默认全部注释,仅在需要完整回退本批次时人工评审后取消注释 -- 四重限定(CreateUser + Remark + Domain + tenant_id)避免误删人工维护数据 -- =========================================================================== -- 回滚 4.1:先 SELECT 预览要删除的行数(应 = 阶段 3 实际 INSERT 行数) -- SELECT COUNT(*) AS rollback_rows_preview -- FROM ItemSubstituteDetail -- WHERE CreateUser = 'data-migration' -- AND Remark LIKE 'migrated from ic_bom_child Id=%, substitute_code=%' -- AND Domain = '8010' -- AND tenant_id = 797403760988229; -- 回滚 4.2:实际 DELETE(仅当预览数对账无误时才取消注释) -- DELETE FROM ItemSubstituteDetail -- WHERE CreateUser = 'data-migration' -- AND Remark LIKE 'migrated from ic_bom_child Id=%, substitute_code=%' -- AND Domain = '8010' -- AND tenant_id = 797403760988229;