最近做了菜单管理的功能,之前对这个功能了解的比较少,也没有仔细想过这个问题。刚开始面对这个问题时,想法很简单,有几层菜单就新建几张DB表,然后通过主键关联起来就行了。但是,当做到新增时就遇到问题了——没法垂直扩展!也就是说,如果菜单层级数是变化的,现有的表就没办法满足需求了,坦白的说,如果层级数变了,我总不能动态生成DB表吧(当然Hibernate动态生成,很显然这不是解决问题的方法)。
在网上一查才恍然大悟,原来是将所有菜单写到一张DB表中,只不过是要定义一个Parent字段来指定该菜单的上一级菜单:
(以下DB表设计参考:https://blog.csdn.net/qq_39187822/article/details/83374049)
菜单表:
角色表:
角色与菜单关联表:
用户表:
用户与角色关联表:
经过如上设计,就实现了垂直扩展(水平扩展自然而然实现)。这样一来的话,就会存在另一个问题,如何查数据,如何解析成树形的菜单??
递归方法解析!
这里引用另一篇博客:
https://www.cnblogs.com/lucky-pin/p/10740037.html
但是,该博客并不完整,因为在将菜单分配给角色时,需要指定到叶子菜单,叶子菜单所属的父级菜单也应该自动指定给角色。下面我将结合该篇博客完成完整的角色分配菜单功能。
public class Menu {
private String id;
private String parentId;
private String text;
private String url;
private boolean selected=false; // 默认是没有被选中的
private String yxbz;
private List<Menu> children;
public Menu(String id,String parentId,String text,String url,String yxbz) {
this.id=id;
this.parentId=parentId;
this.text=text;
this.url=url;
this.yxbz=yxbz;
}
/*省略get\set*/
}
创建树形结构的类MenuTree。方法getRootNode获取所有根节点,方法builTree将根节点汇总创建树形结构,buildChilTree为节点建立次级树并拼接上当前树,递归调用buildChilTree不断为当前树开枝散叶直至找不到新的子树。完成递归,获取树形结构。
public class MenuTree {
private List<Menu> menuList = new ArrayList<Menu>();
public MenuTree(List<Menu> menuList) {
this.menuList=menuList;
}
//建立树形结构
public List<Menu> builTree(){
List<Menu> treeMenus =new ArrayList<Menu>();
for(Menu menuNode : getRootNode()) {
menuNode=buildChilTree(menuNode);
treeMenus.add(menuNode);
}
return treeMenus;
}
//递归,建立子树形结构
private Menu buildChilTree(Menu pNode){
List<Menu> chilMenus =new ArrayList<Menu>();
for(Menu menuNode : menuList) {
if(menuNode.getParentId().equals(pNode.getId())) {
chilMenus.add(buildChilTree(menuNode));
}
}
pNode.setChildren(chilMenus);
return pNode;
}
//获取根节点
private List<Menu> getRootNode() {
List<Menu> rootMenuLists =new ArrayList<Menu>();
for(Menu menuNode : menuList) {
if(menuNode.getParentId().equals("0")) {
rootMenuLists.add(menuNode);
}
}
return rootMenuLists;
}
}
在指定菜单是否被选中时,在菜单实体中新增了private boolean selected=false;成员变量,下面的代码就是指定叶子菜单给角色时的工具类,最完整的代码如下:(当然该类中的方法完全可以写到上面的MenuTree类中)
import java.util.ArrayList;
import java.util.List;
public class MenuTreeSelectedAndBuildTree {
// 全部菜单
private List<Menu> allMenuList = new ArrayList<Menu>();
// 已经被选中的叶子菜单(只有最底层的菜单)
private List<Menu> selectedMenuList = new ArrayList<Menu>();
public MenuTreeSelectedAndBuildTree(List<Menu> allMenuList, List<Menu> selectedMenuList) {
super();
this.allMenuList = allMenuList;
this.selectedMenuList = selectedMenuList;
}
// 设置是否选中状态的公开方法
public List<Menu> setMenuSelected() {
for (Menu menu : selectedMenuList) {
String selectedMenuId = menu.getId();
allMenuList = setSelectMeu(allMenuList, selectedMenuId);
}
return allMenuList;
}
// 对allMenuList中的每个对象开始遍历并判断是否需要设置flag
private List<Menu> setSelectMeu(List<Menu> allMenuList2, String selectedMenuId) {
// TODO Auto-generated method stub
for (Menu menu : allMenuList2) {
if (menu.getId().equals(selectedMenuId)) { // 如果相同则说明已经被选中了
menu.setSelected(true);
if (menu.getParentId() != null) { // 递归的终止条件
selectedMenuId = menu.getParentId(); // 以备下一次递归传入的参数
setSelectMeu(allMenuList2, selectedMenuId); // 开始递归调用
}
}
}
return allMenuList2;
}
//建立树形结构
public List<Menu> builTree(){
List<Menu> treeMenus =new ArrayList<Menu>();
for(Menu menuNode : getRootNode()) {
menuNode=buildChilTree(menuNode);
treeMenus.add(menuNode);
}
return treeMenus;
}
//获取根节点
private List<Menu> getRootNode() {
List<Menu> rootMenuLists =new ArrayList<Menu>();
for(Menu menuNode : allMenuList) {
if(menuNode.getParentId().equals("0")) {
rootMenuLists.add(menuNode);
}
}
return rootMenuLists;
}
//递归,建立子树形结构
private Menu buildChilTree(Menu pNode){
List<Menu> chilMenus =new ArrayList<Menu>();
for(Menu menuNode : allMenuList) {
if(menuNode.getParentId().equals(pNode.getId())) {
chilMenus.add(buildChilTree(menuNode));
}
}
pNode.setChildren(chilMenus);
return pNode;
}
}
最后,插入一些数据试试效果。得到的json就可以生成图一菜单了。
POM.xml中引入依赖:
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
测试类:
import java.util.ArrayList;
import java.util.List;
import com.alibaba.fastjson.JSON;
public class Hello {
public static void main(String []args) {
// 所有的菜单
List<Menu> allMenuList= new ArrayList<Menu>();
/*插入一些数据*/
allMenuList.add(new Menu("GN001D000","0","系统管理","/admin","Y"));
allMenuList.add(new Menu("GN001D100","GN001D000","权限管理","/admin","Y"));
allMenuList.add(new Menu("GN001D110","GN001D100","密码修改","/admin","Y"));
allMenuList.add(new Menu("GN001D120","GN001D100","新加用户","/admin","Y"));
allMenuList.add(new Menu("GN001D200","GN001D000","系统监控","/admin","Y"));
allMenuList.add(new Menu("GN001D210","GN001D200","在线用户","/admin","Y"));
allMenuList.add(new Menu("GN002D000","0","订阅区","/admin","Y"));
allMenuList.add(new Menu("GN003D000","0","未知领域","/admin","Y"));
// 已经选中的菜单
List<Menu> selectedMenuList= new ArrayList<Menu>();
Menu selectedMenu = new Menu("GN001D120","GN001D100","新加用户","/admin","Y");
selectedMenu.setSelected(true); // 设置新加用户被选中
selectedMenuList.add(selectedMenu);
/*设置菜单的选中情况并创建树*/
MenuTreeSelectedAndBuildTree menuTree =new MenuTreeSelectedAndBuildTree(allMenuList, selectedMenuList);
allMenuList = menuTree.setMenuSelected(); // 调用设置是否选中方法
allMenuList = menuTree.setMenuSelected(); // 调用建立树形结构方法
/*转为json看看效果*/
String jsonOutput= JSON.toJSONString(allMenuList);
System.out.println(jsonOutput);
}
}
运行结果:
[{
"id": "GN001D000",
"parentId": "0",
"selected": true,
"text": "系统管理",
"url": "/admin",
"yxbz": "Y"
}, {
"id": "GN001D100",
"parentId": "GN001D000",
"selected": true,
"text": "权限管理",
"url": "/admin",
"yxbz": "Y"
}, {
"id": "GN001D110",
"parentId": "GN001D100",
"selected": false,
"text": "密码修改",
"url": "/admin",
"yxbz": "Y"
}, {
"id": "GN001D120",
"parentId": "GN001D100",
"selected": true,
"text": "新加用户",
"url": "/admin",
"yxbz": "Y"
}, {
"id": "GN001D200",
"parentId": "GN001D000",
"selected": false,
"text": "系统监控",
"url": "/admin",
"yxbz": "Y"
}, {
"id": "GN001D210",
"parentId": "GN001D200",
"selected": false,
"text": "在线用户",
"url": "/admin",
"yxbz": "Y"
}, {
"id": "GN002D000",
"parentId": "0",
"selected": false,
"text": "订阅区",
"url": "/admin",
"yxbz": "Y"
}, {
"id": "GN003D000",
"parentId": "0",
"selected": false,
"text": "未知领域",
"url": "/admin",
"yxbz": "Y"
}]
前端通过flag就可以判断是否被选中了。