Explorar o código

fix(s0): migrate material substitution data

将 ic_bom × ic_bom_child × ic_substitute_group_detail 三表 INNER JOIN
排除原料自身(icitem_number <> item_number)后导入 ItemSubstituteDetail。

- 新增 UpdateScripts/1.0.132.sql:执行前 5 项校验 + INSERT SELECT + 对账 + 回滚草案
- 字段映射:ParentItem=ic_bom.item_number / ItemNum=ic_bom_child.item_number
  / SubstituteItem=ic_substitute_group_detail.icitem_number
  / SubstituteQty=replace_qty / SubstituteType='替代' / Domain='8010'
- 实测落库 1147 行,前端「物料替代」页面 total=1147 + 表格正常显示
- bump server version 1.0.131 → 1.0.132(跳过远端已用的 1.0.130/131)
YY968XX hai 1 mes
pai
achega
10e32b818a

+ 6 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.131</AssemblyVersion>
-    <FileVersion>1.0.131</FileVersion>
-    <Version>1.0.131</Version>
+    <AssemblyVersion>1.0.132</AssemblyVersion>
+    <FileVersion>1.0.132</FileVersion>
+    <Version>1.0.132</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -85,6 +85,9 @@
     <None Update="UpdateScripts\1.0.131.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.132.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
   </ItemGroup>
 
   <ItemGroup>

+ 182 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.132.sql

@@ -0,0 +1,182 @@
+-- 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;