| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- -- =====================================================================================
- -- 1.0.520 · S1 · mdp_stg_so.uk_source_key 补 tenant_id,关闭跨租户互相覆盖的写入路径
- --
- -- ── 缺陷 ────────────────────────────────────────────────────────────────────────────
- -- mdp_stg_so 自 1.0.130 建表起,唯一键就是
- -- uk_source_key (source_system, source_table, source_biz_key) ← 不含 tenant_id
- -- 而同批建的标准层 mdp_std_so 是
- -- uk_source_key (tenant_id, source_system, source_table, source_biz_key)
- -- 两层口径不一致,且贴源层是「少了」隔离列的那一层。
- --
- -- 贴源写入走 DataPlatform/Executors/MdpStagingWriter.cs 的
- -- INSERT ... ON DUPLICATE KEY UPDATE ... raw_data=VALUES(raw_data), tenant_id=VALUES(tenant_id)
- -- 冲突判定用的就是上面这个唯一键。三个键列全都与租户无关:
- -- · source_system = mdp_source.source_code,实测全部 tenant_id=0 的全局行(AIDOPDEV_MYSQL 等),不区分租户
- -- · source_table = 源表名,全租户同名
- -- · source_biz_key= mdp_entity.biz_key_expr 取值后用 # 连接,取的是业务单号,本身不含租户
- -- ⇒ 租户 A 的 SO001#1 与租户 B 的 SO001#1 落在同一行。后写者覆盖 raw_data,
- -- 并且把 tenant_id 一起改写成自己 —— 该行的租户归属被静默篡改,A 的数据无痕消失。
- --
- -- ── 为什么这不是「理论风险」────────────────────────────────────────────────────────
- -- ① 单号生成器本身就是每租户各自从 0001 起编的:
- -- Order/SeOrderService.cs:379 GenerateBillNoAsync = "SO" + yyyyMMdd + 4 位流水,
- -- 取最大值时带 `u.TenantId == tenantId` 过滤,前缀里没有任何租户分量。
- -- ⇒ 任意两个租户在同一天各自建的第一张订单,bill_no 必然同为 SO<yyyymmdd>0001。
- -- entry_seq 是 1 起的行号(SeOrderService.cs:339),⇒ biz_key 必然同为 SO<yyyymmdd>0001#1。
- -- ② 源表没有任何唯一约束兜底:crm_seorder / crm_seorderentry 实测只有 PRIMARY(Id)。
- -- ③ 实测已经存在同号跨租户行:ado_contract_review 的 BillNo='CR2026040001' 有 3 行,
- -- 分属 tenant_id = NULL / 1300000000001 / 797403760988229,而该实体的
- -- biz_key_expr 就是单独一个 'BillNo'。
- -- ④ mdp_stg_so 早已是多租户共存:实测 4 个租户、crm_seorderentry 与 crm_seorder
- -- 两张源表各自都横跨全部 4 个租户,共用 source_system='AIDOPDEV_MYSQL'。
- --
- -- ⚠️ 「今天查不到重复」不能作为安全证据:唯一键本身就使重复无法存在,
- -- 覆盖是就地发生的,不留任何痕迹。要判的是结构契约,不是当前数据。
- --
- -- ── 为什么不需要先去重 ──────────────────────────────────────────────────────────────
- -- 本批是把唯一键「加宽」(3 列 → 4 列)。任何在 (ss,st,sbk) 上唯一的行集,
- -- 在 (tenant_id,ss,st,sbk) 上必然也唯一 —— 加宽只会放松约束,不可能新增冲突。
- -- 故与 1.0.356 那次「同宽度重排 + 清脏租户」不同,本批的 ALTER 不存在因存量行失败的可能。
- -- 实测双库均为 0 组冲突(aidopdev 39583 行 / aidopdev_s8_local 42122 行)。
- -- 下面第 1 步的去重因此在两个库上都是 0 行的空操作,保留它只为覆盖一种情况:
- -- 某个库的 uk_source_key 曾被人手工 DROP 掉(此时存量可能已有重复,直接 ADD UNIQUE 会失败)。
- --
- -- ── 为什么第 2 步必须是「一条 ALTER」────────────────────────────────────────────────
- -- AutoVersionUpdate 按 ; 拆句逐条执行且全程无事务(Update/AutoVersionUpdate.cs)。
- -- 若拆成 DROP INDEX 与 ADD UNIQUE 两句,中间一旦失败,表会停在「完全没有唯一键」的状态,
- -- 比原缺陷更危险。合成一条 ALTER 后,该步要么整体成功、要么整体不发生。
- -- 索引名保持 uk_source_key 不变:1.0.356 里的 IGNORE INDEX (`uk_source_key`) 仍需按名命中,
- -- 且与 mdp_std_so 的同名键保持对称。
- --
- -- ── 不在本批范围 ────────────────────────────────────────────────────────────────────
- -- mdp_stg_ship_trans 由 1.0.130 用 CREATE TABLE ... LIKE mdp_stg_so 建出,继承了同一缺陷,
- -- 实测 986 行横跨 5 个租户。本批不动它:其上下游消费方未在本次审计范围内,
- -- 加宽唯一键会改变它的 upsert 归并行为,须单独评估后另开批次。
- -- =====================================================================================
- SET @tbl := (SELECT COUNT(*) FROM information_schema.TABLES
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mdp_stg_so');
- -- 当前 uk_source_key 的列序列。索引不存在时取空串,便于下面三种分支互斥判定。
- SET @uk := IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
- FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mdp_stg_so'
- AND INDEX_NAME = 'uk_source_key'), '');
- -- ── 1 · 防御性去重(唯一键已在位时恒为 0 行,见上方论证)────────────────────────────
- -- 每个 (tenant_id, source_system, source_table, source_biz_key) 组只保留最新一行,
- -- 口径与 1.0.356 一致:update_time DESC, id DESC。
- -- 不用 IGNORE INDEX:本步恰恰要覆盖「索引已被人工删除」的库,此时该 hint 会直接报错。
- SET @sql := IF(
- @tbl = 1 AND @uk <> 'tenant_id,source_system,source_table,source_biz_key',
- 'DELETE FROM `mdp_stg_so` WHERE `id` IN (
- SELECT `id` FROM (
- SELECT `id`,
- ROW_NUMBER() OVER (
- PARTITION BY `tenant_id`,`source_system`,`source_table`,`source_biz_key`
- ORDER BY `update_time` DESC,`id` DESC) AS `rn`
- FROM `mdp_stg_so`
- ) duplicate_rows
- WHERE `rn` > 1
- )',
- 'DO 0');
- PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
- -- ── 2 · 旧的三列窄键 → 四列租户安全键(单条 ALTER,原子)────────────────────────────
- SET @sql := IF(
- @tbl = 1 AND @uk = 'source_system,source_table,source_biz_key',
- 'ALTER TABLE `mdp_stg_so`
- DROP INDEX `uk_source_key`,
- ADD UNIQUE KEY `uk_source_key` (`tenant_id`,`source_system`,`source_table`,`source_biz_key`)',
- 'DO 0');
- PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
- -- ── 3 · 索引整个不存在(曾被人工 DROP)→ 直接补建 ───────────────────────────────────
- -- 走到这里时第 1 步已经把存量去重过,ADD UNIQUE 不会因重复失败。
- SET @sql := IF(
- @tbl = 1 AND @uk = '',
- 'ALTER TABLE `mdp_stg_so`
- ADD UNIQUE KEY `uk_source_key` (`tenant_id`,`source_system`,`source_table`,`source_biz_key`)',
- 'DO 0');
- PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
- -- 幂等性:重跑时 @uk 已是四列目标值 ⇒ 三步全部落 'DO 0',不改任何东西。
- -- ── 回滚(注释,非可执行)──
- -- 回滚会重新打开跨租户覆盖路径,且此前已按租户分开落库的行会在窄键下冲突、导致 ALTER 失败,
- -- 需先按 (source_system, source_table, source_biz_key) 去重才能退回。故此处不提供可直接执行的回滚。
- -- ALTER TABLE `mdp_stg_so`
- -- DROP INDEX `uk_source_key`,
- -- ADD UNIQUE KEY `uk_source_key` (`source_system`,`source_table`,`source_biz_key`)
|