权限设计器
权限设计器
业务用途
权限设计器是平台在标准 RBAC(用户-角色-菜单)之上的精细化权限扩展,由 PermissionDesignerController(/api/permission)统一提供。它把权限拆成四个维度,分别配置:
| 维度 | 实体 | 解决的问题 |
|---|---|---|
| 角色×菜单矩阵 | sys_role_menu | 哪些角色能访问哪些菜单(与 互补) |
| 按钮权限 | SysRoleButtonPermission | 角色在某菜单下能看到哪些按钮(新增/编辑/删除/导出…) |
| 字段权限 | SysRoleFieldPermission | 角色在某张表上哪些字段可读/可编辑/必填 |
| 数据权限规则 | SysRoleDataPermission | 角色能看到哪些数据行(全部/本部门/本部门及以下/仅本人/自定义/岗位) |
数据权限规则目前为「配置层」
平台已实现数据权限规则的存储与查询(SysRoleDataPermission 的 ruleType 与 customRules),但未发现自动 SQL 注入拦截器(无 @DataScope AOP、无 MyBatis 数据权限插件)。当前仅 TenantInterceptor(租户隔离)自动生效。数据权限规则需业务代码在查询时主动读取并拼接条件,或后续接入 MyBatis-Plus 数据权限插件实现自动注入。

