引言
需求: 菜单管理(服务商角色配置权限管理)、文章分类、MCC类目、区域信息。
地区和菜单数据添加到Redis缓存
框架: SpringBoot+MybatisPlus 对数据表中的菜单进行排序并返回树形Json格式的菜单列表
实现思路:先获取全部菜单,然后再对菜单进行装配,生成树形结构。
先获取一级菜单,再递归获取子节点
{
"data": [
{
"id": "1595742481192857601",
"createTime": "2022-11-24 19:33:50",
"createId": "1",
"updateId": "1",
"updateTime": "2022-11-24 19:33:50",
"code": "1",
"name": "string",
"parentId": null,
"url": "string",
"isExpand": "0",
"isShow": "0",
"type": "1",
"typeText": "菜单",
"sortNum": "0",
"tagsType": "PT",
"tagsTypeText": "平台",
"remark": "string",
"children": []
},
{
"id": "1595742537635606529",
"createTime": "2022-11-24 19:34:03",
"createId": "1",
"updateId": "1",
"updateTime": "2022-11-24 19:34:03",
"code": "2",
"name": "string",
"parentId": null,
"url": "string",
"isExpand": "0",
"isShow": "0",
"type": "1",
"typeText": "菜单",
"sortNum": "0",
"tagsType": "PT",
"tagsTypeText": "平台",
"remark": "string",
"children": []
},
{
"id": "1595742587770122242",
"createTime": "2022-11-24 19:34:15",
"createId": "1",
"updateId": "1",
"updateTime": "2022-11-24 19:34:15",
"code": "3",
"name": "string",
"parentId": null,
"url": "string",
"isExpand": "0",
"isShow": "0",
"type": "1",
"typeText": "菜单",
"sortNum": "0",
"tagsType": "PT",
"tagsTypeText": "平台",
"remark": "string",
"children": [
{
"id": "1595742715377627138",
"createTime": "2022-11-24 19:34:45",
"createId": "1",
"updateId": "1",
"updateTime": "2022-11-24 19:34:45",
"code": "4",
"name": "string",
"parentId": "1595742587770122242",
"url": "string",
"isExpand": "0",
"isShow": "0",
"type": "1",
"typeText": "菜单",
"sortNum": "0",
"tagsType": "PT",
"tagsTypeText": "平台",
"remark": "string",
"children": []
},
{
"id": "1595742770520141826",
"createTime": "2022-11-24 19:34:59",
"createId": "1",
"updateId": "1",
"updateTime": "2022-11-24 19:34:59",
"code": "5",
"name": "string",
"parentId": "1595742587770122242",
"url": "string",
"isExpand": "0",
"isShow": "0",
"type": "1",
"typeText": "菜单",
"sortNum": "0",
"tagsType": "PT",
"tagsTypeText": "平台",
"remark": "string",
"children": [
{
"id": "1595742832834916354",
"createTime": "2022-11-24 19:35:13",
"createId": "1",
"updateId": "1",
"updateTime": "2022-11-24 19:35:13",
"code": "6",
"name": "string",
"parentId": "1595742770520141826",
"url": "string",
"isExpand": "0",
"isShow": "0",
"type": "1",
"typeText": "菜单",
"sortNum": "0",
"tagsType": "PT",
"tagsTypeText": "平台",
"remark": "string",
"children": []
},
{
"id": "1595742854490107906",
"createTime": "2022-11-24 19:35:19",
"createId": "1",
"updateId": "1",
"updateTime": "2022-11-24 19:35:19",
"code": "7",
"name": "string",
"parentId": "1595742770520141826",
"url": "string",
"isExpand": "0",
"isShow": "0",
"type": "1",
"typeText": "菜单",
"sortNum": "0",
"tagsType": "PT",
"tagsTypeText": "平台",
"remark": "string",
"children": []
}
]
}
]
}
]
I 序列化:生成树形结构菜单列表
1.1 读取表数据,获取全部菜单
/**
* 先获取全部菜单,然后再对菜单进行装配,生成树形结构
*/
List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);
List<SysMenuDto> listDto = getSortMenus(list);
1.2 序列化:生成树形结构菜单列表
获取一级菜单,递归获取子节点。
@Override
public List<SysMenuDto> getSortMenus(List<TSysMenu> sourceList) throws Exception {
if (sourceList.size() < 1) {
return null;
}
List<SysMenuDto> dtos = sourceList.stream().map(ele -> {
SysMenuDto dto = new SysMenuDto();
BeanUtils.copyProperties(ele, dto);
return dto;
}).collect(Collectors.toList());
// 获取第一层SysMenuDto: 筛选parentId为空的 或者0的情况: item.getParentId().equals("0")
return getChild(null,dtos);
}
/**
* 递归设置节点
*
* @param id
* @param allMenu
* @return
*/
private List<SysMenuDto> getChild(Long parentId, List<SysMenuDto> allMenu) {
//item.getParentId().equals(id) 会出现空指针的情况
return allMenu.stream()
.filter(item -> {
if (parentId == null) {
return item.getParentId() == null;
} else {
return item.getParentId() != null && item.getParentId().equals(parentId);//集合filter过滤Integer数值为空问题解决方案:使用equal取代==判断。
}
})
.map(item -> item.setChildren(getChild(item.getId(), allMenu)))
.sorted(Comparator.comparingInt(menu -> (menu.getSortNum() == null ? 0 : menu.getSortNum())))
.collect(Collectors.toList());
}
1.3 实体
- 单表查询不用添加事务注解` @Transactional
`
- 新生成实体时,加一下fill,时间和用户ID自动填充。
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_sys_menu")
@ApiModel(value="TSysMenu对象", description="系统菜单表")
public class SysMenu extends Model<SysMenu> {
@ApiModelProperty(value = "主键id")
@TableId("id")
private Long id;
@ApiModelProperty(value = "菜单父级id")
@TableField("parent_id")
private Long parentId;
@ApiModelProperty(value = "排序")
@TableField("sort_num")
private Integer sortNum;
@Override
protected Serializable pkVal() {
return this.id;
}
}
1.4 Dto
@Data
@Accessors(chain = true)//链式访问
public class SysMenuDto extends SysMenu {
@ApiModelProperty(value = "子菜单")
private List<SysMenuDto> children;
}
II 查询优化
2.1 分页查询树结构菜单列表
- PageHelper直接对List进行分页
/*
* PageHelper直接对List进行分页
*/
private PageInfo getPageInfoByList(List all,Integer pageNum,Integer pageSize) {
//创建Page类
Page page = new Page(pageNum, pageSize);
//为Page类中的total属性赋值
int total = all.size();
page.setTotal(total);
//计算当前需要显示的数据下标起始值
int startIndex = (pageNum - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize,total);
//从链表中截取需要显示的子链表,并加入到Page
page.addAll(all.subList(startIndex,endIndex));
//以Page创建PageInfo
PageInfo pageInfo = new PageInfo<>(page);
return pageInfo;
}
PageHelper.startPage
开启分页,通过拦截MySQL的方式,把你的查询语句拦截下来加limit.将查询语句放到PageHelper.startPage后面进行执行
PageHelper.startPage(input.getPageNum(), input.getPageSize());
List<TSysTaoCollege> collegeList = tSysTaoCollegeService.list(lambda);
PageInfo pageInfo = new PageInfo(collegeList);
PageHelper.clearPage();
2.2 地区和菜单数据添加到Redis缓存
使用StringRedisTemplate
- 写入Redis时,手动把对象序列化为json格式字符串。
- 读取Redis时,手动把读取到的JSON反序列化成对象。
/*
先获取全部菜单,然后再对菜单进行装配,生成树形结构
*/
public List<SysMenuDto> getMenus(ETagsType tagsType, Boolean isReadDb) throws Exception {
List<TSysMenu> list = getTmenus(tagsType,isReadDb);//读取表数据
List<SysMenuDto> listDto = getSortMenus(list);//序列化数据
return listDto;
}
public List<TSysMenu> getTmenus(ETagsType tagsType, Boolean isReadDb) throws Exception{
if (tagsType == null) {
throw CommonException.create(ServerResponse.createByError("菜单对象类型不能为空"));
}
//rediskey名:rediskey常量类+菜单对象类型
String keyName = RedisKeyConstant.MENU + "." + tagsType.getCode();//
QueryWrapper<TSysMenu> queryWrapper = new QueryWrapper<>();
//是否读取数据库的菜单,true读取,false不读取
if (isReadDb) {
queryWrapper.eq("tags_type", tagsType);
List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);
stringRedisTemplate.opsForValue().set(keyName, JSONUtil.toJsonStr(list));//写入数据到缓存
return list;
} else {
//读取缓存
if (!stringRedisTemplate.hasKey(keyName)) {
queryWrapper.eq("tags_type", tagsType);
List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);
stringRedisTemplate.opsForValue().set(keyName, JSONUtil.toJsonStr(list));//写入数据到缓存
return list;
} else {
//直接读取缓存数据进行序列化
String jsonStr = stringRedisTemplate.opsForValue().get(keyName);
List<TSysMenu> list = JSONUtil.toList(jsonStr, TSysMenu.class);
return list;
}
}
}
III 排序
3.1 新增节点的默认排序
/*
用于新增时的排序字段
*/
private void setSortNum4create(TSysMenu menu){
LambdaQueryWrapper<TSysMenu> queryWrapper = new LambdaQueryWrapper<>();
if(menu.getParentId() == null){
queryWrapper.isNull(TSysMenu::getParentId);
}else{
queryWrapper.eq(null != menu.getParentId(),TSysMenu::getParentId,menu.getParentId());
}
queryWrapper.orderByDesc(TSysMenu::getSortNum);
var sortCate = tSysMenuMapper.selectOne(queryWrapper.last("limit 1"));
if (sortCate!=null)
{
menu.setSortNum(sortCate.getSortNum()+1);
}else{
menu.setSortNum(0);
}
}
3.2 对分类进行上移和下移排序
查询最近的兄弟节点进行交换
/*
对分类进行排序
*/
public TSysCollegeCategory sortCategory(Long id,Integer type) throws Exception {
if(id ==null){
throw CommonException.create(ServerResponse.createByError("id不存在"));
}
var current = tSysCollegeCategoryMapper.selectById(id);
if(current ==null){
throw CommonException.create(ServerResponse.createByError("分类不存在"));
}
LambdaQueryWrapper<TSysCollegeCategory> queryWrapper = new LambdaQueryWrapper<>();
// 检查ParentId为空的情况。
if(current.getParentId() == null){
queryWrapper.isNull(TSysCollegeCategory::getParentId);
}else{
queryWrapper.eq(null != current.getParentId(),TSysCollegeCategory::getParentId,current.getParentId());
}
if(type ==1){//上移
queryWrapper.lt(TSysCollegeCategory::getSortNum,current.getSortNum())
.orderByAsc(TSysCollegeCategory::getSortNum).last("limit 1");
}else if(type ==2){//下移
queryWrapper.gt(TSysCollegeCategory::getSortNum,current.getSortNum())
.orderByAsc(TSysCollegeCategory::getSortNum).last("limit 1");
}else{
throw CommonException.create(ServerResponse.createByError("type类型错误"));
}
var next = tSyCollegeCategoryMapper.selectOne(queryWrapper);
if(next !=null)
{
var sort = current.getSortNum();
current.setSortNum(next.getSortNum());
next.setSortNum(sort);
tSysCollegeCategoryMapper.updateById(current);
tSysCollegeCategoryMapper.updateById(next);
}
return current;
}
IV 常见问题
预备知识:序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。
4.1 no instance(s) of type variable(s) R exist so that void conforms to R,
实体类上标注有 @Accessors(chain = true)//链式访问
.map(item -> item.setChildren(getChild(item.getId(), dtos)))
4.2 MybatisPlus QueryWrapper的null查询
if(category.getParentId() == null){
queryWrapper.isNull("parent_id");
}else{
queryWrapper
.eq("parent_id", category.getParentId());
}
4.3 集合filter过滤Integer数值为空问题解决方案
/**
* @param parentId 传递的父id 用来过滤用 ,可以为空
* @return
* @throws Exception
*/
private List<SysMenuDto> getChilds(Long parentId, List<SysMenuDto> sourceList) {
List<SysMenuDto> menus = sourceList.stream()
.filter(menu -> {
if (parentId == null) {
return menu.getParentId() == null;
} else {
return menu.getParentId() != null && menu.getParentId().equals(parentId);
}
}).collect(Collectors.toList());
//排序 0表示最前面
return menus;
}