PostgreSQL AFTER INSERT Trigger · 迁移 1779400000001
2026-02-11 · RBAC 统一架构
每次开发者在新迁移中添加权限时,需要手动记住给 superadmin 角色也分配一份。
// 开发者写了一个新迁移,添加 time_clock 权限
// 给 employee、manager、admin 都分配了...
// 但忘了 platform_superadmin!
const adminPermissions = ['time_clock:punch', 'time_clock:pin_manage', ...];
// ✅ admin — 有
for (const perm of adminPermissions) {
pgm.sql(`INSERT INTO role_permissions ... WHERE r.name = 'admin' ...`);
}
// ❌ platform_superadmin — 完全没提到!
之前这不是问题,因为后端中间件有 if (isSuperadminRole) return next() 直接绕过数据库查询。
但项目已切换到统一 RBAC,移除代码绕过后 superadmin 会立即丢失这些权限。
在 permissions 表上创建 AFTER INSERT 触发器。每当新权限被插入,PostgreSQL 引擎自动将其分配给 superadmin 角色。开发者不需要做任何额外操作。
| 角色 | 获得范围 | 排除类别 |
|---|---|---|
platform_superadmin |
全部权限 | 无 |
super_admin |
全部权限 | platform、developer |
排除的具体权限:
platform 类别:tenants:manage、analytics:view、backups:manage、tenants:switchdeveloper 类别:developer:view、developer:managevoice_bot:manage 权限tenants:audit(category=platform)NEW 隐式变量PostgreSQL 触发器的核心概念。在 AFTER INSERT 触发器中,NEW 代表刚刚被插入的那一行的完整数据。通过 NEW.id、NEW.category 可以直接访问该行的所有列值。
(对应地,在 UPDATE 触发器中还有 OLD 变量代表更新前的旧值。DELETE 触发器只有 OLD 没有 NEW。)
ON CONFLICT DO NOTHING 防御性设计如果开发者在迁移中已经手动分配了权限(像以前那样),触发器再次尝试插入时会命中 UNIQUE(role_id, permission_id) 约束。DO NOTHING 让它静默跳过而不报错,避免迁移失败。
FOR EACH ROW 触发粒度意味着每插入一行就执行一次函数。如果一个迁移一次性插入 10 个权限,触发器会执行 10 次,每次 NEW 指向不同的行。
性能影响可以忽略:权限表极少变动(仅在部署迁移时),每次只多两个 INSERT ... ON CONFLICT 小查询。
触发器在触发它的 INSERT 语句所在的同一个事务中执行。如果迁移回滚,触发器插入的 role_permissions 行也会一起回滚,保证数据一致性。
| 维度 | DB 触发器 当前方案 | 启动同步 | 代码绕过 已废弃 |
|---|---|---|---|
| 时机 | INSERT 瞬间,同一事务内 | 服务重启时 | 每次权限检查时 |
| 可靠性 | 无法绕过 | 依赖服务重启 | 100%(但不可审计) |
| 对开发者要求 | 零——只需设对 category | 零——但需重启才生效 | 零 |
| 可审计性 | 数据库有完整记录 | 数据库有完整记录 | 无——权限在内存中 |
| 可撤销性 | 可以从 DB 删除特定权限 | 可以从 DB 删除 | 不可能——代码硬编码 |
-- 触发器函数 CREATE OR REPLACE FUNCTION fn_auto_grant_superadmin_permissions() RETURNS trigger AS $$ DECLARE v_role_id INTEGER; v_excluded_categories TEXT[] := ARRAY['platform', 'developer']; BEGIN -- platform_superadmin: 获得所有权限 SELECT id INTO v_role_id FROM roles WHERE name = 'platform_superadmin' LIMIT 1; IF v_role_id IS NOT NULL THEN INSERT INTO role_permissions (role_id, permission_id, permission_level, is_active, granted_at) VALUES (v_role_id, NEW.id, 'manage', true, NOW()) ON CONFLICT (role_id, permission_id) DO NOTHING; END IF; -- super_admin: 获得非 platform/developer 权限 IF NEW.category IS NULL OR NOT (NEW.category = ANY(v_excluded_categories)) THEN SELECT id INTO v_role_id FROM roles WHERE name = 'super_admin' LIMIT 1; IF v_role_id IS NOT NULL THEN INSERT INTO role_permissions (role_id, permission_id, permission_level, is_active, granted_at) VALUES (v_role_id, NEW.id, 'manage', true, NOW()) ON CONFLICT (role_id, permission_id) DO NOTHING; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- 绑定触发器到 permissions 表 CREATE TRIGGER trg_auto_grant_superadmin AFTER INSERT ON permissions FOR EACH ROW EXECUTE FUNCTION fn_auto_grant_superadmin_permissions();
什么都不需要做。
只需确保 INSERT INTO permissions 时 category 值设置正确:
voice_bot、time_clock)→ 两个 superadmin 角色都会自动获得platform 类别 → 仅 platform_superadmin 获得developer 类别 → 仅 platform_superadmin 获得你仍然需要手动给 admin、manager、employee 等角色分配权限,触发器只负责 superadmin 层。
迁移文件: backend/database/migrations/1779400000001_auto-grant-superadmin-permissions.js