-- ===================================================================================== -- 1.0.525 · S3 · 让 mdp_std_purchase_order 恢复「源的镜像」语义(Rule01 Authority 整改) -- -- ── 缺陷 ──────────────────────────────────────────────────────────────────────────── -- 标准层的 INSERT 不带任何批次条件,每轮重读整张贴源表;贴源层又是纯 upsert、从不淘汰。 -- 于是源侧已删除的采购订单行会: -- ① 永远留在 mdp_stg_purchase_order; -- ② 每轮被重新物化进 mdp_std_purchase_order,并被盖上最新的 sync_batch_id -- —— 所以「看时间戳」根本认不出它们是陈旧数据。 -- -- 实测(aidopdev,2026-09-08)租户 797403760988229: -- 源侧 PurOrdDetail 182 行;贴源最新批次 182 行(与源逐行相等); -- 标准层 518 行,其中 336 行源侧已不存在(64.9%),且 518 行共享同一个批次号。 -- DWD 是标准层的 1:1 复制(518/518,两侧差集均为 0)—— -- 即孤儿首现于标准层,不是 DWD 也不是 Provider。 -- -- 另有身份断裂放大了它:source_biz_key 的格式与 source_system 都换过代 -- (旧 `Domain:PurOrd:Line` + AIDOP → 新的裸 RecID + AIDOPDEV_MYSQL), -- 而贴源唯一键是 (source_system, source_table, source_biz_key), -- 两代永远不会互相 ODKU 覆盖,因此并存至今(实测 568 行旧格式 / 258 行新格式)。 -- -- ── 影响面 ────────────────────────────────────────────────────────────────────────── -- S8 Rule01(采购交付延期预警)读 dwd_supplier_delivery 的最新快照。 -- 若按其判据评估租户 797 当前快照:483 行命中,其中 335 行(69%)对应的采购订单行 -- 在源侧已不存在 —— 启用即凭空建出 335 条永不恢复的异常。 -- -- ── 修法 ──────────────────────────────────────────────────────────────────────────── -- 代码侧(本迁移的前提,随同一版本发布): -- · 标准层 INSERT 按 @BatchId 收窄驱动行; -- · 每轮追加淘汰步骤,删掉本租户下批次号不等于 @BatchId 的行; -- · DWD 追加同日淘汰,使 MAX(stat_date) 取到的必然是单一批次。 -- 本脚本负责两件代码做不到的事: -- ① 把两个实体的 sync_mode 由 INCR 改为 FULL(下方 ① 的理由); -- ② 一次性清掉已经沉淀的孤儿(代码只能阻止新增,删不掉存量)。 -- ===================================================================================== -- ── ① 让声明与实际行为一致:这两个实体每轮都必须全量重读 ───────────────────────────── -- -- 「不在本批次里」只有在**每轮都是全量重读**时才等于「源侧已删除」。 -- S3 的全量作业确实以 fullRefresh:true 调用,但 RunInboundAsync 与 MdpHotWatchService 不是, -- 而贴源层里已经存在这样一个部分批次(`hot-20260813003331`,3 行)—— -- 若它恰好是最新批次,按批次淘汰会把该租户的标准层几乎删空。 -- 只有 sync_mode='FULL' 能让 MdpSyncWindowResolver 在**所有**调用路径上强制 FullRefresh=true。 -- -- 只改这两个正式实体;*_API 与 *_SQLSERVER 变体当前未被调度,保持原样不动。 UPDATE `mdp_entity` SET `sync_mode` = 'FULL', `incr_column` = NULL, `update_time` = NOW() WHERE `entity_code` IN ('S3_PURCHASE_ORDER_MASTER', 'S3_PURCHASE_ORDER_DETAIL') AND `sync_mode` <> 'FULL'; -- ── ② 一次性清除标准层已沉淀的孤儿 ────────────────────────────────────────────────── -- -- 判据:该 (tenant_id, po_no, po_line) 在源表 PurOrdDetail 里已不存在。 -- 直接对源表求证而不是对贴源层 —— 贴源层本身也在累积,拿它当基准会漏掉一部分。 -- -- ⚠️ 安全闸门 —— 「该租户在源表里确实有数据」: -- 若某租户在 PurOrdDetail 里一行都没有(从未接入 / 源侧被清空), -- 没有这道闸门就会把该租户的标准层整个删空。这里宁可不删,也不冒清空的风险。 -- (源侧合法清空的场景由代码侧每轮淘汰处理,它用 mdp_sync_log 的成功记录做闸门, -- 能区分「拉取成功且确实 0 行」与「拉取没跑成」。) -- -- 规模:实测全表 787 行、孤儿 339 行,DELETE 走 uk_po_line 主键回表,毫秒级。 DELETE s FROM `mdp_std_purchase_order` s WHERE EXISTS (SELECT 1 FROM `PurOrdDetail` g WHERE g.`tenant_id` = s.`tenant_id`) AND NOT EXISTS ( SELECT 1 FROM `PurOrdDetail` d WHERE d.`tenant_id` = s.`tenant_id` AND d.`PurOrd` = s.`po_no` AND CAST(d.`Line` AS CHAR) = s.`po_line`); -- ── ③ 清除 DWD 当前快照里已失去标准层依据的行 ─────────────────────────────────────── -- -- Rule01 只读每租户 stat_date 最大的那一天。②之后标准层已经干净, -- 但当天的 DWD 快照仍留着孤儿 —— 代码侧的同日淘汰要等下一轮跑批才会生效, -- 而部署后规则可能先于跑批被启用。这里把当前快照一次性对齐。 -- -- 只处理每租户的最新 stat_date,不动历史快照(历史是留档,Provider 不读)。 -- 用派生表而不是相关子查询:dwd_supplier_delivery 实测 41,422 行, -- 相关子查询在该表上已多次超时(历史上有 600s 迁移超时的先例)。 DELETE w FROM `dwd_supplier_delivery` w JOIN (SELECT `tenant_id`, MAX(`stat_date`) AS `latest_stat_date` FROM `dwd_supplier_delivery` GROUP BY `tenant_id`) t ON t.`tenant_id` = w.`tenant_id` AND t.`latest_stat_date` = w.`stat_date` WHERE NOT EXISTS ( SELECT 1 FROM `mdp_std_purchase_order` s WHERE s.`tenant_id` = w.`tenant_id` AND s.`po_no` = w.`po_no` AND s.`po_line` = w.`po_line`); -- ── 回滚(注释,非可执行)── -- UPDATE mdp_entity SET sync_mode='INCR', incr_column='UpdateTime' -- WHERE entity_code IN ('S3_PURCHASE_ORDER_MASTER','S3_PURCHASE_ORDER_DETAIL'); -- 被删除的孤儿不做回滚:它们对应的采购订单行在源侧已不存在, -- 重建方式是重跑 S3 全量跑批(标准层与 DWD 均为纯派生)。