Java系列文章
文章目录
引言:为什么需要树形权限设计?
在企业级后台系统中,权限管理常涉及多层级结构(如菜单权限、操作权限、数据权限)。
痛点场景:
- 权限层级嵌套复杂(如:系统管理 > 角色管理 > 新增/删除按钮)
- 前端展示需动态渲染树形组件
- 后端需高效查询并转换嵌套结构
本文目标:
从数据库设计 → 后端递归逻辑 → 前端树组件渲染,实现完整的树形权限链路,并提供高性能优化方案。
一、数据库设计:如何存储层级关系?
1.1 表结构设计(核心字段)
CREATE TABLE `tb_permission` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`parent_id` int DEFAULT NULL COMMENT '父级id',
`permission_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '权限标识',
`module_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '模块名称',
`menu_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单类型',
`icon` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单图标',
`path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单路由',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `unq_permission` (`permission_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=129 DEFAULT CHARSET=utf8 COMMENT='权限表';
设计要点:
- parent_id:权限菜单通常采用树形结构,包含多级菜单。每一级菜单都有一个唯一的父级ID(Parent
id),用于表示该菜单项的上级菜单。顶级菜单的父级ID可以设置为0或null。 - permission_name:每个菜单或按钮项对应的操作权限需要通过权限标识来定义。权限标识通常是字符串格式,它描述了用户对于某个资源的访问或操作权限。例如,“ORGANIZE:USER”,代表用户管理模块-菜单权限,“ORGANIZE:USER:UPDATE”代表用户更新-按钮权限。
1.2 示例数据
二、后端实现:SpringBoot如何生成树结构?
2.1 实体类设计(关键注解)
@Data
public class TreeMenu {
private Integer id;
private Integer parentId;
private String permissionName;
private String moduleName;
private String menuType;
private String icon;
private String path;
private String createTime;
private Integer level;
private Integer key;
private List<TreeMenu> children;
}
2.2 核心算法:递归构建树
//权限管理-树形权限
public ArrayList<TreeMenu> searchPermissionTree() {
return deepTree(permissionDao.searchPermissionTree());
}
public List<TreeMenu> buildPermissionTree() {
// 1. 查询所有权限(单次数据库查询!)
List<TreeMenu> allPermissions = permissionDao.searchPermissionAll();
// 2. 递归构建树(父节点ID=0为根)
return deepTree(allPermissions );
}
/**
* 转换树形结构
* @param menuList
* @return
*/
public ArrayList<TreeMenu> deepTree(ArrayList<TreeMenu> menuList) {
//创建list集合,用于数据最终封装
ArrayList<TreeMenu> finalNode = new ArrayList<>();
for (TreeMenu menus : menuList) {
Integer topId = 0;
//判断Pid是否等于0 0是最高的节点 将查询出的数据放进list集合
if (topId.equals(menus.getParentId())) {
finalNode.add(selectTree(menus, menuList));
}
}
// 递归设置节点层级
for (TreeMenu menu : finalNode) {
setNodeLevel(menu,1);
}
return finalNode;
}
public TreeMenu selectTree(TreeMenu m1, ArrayList<TreeMenu> menuList) {
//因为向一层菜单里面放二层菜单,二层里面还要放三层,把对象初始化
m1.setChildren(new ArrayList<TreeMenu>());
//遍历所有菜单list集合,进行判断比较,比较id和pid值是否相同
for (TreeMenu m2 : menuList) {
//判断 id和pid值是否相同
if (m1.getId().equals(m2.getParentId())) {
//如果children为空,进行初始化操作
if (m1.getChildren() == null) {
m1.setChildren(new ArrayList<TreeMenu>());
}
//把查询出来的子菜单放到父菜单里面
m1.getChildren().add(selectTree(m2, menuList));
}
}
return m1;
}
// 递归设置节点层级
public void setNodeLevel(TreeMenu node, int level) {
node.setLevel(level);
node.setKey(node.getId());
for (TreeMenu child : node.getChildren()) {
setNodeLevel(child, level + 1);
}
}
提示:这里把查询出来的权限数组,通过工具类转换成树形结构,实现方法不一
2.3 Controller:树形结构数据
2.3.1 编写树形数据接口
/**
1. 权限管理-树形结构
2. @return
*/
@GetMapping("/permission/searchPermissionTree")
@SaCheckLogin
public R searchPermissionTree() {
ArrayList<TreeMenu> list = permissionService.searchPermissionTree();
return R.success(list);
}
2.3.2 Apifox测试接口
三、前端渲染:Vue3 + Ant Design 动态树实现
3.1前端页面实现
3.2 前端代码
<template>
<div class="menu">
<a-card shadow="never">
<template #title>
<div class="menu-header">
<div class="menu-nav">
<a-input placeholder="请输入搜索内容" allow-clear />
<a-button type="primary">查询</a-button>
<a-button type="primary">重置</a-button>
</div>
<div><a-button type="primary" @click="onAdd">新增</a-button></div>
</div>
</template>
<a-table :columns="menuTableColumn" :data-source="menuTableData" :pagination="false" rowKey="id" size="middle" bordered>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button type="text" @click="onAddBatch(record)">新增</a-button>
<a-button type="text" @click="onEdit(record)">编辑</a-button>
<a-button type="text" @click="onDelete(record)">删除</a-button>
</template>
</template>
</a-table>
</a-card>
<diaLogModal v-model="diaLogData.show" :diaLogData="diaLogData" @submit="onDialogConfirm">
<a-form ref="permissionFormRef" :model="dataForm" :label-col="{ style: { width: '150px' } }" :wrapper-col="{ span: 14 }">
<a-form-item label="菜单名称" name="moduleName" :rules="[{ required: true, message: '菜单名称不能为空' }]">
<a-input v-model:value="dataForm.moduleName" placeholder="请选择菜单名称" />
</a-form-item>
<a-form-item label="图标" name="icon">
<a-input v-model:value="dataForm.icon" placeholder="请输入图标名称" />
</a-form-item>
<a-form-item label="权限标识" name="permissionName" :rules="[{ required: true, message: '权限标识不能为空' }]">
<a-input v-model:value="dataForm.permissionName" placeholder="请输入权限标识" />
</a-form-item>
<a-form-item label="路由地址" name="path">
<a-input v-model:value="dataForm.path" placeholder="请输入路由地址" />
</a-form-item>
<a-form-item label="菜单类型" name="menuType">
<a-select v-model:value="dataForm.menuType" :options="menuTypeData" placeholder="请选择菜单类型"></a-select>
</a-form-item>
</a-form>
</diaLogModal>
<sunLoading v-model="loading"></sunLoading>
</div>
</template>
<script setup>
import { onMounted, ref, reactive, h, watch } from 'vue'
import { searchPermissionTree, editPermission, deletePermission } from '@/api/modules/system'
import { deepData, getAllChildIds } from '@/util/util'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
import { Modal, message } from 'ant-design-vue'
// 初始化数据
function useInitData() {
const loading = ref(false)
const menuTableData = ref([])
const menuTableColumn = ref([
{
title: '菜单名称',
dataIndex: 'moduleName',
key: 'moduleName',
width: 150
},
{
title: '图标',
dataIndex: 'icon',
key: 'icon',
width: 100
},
{
title: '权限标识',
dataIndex: 'permissionName',
key: 'permissionName',
width: 200,
ellipsis: true
},
{
title: '路由地址',
dataIndex: 'path',
key: 'path',
width: 150
},
{
title: '菜单类型',
dataIndex: 'menuType',
key: 'menuType',
width: 100
},
{
title: '菜单层级',
dataIndex: 'level',
key: 'level',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 150
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 160
}
])
const diaLogData = reactive({
show: false,
title: '菜单管理',
width: '40%',
buttons: ['取消', '确定']
})
const dataForm = reactive({
id: '',
parentId: '',
permissionName: '',
moduleName: '',
menuType: '菜单',
icon: '',
path: ''
})
const originForm = reactive({
id: '',
parentId: '',
permissionName: '',
moduleName: '',
menuType: '菜单',
icon: '',
path: ''
})
const menuTypeData = ref([
{
label: '菜单',
value: '菜单'
},
{
label: '按钮',
value: '按钮'
}
])
const permissionFormRef = ref()
const getMenuData = () => {
loading.value = true
searchPermissionTree()
.then(res => {
loading.value = false
menuTableData.value = res ? deepData(res) : []
})
.catch(err => {})
}
const getInitData = () => {
// getAllPermissions()
// .then(res => {
// })
// .catch(err => {})
}
onMounted(() => {
getMenuData()
getInitData()
})
return {
menuTableData,
menuTableColumn,
diaLogData,
dataForm,
originForm,
menuTypeData,
loading,
permissionFormRef,
getMenuData
}
}
// 操作事件
function useAction() {
const onAdd = () => {
diaLogData.show = true
Object.assign(dataForm, originForm)
dataForm.parentId = 0
}
const onAddBatch = row => {
diaLogData.show = true
Object.assign(dataForm, originForm)
dataForm.parentId = row.id
}
const onEdit = row => {
diaLogData.show = true
Object.assign(dataForm, row)
}
const onDelete = row => {
Modal.confirm({
title: '警告',
icon: h(ExclamationCircleOutlined),
content: '是否确认删除?',
okText: '确认',
cancelText: '取消',
onOk() {
let deleteIds = [row.id, ...getAllChildIds(row)]
// 获取所有的子级ID
deletePermission({ ids: deleteIds })
.then(res => {
message.success('删除成功')
diaLogData.show = false
getMenuData()
})
.catch(err => {})
}
})
}
const onDialogConfirm = () => {
permissionFormRef.value
.validateFields()
.then(res => {
console.log('dataForm', dataForm)
editPermission(dataForm)
.then(res => {
message.success('操作成功')
diaLogData.show = false
getMenuData()
})
.catch(err => {})
})
.catch(err => {})
}
watch(
() => diaLogData.show,
res => {
if (!res) permissionFormRef.value.resetFields()
}
)
return {
onAdd,
onEdit,
onAddBatch,
onDelete,
onDialogConfirm
}
}
const { menuTableData, menuTableColumn, diaLogData, dataForm, originForm, menuTypeData, loading, permissionFormRef, getMenuData } = useInitData()
const { onAdd, onEdit, onAddBatch, onDelete, onDialogConfirm } = useAction()
</script>
<style lang="scss" scoped>
.menu {
.menu-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 50px;
.menu-nav {
display: flex;
gap: 16px;
}
}
}
</style>
四、总结
树形权限在Spring Boot中的实现要点
- 模型设计:
- 使用自关联表结构存储具有层级关系的数据(如角色或菜单)。
- 关键字段包括id, module_name, parent_id等。
- 递归查询:
- 实现获取完整树形结构的功能。可以使用Hutool工具类、MyBatis等框架简化操作。
- 权限验证:
- 基于用户角色和权限进行访问控制。
- 考虑子节点继承父节点权限的逻辑。
- 前端展示:
- 利用JavaScript库(如Ant-Design, jsTree)将后端数据转化为树状图或菜单。