企业权限系统技术方案

发表信息: by

企业权限系统技术方案

核心调整:Role 是 Permission 的打包模板,真正的授权单元是 Permission,过期时间挂在用户-权限点授权上。数据范围通过 sys_scope_data 统一管理,外部系统只需对接同步任务即可接入新的数据维度。


一、核心设计思路

Role
  └─ 只是权限点的集合模板,方便批量操作,本身不是授权单元

Permission(权限点)
  └─ 才是真正的授权单元
  └─ 关联一个 DataResource(数据资源点)

用户授权流程:
  方式一:选角色 → 展开角色下所有权限点 → 逐个配置数据范围 + 过期时间 → 批量写入授权记录
  方式二:直接选权限点 → 配置数据范围 + 过期时间 → 写入单条授权记录

授权记录(sys_user_permission_grant)
  └─ 一条记录 = 用户拥有某个权限点的一次授权
  └─ 记录来源(通过哪个角色批量授予 or 单独授予)
  └─ 记录数据范围
  └─ 记录过期时间

二、表结构设计

2.1 权限点表 sys_permission

CREATE TABLE sys_permission (
  id               BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id        BIGINT       NOT NULL COMMENT '租户ID',
  perm_code        VARCHAR(128) NOT NULL COMMENT '权限编码,如 order:view',
  perm_name        VARCHAR(128) NOT NULL COMMENT '权限名称',
  type             TINYINT      NOT NULL DEFAULT 2 COMMENT '1菜单 2操作按钮 3API',
  data_resource_id BIGINT       COMMENT '关联的数据资源点,NULL 表示无数据权限',
  sort             INT          DEFAULT 0,
  remark           VARCHAR(255),
  created_at       DATETIME     DEFAULT CURRENT_TIMESTAMP,

  UNIQUE KEY uk_tenant_perm_code (tenant_id, perm_code)
);

2.2 数据资源点表 sys_data_resource

CREATE TABLE sys_data_resource (
  id            BIGINT PRIMARY KEY AUTO_INCREMENT,
  resource_code VARCHAR(128) NOT NULL UNIQUE COMMENT '资源编码,如 data:order',
  resource_name VARCHAR(128) NOT NULL COMMENT '资源名称,如 订单数据',
  description   VARCHAR(255),
  created_at    DATETIME DEFAULT CURRENT_TIMESTAMP
);

2.3 数据资源点可选范围表 sys_data_resource_scope

-- 元数据:定义某个数据资源点开放哪些范围选项
-- 授权配置时用,运行时不参与请求链路
CREATE TABLE sys_data_resource_scope (
  id               BIGINT PRIMARY KEY AUTO_INCREMENT,
  data_resource_id BIGINT       NOT NULL,
  scope_code       VARCHAR(64)  NOT NULL COMMENT '范围编码,如 ALL / DEPT / SELF / CITY / PROJECT',
  scope_name       VARCHAR(128) NOT NULL COMMENT '展示给管理员的名称',
  scope_type       TINYINT      NOT NULL COMMENT '1全量 2部门树 3本人 4实体列表(对接 sys_scope_data)',
  sort             INT          DEFAULT 0,

  UNIQUE KEY uk_resource_scope (data_resource_id, scope_code)
);

scope_type = 4 表示该范围的候选值来自 sys_scope_data,前端授权页面通过 scope_codesys_scope_data 查候选列表,不需要调任何外部接口。


2.4 ⭐ 范围数据字典表 sys_scope_data

-- 统一存储所有数据范围的候选值
-- 外部系统通过同步任务写入,权限系统内部完全自洽
CREATE TABLE sys_scope_data (
  id           BIGINT PRIMARY KEY AUTO_INCREMENT,
  scope_code   VARCHAR(64)  NOT NULL COMMENT '范围类型,对应 sys_data_resource_scope.scope_code,如 CITY / PROJECT',
  entity_id    VARCHAR(64)  NOT NULL COMMENT '外部实体ID,VARCHAR 兼容各种ID类型',
  entity_name  VARCHAR(128) NOT NULL COMMENT '实体名称,授权页面展示用',
  entity_extra JSON         COMMENT '扩展信息,如层级路径、所属上级等',
  status       TINYINT      DEFAULT 1 COMMENT '1有效 0已下线',
  synced_at    DATETIME     COMMENT '最后同步时间',

  UNIQUE KEY uk_scope_entity (scope_code, entity_id),
  KEY idx_scope_code (scope_code)
);

entity_extra 示例:

