-- ===================================================================================== -- 1.0.496 S8-SYSUSER-ONLY-1 · 人员身份统一为系统账号 + 旧运行数据 CLEAN RESET -- -- 背景(真库取证,只读): -- · ado_s8_exception.assignee_id 34 个非空值里只有 20 个能解析到 EmployeeMaster.RecID, -- 0 个能解析到 SysUser.Id,另有 14 行是 assignee_id = 1(两边都不存在的脏值)。 -- · ado_s8_exception_timeline.operator_id 同一列混装两套 ID 空间: -- CLAIM 写 EmployeeMaster.RecID(19 行),START_PROGRESS / VERIFY_* / CREATE 写 SysUser.Id。 -- · EmployeeMaster 1105 行只有 8 行绑定了 sys_user_id(0.7%), -- 所有「经 EmployeeMaster.SysUserId 换算」的通知与审批流转办路径实际上长期静默失败。 -- -- 本次动作:物理列改名为显式 *_user_id,并清空旧运行数据(不做逐条身份迁移 —— 业务已确认 -- 旧 S8 历史数据无保留要求;混装两套 ID 空间的历史值也没有可靠的迁移依据)。 -- -- 幂等:六个阶段全部可重复执行。第二次运行时 PHASE 3 清 0 行、PHASE 4 全部跳过。 -- -- 安全边界: -- · RESET 表为**硬编码白名单**,绝不使用 LIKE 'ado_s8_%'(否则将来新增的表会被无声删掉)。 -- · 只清 S8 自己的运行数据。SysUser / SysRole / SysUserRole / SysOrg / -- DepartmentMaster / EmployeeMaster 一行不动。 -- · 配置态表(exception_type / scene_config / notification_layer / role_permission_config / -- dashboard_cell_config / watch_rule 的参数列)一行不动。 -- · 审批流表按 **BizType 白名单**定点删除 S8 自己发起的实例与任务, -- 绝不整表清空(ApprovalFlowInstance / Task 由 S5/S6/S7 共用)。 -- ===================================================================================== -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 1 · PREFLIGHT -- 目的:把「执行前长什么样」原样记下来。出问题时这张表是唯一的现场。 -- ───────────────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS `ado_s8_sysuser_only_reset_1_0_496` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `phase` VARCHAR(16) NOT NULL COMMENT 'BEFORE / AFTER', `table_name` VARCHAR(96) NOT NULL, `table_class` VARCHAR(32) NOT NULL COMMENT 'RUNTIME / FLOW_RUNTIME / POLICY_RUNTIME_STATE', `row_count` BIGINT NOT NULL, `noted_at` DATETIME NOT NULL, PRIMARY KEY (`id`), KEY `idx_reset_1_0_496_phase` (`phase`, `table_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='1.0.496 CLEAN RESET 前后行数留证'; -- 重复执行时先清掉上一轮的留证,避免同一 phase 出现多份互相矛盾的计数。 DELETE FROM `ado_s8_sysuser_only_reset_1_0_496`; -- ── RESET 白名单(就是下面这 7 张,没有第 8 张)── INSERT INTO `ado_s8_sysuser_only_reset_1_0_496` (`phase`,`table_name`,`table_class`,`row_count`,`noted_at`) SELECT 'BEFORE','ado_s8_exception','RUNTIME',COUNT(*),NOW() FROM `ado_s8_exception` UNION ALL SELECT 'BEFORE','ado_s8_exception_timeline','RUNTIME',COUNT(*),NOW() FROM `ado_s8_exception_timeline` UNION ALL SELECT 'BEFORE','ado_s8_detection_log','RUNTIME',COUNT(*),NOW() FROM `ado_s8_detection_log` UNION ALL SELECT 'BEFORE','ado_s8_rule_detection_state','RUNTIME',COUNT(*),NOW() FROM `ado_s8_rule_detection_state` UNION ALL SELECT 'BEFORE','ado_s8_notification_log','RUNTIME',COUNT(*),NOW() FROM `ado_s8_notification_log` UNION ALL SELECT 'BEFORE','ado_s8_evidence','RUNTIME',COUNT(*),NOW() FROM `ado_s8_evidence` UNION ALL SELECT 'BEFORE','ado_s8_decision_record','RUNTIME',COUNT(*),NOW() FROM `ado_s8_decision_record`; -- 审批流:只统计 S8 自己的三种 BizType,别的模块的行不在本次视野里。 INSERT INTO `ado_s8_sysuser_only_reset_1_0_496` (`phase`,`table_name`,`table_class`,`row_count`,`noted_at`) SELECT 'BEFORE','ApprovalFlowInstance(S8 BizType)','FLOW_RUNTIME',COUNT(*),NOW() FROM `ApprovalFlowInstance` WHERE `BizType` IN ('EXCEPTION_REPORT','EXCEPTION_ESCALATION','EXCEPTION_CLOSURE') UNION ALL SELECT 'BEFORE','ApprovalFlowTask(S8 BizType)','FLOW_RUNTIME',COUNT(*),NOW() FROM `ApprovalFlowTask` t JOIN `ApprovalFlowInstance` i ON i.`Id` = t.`InstanceId` WHERE i.`BizType` IN ('EXCEPTION_REPORT','EXCEPTION_ESCALATION','EXCEPTION_CLOSURE'); -- 规则:只清运行态列,参数列(含租户已配的默认部门)保持不动,见 PHASE 3 说明。 INSERT INTO `ado_s8_sysuser_only_reset_1_0_496` (`phase`,`table_name`,`table_class`,`row_count`,`noted_at`) SELECT 'BEFORE','ado_s8_watch_rule(last_run_at NOT NULL)','POLICY_RUNTIME_STATE',COUNT(*),NOW() FROM `ado_s8_watch_rule` WHERE `last_run_at` IS NOT NULL; -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 2 · ARCHIVE / DIAGNOSTICS -- 不做整表备份(业务已确认旧数据无保留要求,全量备份只会把混装 ID 的脏数据换个地方留着)。 -- 只留一份**证据摘要**:这次到底删掉了什么形状的身份数据。 -- ───────────────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS `ado_s8_idspace_evidence_1_0_496` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `metric` VARCHAR(96) NOT NULL, `metric_value` BIGINT NOT NULL, `noted_at` DATETIME NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='1.0.496 清理前的 ID 空间混用取证'; DELETE FROM `ado_s8_idspace_evidence_1_0_496`; -- 只在旧列还在时取证;改过名之后这些指标已无处可取,跳过即可(幂等)。 SET @has_old_assignee := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ado_s8_exception' AND COLUMN_NAME = 'assignee_id'); SET @sql := IF(@has_old_assignee = 1, ' INSERT INTO `ado_s8_idspace_evidence_1_0_496` (`metric`,`metric_value`,`noted_at`) SELECT ''assignee_notnull'', COUNT(*), NOW() FROM `ado_s8_exception` WHERE `assignee_id` IS NOT NULL UNION ALL SELECT ''assignee_resolves_employee'', COUNT(*), NOW() FROM `ado_s8_exception` e JOIN `EmployeeMaster` m ON m.`RecID` = e.`assignee_id` UNION ALL SELECT ''assignee_resolves_sysuser'', COUNT(*), NOW() FROM `ado_s8_exception` e JOIN `SysUser` u ON u.`Id` = e.`assignee_id` UNION ALL SELECT ''assignee_dangling'', COUNT(*), NOW() FROM `ado_s8_exception` e WHERE e.`assignee_id` IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `EmployeeMaster` m WHERE m.`RecID` = e.`assignee_id`) AND NOT EXISTS (SELECT 1 FROM `SysUser` u WHERE u.`Id` = e.`assignee_id`) ', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 3 · RUNTIME DATA RESET -- -- 删除顺序:明细 → 主表 → 审批流。全部按白名单点名,没有一条通配。 -- ───────────────────────────────────────────────────────────────────────────────────── -- ① S8 异常运行数据 DELETE FROM `ado_s8_exception_timeline`; DELETE FROM `ado_s8_evidence`; DELETE FROM `ado_s8_decision_record`; DELETE FROM `ado_s8_exception`; -- ② 规则检测运行数据(不清会让规则以为"这些对象我已经报过了",重启后一条都建不出来) DELETE FROM `ado_s8_detection_log`; DELETE FROM `ado_s8_rule_detection_state`; -- ③ 通知投递日志(全部指向已删除的异常) DELETE FROM `ado_s8_notification_log`; -- ④ 审批流:**只删 S8 自己的三种 BizType**。 -- ApprovalFlowInstance / ApprovalFlowTask 由 S5/S6/S7 共用,整表清空会毁掉别的模块。 -- 先删子表(Task)再删父表(Instance),否则 Task 会成为查不到来源的孤儿。 DELETE t FROM `ApprovalFlowTask` t JOIN `ApprovalFlowInstance` i ON i.`Id` = t.`InstanceId` WHERE i.`BizType` IN ('EXCEPTION_REPORT','EXCEPTION_ESCALATION','EXCEPTION_CLOSURE'); DELETE FROM `ApprovalFlowInstance` WHERE `BizType` IN ('EXCEPTION_REPORT','EXCEPTION_ESCALATION','EXCEPTION_CLOSURE'); -- ⑤ 规则的**运行态列**清零。 -- 刻意只清运行态、不动参数列,也不删行: -- · watch_rule 上没有任何人员身份列,本次改造不要求重建它; -- · 删行会连带删掉租户已配的默认部门(实测 2 个租户已配 245 / 253), -- 而 Provisioning 只能把它们恢复成 NULL —— 那是纯粹的损失,换不来任何东西; -- · 也会删掉无代码定义的孤儿行,与 S8RuleProvisioningService「孤儿不删、不改、不启用」相矛盾。 -- enabled 一列同样不碰:真库 11 行当前全部为 0,Provisioning 新建行也恒为 false, -- 因此 reset 之后 Rule 01 仍然是停用状态 —— §十八 要的就是这个结果。 UPDATE `ado_s8_watch_rule` SET `last_run_at` = NULL, `last_status` = NULL, `last_error` = NULL, `last_duration_ms` = NULL, `last_run_id` = NULL, `next_run_at` = NULL, `lock_token` = NULL, `locked_by` = NULL, `lock_until` = NULL, `running_started_at` = NULL, `consecutive_failure_count` = 0 WHERE `last_run_at` IS NOT NULL OR `last_status` IS NOT NULL OR `last_run_id` IS NOT NULL OR `lock_token` IS NOT NULL OR `running_started_at` IS NOT NULL OR `consecutive_failure_count` <> 0; -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 4 · SCHEMA IDENTITY CHANGE -- -- 三种到场状态都要能收敛(幂等): -- (a) 只有旧列 → CHANGE COLUMN 改名 -- (b) 新旧列同时存在 → CodeFirst 已经把新列加进来了;此时数据已在 PHASE 3 清空, -- 直接丢掉旧列即可,不需要也不应该做数据搬运 -- (c) 只有新列 → 已经改过,跳过 -- ───────────────────────────────────────────────────────────────────────────────────── -- ── ado_s8_exception.assignee_id → assignee_user_id ── SET @old := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND COLUMN_NAME='assignee_id'); SET @new := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND COLUMN_NAME='assignee_user_id'); SET @sql := CASE WHEN @old = 1 AND @new = 0 THEN 'ALTER TABLE `ado_s8_exception` CHANGE COLUMN `assignee_id` `assignee_user_id` BIGINT NULL COMMENT ''处理人账号Id(SysUser.Id)''' WHEN @old = 1 AND @new = 1 THEN 'ALTER TABLE `ado_s8_exception` DROP COLUMN `assignee_id`' ELSE 'DO 0' END; PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- ── ado_s8_exception.verifier_id → verifier_user_id ── SET @old := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND COLUMN_NAME='verifier_id'); SET @new := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND COLUMN_NAME='verifier_user_id'); SET @sql := CASE WHEN @old = 1 AND @new = 0 THEN 'ALTER TABLE `ado_s8_exception` CHANGE COLUMN `verifier_id` `verifier_user_id` BIGINT NULL COMMENT ''检验人账号Id(SysUser.Id)''' WHEN @old = 1 AND @new = 1 THEN 'ALTER TABLE `ado_s8_exception` DROP COLUMN `verifier_id`' ELSE 'DO 0' END; PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- ── ado_s8_exception.reporter_id → reporter_user_id ── SET @old := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND COLUMN_NAME='reporter_id'); SET @new := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND COLUMN_NAME='reporter_user_id'); SET @sql := CASE WHEN @old = 1 AND @new = 0 THEN 'ALTER TABLE `ado_s8_exception` CHANGE COLUMN `reporter_id` `reporter_user_id` BIGINT NULL COMMENT ''提报人账号Id(SysUser.Id)''' WHEN @old = 1 AND @new = 1 THEN 'ALTER TABLE `ado_s8_exception` DROP COLUMN `reporter_id`' ELSE 'DO 0' END; PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- ── ado_s8_exception_timeline.operator_id → operator_user_id ── SET @old := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception_timeline' AND COLUMN_NAME='operator_id'); SET @new := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception_timeline' AND COLUMN_NAME='operator_user_id'); SET @sql := CASE WHEN @old = 1 AND @new = 0 THEN 'ALTER TABLE `ado_s8_exception_timeline` CHANGE COLUMN `operator_id` `operator_user_id` BIGINT NULL COMMENT ''操作人账号Id(SysUser.Id);系统动作为 NULL''' WHEN @old = 1 AND @new = 1 THEN 'ALTER TABLE `ado_s8_exception_timeline` DROP COLUMN `operator_id`' ELSE 'DO 0' END; PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- ── 索引重建 ── -- -- 这里有一个**实测踩到过的坑**,不要简化成「索引名不存在才建」: -- CodeFirst 若先于本脚本把 assignee_user_id 建了出来,PHASE 4 会走 (b) DROP COLUMN 分支, -- 而 MySQL 在 DROP COLUMN 时会把该列**从复合索引里摘掉、索引本身保留**。 -- 结果是 idx_s8_exception_assignee_status 退化成 (tenant_id, factory_id, status, is_deleted) —— -- 索引名还在,覆盖的列却少了最关键的那一个,「我的待办」从此走不到索引, -- 而且没有任何报错。因此判据必须是「索引里有没有那一列」,不是「索引名在不在」。 SET @has_col := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND INDEX_NAME='idx_s8_exception_assignee_status' AND COLUMN_NAME='assignee_user_id'); SET @has_idx := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND INDEX_NAME='idx_s8_exception_assignee_status'); SET @sql := IF(@has_idx = 1 AND @has_col = 0, 'ALTER TABLE `ado_s8_exception` DROP INDEX `idx_s8_exception_assignee_status`', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; SET @has_idx := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND INDEX_NAME='idx_s8_exception_assignee_status'); SET @sql := IF(@has_idx = 0, 'ALTER TABLE `ado_s8_exception` ADD INDEX `idx_s8_exception_assignee_status` (`tenant_id`,`factory_id`,`assignee_user_id`,`status`,`is_deleted`)', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; SET @has_col := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND INDEX_NAME='idx_s8_exception_verifier_status' AND COLUMN_NAME='verifier_user_id'); SET @has_idx := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND INDEX_NAME='idx_s8_exception_verifier_status'); SET @sql := IF(@has_idx = 1 AND @has_col = 0, 'ALTER TABLE `ado_s8_exception` DROP INDEX `idx_s8_exception_verifier_status`', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; SET @has_idx := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND INDEX_NAME='idx_s8_exception_verifier_status'); SET @sql := IF(@has_idx = 0, 'ALTER TABLE `ado_s8_exception` ADD INDEX `idx_s8_exception_verifier_status` (`tenant_id`,`factory_id`,`verifier_user_id`,`status`,`is_deleted`)', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 5 · POSTCHECK -- 任何一条不成立就抛错中止,不让"半成功"通过。 -- ───────────────────────────────────────────────────────────────────────────────────── INSERT INTO `ado_s8_sysuser_only_reset_1_0_496` (`phase`,`table_name`,`table_class`,`row_count`,`noted_at`) SELECT 'AFTER','ado_s8_exception','RUNTIME',COUNT(*),NOW() FROM `ado_s8_exception` UNION ALL SELECT 'AFTER','ado_s8_exception_timeline','RUNTIME',COUNT(*),NOW() FROM `ado_s8_exception_timeline` UNION ALL SELECT 'AFTER','ado_s8_detection_log','RUNTIME',COUNT(*),NOW() FROM `ado_s8_detection_log` UNION ALL SELECT 'AFTER','ado_s8_rule_detection_state','RUNTIME',COUNT(*),NOW() FROM `ado_s8_rule_detection_state` UNION ALL SELECT 'AFTER','ado_s8_notification_log','RUNTIME',COUNT(*),NOW() FROM `ado_s8_notification_log` UNION ALL SELECT 'AFTER','ado_s8_evidence','RUNTIME',COUNT(*),NOW() FROM `ado_s8_evidence` UNION ALL SELECT 'AFTER','ado_s8_decision_record','RUNTIME',COUNT(*),NOW() FROM `ado_s8_decision_record`; INSERT INTO `ado_s8_sysuser_only_reset_1_0_496` (`phase`,`table_name`,`table_class`,`row_count`,`noted_at`) SELECT 'AFTER','ApprovalFlowInstance(S8 BizType)','FLOW_RUNTIME',COUNT(*),NOW() FROM `ApprovalFlowInstance` WHERE `BizType` IN ('EXCEPTION_REPORT','EXCEPTION_ESCALATION','EXCEPTION_CLOSURE') UNION ALL SELECT 'AFTER','ApprovalFlowTask(S8 BizType)','FLOW_RUNTIME',COUNT(*),NOW() FROM `ApprovalFlowTask` t JOIN `ApprovalFlowInstance` i ON i.`Id` = t.`InstanceId` WHERE i.`BizType` IN ('EXCEPTION_REPORT','EXCEPTION_ESCALATION','EXCEPTION_CLOSURE') UNION ALL SELECT 'AFTER','ado_s8_watch_rule(last_run_at NOT NULL)','POLICY_RUNTIME_STATE',COUNT(*),NOW() FROM `ado_s8_watch_rule` WHERE `last_run_at` IS NOT NULL; -- ① 白名单表必须为 0 SET @left := (SELECT COALESCE(SUM(`row_count`),0) FROM `ado_s8_sysuser_only_reset_1_0_496` WHERE `phase`='AFTER'); DROP TABLE IF EXISTS `tmp_s8_496_guard`; CREATE TABLE `tmp_s8_496_guard` ( probe BIGINT NOT NULL, CONSTRAINT s8_496_BLOCKED_reset_tables_not_empty CHECK (probe = 0) ) COMMENT '1.0.496 断言门禁;通过后自动删除'; INSERT INTO `tmp_s8_496_guard` (probe) VALUES (@left); DROP TABLE IF EXISTS `tmp_s8_496_guard`; -- ② 四个身份列必须已改名到位(新列在、旧列不在) SET @bad := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND ((TABLE_NAME='ado_s8_exception' AND COLUMN_NAME IN ('assignee_id','verifier_id','reporter_id')) OR (TABLE_NAME='ado_s8_exception_timeline' AND COLUMN_NAME='operator_id'))); DROP TABLE IF EXISTS `tmp_s8_496_guard`; CREATE TABLE `tmp_s8_496_guard` ( probe BIGINT NOT NULL, CONSTRAINT s8_496_BLOCKED_old_identity_column_still_present CHECK (probe = 0) ) COMMENT '1.0.496 断言门禁;通过后自动删除'; INSERT INTO `tmp_s8_496_guard` (probe) VALUES (@bad); DROP TABLE IF EXISTS `tmp_s8_496_guard`; SET @ok := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND ((TABLE_NAME='ado_s8_exception' AND COLUMN_NAME IN ('assignee_user_id','verifier_user_id','reporter_user_id')) OR (TABLE_NAME='ado_s8_exception_timeline' AND COLUMN_NAME='operator_user_id'))); DROP TABLE IF EXISTS `tmp_s8_496_guard`; CREATE TABLE `tmp_s8_496_guard` ( probe BIGINT NOT NULL, CONSTRAINT s8_496_BLOCKED_new_identity_column_missing CHECK (probe = 0) ) COMMENT '1.0.496 断言门禁;通过后自动删除'; INSERT INTO `tmp_s8_496_guard` (probe) VALUES ((4 - @ok)); DROP TABLE IF EXISTS `tmp_s8_496_guard`; -- ③ 两个身份索引必须真的覆盖身份列(不是「索引名还在」就算数,见 PHASE 4 的坑) SET @idx_ok := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_exception' AND ((INDEX_NAME='idx_s8_exception_assignee_status' AND COLUMN_NAME='assignee_user_id') OR (INDEX_NAME='idx_s8_exception_verifier_status' AND COLUMN_NAME='verifier_user_id'))); DROP TABLE IF EXISTS `tmp_s8_496_guard`; CREATE TABLE `tmp_s8_496_guard` ( probe BIGINT NOT NULL, CONSTRAINT s8_496_BLOCKED_identity_index_lost_its_column CHECK (probe = 0) ) COMMENT '1.0.496 断言门禁;通过后自动删除'; INSERT INTO `tmp_s8_496_guard` (probe) VALUES ((2 - @idx_ok)); DROP TABLE IF EXISTS `tmp_s8_496_guard`; -- ④ 平台主数据与配置态一行都不能少 —— 这是本脚本最重要的一条断言。 -- RESET 允许清运行数据,但只要下面任何一张表被清空,就说明白名单被写错了。 SET @masters := (SELECT (SELECT COUNT(*) FROM `SysUser`) + (SELECT COUNT(*) FROM `SysRole`) + (SELECT COUNT(*) FROM `EmployeeMaster`) + (SELECT COUNT(*) FROM `DepartmentMaster`)); DROP TABLE IF EXISTS `tmp_s8_496_guard`; CREATE TABLE `tmp_s8_496_guard` ( probe BIGINT NOT NULL, CONSTRAINT s8_496_BLOCKED_platform_master_wiped CHECK (probe = 0) ) COMMENT '1.0.496 断言门禁;通过后自动删除'; INSERT INTO `tmp_s8_496_guard` (probe) VALUES (IF(@masters = 0, 1, 0)); DROP TABLE IF EXISTS `tmp_s8_496_guard`; SET @configs := (SELECT (SELECT COUNT(*) FROM `ado_s8_exception_type`) + (SELECT COUNT(*) FROM `ado_s8_scene_config`) + (SELECT COUNT(*) FROM `ado_s8_notification_layer`) + (SELECT COUNT(*) FROM `ado_s8_role_permission_config`) + (SELECT COUNT(*) FROM `ado_s8_watch_rule`)); DROP TABLE IF EXISTS `tmp_s8_496_guard`; CREATE TABLE `tmp_s8_496_guard` ( probe BIGINT NOT NULL, CONSTRAINT s8_496_BLOCKED_product_config_wiped CHECK (probe = 0) ) COMMENT '1.0.496 断言门禁;通过后自动删除'; INSERT INTO `tmp_s8_496_guard` (probe) VALUES (IF(@configs = 0, 1, 0)); DROP TABLE IF EXISTS `tmp_s8_496_guard`; -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 6 · IDEMPOTENT RERUN GUARD -- 第二次执行时:PHASE 3 的 DELETE 命中 0 行、PHASE 4 全部落到 'DO 0'、 -- PHASE 5 的断言依旧成立。下面这条只是把「本脚本已收敛」这件事也记进留证表。 -- ───────────────────────────────────────────────────────────────────────────────────── INSERT INTO `ado_s8_sysuser_only_reset_1_0_496` (`phase`,`table_name`,`table_class`,`row_count`,`noted_at`) VALUES ('AFTER','__migration_converged__','GUARD',1,NOW());