涉及文件
| 层 | 文件路径 |
|---|---|
| 控制器 | backend/src/main/java/com/lowcode/controller/PermissionDesignerController.java |
| 按钮权限实体 | backend/src/main/java/com/lowcode/entity/SysRoleButtonPermission.java |
| 字段权限实体 | backend/src/main/java/com/lowcode/entity/SysRoleFieldPermission.java |
| 数据权限实体 | backend/src/main/java/com/lowcode/entity/SysRoleDataPermission.java |
| Mapper | SysRoleButtonPermissionMapper、SysRoleFieldPermissionMapper、SysRoleDataPermissionMapper、SysMenuMapper |
| 前端 API | frontend/src/api/role.ts(权限设计器相关方法集中在此) |
| 前端页面 | frontend/src/views/system/role/index.vue(权限设计器 Tab/弹窗) |
数据库表
sys_role_button_permission(按钮权限表)
| 字段 | 类型 | 说明 |
|---|---|---|
role_id | Long | 角色 ID |
menu_id | String | 所属菜单 ID(注意是 String 类型) |
button_code | String | 按钮编码(如 add/edit/delete/export/import/approve) |
button_name | String | 按钮名称(如「新增」) |
enabled | Boolean | 是否启用(true该角色可见此按钮) |
tenant_id | Long | 租户 ID |
sys_role_field_permission(字段权限表)
| 字段 | 类型 | 说明 |
|---|---|---|
role_id | Long | 角色 ID |
table_name | String | 业务表名(如 sys_user) |
field_name | String | 字段名(如 name) |
field_label | String | 字段中文名(如「名称」) |
readable | Boolean | 是否可读(可见) |
editable | Boolean | 是否可编辑 |
required | Boolean | 是否必填 |
tenant_id | Long | 租户 ID |
sys_role_data_permission(数据权限规则表)
| 字段 | 类型 | 说明 |
|---|---|---|
role_id | Long | 角色 ID |
rule_type | Integer | 规则类型(1~6,见下表) |
custom_rules | String | 自定义规则(JSON 字符串,仅 ruleType=5 用) |
tenant_id | Long | 租户 ID |
数据权限规则类型(ruleType)
| ruleType | 名称 | 说明 |
|---|---|---|
| 1 | 全部数据权限 | 可查看所有数据 |
| 2 | 本部门数据 | 仅可查看本部门数据 |
| 3 | 本部门及以下 | 可查看本部门及下级部门数据 |
| 4 | 仅本人数据 | 仅可查看本人创建的数据 |
| 5 | 自定义数据权限 | 按配置的 customRules JSON 过滤 |
| 6 | 岗位数据 | 仅可查看本岗位数据 |
后端实现
端点一览
PermissionDesignerController 基础路径:/api/permission
角色×菜单矩阵
| HTTP 方法 | 路径 | 方法名 | 说明 |
|---|---|---|---|
| GET | /api/permission/role-permission-matrix | getRolePermissionMatrix | 获取角色×菜单权限矩阵(返回 roles + menus + 各角色已勾选 menuIds) |
| PUT | /api/permission/role/{roleId}/menus | saveRoleMenus | 保存某角色的菜单授权(先删后插 sys_role_menu) |
按钮权限
| HTTP 方法 | 路径 | 方法名 | 说明 |
|---|---|---|---|
| GET | /api/permission/button-permissions/{roleId}?menuId= | getButtonPermissions | 获取角色的按钮权限配置 |
| PUT | /api/permission/button-permissions/{roleId} | saveButtonPermissions | 保存角色的按钮权限(先删后插) |
字段权限
| HTTP 方法 | 路径 | 方法名 | 说明 |
|---|---|---|---|
| GET | /api/permission/field-permissions/{roleId}?tableName= | getFieldPermissions | 获取角色的字段权限配置 |
| PUT | /api/permission/field-permissions/{roleId} | saveFieldPermissions | 保存角色的字段权限(先删后插) |
数据权限规则
| HTTP 方法 | 路径 | 方法名 | 说明 |
|---|---|---|---|
| GET | /api/permission/data-permission-rules/{roleId} | getDataPermissionRules | 获取角色的数据权限规则(返回 ruleTypes 枚举 + 当前 ruleType + customRules) |
| PUT | /api/permission/data-permission-rules/{roleId} | saveDataPermissionRules | 保存角色的数据权限规则(upsert) |
关键代码
角色×菜单矩阵
矩阵接口一次性返回所有角色、所有菜单、以及每个角色已勾选的菜单 ID 集合,供前端渲染勾选矩阵:
@GetMapping("/role-permission-matrix")
public Result<Map<String, Object>> getRolePermissionMatrix() {
List<SysRole> roles = roleMapper.selectList(null);
List<SysMenu> menus = menuMapper.selectList(new LambdaQueryWrapper<SysMenu>()
.in(SysMenu::getMenuType, "M", "C", "F", "0", "1", "page", "button")
.orderByAsc(SysMenu::getSortOrder).orderByAsc(SysMenu::getId));
List<Map<String, Object>> matrix = new ArrayList<>();
for (SysRole role : roles) {
Map<String, Object> row = new HashMap<>();
row.put("roleId", role.getId());
row.put("roleName", role.getRoleName());
row.put("roleCode", role.getRoleCode());
// 查该角色已分配的菜单
List<SysMenu> roleMenus = menuMapper.selectMenusByRoleId(role.getId());
Set<Long> menuIds = new HashSet<>();
for (SysMenu menu : roleMenus) menuIds.add(menu.getId());
row.put("menuIds", menuIds);
matrix.add(row);
}
Map<String, Object> result = new HashMap<>();
result.put("roles", roles);
result.put("menus", menus);
result.put("matrix", matrix);
return Result.success(result);
}按钮权限
按钮权限读取时,若无配置则返回一组默认按钮(add/edit/delete/export/import/approve),方便前端初始化:
// 无配置时的默认按钮
buttonPermissions.add(createButtonPermission("add", "新增", true));
buttonPermissions.add(createButtonPermission("edit", "编辑", true));
buttonPermissions.add(createButtonPermission("delete", "删除", true));
buttonPermissions.add(createButtonPermission("export", "导出", false));
buttonPermissions.add(createButtonPermission("import", "导入", false));
buttonPermissions.add(createButtonPermission("approve", "审批", true));保存按钮权限同样采用「先删后插」覆盖策略,按 roleId + menuId 定位:
@PutMapping("/button-permissions/{roleId}")
public Result<Void> saveButtonPermissions(@PathVariable Long roleId,
@RequestBody SaveButtonPermissionsRequest request) {
SysRole role = roleMapper.selectById(roleId);
if (role == null) return Result.error("角色不存在");
String menuId = request.getMenuId();
buttonPermissionMapper.delete(new LambdaQueryWrapper<SysRoleButtonPermission>()
.eq(SysRoleButtonPermission::getRoleId, roleId)
.eq(menuId != null && !menuId.isEmpty(), SysRoleButtonPermission::getMenuId, menuId));
for (Map<String, Object> btn : request.getButtons()) {
SysRoleButtonPermission p = new SysRoleButtonPermission();
p.setRoleId(roleId);
p.setMenuId((String) btn.get("menuId"));
p.setButtonCode((String) btn.get("code"));
p.setButtonName((String) btn.get("name"));
p.setEnabled((Boolean) btn.get("enabled"));
buttonPermissionMapper.insert(p);
}
return Result.success("按钮权限保存成功");
}字段权限
字段权限以「表」为粒度配置,每条记录描述某角色对某表的某字段的读/写/必填控制。无配置时返回默认字段模板(name/status/remark/createTime):
// 无配置时的默认字段模板
fieldConfigs.add(createFieldConfig("name", "名称", true, true, true));
fieldConfigs.add(createFieldConfig("status", "状态", true, true, true));
fieldConfigs.add(createFieldConfig("remark", "备注", true, true, false));
fieldConfigs.add(createFieldConfig("createTime", "创建时间", false, true, false));数据权限规则
数据权限规则是单行 upsert(一个角色一条规则)。读取时返回规则类型枚举与当前配置;customRules 以 JSON 解析后返回:
@PutMapping("/data-permission-rules/{roleId}")
public Result<Void> saveDataPermissionRules(@PathVariable Long roleId,
@RequestBody Map<String, Object> config) {
SysRole role = roleMapper.selectById(roleId);
if (role == null) return Result.error("角色不存在");
Integer ruleType = (Integer) config.get("currentRuleType");
String customRulesJson = null;
if (config.get("customRules") != null) {
customRulesJson = objectMapper.writeValueAsString(config.get("customRules"));
}
SysRoleDataPermission existing = dataPermissionMapper.selectOne(
new LambdaQueryWrapper<SysRoleDataPermission>().eq(SysRoleDataPermission::getRoleId, roleId));
if (existing != null) {
existing.setRuleType(ruleType);
existing.setCustomRules(customRulesJson);
dataPermissionMapper.updateById(existing);
} else {
SysRoleDataPermission p = new SysRoleDataPermission();
p.setRoleId(roleId);
p.setRuleType(ruleType);
p.setCustomRules(customRulesJson);
dataPermissionMapper.insert(p);
}
return Result.success("数据权限规则保存成功");
}customRules 的结构
customRules 是一个 JSON 数组字符串,由前端设计器生成,用于 ruleType=5(自定义)时描述过滤条件。具体结构由前端约定,后端仅做存取与 JSON 序列化,不解析执行。业务侧如需应用,需自行读取该 JSON 并拼接 SQL 条件。
前端实现
权限设计器的前端方法集中在 frontend/src/api/role.ts:
// 矩阵与菜单授权
export function getPermissionMatrix() {
return request.get('/permission/role-permission-matrix')
}
export function saveDesignerRoleMenus(roleId, menuIds) {
return request.put(`/permission/role/${roleId}/menus`, { menuIds })
}
// 字段权限
export function getFieldPermissions(roleId, tableName?) {
return request.get(`/permission/field-permissions/${roleId}`,
{ params: tableName ? { tableName } : {} })
}
export function saveFieldPermissions(roleId, data: { tableName: string; fields: any[] }) {
return request.put(`/permission/field-permissions/${roleId}`, data)
}
// 按钮权限
export function getButtonPermissions(roleId, menuId?) {
return request.get(`/permission/button-permissions/${roleId}`,
{ params: menuId ? { menuId } : {} })
}
export function saveButtonPermissions(roleId, data: { menuId: string; buttons: any[] }) {
return request.put(`/permission/button-permissions/${roleId}`, data)
}
// 数据权限
export function getRoleDataPermission(roleId) {
return request.get(`/permission/data-permission-rules/${roleId}`)
}
export function saveRoleDataPermission(roleId, data: { currentRuleType: number; customRules?: any[] }) {
return request.put(`/permission/data-permission-rules/${roleId}`, data)
}页面入口在 frontend/src/views/system/role/index.vue,通常以角色列表 + 权限设计弹窗/Tab 的形式,分别切换「菜单矩阵」「按钮权限」「字段权限」「数据权限」四个面板。