// 城市,带省份信息
{ "province": "广东省", "province_id": "44" }

// 项目,带所属业务线
{ "biz_line": "电商", "owner": "张三" }

外部系统接入方式(唯一入口):

INSERT INTO sys_scope_data (scope_code, entity_id, entity_name, entity_extra, synced_at)
VALUES ('CITY', '101', '上海', '{"province":"上海市"}', NOW())
ON DUPLICATE KEY UPDATE
  entity_name  = VALUES(entity_name),
  entity_extra = VALUES(entity_extra),
  synced_at    = VALUES(synced_at);

新接入一种数据维度(如"项目"、"区域"),只需要:

  1. sys_data_resource_scope 新增一条 scope_type=4 的配置记录
  2. 跑一个同步任务往 sys_scope_data 写数据

权限系统本身零改动。


2.5 角色表 sys_role

-- Role 只是权限点的打包模板,不是授权单元
CREATE TABLE sys_role (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id   BIGINT       NOT NULL COMMENT '租户ID',
  role_code   VARCHAR(64)  NOT NULL COMMENT '角色编码',
  role_name   VARCHAR(128) NOT NULL,
  is_super    TINYINT      DEFAULT 0 COMMENT '1超管角色,鉴权时直接放行,无需配置权限点',
  status      TINYINT      DEFAULT 1,
  remark      VARCHAR(255),
  created_at  DATETIME     DEFAULT CURRENT_TIMESTAMP,

  UNIQUE KEY uk_tenant_role_code (tenant_id, role_code)
);

2.6 用户-角色关系表 sys_user_role

-- 记录"这个人被分配了哪些角色"这件事本身
-- 职责:角色列表展示、角色级撤销/续期操作
-- 不参与鉴权,运行时权限校验只走 sys_user_permission_grant
CREATE TABLE sys_user_role (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id     BIGINT   NOT NULL,
  role_id     BIGINT   NOT NULL,
  grant_by    BIGINT   COMMENT '授权操作人 user_id',
  start_time  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',
  expire_time DATETIME COMMENT '过期时间,冗余字段,仅用于展示,鉴权以 sys_user_permission_grant 为准',
  status      TINYINT  DEFAULT 1 COMMENT '1有效 0已撤销',
  created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,

  UNIQUE KEY uk_user_role (tenant_id, user_id, role_id),
  KEY idx_user_id (user_id),
  KEY idx_role_id (role_id)
);

职责边界:

  • 写入时机:管理员通过角色授权,同时写入本表(1条)+ sys_user_permission_grant(N条,每个权限点一条)
  • 读取场景:展示用户持有的角色列表、角色续期、一键撤销整个角色
  • expire_time 冗余存储,方便 UI 展示剩余有效期,但鉴权不依赖此字段

sys_user_permission_grant 的联动逻辑:

  • 给用户分配角色 → 写入 sys_user_role + 展开权限点批量写入 sys_user_permission_grant(source_type=1, source_id=role_id)
  • 撤销用户角色 → DELETE sys_user_role + UPDATE sys_user_permission_grant SET status=0 WHERE source_type=1 AND source_id=role_id
  • 查看用户拥有哪些角色 → 查 sys_user_role JOIN sys_role

2.7 角色-权限点关联表 sys_role_permission

-- 仅描述角色包含哪些权限点,是模板定义,不涉及授权
CREATE TABLE sys_role_permission (
  id            BIGINT PRIMARY KEY AUTO_INCREMENT,
  role_id       BIGINT NOT NULL,
  permission_id BIGINT NOT NULL,

  UNIQUE KEY uk_role_perm (role_id, permission_id)
);

2.8 ⭐ 用户-权限点授权表 sys_user_permission_grant

