-- ===================================================================================== -- 1.0.539 · S8 Stage-3 采购订单身份桥(Order Identity Propagation → MDP) -- -- ── 背景 ──────────────────────────────────────────────────────────────────────────── -- 采购的订单归因链在【源侧】已实证成立,但走的不是 PurOrdDetail.WorkOrd,而是 Req: -- PurOrdDetail.Req → srm_pr_main.pr_billno → srm_pr_main.pr_mono -- → WorkOrdMaster.WorkOrd → WorkOrdMaster.BusinessID (= crm_seorderentry.Id) -- → crm_seorderentry.seorder_id → crm_seorder.Id -- 实测:PurOrdDetail.Req 207/207 命中 pr_billno;89 条 PO 行归属 13 张真实销售订单。 -- -- 但标准层缺两段投影,导致 S8 无法在「只读中台」前提下完成同样归因: -- · mdp_std_purchase_request 没有 pr_mono / 订单行 Id -- · mdp_std_purchase_order 没有 Req -- -- 本脚本补这两段,使下面这条链可以【全程只读 mdp_*】跑通: -- mdp_std_purchase_receipt(ord_nbr, ord_line) -- → mdp_std_purchase_order(po_no, po_line).purchase_request_no -- → mdp_std_purchase_request(pr_no).sales_order_entry_id -- → mdp_std_so(order_entry_id) → order_id -- -- ── 为什么不回填源表 srm_pr_main.sentry_id ────────────────────────────────────────── -- 该字段 schema 存在但源侧 1012/1012 全为 NULL(贴源层亦 0 命中),说明生产代码从未写它。 -- 本批改为在标准层用 pr_mono → mdp_std_work_order_schedule.sales_order_entry_id 派生, -- 因此【不需要写任何业务源表】。源侧补写 sentry_id 属独立的数据治理批次。 -- -- ── 本脚本不做 ────────────────────────────────────────────────────────────────────── -- 不建 dwd_procurement_kpi;不加 S3_PROCUREMENT_COMPLETE;不写采购完成时间; -- 不修 239 个 BusinessID=0 的历史/UAT 导入工单(登记为 DATA_QUALITY_KNOWN)。 -- ===================================================================================== -- --------------------------------------------------------------------------- -- ① mdp_std_purchase_request 补订单身份 -- work_order = srm_pr_main.pr_mono(PR 所属工单号) -- sales_order_entry_id = 经 mdp_std_work_order_schedule 派生的订单行 Id -- ⚠️ 归因 Grain 必须保持 PR 行级:同一物料可跨多工单共享采购(实测 152 个物料跨工单), -- 但一条 PR 恒属单一工单(实测 pr_multi_wo=0),因此行级归因不会把共享采购 -- 错并给某一张订单。 -- --------------------------------------------------------------------------- SET @ddl := (SELECT IF(EXISTS( SELECT 1 FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_request' AND `COLUMN_NAME` = 'work_order'), 'SELECT 1', '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`')); PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; SET @ddl := (SELECT IF(EXISTS( SELECT 1 FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_request' AND `COLUMN_NAME` = 'sales_order_entry_id'), 'SELECT 1', '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`')); PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; SET @ddl := (SELECT IF(EXISTS( SELECT 1 FROM `information_schema`.`STATISTICS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_request' AND `INDEX_NAME` = 'idx_std_pr_entry'), 'SELECT 1', 'ALTER TABLE `mdp_std_purchase_request` ADD INDEX `idx_std_pr_entry` (`tenant_id`, `sales_order_entry_id`)')); PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; SET @ddl := (SELECT IF(EXISTS( SELECT 1 FROM `information_schema`.`STATISTICS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_request' AND `INDEX_NAME` = 'idx_std_pr_wo'), 'SELECT 1', 'ALTER TABLE `mdp_std_purchase_request` ADD INDEX `idx_std_pr_wo` (`tenant_id`, `work_order`)')); PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -- --------------------------------------------------------------------------- -- ② mdp_std_purchase_order 补 PR 指针 -- purchase_request_no = PurOrdDetail.Req(PO 行 → PR 的 1:1 Authority) -- ⚠️ 这才是 Stage-3 的主归因列。既有 work_order 列【不可】作主归因: -- 它只在历史/UAT 批量导入的工单上有值,且 451 行中 208 行是字面量 'null'。 -- --------------------------------------------------------------------------- SET @ddl := (SELECT IF(EXISTS( SELECT 1 FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_order' AND `COLUMN_NAME` = 'purchase_request_no'), 'SELECT 1', '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`')); PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; SET @ddl := (SELECT IF(EXISTS( SELECT 1 FROM `information_schema`.`STATISTICS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_order' AND `INDEX_NAME` = 'idx_std_po_req'), 'SELECT 1', 'ALTER TABLE `mdp_std_purchase_order` ADD INDEX `idx_std_po_req` (`tenant_id`, `purchase_request_no`)')); PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -- --------------------------------------------------------------------------- -- ③ mdp_std_purchase_receipt 补桥索引 -- 收货 → PO 的键已存在(ord_nbr / ord_line,源 PurOrdRctDetail.OrdNbr/OrdLine, -- 源侧实测 245/245 有值、240 命中 PurOrdDetail(PurOrd,Line)),只缺索引。 -- 收货侧 raw_data 无 Req,故收货必须经 PO 才能到 PR,不可直连。 -- --------------------------------------------------------------------------- SET @ddl := (SELECT IF(EXISTS( SELECT 1 FROM `information_schema`.`STATISTICS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = 'mdp_std_purchase_receipt' AND `INDEX_NAME` = 'idx_std_rct_po'), 'SELECT 1', 'ALTER TABLE `mdp_std_purchase_receipt` ADD INDEX `idx_std_rct_po` (`tenant_id`, `ord_nbr`, `ord_line`)')); PREPARE s FROM @ddl; EXECUTE s; DEALLOCATE PREPARE s; -- --------------------------------------------------------------------------- -- ④ 清掉标准层的 tenant_id=0 遗留行(租户隔离缺陷) -- 实测:mdp_std_purchase_request 224 行、mdp_std_purchase_order 2 行 tenant_id=0, -- 全部来自单一历史批次 S3_MDP_FULL_20260810023343(2026-08-10,批次号无租户段, -- 属租户化改造前的旧格式)。贴源层 mdp_stg_supply_demand 侧 tenant_id=0 为 0 行, -- 故本次清理后正常跑批不会重新产生。 -- 新的 Stage-3 身份桥绝不能建立在 tenant=0 的数据上 —— 无法确定租户的行一律删除, -- 不猜测、不补默认租户。同类先例:1.0.355 / 1.0.356 对 mdp_std_so 的处理。 -- --------------------------------------------------------------------------- DELETE FROM `mdp_std_purchase_request` WHERE `tenant_id` IS NULL OR `tenant_id` <= 0; DELETE FROM `mdp_std_purchase_order` WHERE `tenant_id` IS NULL OR `tenant_id` <= 0; DELETE FROM `mdp_std_purchase_receipt` WHERE `tenant_id` IS NULL OR `tenant_id` <= 0; -- --------------------------------------------------------------------------- -- ⑤ 归一既有 work_order 的字面量 'null' -- 实测 451 行里 208 行存的是 4 字符字符串 "null"(JSON_UNQUOTE 对 JSON null -- 的既有行为)。留着它,任何按 work_order 的 JOIN 都会去匹配一个叫 "null" -- 的工单号并互相串起来。本列改造后只作参考、不作归因,但仍必须归一。 -- --------------------------------------------------------------------------- UPDATE `mdp_std_purchase_order` SET `work_order` = NULL WHERE `work_order` IN ('null', ''); -- --------------------------------------------------------------------------- -- ⑥ 一次性身份回填(全程只读 mdp_stg_* / mdp_std_*,不读任何源业务表) -- -- 为什么要在迁移里回填,而不是等下一次跑批: -- · PR 侧转换不按批次收窄,下次跑批会自然覆盖全量; -- · 但 PO 侧转换按 `sync_batch_id=@BatchId` 收窄 + 每轮淘汰, -- 新列要等该租户下一次真实拉取才会被填上。 -- 两者节奏不一致会让身份链在「已建列但半空」的状态里停留不确定的时间。 -- 这里用贴源层已经落地的数据把三列一次性补齐,使身份链在迁移完成那一刻 -- 即可被消费,且与转换逻辑同口径(同样的 JSON 路径、同样的 'null' 归一)。 -- -- 回填只写 mdp 标准层的三个身份列,不写任何业务字段、不造任何业务事实, -- 不涉及采购完成时间 / Stage-3 事件 / ActualHours。 -- -- 去重取 MAX:贴源层同时存在两代 source_biz_key(旧 `Domain:PurOrd:Line` 与 -- 新的裸 RecID),同一逻辑行可能有多条贴源记录。按 (租户, source_biz_key) -- 聚合可保证结果确定,不依赖 MySQL 任取一行。 -- --------------------------------------------------------------------------- -- ⑥-1 PR → 工单号(源 srm_pr_main.pr_mono) UPDATE `mdp_std_purchase_request` p JOIN ( SELECT g.`tenant_id`, g.`source_biz_key`, MAX(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(g.`raw_data`, '$.pr_mono')), 'null'), '')) AS `wo` FROM `mdp_stg_supply_demand` g WHERE g.`source_table` = 'srm_pr_main' AND g.`tenant_id` > 0 GROUP BY g.`tenant_id`, g.`source_biz_key` ) s ON s.`tenant_id` = p.`tenant_id` AND s.`source_biz_key` = p.`source_biz_key` SET p.`work_order` = s.`wo` WHERE p.`tenant_id` > 0; -- ⑥-2 PR → 销售订单明细行 Id(经 1.0.532 建立的 S2 工单桥派生) -- 同租户同工单号在 mdp_std_work_order_schedule 内唯一(实测重复组 = 0), -- 故这里不会扇出、不需要再取一行。 -- 未被 S2 跑批覆盖的工单保持 NULL —— 宁可为空,不猜订单。 UPDATE `mdp_std_purchase_request` p JOIN `mdp_std_work_order_schedule` w ON w.`tenant_id` = p.`tenant_id` AND w.`work_order` = p.`work_order` SET p.`sales_order_entry_id` = w.`sales_order_entry_id` WHERE p.`tenant_id` > 0 AND p.`work_order` IS NOT NULL AND w.`sales_order_entry_id` IS NOT NULL; -- ⑥-3 PO → 采购申请单号(源 PurOrdDetail.Req) UPDATE `mdp_std_purchase_order` o JOIN ( SELECT g.`tenant_id`, g.`source_biz_key`, MAX(NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(g.`raw_data`, '$.Req')), 'null'), '')) AS `req` FROM `mdp_stg_purchase_order` g WHERE g.`source_table` = 'PurOrdDetail' AND g.`tenant_id` > 0 GROUP BY g.`tenant_id`, g.`source_biz_key` ) s ON s.`tenant_id` = o.`tenant_id` AND s.`source_biz_key` = o.`source_biz_key` SET o.`purchase_request_no` = s.`req` WHERE o.`tenant_id` > 0;