1.0.539.sql 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. -- =====================================================================================
  2. -- 1.0.539 · S8 Stage-3 采购订单身份桥(Order Identity Propagation → MDP)
  3. --
  4. -- ── 背景 ────────────────────────────────────────────────────────────────────────────
  5. -- 采购的订单归因链在【源侧】已实证成立,但走的不是 PurOrdDetail.WorkOrd,而是 Req:
  6. -- PurOrdDetail.Req → srm_pr_main.pr_billno → srm_pr_main.pr_mono
  7. -- → WorkOrdMaster.WorkOrd → WorkOrdMaster.BusinessID (= crm_seorderentry.Id)
  8. -- → crm_seorderentry.seorder_id → crm_seorder.Id
  9. -- 实测:PurOrdDetail.Req 207/207 命中 pr_billno;89 条 PO 行归属 13 张真实销售订单。
  10. --
  11. -- 但标准层缺两段投影,导致 S8 无法在「只读中台」前提下完成同样归因:
  12. -- · mdp_std_purchase_request 没有 pr_mono / 订单行 Id
  13. -- · mdp_std_purchase_order 没有 Req
  14. --
  15. -- 本脚本补这两段,使下面这条链可以【全程只读 mdp_*】跑通:
  16. -- mdp_std_purchase_receipt(ord_nbr, ord_line)
  17. -- → mdp_std_purchase_order(po_no, po_line).purchase_request_no
  18. -- → mdp_std_purchase_request(pr_no).sales_order_entry_id
  19. -- → mdp_std_so(order_entry_id) → order_id
  20. --
  21. -- ── 为什么不回填源表 srm_pr_main.sentry_id ──────────────────────────────────────────
  22. -- 该字段 schema 存在但源侧 1012/1012 全为 NULL(贴源层亦 0 命中),说明生产代码从未写它。
  23. -- 本批改为在标准层用 pr_mono → mdp_std_work_order_schedule.sales_order_entry_id 派生,
  24. -- 因此【不需要写任何业务源表】。源侧补写 sentry_id 属独立的数据治理批次。
  25. --
  26. -- ── 本脚本不做 ──────────────────────────────────────────────────────────────────────
  27. -- 不建 dwd_procurement_kpi;不加 S3_PROCUREMENT_COMPLETE;不写采购完成时间;
  28. -- 不修 239 个 BusinessID=0 的历史/UAT 导入工单(登记为 DATA_QUALITY_KNOWN)。
  29. -- =====================================================================================
  30. -- ---------------------------------------------------------------------------
  31. -- ① mdp_std_purchase_request 补订单身份
  32. -- work_order = srm_pr_main.pr_mono(PR 所属工单号)
  33. -- sales_order_entry_id = 经 mdp_std_work_order_schedule 派生的订单行 Id
  34. -- ⚠️ 归因 Grain 必须保持 PR 行级:同一物料可跨多工单共享采购(实测 152 个物料跨工单),
  35. -- 但一条 PR 恒属单一工单(实测 pr_multi_wo=0),因此行级归因不会把共享采购
  36. -- 错并给某一张订单。
  37. -- ---------------------------------------------------------------------------
  38. SET @ddl := (SELECT IF(EXISTS(
  39. SELECT 1 FROM `information_schema`.`COLUMNS`
  40. WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_request'
  41. AND `COLUMN_NAME` = 'work_order'),
  42. 'SELECT 1',
  43. 'ALTER TABLE `mdp_std_purchase_request` ADD COLUMN `work_order` VARCHAR(64) NULL COMMENT ''PR 所属工单号(源 srm_pr_main.pr_mono);字面量 null 已归一为 NULL'' AFTER `pr_line`'));
  44. PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
  45. SET @ddl := (SELECT IF(EXISTS(
  46. SELECT 1 FROM `information_schema`.`COLUMNS`
  47. WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_request'
  48. AND `COLUMN_NAME` = 'sales_order_entry_id'),
  49. 'SELECT 1',
  50. 'ALTER TABLE `mdp_std_purchase_request` ADD COLUMN `sales_order_entry_id` BIGINT NULL COMMENT ''销售订单明细行Id = crm_seorderentry.Id;经 work_order 从 mdp_std_work_order_schedule 派生,未知时为 NULL'' AFTER `work_order`'));
  51. PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
  52. SET @ddl := (SELECT IF(EXISTS(
  53. SELECT 1 FROM `information_schema`.`STATISTICS`
  54. WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_request'
  55. AND `INDEX_NAME` = 'idx_std_pr_entry'),
  56. 'SELECT 1',
  57. 'ALTER TABLE `mdp_std_purchase_request` ADD INDEX `idx_std_pr_entry` (`tenant_id`, `sales_order_entry_id`)'));
  58. PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
  59. SET @ddl := (SELECT IF(EXISTS(
  60. SELECT 1 FROM `information_schema`.`STATISTICS`
  61. WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_request'
  62. AND `INDEX_NAME` = 'idx_std_pr_wo'),
  63. 'SELECT 1',
  64. 'ALTER TABLE `mdp_std_purchase_request` ADD INDEX `idx_std_pr_wo` (`tenant_id`, `work_order`)'));
  65. PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
  66. -- ---------------------------------------------------------------------------
  67. -- ② mdp_std_purchase_order 补 PR 指针
  68. -- purchase_request_no = PurOrdDetail.Req(PO 行 → PR 的 1:1 Authority)
  69. -- ⚠️ 这才是 Stage-3 的主归因列。既有 work_order 列【不可】作主归因:
  70. -- 它只在历史/UAT 批量导入的工单上有值,且 451 行中 208 行是字面量 'null'。
  71. -- ---------------------------------------------------------------------------
  72. SET @ddl := (SELECT IF(EXISTS(
  73. SELECT 1 FROM `information_schema`.`COLUMNS`
  74. WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_order'
  75. AND `COLUMN_NAME` = 'purchase_request_no'),
  76. 'SELECT 1',
  77. 'ALTER TABLE `mdp_std_purchase_order` ADD COLUMN `purchase_request_no` VARCHAR(64) NULL COMMENT ''采购申请单号(源 PurOrdDetail.Req)= mdp_std_purchase_request.pr_no;Stage-3 订单归因主键路径'' AFTER `po_line`'));
  78. PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
  79. SET @ddl := (SELECT IF(EXISTS(
  80. SELECT 1 FROM `information_schema`.`STATISTICS`
  81. WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_order'
  82. AND `INDEX_NAME` = 'idx_std_po_req'),
  83. 'SELECT 1',
  84. 'ALTER TABLE `mdp_std_purchase_order` ADD INDEX `idx_std_po_req` (`tenant_id`, `purchase_request_no`)'));
  85. PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
  86. -- ---------------------------------------------------------------------------
  87. -- ③ mdp_std_purchase_receipt 补桥索引
  88. -- 收货 → PO 的键已存在(ord_nbr / ord_line,源 PurOrdRctDetail.OrdNbr/OrdLine,
  89. -- 源侧实测 245/245 有值、240 命中 PurOrdDetail(PurOrd,Line)),只缺索引。
  90. -- 收货侧 raw_data 无 Req,故收货必须经 PO 才能到 PR,不可直连。
  91. -- ---------------------------------------------------------------------------
  92. SET @ddl := (SELECT IF(EXISTS(
  93. SELECT 1 FROM `information_schema`.`STATISTICS`
  94. WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_receipt'
  95. AND `INDEX_NAME` = 'idx_std_rct_po'),
  96. 'SELECT 1',
  97. 'ALTER TABLE `mdp_std_purchase_receipt` ADD INDEX `idx_std_rct_po` (`tenant_id`, `ord_nbr`, `ord_line`)'));
  98. PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s;
  99. -- ---------------------------------------------------------------------------
  100. -- ④ 清掉标准层的 tenant_id=0 遗留行(租户隔离缺陷)
  101. -- 实测:mdp_std_purchase_request 224 行、mdp_std_purchase_order 2 行 tenant_id=0,
  102. -- 全部来自单一历史批次 S3_MDP_FULL_20260810023343(2026-08-10,批次号无租户段,
  103. -- 属租户化改造前的旧格式)。贴源层 mdp_stg_supply_demand 侧 tenant_id=0 为 0 行,
  104. -- 故本次清理后正常跑批不会重新产生。
  105. -- 新的 Stage-3 身份桥绝不能建立在 tenant=0 的数据上 —— 无法确定租户的行一律删除,
  106. -- 不猜测、不补默认租户。同类先例:1.0.355 / 1.0.356 对 mdp_std_so 的处理。
  107. -- ---------------------------------------------------------------------------
  108. DELETE FROM `mdp_std_purchase_request` WHERE `tenant_id` IS NULL OR `tenant_id` <= 0;
  109. DELETE FROM `mdp_std_purchase_order` WHERE `tenant_id` IS NULL OR `tenant_id` <= 0;
  110. DELETE FROM `mdp_std_purchase_receipt` WHERE `tenant_id` IS NULL OR `tenant_id` <= 0;
  111. -- ---------------------------------------------------------------------------
  112. -- ⑤ 归一既有 work_order 的字面量 'null'
  113. -- 实测 451 行里 208 行存的是 4 字符字符串 "null"(JSON_UNQUOTE 对 JSON null
  114. -- 的既有行为)。留着它,任何按 work_order 的 JOIN 都会去匹配一个叫 "null"
  115. -- 的工单号并互相串起来。本列改造后只作参考、不作归因,但仍必须归一。
  116. -- ---------------------------------------------------------------------------
  117. UPDATE `mdp_std_purchase_order` SET `work_order` = NULL WHERE `work_order` IN ('null', '');
  118. -- ---------------------------------------------------------------------------
  119. -- ⑥ 一次性身份回填(全程只读 mdp_stg_* / mdp_std_*,不读任何源业务表)
  120. --
  121. -- 为什么要在迁移里回填,而不是等下一次跑批:
  122. -- · PR 侧转换不按批次收窄,下次跑批会自然覆盖全量;
  123. -- · 但 PO 侧转换按 `sync_batch_id=@BatchId` 收窄 + 每轮淘汰,
  124. -- 新列要等该租户下一次真实拉取才会被填上。
  125. -- 两者节奏不一致会让身份链在「已建列但半空」的状态里停留不确定的时间。
  126. -- 这里用贴源层已经落地的数据把三列一次性补齐,使身份链在迁移完成那一刻
  127. -- 即可被消费,且与转换逻辑同口径(同样的 JSON 路径、同样的 'null' 归一)。
  128. --
  129. -- 回填只写 mdp 标准层的三个身份列,不写任何业务字段、不造任何业务事实,
  130. -- 不涉及采购完成时间 / Stage-3 事件 / ActualHours。
  131. --
  132. -- 去重取 MAX:贴源层同时存在两代 source_biz_key(旧 `Domain:PurOrd:Line` 与
  133. -- 新的裸 RecID),同一逻辑行可能有多条贴源记录。按 (租户, source_biz_key)
  134. -- 聚合可保证结果确定,不依赖 MySQL 任取一行。
  135. -- ---------------------------------------------------------------------------
  136. -- ⑥-1 PR → 工单号(源 srm_pr_main.pr_mono)
  137. UPDATE `mdp_std_purchase_request` p
  138. JOIN ( SELECT g.`tenant_id`, g.`source_biz_key`,
  139. MAX(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(g.`raw_data`, '$.pr_mono')), 'null'), '')) AS `wo`
  140. FROM `mdp_stg_supply_demand` g
  141. WHERE g.`source_table` = 'srm_pr_main' AND g.`tenant_id` > 0
  142. GROUP BY g.`tenant_id`, g.`source_biz_key` ) s
  143. ON s.`tenant_id` = p.`tenant_id` AND s.`source_biz_key` = p.`source_biz_key`
  144. SET p.`work_order` = s.`wo`
  145. WHERE p.`tenant_id` > 0;
  146. -- ⑥-2 PR → 销售订单明细行 Id(经 1.0.532 建立的 S2 工单桥派生)
  147. -- 同租户同工单号在 mdp_std_work_order_schedule 内唯一(实测重复组 = 0),
  148. -- 故这里不会扇出、不需要再取一行。
  149. -- 未被 S2 跑批覆盖的工单保持 NULL —— 宁可为空,不猜订单。
  150. UPDATE `mdp_std_purchase_request` p
  151. JOIN `mdp_std_work_order_schedule` w
  152. ON w.`tenant_id` = p.`tenant_id` AND w.`work_order` = p.`work_order`
  153. SET p.`sales_order_entry_id` = w.`sales_order_entry_id`
  154. WHERE p.`tenant_id` > 0
  155. AND p.`work_order` IS NOT NULL
  156. AND w.`sales_order_entry_id` IS NOT NULL;
  157. -- ⑥-3 PO → 采购申请单号(源 PurOrdDetail.Req)
  158. UPDATE `mdp_std_purchase_order` o
  159. JOIN ( SELECT g.`tenant_id`, g.`source_biz_key`,
  160. MAX(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(g.`raw_data`, '$.Req')), 'null'), '')) AS `req`
  161. FROM `mdp_stg_purchase_order` g
  162. WHERE g.`source_table` = 'PurOrdDetail' AND g.`tenant_id` > 0
  163. GROUP BY g.`tenant_id`, g.`source_biz_key` ) s
  164. ON s.`tenant_id` = o.`tenant_id` AND s.`source_biz_key` = o.`source_biz_key`
  165. SET o.`purchase_request_no` = s.`req`
  166. WHERE o.`tenant_id` > 0;