-- 核心授权表:真正的授权单元是权限点,过期时间在这里
CREATE TABLE sys_user_permission_grant (
  id               BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id          BIGINT       NOT NULL COMMENT '被授权用户',
  permission_id    BIGINT       NOT NULL COMMENT '授权的权限点',
  data_resource_id BIGINT       COMMENT '关联的数据资源点(冗余存储,方便查询)',
  scope_code       VARCHAR(64)  COMMENT '数据范围编码,来自资源点可选范围的子集',
  -- 具体的实体ID列表已拆出到 sys_grant_scope_entity,不再存 JSON

  -- 授权来源
  source_type      TINYINT      NOT NULL COMMENT '1通过角色批量授予 2单独授予 (可扩展:3岗位模板 4用户组等)',
  source_id        BIGINT       COMMENT '来源ID,与 source_type 配合使用,source_type=2 时为 NULL',
  grant_by         BIGINT       COMMENT '授权操作人 user_id',

  -- 有效期
  start_time       DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',
  expire_time      DATETIME     COMMENT '过期时间,NULL 表示永久有效',

  status           TINYINT      DEFAULT 1 COMMENT '1有效 0已撤销',
  revoked_at       DATETIME     COMMENT '撤销时间,status 变为 0 时记录,用于审计',
  created_at       DATETIME     DEFAULT CURRENT_TIMESTAMP,
  updated_at       DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  KEY idx_user_id (user_id),
  KEY idx_expire_time (expire_time),
  KEY idx_source (source_type, source_id),
  KEY idx_user_perm (user_id, permission_id)
);

2.9 授权实体明细表 sys_grant_scope_entity

-- 将原 custom_value JSON 拆成关系表
-- 支持正向查(用户被授权了哪些实体)和反查(某实体被授权给了哪些用户)
CREATE TABLE sys_grant_scope_entity (
  id           BIGINT PRIMARY KEY AUTO_INCREMENT,
  grant_id     BIGINT      NOT NULL COMMENT '关联 sys_user_permission_grant.id',
  scope_code   VARCHAR(64) NOT NULL COMMENT '范围类型,如 CITY / PROJECT',
  entity_id    VARCHAR(64) NOT NULL COMMENT '实体ID,对应 sys_scope_data.entity_id',

  KEY idx_grant_id (grant_id),
  KEY idx_entity (scope_code, entity_id)
);

查询示例:

-- 正向查:张三在 order:view 权限下被授权了哪些城市
SELECT e.entity_id, e.scope_code
FROM sys_grant_scope_entity e
JOIN sys_user_permission_grant g ON g.id = e.grant_id
JOIN sys_permission p ON p.id = g.permission_id
WHERE g.tenant_id = #{tenantId}
  AND g.user_id = #{userId}
  AND p.perm_code = 'order:view'
  AND g.status = 1
  AND (g.expire_time IS NULL OR g.expire_time > NOW());

-- 反查:城市101被授权给了哪些用户
SELECT g.user_id, g.expire_time
FROM sys_grant_scope_entity e
JOIN sys_user_permission_grant g ON g.id = e.grant_id
WHERE e.scope_code = 'CITY'
  AND e.entity_id = '101'
  AND g.tenant_id = #{tenantId}
  AND g.status = 1;

注意:查询时必须 JOIN sys_user_permission_grant 带上 tenant_id 过滤,sys_grant_scope_entity 本身不存 tenant_id,通过 grant 的 tenant_id 完成租户隔离。


2.10 权限操作审计日志表 sys_permission_audit_log

-- 记录所有权限变更操作,用于审计和问题排查
-- 只追加写入,不做 UPDATE/DELETE
CREATE TABLE sys_permission_audit_log (
  id             BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id      BIGINT      COMMENT '租户ID',
  operator_id    BIGINT      NOT NULL COMMENT '操作人 user_id',
  target_user_id BIGINT      NOT NULL COMMENT '被操作用户 user_id',
  action         VARCHAR(32) NOT NULL COMMENT 'GRANT_ROLE / REVOKE_ROLE / GRANT_PERM / REVOKE_PERM / MODIFY_SCOPE / MODIFY_EXPIRE',
  source_type    TINYINT     COMMENT '1角色授权 2单独授权',
  source_id      BIGINT      COMMENT '关联角色ID等',
  biz_data       JSON        COMMENT '操作详情快照,如授权的权限点列表、数据范围等',
  created_at     DATETIME    DEFAULT CURRENT_TIMESTAMP,

  KEY idx_tenant_target (tenant_id, target_user_id),
  KEY idx_operator (operator_id),
  KEY idx_created_at (created_at)
);

biz_data 示例:

{
  "roleId": 10,
  "roleName": "销售经理",
  "permissions": [
    { "permCode": "order:view", "scopeCode": "CITY", "entityIds": ["101", "102"] }
  ],
  "expireTime": "2027-03-05T00:00:00"
}

审计日志只追加,不更新。所有授权变更操作完成后同步写入一条记录,配合 revoked_at 字段可完整还原权限的生命周期。


三、关键设计决策说明

3.1 同一用户同一权限点可以有多条记录

场景:张三通过"销售角色"获得了order:view(永久),管理员又单独给他授予了order:export(30天)。