操作步骤
- 进入「系统管理 -> 角色管理」,选中某角色打开「权限设计器」。
- 菜单矩阵:在角色×菜单勾选矩阵中勾选该角色可访问的菜单,保存。
- 按钮权限:选择某个菜单,配置该角色在此菜单下可见的按钮(新增/编辑/删除等开关),保存。
- 字段权限:输入业务表名,配置该角色对该表各字段的可读/可编辑/必填,保存。
- 数据权限:选择规则类型(如「本部门及以下」=3),保存。自定义规则(5)可配置
customRulesJSON。

常见问题
按钮权限和菜单的 perms 有什么区别?
菜单 perms(如 system:user:add)是「定义」按钮权限标识;SysRoleButtonPermission 是「配置」某角色是否启用该按钮。前者是菜单树的属性,后者是角色维度的开关。两者配合:前端既校验 perms 标识,也参考按钮权限配置。
字段权限在前端怎么生效?
字段权限配置存储后,前端表单渲染时可调用 getFieldPermissions 拿到该角色的字段配置,按 readable/editable/required 控制字段的显隐、禁用、必填校验。后端不自动拦截,需前端配合。
数据权限规则配了为什么没生效?
当前数据权限规则只做了存储,没有自动 SQL 注入拦截器。要让规则生效,需在业务查询代码中主动读取当前用户的角色 ruleType,手动拼接 WHERE 条件(如 ruleType=2 加 dept_id = 当前用户部门)。可参考若依的 @DataScope 注解思路自行实现 AOP。
自定义数据权限(ruleType=5)的 customRules 怎么用?
customRules 是前端设计器生成的 JSON 数组,描述过滤条件(如字段、操作符、值)。后端只存取不解析,需业务侧自行实现解析与 SQL 拼接逻辑。
