| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- -- S8-TENANT-ONLY-BATCH5:把 S8 规则执行主链的身份从 (Tenant, Factory) 收敛为 Tenant。
- --
- -- 本脚本与同批代码是**一个不可分割的单元**。单独跑任何一边都会出事:
- -- · 只改代码不迁数据 → 旧 active exception 仍是 T{t}:F{f}:R... 而新 detection 产出 T{t}:R...,
- -- 调度器会认为旧异常"不再命中"从而开始 recovery 计数,同时按新键再建一条重复异常;
- -- · 只迁数据不改代码 → 每次 tick 生成的仍是含 F 段的键,与库里刚改完的键对不上,同样重复建单。
- --
- -- 变更(DDL 部分在 MySQL 下会自动提交,脚本按"每步可独立重跑"设计,不依赖事务回滚):
- -- ① preflight:去掉 :F{factory} 段后是否产生键碰撞 —— 有则整脚本中止,不自动挑一条
- -- ② 归档受影响行的最小恢复字段
- -- ③ 重写 dedup_key:仅限严格匹配 ^T{数字}:F{数字}:R 的行
- -- ④ 复验:重写后 collision 必须为 0
- -- ⑤ ado_s8_rule_detection_state 唯一键 (tenant,factory,rule_code,dedup_key) → (tenant,rule_code,dedup_key)
- -- ⑥ ado_s8_watch_rule 新增 UNIQUE(tenant_id, rule_code)
- --
- -- 明确**不做**:
- -- · 不 DROP 任何 factory_id 列(本批做的是"移除隔离语义",不是 schema 美化)
- -- · 不动 ado_s8_exception 的 uk(tenant,factory,exception_code)(异常侧属 Batch 6)
- -- · 不动 ado_s8_exception_bak_1_0_487(那是 1.0.487 的还原点,重写它等于毁掉备份)
- -- · 不动 mdp_entity.dedup_key_path(数据中台的同名概念,与 S8 规则去重无关)
- --
- -- 迁移前实测(2026-09-06,本地隔离克隆 aidopdev_s8_local,非沿用历史数字):
- -- 旧格式行数 : exception 33 · detection_log 155 · detection_state 28
- -- 非旧格式行数 : exception 68(不得触碰)· detection_log 0 · detection_state 0
- -- 去 F 段后碰撞: exception 活动单 0 · detection_state 0
- -- watch_rule (tenant_id, rule_code) 重复组: 0
- -- ============================================================================
- -- ① PREFLIGHT:碰撞门禁
- --
- -- 去掉 F 段后若出现「同租户 + 同规则 + 同对象」的多个旧键映射到同一个新键,
- -- 说明历史上确实靠工厂区分过两条不同的异常。那种情况下自动挑一条留下会静默丢数据,
- -- 因此直接让脚本失败、交人工治理。
- -- ============================================================================
- SET @collision_exception := (
- SELECT COUNT(*) FROM (
- SELECT tenant_id, REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:') nk, COUNT(*) c
- FROM ado_s8_exception
- WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R' AND is_deleted = 0 AND status <> 'CLOSED'
- GROUP BY 1, 2 HAVING c > 1) z);
- SET @collision_state := (
- SELECT COUNT(*) FROM (
- SELECT tenant_id, rule_code, REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:') nk, COUNT(*) c
- FROM ado_s8_rule_detection_state
- WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R'
- GROUP BY 1, 2, 3 HAVING c > 1) z);
- SET @dup_watch_rule := (
- SELECT COUNT(*) FROM (
- SELECT tenant_id, rule_code, COUNT(*) c FROM ado_s8_watch_rule GROUP BY 1, 2 HAVING c > 1) z);
- -- 门禁实现说明:
- -- AutoVersionUpdate 明确拒绝含自定义语句分隔符指令的脚本("当前执行器不支持"),
- -- 而且它是对全文做子串匹配 —— 连注释里出现那个关键字都会被拦,
- -- 因此本文件通篇不出现该词。既然用不了存储过程 + SIGNAL,
- -- 改用**具名 CHECK 约束**:约束名本身携带失败原因,
- -- MySQL 的报错文案会带上它("Check constraint 'xxx' is violated"),
- -- 而该文案会被 AutoVersionUpdate 写进 sys_db_migration_log.error_message,
- -- 于是运维在日志里直接看得到"为什么被挡",不必回头翻脚本。
- -- 本版本随即被记为 Failed 并阻止应用启动 —— 迁移没过这件事不可能被忽略。
- -- 先把碰撞明细落一张诊断表:被挡住时运维需要知道**具体哪几个键**撞了,
- -- 只给一个计数会让人无从下手。
- DROP TABLE IF EXISTS ado_s8_tenant_only_collision_1_0_493;
- CREATE TABLE ado_s8_tenant_only_collision_1_0_493 (
- source_table VARCHAR(64) NOT NULL,
- tenant_id BIGINT NULL,
- rule_code VARCHAR(64) NULL,
- new_dedup_key VARCHAR(256) NULL,
- row_count INT NULL
- ) COMMENT 'S8-TENANT-ONLY-BATCH5 去 F 段后的 dedup_key 碰撞明细(为空表示无碰撞)';
- INSERT INTO ado_s8_tenant_only_collision_1_0_493 (source_table, tenant_id, rule_code, new_dedup_key, row_count)
- SELECT 'ado_s8_exception', tenant_id, source_rule_code,
- REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:'), COUNT(*)
- FROM ado_s8_exception
- WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R' AND is_deleted = 0 AND status <> 'CLOSED'
- GROUP BY 1, 2, 3, 4 HAVING COUNT(*) > 1;
- INSERT INTO ado_s8_tenant_only_collision_1_0_493 (source_table, tenant_id, rule_code, new_dedup_key, row_count)
- SELECT 'ado_s8_rule_detection_state', tenant_id, rule_code,
- REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:'), COUNT(*)
- FROM ado_s8_rule_detection_state
- WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R'
- GROUP BY 1, 2, 3, 4 HAVING COUNT(*) > 1;
- INSERT INTO ado_s8_tenant_only_collision_1_0_493 (source_table, tenant_id, rule_code, new_dedup_key, row_count)
- SELECT 'ado_s8_watch_rule_duplicate_identity', tenant_id, rule_code, NULL, COUNT(*)
- FROM ado_s8_watch_rule GROUP BY 1, 2, 3, 4 HAVING COUNT(*) > 1;
- -- 门禁本体:probe 非 0 即违反 CHECK,脚本在**任何写入之前**失败。
- DROP TABLE IF EXISTS tmp_s8_493_preflight_guard;
- CREATE TABLE tmp_s8_493_preflight_guard (
- probe INT NOT NULL,
- CONSTRAINT s8_tenant_only_BLOCKED_see_collision_table_1_0_493 CHECK (probe = 0)
- ) COMMENT 'S8-TENANT-ONLY-BATCH5 preflight 门禁;成功后自动删除';
- INSERT INTO tmp_s8_493_preflight_guard (probe)
- VALUES (@collision_exception + @collision_state + @dup_watch_rule);
- DROP TABLE IF EXISTS tmp_s8_493_preflight_guard;
- -- ============================================================================
- -- ② 归档
- --
- -- 只存恢复所需的最小字段,不整表复制:受影响的是 dedup_key 这一列,
- -- 备份整张 exception 表既慢又没有额外价值。
- -- INSERT IGNORE + 主键:脚本重跑时不会把已改写后的值再存一遍覆盖原值。
- -- ============================================================================
- CREATE TABLE IF NOT EXISTS ado_s8_exception_dedup_bak_1_0_493 (
- id BIGINT NOT NULL PRIMARY KEY,
- tenant_id BIGINT NULL,
- factory_id BIGINT NULL,
- source_rule_code VARCHAR(64) NULL,
- dedup_key VARCHAR(128) NULL
- ) COMMENT 'S8-TENANT-ONLY-BATCH5 dedup_key 迁移前快照';
- CREATE TABLE IF NOT EXISTS ado_s8_detection_log_dedup_bak_1_0_493 (
- id BIGINT NOT NULL PRIMARY KEY,
- tenant_id BIGINT NULL,
- factory_id BIGINT NULL,
- rule_code VARCHAR(64) NULL,
- dedup_key VARCHAR(256) NULL
- ) COMMENT 'S8-TENANT-ONLY-BATCH5 dedup_key 迁移前快照';
- CREATE TABLE IF NOT EXISTS ado_s8_rule_detection_state_dedup_bak_1_0_493 (
- id BIGINT NOT NULL PRIMARY KEY,
- tenant_id BIGINT NULL,
- factory_id BIGINT NULL,
- rule_code VARCHAR(64) NULL,
- dedup_key VARCHAR(128) NULL
- ) COMMENT 'S8-TENANT-ONLY-BATCH5 dedup_key 迁移前快照';
- INSERT IGNORE INTO ado_s8_exception_dedup_bak_1_0_493 (id, tenant_id, factory_id, source_rule_code, dedup_key)
- SELECT id, tenant_id, factory_id, source_rule_code, dedup_key
- FROM ado_s8_exception WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
- INSERT IGNORE INTO ado_s8_detection_log_dedup_bak_1_0_493 (id, tenant_id, factory_id, rule_code, dedup_key)
- SELECT id, tenant_id, factory_id, rule_code, dedup_key
- FROM ado_s8_detection_log WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
- INSERT IGNORE INTO ado_s8_rule_detection_state_dedup_bak_1_0_493 (id, tenant_id, factory_id, rule_code, dedup_key)
- SELECT id, tenant_id, factory_id, rule_code, dedup_key
- FROM ado_s8_rule_detection_state WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
- -- ============================================================================
- -- ③ 重写 dedup_key
- --
- -- 只改严格匹配 ^T{数字}:F{数字}:R 的行,且只去掉 :F{数字} 这一段 ——
- -- ⚠️ 反向引用必须写 $1:MySQL 的 REGEXP_REPLACE 用 ICU 语法,
- -- 写成 \1 不会被当作分组引用,而是被吞成字面量 "1" —— 租户前缀会整段丢失,
- -- 得到 "1:RRULE_..." 这种既不是旧格式也不是新格式的坏键。
- -- 本地隔离库首次执行时实测踩到过,已由归档表还原后修正。
- -- 不做全表 REPLACE(':F', ''):那会误伤任何恰好含 ":F" 的对象标识(例如 PO 号里带 :F)。
- -- 正则锚定行首 + 数字类,把改写面收敛到确实由旧 BuildDedupKey 生成的键。
- -- 幂等:改写后的行不再匹配该正则,重跑即 0 行受影响。
- -- ============================================================================
- UPDATE ado_s8_exception
- SET dedup_key = REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:')
- WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
- UPDATE ado_s8_detection_log
- SET dedup_key = REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:')
- WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
- UPDATE ado_s8_rule_detection_state
- SET dedup_key = REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:')
- WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
- -- ============================================================================
- -- ④ 复验:重写后不得残留旧格式、不得出现碰撞
- -- ============================================================================
- SET @left_over := (
- (SELECT COUNT(*) FROM ado_s8_exception WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R')
- + (SELECT COUNT(*) FROM ado_s8_detection_log WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R')
- + (SELECT COUNT(*) FROM ado_s8_rule_detection_state WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R'));
- SET @post_collision := (
- SELECT COUNT(*) FROM (
- SELECT tenant_id, rule_code, dedup_key, COUNT(*) c
- FROM ado_s8_rule_detection_state GROUP BY 1, 2, 3 HAVING c > 1) z);
- -- 与 preflight 同一机制:约束名即失败原因。
- DROP TABLE IF EXISTS tmp_s8_493_postcheck_guard;
- CREATE TABLE tmp_s8_493_postcheck_guard (
- probe INT NOT NULL,
- CONSTRAINT s8_tenant_only_POSTCHECK_leftover_or_collision_1_0_493 CHECK (probe = 0)
- ) COMMENT 'S8-TENANT-ONLY-BATCH5 改写后复验门禁;成功后自动删除';
- INSERT INTO tmp_s8_493_postcheck_guard (probe) VALUES (@left_over + @post_collision);
- DROP TABLE IF EXISTS tmp_s8_493_postcheck_guard;
- -- ============================================================================
- -- ⑤ ado_s8_rule_detection_state 唯一键去掉 factory_id
- --
- -- 必须与代码同批:upsert 的 WHERE 已改为 (tenant, rule_code, dedup_key),
- -- 而旧唯一键仍允许同键在不同 factory 下存两行 —— 抗抖计数会分裂,
- -- trigger/recover 阈值静默失效。
- -- 用 information_schema 判存在性:MySQL 无 DROP INDEX IF EXISTS。
- -- ============================================================================
- SET @has_old_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_rule_detection_state'
- AND INDEX_NAME = 'uk_s8_rule_detection_state_dedup');
- SET @sql := IF(@has_old_uk > 0,
- 'ALTER TABLE ado_s8_rule_detection_state DROP INDEX uk_s8_rule_detection_state_dedup', 'DO 0');
- PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
- SET @has_new_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_rule_detection_state'
- AND INDEX_NAME = 'uk_s8_rule_detection_state_dedup');
- SET @sql := IF(@has_new_uk = 0,
- 'ALTER TABLE ado_s8_rule_detection_state ADD UNIQUE INDEX uk_s8_rule_detection_state_dedup (tenant_id, rule_code, dedup_key)', 'DO 0');
- PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
- -- 辅助索引同步去掉 factory_id(非唯一,纯查询效率)。
- SET @has_idx := (SELECT COUNT(*) FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_rule_detection_state'
- AND INDEX_NAME = 'idx_s8_rule_detection_state_rule' AND COLUMN_NAME = 'factory_id');
- SET @sql := IF(@has_idx > 0,
- 'ALTER TABLE ado_s8_rule_detection_state DROP INDEX idx_s8_rule_detection_state_rule', 'DO 0');
- PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
- SET @has_idx2 := (SELECT COUNT(*) FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_rule_detection_state'
- AND INDEX_NAME = 'idx_s8_rule_detection_state_rule');
- SET @sql := IF(@has_idx2 = 0,
- 'ALTER TABLE ado_s8_rule_detection_state ADD INDEX idx_s8_rule_detection_state_rule (tenant_id, rule_code)', 'DO 0');
- PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
- -- ============================================================================
- -- ⑥ ado_s8_watch_rule 新增 UNIQUE(tenant_id, rule_code)
- --
- -- Batch 2 把运行策略身份定为 (TenantId, RuleCode),但当时只有应用层 AnyAsync 兜底,
- -- 因此只保证"顺序幂等",两个实例同时供给理论上仍可能建出重复行。
- -- 本索引把那个缺口在 DB 层封死。preflight 已确认无重复行。
- -- ============================================================================
- SET @has_rule_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_watch_rule'
- AND INDEX_NAME = 'uk_s8_watch_rule_tenant_code');
- SET @sql := IF(@has_rule_uk = 0,
- 'ALTER TABLE ado_s8_watch_rule ADD UNIQUE INDEX uk_s8_watch_rule_tenant_code (tenant_id, rule_code)', 'DO 0');
- PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|