user_id | permission_id | source_type | source_id  | expire_time
张三    | order:view    | 1(角色)     | 销售角色ID | NULL(永久)
张三    | order:export  | 2(单独)     | NULL       | 2026-04-01

查询时取所有 status=1 AND (expire_time IS NULL OR expire_time > NOW()) 的记录并集。


3.2 角色变更不影响已有授权

Role 只是批量授权的入口,授权完成后,记录独立存在。
如果角色新增了一个权限点,不会自动同步给已授权的用户,需要重新授权(或提供"同步角色权限"的操作)。
这样设计保证了授权记录的稳定性,避免角色变更产生意外的权限扩散。

如果业务需要"角色变更自动同步",可以通过 source_id(配合 source_type=1)找到所有关联用户批量更新,但需要明确产品策略。


3.3 撤销授权的两种粒度

-- 撤销单个权限点(同时记录撤销时间)
UPDATE sys_user_permission_grant
SET status = 0, revoked_at = NOW()
WHERE id = #{grantId};

-- 撤销某次角色授权(批量撤销该角色带来的所有权限点)
UPDATE sys_user_permission_grant
SET status = 0, revoked_at = NOW()
WHERE user_id = #{userId} AND source_type = 1 AND source_id = #{roleId};

3.4 超管优先级与判断链路

鉴权链路中,超管判断优先于一切权限点校验,按以下顺序执行:

1. JWT 解析 → 拿到 userId + tenantId
        ↓
2. 查 sys_user_tenant.is_super
   → is_super = 1:租户内超管,直接放行,跳过所有后续校验
        ↓
3. 查 sys_user_role JOIN sys_role WHERE is_super = 1
   → 用户持有超管角色:直接放行
        ↓
4. 普通权限点校验:查 sys_user_permission_grant
        ↓
5. 数据范围注入:根据 scope_code 注入 WHERE 条件

两种超管的区别:

  • sys_user_tenant.is_super:平台运营直接在用户-租户关系上打标,不占用角色资源
  • sys_role.is_super:通过分配超管角色实现,可以走正常的角色管理流程授权/撤销,更灵活

四、授权流程

4.1 通过角色批量授权

1. 管理员选择用户 + 角色
2. 系统查询角色下所有权限点(sys_role_permission JOIN sys_permission)
3. 对每个权限点,查出其关联的数据资源点及可选范围
4. 管理员逐一配置:数据范围 + 过期时间(可批量设置默认值)
5. 提交 → 批量写入 sys_user_permission_grant(每个权限点一条记录)
   source_type = 1, source_id = 角色ID

4.2 单独授权某个权限点

1. 管理员选择用户 + 权限点
2. 系统展示该权限点关联的数据资源点及可选范围
3. 管理员配置:数据范围 + 过期时间
4. 提交 → 写入一条 sys_user_permission_grant
   source_type = 2, source_id = NULL

五、运行时校验流程

5.1 加载用户权限

SELECT
  p.perm_code,
  g.data_resource_id,
  g.scope_code,
  g.expire_time
FROM sys_user_permission_grant g
JOIN sys_permission p ON p.id = g.permission_id
WHERE g.user_id = #{userId}
  AND g.status = 1
  AND (g.expire_time IS NULL OR g.expire_time > NOW())

5.2 操作权限校验(AOP)

@RequiresPermission("order:view")
// AOP 拦截 → 检查用户权限集合中是否包含 "order:view"

5.3 数据权限注入(MyBatis 拦截器)

GrantInfo grant = permissionCache.getGrant(userId, "order:view");

switch (grant.getScopeCode()) {
    case "ALL":            // 不追加条件
    case "DEPT_AND_CHILD": // WHERE dept_id IN (当前部门及子部门)
    case "DEPT":           // WHERE dept_id = #{deptId}
    case "SELF":           // WHERE created_by = #{userId}
    case "CITY":           // WHERE city_id IN (#{entityIds})
    case "PROJECT":        // WHERE project_id IN (#{entityIds})
}

六、缓存设计

缓存 Key 内容 TTL
perm:user:{userId}:grants 用户所有有效授权列表 动态计算
perm:resource:{resourceId}:scopes 数据资源点可选范围元数据 120分钟
sys:dept:tree 全局组织架构树 120分钟

TTL 动态计算:

long ttl = grants.stream()
    .filter(g -> g.getExpireTime() != null)
    .mapToLong(g -> Duration.between(now, g.getExpireTime()).getSeconds())
    .min()
    .orElse(1800);

