-- ===================================================================================== -- 1.0.498 S8-HANDLER-POOL-1 · 规则处理账号池 -- -- 目标:回答「哪些账号负责这条规则」。与「异常操作权限」(1.0.497,哪个角色能做认领 -- 这类动作)正交 —— 最终能否认领 = 动作权限 ∩ 规则责任 ∩ 状态机。 -- -- 存储选型(基于本仓真实证据,不是偏好): -- · 不放 params_json。① 认领资格、列表可见性过滤、通知收件人解析都需要 -- 「按 user 反查规则」与「与 SysUser 关联校验租户/启用」,JSON 数组做不到; -- ② 该字段有事故先例 —— PUT /params {"enabled":false} 曾把整块 JSON 抹成 NULL; -- ③ 去重与合法性可由唯一索引在 DB 层保证,JSON 只能靠应用层自觉。 -- · 身份用 rule_code 而非 rule_id:provisioning 的身份口径就是 (TenantId, RuleCode), -- 而 watch_rule 的行是可以被重建的(1.0.496 的 clean reset 之后 id 全部变化)。 -- 绑 id 会让人员池在那一刻集体失联,而且不报错:规则照常跑,只是谁都不再"负责"它。 -- -- 表由 CodeFirst(InitTables) 创建;本脚本只补索引 + postcheck,不写任何成员数据 -- (成员是 per-tenant 的人工配置,写死在迁移里既不适配新租户、也会覆盖管理员的选择)。 -- ===================================================================================== -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 1 · PREFLIGHT -- ───────────────────────────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS `ado_s8_rule_handler_user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `tenant_id` BIGINT NOT NULL COMMENT '租户(唯一安全边界)', `rule_code` VARCHAR(128) NOT NULL COMMENT '规则编码,与 provisioning 身份口径一致', `user_id` BIGINT NOT NULL COMMENT '处理账号 SysUser.Id', `created_at` DATETIME NOT NULL, `created_by` VARCHAR(64) NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='S8 规则处理账号池'; -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 2 · SCHEMA -- 唯一索引:同一 (租户, 规则, 账号) 只允许一行。没有它,重复成员会让"移出池" -- 变成删一行、剩一行 —— 界面显示已移出,实际仍然可以认领。 -- 反查索引:可见性过滤要按 (tenant, user) 反查负责的规则,没有它会全表扫。 -- ───────────────────────────────────────────────────────────────────────────────────── SET @has_uk := (SELECT COUNT(DISTINCT INDEX_NAME) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_rule_handler_user' AND INDEX_NAME='uk_s8_rule_handler'); SET @sql := IF(@has_uk = 0, ' DELETE a FROM `ado_s8_rule_handler_user` a JOIN `ado_s8_rule_handler_user` b ON a.`tenant_id` = b.`tenant_id` AND a.`rule_code` = b.`rule_code` AND a.`user_id` = b.`user_id` AND a.`id` > b.`id`', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; SET @sql := IF(@has_uk = 0, 'ALTER TABLE `ado_s8_rule_handler_user` ADD UNIQUE INDEX `uk_s8_rule_handler` (`tenant_id`,`rule_code`,`user_id`)', '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_rule_handler_user' AND INDEX_NAME='idx_s8_rule_handler_user'); SET @sql := IF(@has_idx = 0, 'ALTER TABLE `ado_s8_rule_handler_user` ADD INDEX `idx_s8_rule_handler_user` (`tenant_id`,`user_id`)', 'DO 0'); PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s; -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 3 · POSTCHECK -- ───────────────────────────────────────────────────────────────────────────────────── -- ① 唯一索引必须覆盖三列 SET @cols := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='ado_s8_rule_handler_user' AND INDEX_NAME='uk_s8_rule_handler' AND COLUMN_NAME IN ('tenant_id','rule_code','user_id')); DROP TABLE IF EXISTS `tmp_s8_498_guard`; CREATE TABLE `tmp_s8_498_guard` ( probe BIGINT NOT NULL, CONSTRAINT s8_498_BLOCKED_unique_index_missing CHECK (probe = 0) ) COMMENT '1.0.498 断言门禁;通过后自动删除'; INSERT INTO `tmp_s8_498_guard` (probe) VALUES ((3 - @cols)); DROP TABLE IF EXISTS `tmp_s8_498_guard`; -- ② 不允许出现「池成员不属于该行租户」的数据 SET @bad := (SELECT COUNT(*) FROM `ado_s8_rule_handler_user` h JOIN `SysUser` u ON u.`Id` = h.`user_id` WHERE u.`TenantId` <> h.`tenant_id`); DROP TABLE IF EXISTS `tmp_s8_498_guard`; CREATE TABLE `tmp_s8_498_guard` ( probe BIGINT NOT NULL, CONSTRAINT s8_498_BLOCKED_cross_tenant_handler CHECK (probe = 0) ) COMMENT '1.0.498 断言门禁;通过后自动删除'; INSERT INTO `tmp_s8_498_guard` (probe) VALUES (@bad); DROP TABLE IF EXISTS `tmp_s8_498_guard`; -- ───────────────────────────────────────────────────────────────────────────────────── -- PHASE 4 · IDEMPOTENT RERUN GUARD -- 第二次执行:建表跳过、索引已存在跳过、断言依旧成立。本脚本不写任何成员数据, -- 因此重跑不会改变任何一条责任归属。 -- ─────────────────────────────────────────────────────────────────────────────────────