1.0.493.sql 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. -- S8-TENANT-ONLY-BATCH5:把 S8 规则执行主链的身份从 (Tenant, Factory) 收敛为 Tenant。
  2. --
  3. -- 本脚本与同批代码是**一个不可分割的单元**。单独跑任何一边都会出事:
  4. -- · 只改代码不迁数据 → 旧 active exception 仍是 T{t}:F{f}:R... 而新 detection 产出 T{t}:R...,
  5. -- 调度器会认为旧异常"不再命中"从而开始 recovery 计数,同时按新键再建一条重复异常;
  6. -- · 只迁数据不改代码 → 每次 tick 生成的仍是含 F 段的键,与库里刚改完的键对不上,同样重复建单。
  7. --
  8. -- 变更(DDL 部分在 MySQL 下会自动提交,脚本按"每步可独立重跑"设计,不依赖事务回滚):
  9. -- ① preflight:去掉 :F{factory} 段后是否产生键碰撞 —— 有则整脚本中止,不自动挑一条
  10. -- ② 归档受影响行的最小恢复字段
  11. -- ③ 重写 dedup_key:仅限严格匹配 ^T{数字}:F{数字}:R 的行
  12. -- ④ 复验:重写后 collision 必须为 0
  13. -- ⑤ ado_s8_rule_detection_state 唯一键 (tenant,factory,rule_code,dedup_key) → (tenant,rule_code,dedup_key)
  14. -- ⑥ ado_s8_watch_rule 新增 UNIQUE(tenant_id, rule_code)
  15. --
  16. -- 明确**不做**:
  17. -- · 不 DROP 任何 factory_id 列(本批做的是"移除隔离语义",不是 schema 美化)
  18. -- · 不动 ado_s8_exception 的 uk(tenant,factory,exception_code)(异常侧属 Batch 6)
  19. -- · 不动 ado_s8_exception_bak_1_0_487(那是 1.0.487 的还原点,重写它等于毁掉备份)
  20. -- · 不动 mdp_entity.dedup_key_path(数据中台的同名概念,与 S8 规则去重无关)
  21. --
  22. -- 迁移前实测(2026-09-06,本地隔离克隆 aidopdev_s8_local,非沿用历史数字):
  23. -- 旧格式行数 : exception 33 · detection_log 155 · detection_state 28
  24. -- 非旧格式行数 : exception 68(不得触碰)· detection_log 0 · detection_state 0
  25. -- 去 F 段后碰撞: exception 活动单 0 · detection_state 0
  26. -- watch_rule (tenant_id, rule_code) 重复组: 0
  27. -- ============================================================================
  28. -- ① PREFLIGHT:碰撞门禁
  29. --
  30. -- 去掉 F 段后若出现「同租户 + 同规则 + 同对象」的多个旧键映射到同一个新键,
  31. -- 说明历史上确实靠工厂区分过两条不同的异常。那种情况下自动挑一条留下会静默丢数据,
  32. -- 因此直接让脚本失败、交人工治理。
  33. -- ============================================================================
  34. SET @collision_exception := (
  35. SELECT COUNT(*) FROM (
  36. SELECT tenant_id, REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:') nk, COUNT(*) c
  37. FROM ado_s8_exception
  38. WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R' AND is_deleted = 0 AND status <> 'CLOSED'
  39. GROUP BY 1, 2 HAVING c > 1) z);
  40. SET @collision_state := (
  41. SELECT COUNT(*) FROM (
  42. SELECT tenant_id, rule_code, REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:') nk, COUNT(*) c
  43. FROM ado_s8_rule_detection_state
  44. WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R'
  45. GROUP BY 1, 2, 3 HAVING c > 1) z);
  46. SET @dup_watch_rule := (
  47. SELECT COUNT(*) FROM (
  48. SELECT tenant_id, rule_code, COUNT(*) c FROM ado_s8_watch_rule GROUP BY 1, 2 HAVING c > 1) z);
  49. -- 门禁实现说明:
  50. -- AutoVersionUpdate 明确拒绝含自定义语句分隔符指令的脚本("当前执行器不支持"),
  51. -- 而且它是对全文做子串匹配 —— 连注释里出现那个关键字都会被拦,
  52. -- 因此本文件通篇不出现该词。既然用不了存储过程 + SIGNAL,
  53. -- 改用**具名 CHECK 约束**:约束名本身携带失败原因,
  54. -- MySQL 的报错文案会带上它("Check constraint 'xxx' is violated"),
  55. -- 而该文案会被 AutoVersionUpdate 写进 sys_db_migration_log.error_message,
  56. -- 于是运维在日志里直接看得到"为什么被挡",不必回头翻脚本。
  57. -- 本版本随即被记为 Failed 并阻止应用启动 —— 迁移没过这件事不可能被忽略。
  58. -- 先把碰撞明细落一张诊断表:被挡住时运维需要知道**具体哪几个键**撞了,
  59. -- 只给一个计数会让人无从下手。
  60. DROP TABLE IF EXISTS ado_s8_tenant_only_collision_1_0_493;
  61. CREATE TABLE ado_s8_tenant_only_collision_1_0_493 (
  62. source_table VARCHAR(64) NOT NULL,
  63. tenant_id BIGINT NULL,
  64. rule_code VARCHAR(64) NULL,
  65. new_dedup_key VARCHAR(256) NULL,
  66. row_count INT NULL
  67. ) COMMENT 'S8-TENANT-ONLY-BATCH5 去 F 段后的 dedup_key 碰撞明细(为空表示无碰撞)';
  68. INSERT INTO ado_s8_tenant_only_collision_1_0_493 (source_table, tenant_id, rule_code, new_dedup_key, row_count)
  69. SELECT 'ado_s8_exception', tenant_id, source_rule_code,
  70. REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:'), COUNT(*)
  71. FROM ado_s8_exception
  72. WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R' AND is_deleted = 0 AND status <> 'CLOSED'
  73. GROUP BY 1, 2, 3, 4 HAVING COUNT(*) > 1;
  74. INSERT INTO ado_s8_tenant_only_collision_1_0_493 (source_table, tenant_id, rule_code, new_dedup_key, row_count)
  75. SELECT 'ado_s8_rule_detection_state', tenant_id, rule_code,
  76. REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:'), COUNT(*)
  77. FROM ado_s8_rule_detection_state
  78. WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R'
  79. GROUP BY 1, 2, 3, 4 HAVING COUNT(*) > 1;
  80. INSERT INTO ado_s8_tenant_only_collision_1_0_493 (source_table, tenant_id, rule_code, new_dedup_key, row_count)
  81. SELECT 'ado_s8_watch_rule_duplicate_identity', tenant_id, rule_code, NULL, COUNT(*)
  82. FROM ado_s8_watch_rule GROUP BY 1, 2, 3, 4 HAVING COUNT(*) > 1;
  83. -- 门禁本体:probe 非 0 即违反 CHECK,脚本在**任何写入之前**失败。
  84. DROP TABLE IF EXISTS tmp_s8_493_preflight_guard;
  85. CREATE TABLE tmp_s8_493_preflight_guard (
  86. probe INT NOT NULL,
  87. CONSTRAINT s8_tenant_only_BLOCKED_see_collision_table_1_0_493 CHECK (probe = 0)
  88. ) COMMENT 'S8-TENANT-ONLY-BATCH5 preflight 门禁;成功后自动删除';
  89. INSERT INTO tmp_s8_493_preflight_guard (probe)
  90. VALUES (@collision_exception + @collision_state + @dup_watch_rule);
  91. DROP TABLE IF EXISTS tmp_s8_493_preflight_guard;
  92. -- ============================================================================
  93. -- ② 归档
  94. --
  95. -- 只存恢复所需的最小字段,不整表复制:受影响的是 dedup_key 这一列,
  96. -- 备份整张 exception 表既慢又没有额外价值。
  97. -- INSERT IGNORE + 主键:脚本重跑时不会把已改写后的值再存一遍覆盖原值。
  98. -- ============================================================================
  99. CREATE TABLE IF NOT EXISTS ado_s8_exception_dedup_bak_1_0_493 (
  100. id BIGINT NOT NULL PRIMARY KEY,
  101. tenant_id BIGINT NULL,
  102. factory_id BIGINT NULL,
  103. source_rule_code VARCHAR(64) NULL,
  104. dedup_key VARCHAR(128) NULL
  105. ) COMMENT 'S8-TENANT-ONLY-BATCH5 dedup_key 迁移前快照';
  106. CREATE TABLE IF NOT EXISTS ado_s8_detection_log_dedup_bak_1_0_493 (
  107. id BIGINT NOT NULL PRIMARY KEY,
  108. tenant_id BIGINT NULL,
  109. factory_id BIGINT NULL,
  110. rule_code VARCHAR(64) NULL,
  111. dedup_key VARCHAR(256) NULL
  112. ) COMMENT 'S8-TENANT-ONLY-BATCH5 dedup_key 迁移前快照';
  113. CREATE TABLE IF NOT EXISTS ado_s8_rule_detection_state_dedup_bak_1_0_493 (
  114. id BIGINT NOT NULL PRIMARY KEY,
  115. tenant_id BIGINT NULL,
  116. factory_id BIGINT NULL,
  117. rule_code VARCHAR(64) NULL,
  118. dedup_key VARCHAR(128) NULL
  119. ) COMMENT 'S8-TENANT-ONLY-BATCH5 dedup_key 迁移前快照';
  120. INSERT IGNORE INTO ado_s8_exception_dedup_bak_1_0_493 (id, tenant_id, factory_id, source_rule_code, dedup_key)
  121. SELECT id, tenant_id, factory_id, source_rule_code, dedup_key
  122. FROM ado_s8_exception WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
  123. INSERT IGNORE INTO ado_s8_detection_log_dedup_bak_1_0_493 (id, tenant_id, factory_id, rule_code, dedup_key)
  124. SELECT id, tenant_id, factory_id, rule_code, dedup_key
  125. FROM ado_s8_detection_log WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
  126. INSERT IGNORE INTO ado_s8_rule_detection_state_dedup_bak_1_0_493 (id, tenant_id, factory_id, rule_code, dedup_key)
  127. SELECT id, tenant_id, factory_id, rule_code, dedup_key
  128. FROM ado_s8_rule_detection_state WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
  129. -- ============================================================================
  130. -- ③ 重写 dedup_key
  131. --
  132. -- 只改严格匹配 ^T{数字}:F{数字}:R 的行,且只去掉 :F{数字} 这一段 ——
  133. -- ⚠️ 反向引用必须写 $1:MySQL 的 REGEXP_REPLACE 用 ICU 语法,
  134. -- 写成 \1 不会被当作分组引用,而是被吞成字面量 "1" —— 租户前缀会整段丢失,
  135. -- 得到 "1:RRULE_..." 这种既不是旧格式也不是新格式的坏键。
  136. -- 本地隔离库首次执行时实测踩到过,已由归档表还原后修正。
  137. -- 不做全表 REPLACE(':F', ''):那会误伤任何恰好含 ":F" 的对象标识(例如 PO 号里带 :F)。
  138. -- 正则锚定行首 + 数字类,把改写面收敛到确实由旧 BuildDedupKey 生成的键。
  139. -- 幂等:改写后的行不再匹配该正则,重跑即 0 行受影响。
  140. -- ============================================================================
  141. UPDATE ado_s8_exception
  142. SET dedup_key = REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:')
  143. WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
  144. UPDATE ado_s8_detection_log
  145. SET dedup_key = REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:')
  146. WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
  147. UPDATE ado_s8_rule_detection_state
  148. SET dedup_key = REGEXP_REPLACE(dedup_key, '^(T[0-9]+):F[0-9]+:', '$1:')
  149. WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R';
  150. -- ============================================================================
  151. -- ④ 复验:重写后不得残留旧格式、不得出现碰撞
  152. -- ============================================================================
  153. SET @left_over := (
  154. (SELECT COUNT(*) FROM ado_s8_exception WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R')
  155. + (SELECT COUNT(*) FROM ado_s8_detection_log WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R')
  156. + (SELECT COUNT(*) FROM ado_s8_rule_detection_state WHERE dedup_key REGEXP '^T[0-9]+:F[0-9]+:R'));
  157. SET @post_collision := (
  158. SELECT COUNT(*) FROM (
  159. SELECT tenant_id, rule_code, dedup_key, COUNT(*) c
  160. FROM ado_s8_rule_detection_state GROUP BY 1, 2, 3 HAVING c > 1) z);
  161. -- 与 preflight 同一机制:约束名即失败原因。
  162. DROP TABLE IF EXISTS tmp_s8_493_postcheck_guard;
  163. CREATE TABLE tmp_s8_493_postcheck_guard (
  164. probe INT NOT NULL,
  165. CONSTRAINT s8_tenant_only_POSTCHECK_leftover_or_collision_1_0_493 CHECK (probe = 0)
  166. ) COMMENT 'S8-TENANT-ONLY-BATCH5 改写后复验门禁;成功后自动删除';
  167. INSERT INTO tmp_s8_493_postcheck_guard (probe) VALUES (@left_over + @post_collision);
  168. DROP TABLE IF EXISTS tmp_s8_493_postcheck_guard;
  169. -- ============================================================================
  170. -- ⑤ ado_s8_rule_detection_state 唯一键去掉 factory_id
  171. --
  172. -- 必须与代码同批:upsert 的 WHERE 已改为 (tenant, rule_code, dedup_key),
  173. -- 而旧唯一键仍允许同键在不同 factory 下存两行 —— 抗抖计数会分裂,
  174. -- trigger/recover 阈值静默失效。
  175. -- 用 information_schema 判存在性:MySQL 无 DROP INDEX IF EXISTS。
  176. -- ============================================================================
  177. SET @has_old_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  178. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_rule_detection_state'
  179. AND INDEX_NAME = 'uk_s8_rule_detection_state_dedup');
  180. SET @sql := IF(@has_old_uk > 0,
  181. 'ALTER TABLE ado_s8_rule_detection_state DROP INDEX uk_s8_rule_detection_state_dedup', 'DO 0');
  182. PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
  183. SET @has_new_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  184. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_rule_detection_state'
  185. AND INDEX_NAME = 'uk_s8_rule_detection_state_dedup');
  186. SET @sql := IF(@has_new_uk = 0,
  187. 'ALTER TABLE ado_s8_rule_detection_state ADD UNIQUE INDEX uk_s8_rule_detection_state_dedup (tenant_id, rule_code, dedup_key)', 'DO 0');
  188. PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
  189. -- 辅助索引同步去掉 factory_id(非唯一,纯查询效率)。
  190. SET @has_idx := (SELECT COUNT(*) FROM information_schema.STATISTICS
  191. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_rule_detection_state'
  192. AND INDEX_NAME = 'idx_s8_rule_detection_state_rule' AND COLUMN_NAME = 'factory_id');
  193. SET @sql := IF(@has_idx > 0,
  194. 'ALTER TABLE ado_s8_rule_detection_state DROP INDEX idx_s8_rule_detection_state_rule', 'DO 0');
  195. PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
  196. SET @has_idx2 := (SELECT COUNT(*) FROM information_schema.STATISTICS
  197. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_rule_detection_state'
  198. AND INDEX_NAME = 'idx_s8_rule_detection_state_rule');
  199. SET @sql := IF(@has_idx2 = 0,
  200. 'ALTER TABLE ado_s8_rule_detection_state ADD INDEX idx_s8_rule_detection_state_rule (tenant_id, rule_code)', 'DO 0');
  201. PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
  202. -- ============================================================================
  203. -- ⑥ ado_s8_watch_rule 新增 UNIQUE(tenant_id, rule_code)
  204. --
  205. -- Batch 2 把运行策略身份定为 (TenantId, RuleCode),但当时只有应用层 AnyAsync 兜底,
  206. -- 因此只保证"顺序幂等",两个实例同时供给理论上仍可能建出重复行。
  207. -- 本索引把那个缺口在 DB 层封死。preflight 已确认无重复行。
  208. -- ============================================================================
  209. SET @has_rule_uk := (SELECT COUNT(*) FROM information_schema.STATISTICS
  210. WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_watch_rule'
  211. AND INDEX_NAME = 'uk_s8_watch_rule_tenant_code');
  212. SET @sql := IF(@has_rule_uk = 0,
  213. 'ALTER TABLE ado_s8_watch_rule ADD UNIQUE INDEX uk_s8_watch_rule_tenant_code (tenant_id, rule_code)', 'DO 0');
  214. PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;