ttl = Math.min(ttl + 60, 1800); // 最长不超过30分钟

缓存失效触发点:

  • 新增/撤销/修改授权 → 失效 perm:user:{userId}:grants
  • 授权到期(定时任务扫描)→ 失效 perm:user:{userId}:grants

七、系统架构图

系统架构图


八、ER 图

ER图


九、接口设计

接口分两侧:管理侧(权限配置与授权操作)和使用侧(鉴权与权限查询)。

租户隔离说明: 所有接口通过 JWT payload 中的 tenant_id 隔离租户,无需在请求参数中显式传递。切换租户需调用 /auth/switch-tenant 重新签发 JWT。

维度 管理侧 使用侧
调用方 管理后台 业务前端 / 业务服务 / 网关
鉴权方式 需要管理员角色权限 只需登录态(JWT)
操作特征 写多读少 只读,高频
缓存策略 写完主动失效缓存 全走缓存,不直接查库

9.1 管理侧接口

权限点管理

方法 路径 说明
GET /permissions 查询权限点列表
POST /permissions 新建权限点
PUT /permissions/{id} 修改权限点
DELETE /permissions/{id} 删除权限点

数据资源点管理

方法 路径 说明
GET /data-resources 查询数据资源点列表
POST /data-resources 新建数据资源点
GET /data-resources/{id}/scopes 查询某资源点的可选范围列表
POST /data-resources/{id}/scopes 新增可选范围
DELETE /data-resources/{id}/scopes/{scopeCode} 删除可选范围

范围数据字典(外部同步入口)

方法 路径 说明
POST /scope-data/sync 外部系统批量同步范围数据(UPSERT)
GET /scope-data/{scopeCode} 查询某 scopeCode 下的实体列表

角色管理

方法 路径 说明
GET /roles 查询角色列表
POST /roles 新建角色
PUT /roles/{id} 修改角色
DELETE /roles/{id} 删除角色
GET /roles/{id}/permissions 查询角色下的权限点列表
PUT /roles/{id}/permissions 更新角色权限点,全量覆盖

用户授权管理

方法 路径 说明
GET /users/{userId}/roles 查询用户持有的角色列表
POST /users/{userId}/grant/by-role 通过角色批量授权
POST /users/{userId}/grant/by-permission 单独授权某个权限点
PUT /grants/{grantId} 修改单条授权
DELETE /grants/{grantId} 撤销单条授权
DELETE /users/{userId}/roles/{roleId}/grants 撤销某角色带来的所有授权
PUT /users/{userId}/roles/{roleId}/expire 续期某角色授权

POST /users/{userId}/grant/by-role 请求体示例:

{
  "roleId": 10,
  "startTime": "2026-03-05T00:00:00",
  "expireTime": "2027-03-05T00:00:00",
  "scopeConfigs": [
    {
      "permissionId": 101,
      "dataResourceId": 1,
      "scopeCode": "CITY",
      "entityIds": ["101", "102", "108"]
    },
    {
      "permissionId": 102,
      "dataResourceId": 1,
      "scopeCode": "SELF"
    }
  ]
}

9.2 使用侧接口

权限加载

方法 路径 说明
GET /auth/me/permissions 返回当前用户权限点、数据范围、菜单树

响应体示例:

{
  "permCodes": ["order:view", "order:export", "customer:edit"],
  "dataScopes": {
    "order:view": { "scopeCode": "CITY", "entityIds": ["101", "102"] },
    "customer:edit": { "scopeCode": "SELF" }
  },
  "menus": [
    {
      "permCode": "menu:order",
      "name": "订单管理",
      "path": "/order",
      "icon": "order-icon",
      "sort": 1,
      "children": [
        { "permCode": "menu:order:list", "name": "订单列表", "path": "/order/list", "sort": 1 }
      ]
    }
  ]
}

菜单是权限点中 type=1 的数据,不单独提供菜单接口,由本接口统一返回。

鉴权校验

方法 路径 说明
POST /auth/check 校验某 userId 是否拥有某 permCode
GET /auth/data-scope 查询用户在某权限点上的数据范围
POST /auth/cache/refresh/{userId} 主动刷新某用户权限缓存

十、多租户扩展方案

适用场景:平台需要支持多个租户(App)完全隔离,每个租户有自己独立的角色、权限点配置,用户可同时属于多个租户并自由切换身份。

10.1 核心设计思路

租户完全隔离 + 用户跨租户共享 + 租户级超管

- sys_role、sys_permission、sys_data_resource 等配置表加 tenant_id,数据完全隔离
- sys_user 不加 tenant_id,用户是平台级共享身份
- 用户与租户的归属关系通过 sys_user_tenant 管理
- 鉴权时 JWT 携带当前 tenant_id,所有查询强制带 tenant_id 过滤
- 超管标记在 sys_user_tenant 上,只在该租户内生效

10.2 新增表

租户表 sys_tenant

CREATE TABLE sys_tenant (
  id           BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_code  VARCHAR(64)  NOT NULL UNIQUE COMMENT '租户编码',
  tenant_name  VARCHAR(128) NOT NULL COMMENT '租户名称',
  status       TINYINT      DEFAULT 1 COMMENT '1正常 0禁用',
  created_at   DATETIME     DEFAULT CURRENT_TIMESTAMP
);

用户-租户关系表 sys_user_tenant

CREATE TABLE sys_user_tenant (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id     BIGINT  NOT NULL,
  tenant_id   BIGINT  NOT NULL,
  is_super    TINYINT DEFAULT 0 COMMENT '1该租户下的超管,跳过权限校验',
  status      TINYINT DEFAULT 1,
  created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,

  UNIQUE KEY uk_user_tenant (user_id, tenant_id),
  KEY idx_user_id (user_id),
  KEY idx_tenant_id (tenant_id)
);

10.3 现有表字段改动

ALTER TABLE sys_role                  ADD COLUMN tenant_id BIGINT NOT NULL COMMENT '租户ID';
ALTER TABLE sys_permission            ADD COLUMN tenant_id BIGINT NOT NULL COMMENT '租户ID';
ALTER TABLE sys_data_resource         ADD COLUMN tenant_id BIGINT NOT NULL COMMENT '租户ID';
ALTER TABLE sys_data_resource_scope   ADD COLUMN tenant_id BIGINT NOT NULL COMMENT '租户ID';
ALTER TABLE sys_user_role             ADD COLUMN tenant_id BIGINT NOT NULL COMMENT '租户ID';
ALTER TABLE sys_user_permission_grant ADD COLUMN tenant_id BIGINT NOT NULL COMMENT '租户ID';

ALTER TABLE sys_user_permission_grant ADD INDEX idx_tenant_user (tenant_id, user_id);
ALTER TABLE sys_role                  ADD INDEX idx_tenant (tenant_id);
ALTER TABLE sys_permission            ADD INDEX idx_tenant (tenant_id);

不需要加 tenant_id 的表:

原因
sys_user 用户是平台级共享身份,通过 sys_user_tenant 关联租户
sys_scope_data 范围数据字典全局共享(城市、区域等不属于某个租户)
sys_grant_scope_entity 跟随 sys_user_permission_grant,grant 有 tenant_id 即可
sys_dept 部门可按需决定,若需租户隔离则加,否则通过 grant 的 tenant_id 间接隔离

10.4 鉴权链路改动

Long userId   = token.getUserId();
Long tenantId = token.getTenantId();

boolean isSuperAdmin = userTenantService.isSuper(userId, tenantId);
if (isSuperAdmin) return;

Set<String> permCodes = permissionService.getPermCodes(userId, tenantId);
GrantInfo grant = permissionService.getGrant(userId, tenantId, "order:view");

10.5 用户切换租户

GET  /auth/me/tenants    查询当前用户所属的租户列表
POST /auth/switch-tenant 切换租户,返回新 token

GET /auth/me/tenants 响应示例:

{
  "tenants": [
    { "tenantId": 1, "tenantName": "华东事业部", "isSuper": false },
    { "tenantId": 2, "tenantName": "华南事业部", "isSuper": true }
  ],
  "current": 1
}

10.6 缓存 Key 加入 tenant_id

缓存 Key TTL
perm:tenant:{tenantId}:user:{userId}:grants 动态
perm:tenant:{tenantId}:role:{roleId}:perms 60min
perm:tenant:{tenantId}:resource:{id}:scopes 120min
sys:tenant:{tenantId}:dept:tree 120min

10.7 改动量总结

改动项 内容
新增表 sys_tenant、sys_user_tenant
字段改动 6 张核心表加 tenant_id
鉴权链路 JWT 带 tenant_id,所有查询加 tenant_id 过滤
缓存 Key 全部加 tenant_id 维度
接口新增 /auth/me/tenants、/auth/switch-tenant

现有权限模型无需推倒重建,多租户是在原有模型上套了一层租户维度的壳,改动最小化。