项目实战八-系统模块
权限管理分析
- 权限管理:对已经认证成功的用户进行授权
- 认证成功:比如使用用户名、密码登录成功
- 授权:比如赋值相应的操作权限(资源)
- 实现方案
- 最简单粗暴:用户直接跟资源(权限)挂钩
-
设计成3张表:用户表(sys_user)、资源表(sys_resource)、用户资源表(sys_user_resource)
sys_user id name 1 jack 2 rose sys_resource id name 1 用户-查询 2 用户-添加 3 用户-删除 sys_user_resource user_id resource_id 1 1 1 3 2 2 2 3
-
缺点:sys_user_resource表中数据冗余
-
- 建议采用:RBAC(Role-base Access Control),以角色为基础的访问控制
- 用户跟角色挂钩、角色与资源挂钩,设计成5张表:用户表(sys_user)、角色表(sys_role)、资源(sys_resource)、用户角色表(sys_user_role)、角色资源表(sys_role_resource)
sys_user id name 1 jack 2 rose sys_role id name 1 总经理 2 销售经理 3 前台 sys_resource id name 1 用户-查询 2 用户-添加 3 用户-删除 sys_role_resource role_id resource_id 1 1 1 2 1 3 2 1 2 3 3 1 sys_user_role user_id role_id 1 1 2 2 2 3
- 用户跟角色挂钩、角色与资源挂钩,设计成5张表:用户表(sys_user)、角色表(sys_role)、资源(sys_resource)、用户角色表(sys_user_role)、角色资源表(sys_role_resource)
- 最简单粗暴:用户直接跟资源(权限)挂钩
初始化各个层的类
- 在EasyCode-MybatisCodeHepler中新增erqVo模板、vo模板
- 同时选中sys_xxx的5张表,右击,通过EasyCode-MybatisCodeHepler生成对应层的类
- 简单改造各个层
- 删除SysUserRoleController、SysRoleResourceController、SysRoleResourceReqVo、SysUserRoleReqVo
-
vo/req/save下面的xxxreqvo,添加后台校验的注解字段
SysResourceReqVo SysRoleReqVo SysUserReqVo
-
vo/list下面的返回值xxxVo
//资源列表 SysResourceVo //角色列表 SysRoleVo //用户列表 SysUserVo
-
po包下SysRoleResource、SysUserRole添加注解外键关联
@Data public class SysUserRole { /** * 角色id */ @ForeignField(SysRole.class) private Short roleId; /** * 用户id */ @ForeignField(SysUser.class) private Integer userId; } @Data public class SysRoleResource { /** * 角色id */ @ForeignField(SysRole.class) private Short roleId; /** * 资源id */ @ForeignField(SysResource.class) private Short resourceId; }
用户模块
列表展示功能
-
vo/req/page下新增模糊查询的的SysUserPageReqVo
@EqualsAndHashCode(callSuper = true) @Data public class SysUserPageReqVo extends KeywordPageReqVo {}
-
SysUserController如下:
@RestController @RequestMapping("/sysUsers") public class SysUserController extends BaseController<SysUser, SysUserReqVo> { @Autowired private SysUserService service; @Override protected IService<SysUser> getService() { return service; } //分页查询方法 @GetMapping public PageJsonVo<SysUserVo> list(SysUserPageReqVo reqVo) { return JsonVos.ok(service.list(reqVo)); } @Override protected Function<SysUserReqVo, SysUser> getFunction() { return MapStructs.INSTANCE::reqVo2po; } }
-
service层
//SysUserService public interface SysUserService extends IService<SysUser> { PageVo<SysUserVo> list(SysUserPageReqVo reqVo);} //SysUserServiceImpl @Service @Transactional public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { @Override @Transactional(readOnly = true) public PageVo<SysUserVo> list(SysUserPageReqVo reqVo) { //1. 单表查询直接使用MpLambdaQueryWrapper MpLambdaQueryWrapper<SysUser> wrapper = new MpLambdaQueryWrapper<>(); wrapper.like(reqVo.getKeyword(),SysUser::getNickname,SysUser::getUsername); wrapper.orderByDesc(SysUser::getId); //2. 将查询的po类型结果通过po2vo转化为Vo类型 return baseMapper.selectPage(new MpPage<>(reqVo),wrapper).buildVo(MapStructs.INSTANCE::po2vo); } }
- 运行项目会报错,有以下2个原因
- 原因1:MapStructs中没有增加SysUserVo<->SysUser即vo与po之间的转化方式,新增完后,运行项目还会报错
- 原因2:po(SysUser)的loginTime属性的类型为:
Date
,Vo(SysUserVo)的loginTime属性类型为Long
- 注意:时间的返回通常是返回给客户端毫秒的时间戳
- 解决办法:
-
新增转化格式类:
//com.zh.jk.common.mapStruct public class MapStructFormatter { //自定义Date2Millis注解 //必须添加@Qualifier注解,才能作为属性转化器注解 @Qualifier @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface Date2Millis {} //使用自定义的Date2Millis注解,注解一个方法 @Date2Millis public static Long date2millis(Date date) { if (date == null) return null; //将Date类型转化为毫秒数 return date.getTime(); } //自定义Mills2Date注解 //必须添加@Qualifier注解,才能作为属性转化器注解 @Qualifier @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface Mills2Date {} //使用自定义的Mills2Date注解,注释一个转化方法 @Mills2Date public static Date millis2date(Long mills) { if (mills == null) return null; //将毫秒数转化为Date类型 return new Date(mills); } }
-
MapStructs中使用
//com.zh.jk.common.mapStruct /** * ReqVo -> Po 将客户端传递的ReqVo转化为Vo对象 * Po -> Vo 通过Mybits-plus到数据库查询的po对象转为Vo对象传递给客户端 */ @Mapper(uses = { //指明用哪些格式化器 MapStructFormatter.class }) public interface MapStructs { MapStructs INSTANCE = Mappers.getMapper(MapStructs.class); /**po-vo***/ DictItemVo po2vo(DictItem po); DictTypeVo po2vo(DictType po); ExamPlaceVo po2vo(ExamPlace po); PlateRegionVo po2vo(PlateRegion po); ExamPlaceCourseVo po2vo(ExamPlaceCourse po); //由于数据库返回的po对象loginTime字段的类型是Date,返回给客户端的Vo对象是Long类型,因此需要做一个转化 @Mapping(source = "loginTime", target = "loginTime", //通过什么转化:这里必须传递一个自定义的注解,意思是通过MapStructFormatter类中的Date2Millis注解的方法来转化 qualifiedBy = MapStructFormatter.Date2Millis.class) SysUserVo po2vo(SysUser po); SysRoleVo po2vo(SysRole po); SysResourceVo po2vo(SysResource po); /**vo-po***/ DictItem reqVo2po(DictItemReqVo reqVo); DictType reqVo2po(DictTypeReqVo reqVo); ExamPlace reqVo2po(ExamPlaceReqVo reqVo); PlateRegion reqVo2po(PlateRegionReqVo reqVo); ExamPlaceCourse reqVo2po(ExamPlaceCourseReqVo reqVo); SysUser reqVo2po(SysUserReqVo reqVo); SysRole reqVo2po(SysRoleReqVo reqVo); SysResource reqVo2po(SysResourceReqVo reqVo); }
-
- 运行项目,列表页面能够正常展示、关键字搜索
添加用户功能
-
点击列表左上角的添加,如下图:
- 从上面图中可以看出,保存页面还需要获取到当前系统的所有角色信息
- 保存一个用户涉及2张表的保存,一个是用户表,一个是用户角色表
-
SysRoleController新增获取全部角色的接口,提供给前端即可
//不分页查询所有角色法 @GetMapping("/list") public DataJsonVo<List<SysRoleVo>> list() { List<SysRoleVo> vos = Streams.map(service.list(),MapStructs.INSTANCE::po2vo); return JsonVos.ok(vos); }
- SysRoleServiceImpl重写list方法
- 本来不需要在SysRoleServiceImpl中写任何东西,因为list()方法本身就是mybatis-plus自带的方法
-
但是需要在返回值的数据进行按照一定条件排序,因此要重写mybatis-plus的list()方法
//重写Mybatis-plus的list方法,内部设置数据的排序方式 @Override @Transactional(readOnly = true) public List<SysRole> list() { MpLambdaQueryWrapper<SysRole> wrapper = new MpLambdaQueryWrapper<>(); wrapper.orderByDesc(SysRole::getId); return super.list(wrapper); }
- SysUserController新增用户信息保存方法
- 注意: 该保存不能直接使用父类BaseController的save,因为保存的是2张表,需要父类的保存方法;而且不能放在controller中,需要放在service层的impl中,因为涉及到事务管理,两张表要么都成功,要么都失败,impl中使用了事务管理@Transactional
-
重写父类save接口
/** * 添加、更新保存用户信息,包含用户绑定的角色信息 * 这里涉及到2张表的操作:用户表、用户角色表 * */ //重写父类的save方法 @Override public JsonVo save(@Valid SysUserReqVo reqVo) { if (service.saveOrUpdate(reqVo)){ return JsonVos.ok(CodeMsg.SAVE_OK); }else { return JsonVos.error(CodeMsg.SAVE_ERROR); } }
-
SysUserReqVo需要新增roleIds字段
/** * 保存用户时传递的用户绑定的角色ids【多个id之间用逗号,隔开】 * */ private String roleIds;
-
service层新增saveOrUpdate方法
//SysUserService boolean saveOrUpdate(SysUserReqVo reqVo); //SysUserServiceImpl新增 @Autowired private SysUserRoleService userRoleService; /** * 添加、更新保存用户信息,包含用户绑定的角色信息 * 这里涉及到2张表的操作:用户表、用户角色表 * */ @Override public boolean saveOrUpdate(SysUserReqVo reqVo) { //转成PO SysUser po = MapStructs.INSTANCE.reqVo2po(reqVo); //保存用户信息 //这里的saveOrUpdate,使用的是mybatis-plus自带的方法,添加完会自动生成一个主键id //但是如果需要将保存后自动生成的id赋值到po对象的id属性,需要在application.yml配置use-generated-keys: true if (!saveOrUpdate(po)) return false; //保存角色信息 //拿到选择的角色id String roleIdsStr = reqVo.getRoleIds(); if (Strings.isEmpty(roleIdsStr)) return true; String[] roleIds = roleIdsStr.split(","); List<SysUserRole> userRoles = new ArrayList<>(); Integer userId = po.getId(); //构建SysUserRole的po类型对象数组 for (String roleId : roleIds) { SysUserRole userRole = new SysUserRole(); userRole.setUserId(userId); userRole.setRoleId(Short.parseShort(roleId)); userRoles.add(userRole); } //mybatis-plus自带批量保存方法 return userRoleService.saveBatch(userRoles); }
-
application.xml中新增配置
mybatis-plus: # 使用自动生成的key赋值给po的id属性 configuration: use-generated-keys: true
-
编辑用户功能
- 点击编辑,展示编辑页面,除了要显示用户的信息外,还需要显示用户所绑定的角色信息
- 有2种数据的返回方式:
- 返回列表的时候将用户所绑定的角色信息返回,需要在SysUserVo新增绑定角色的字段
- 单独提供一个接口通过用户id查询该用户绑定的角色信息,前端点击编辑的时候请求数据
- 如果一个用户有n个角色,而且列表有n条数据,选用方式1比较消耗性能,因此选择方式2
-
SysRoleController新增ids接口
@GetMapping("/ids") public DataJsonVo<List<Short>> ids(Integer userId) { return JsonVos.ok(service.listIds(userId)); }
-
service层(SysRoleService)
//新增接口 List<Short> listIds(Integer userId); //impl新增实现 //因为需要查询用户角色表,因此需要导入对应的mapper,也可以导入service @Autowired private SysUserRoleMapper userRoleMapper; @Override @Transactional(readOnly = true) public List<Short> listIds(Integer userId) { if (userId == null || userId <= 0) return null; MpLambdaQueryWrapper<SysUserRole> wrapper = new MpLambdaQueryWrapper<>(); wrapper.select(SysUserRole::getRoleId); wrapper.eq(SysUserRole::getUserId, userId); List<Object> ids = userRoleMapper.selectObjs(wrapper); return Streams.map(ids, (id) -> ((Integer) id).shortValue()); }
- 编辑保存也需要改造
-
SysUserServiceImpl下的saveOrUpdate方法中新增如下代码
//编辑->保存 Integer id = reqVo.getId(); if (id != null && id > 0) { // 如果是做更新 // 删除当前用户的所有角色信息 userRoleService.removeByUserId(reqVo.getId()); } //保存角色信息
-
SysUserRole的service层新增
//新增接口 boolean removeByUserId(Integer userId); //impl新增实现 @Override public boolean removeByUserId(Integer userId) { if (userId == null || userId <= 0) return false; MpLambdaQueryWrapper<SysUserRole> wrapper = new MpLambdaQueryWrapper<>(); wrapper.eq(SysUserRole::getUserId, userId); return baseMapper.delete(wrapper) > 0; }
-
资源模块
- 从列表数据查看可知,有三种数据列表,目录数据列表、菜单数据列表、按钮数据列表,可以将这三种数据列表全部放在一张表中即可
-
表中数据结构如下
id name uri permission type icon sn parent_id
- uri:点击跳转路径(目录才会有)
- permission:目录下的菜单权限(菜单才会有)
- type:有三种,目录、菜单、按钮,表示当前数据是哪一类
- sn:在当前类中的排序
- parent_id: 当前类的父id是什么
列表展示功能
-
vo/req/page下新增模糊查询的的SysResourcePageReqVo
@EqualsAndHashCode(callSuper = true) @Data public class SysResourcePageReqVo extends KeywordPageReqVo { }
-
SysResourceController
@RestController @RequestMapping("/sysRoles") public class SysResourceController extends BaseController<SysResource, SysResourceReqVo> { @Autowired private SysResourceService service; @Override protected IService<SysResource> getService() { return service; } //不分页全量查询所有数据 @GetMapping("/list") public DataJsonVo<List<SysResourceVo>> list() { return JsonVos.ok(Streams.map(service.list(),MapStructs.INSTANCE::po2vo)); } @Override protected Function<SysResourceReqVo, SysResource> getFunction() { return MapStructs.INSTANCE::reqVo2po; } }
-
上面即可完成,返回去的数据是不分页、不分树结构的全量列表数据,由前端自己根据parent_id分析展示树状结构
添加、编辑功能
- 在添加、编辑页面,选择父资源时,需要将所有数据,除了按钮类型(按钮不会是父资源,最底层)全部展示,因此需要服务器新增接口
-
SysResourceController新增api
//加载父资源:编辑、添加的时候选择父资源类型,而且父资源只有目录跟菜单 @GetMapping("/listParents") public DataJsonVo<List<SysResourceVo>> listParents() { return JsonVos.ok(service.listParents()); }
-
service层
public interface SysResourceService extends IService<SysResource> { List<SysResourceVo> listParents(); } @Service @Transactional public class SysResourceServiceImpl extends ServiceImpl<SysResourceMapper, SysResource> implements SysResourceService { @Override @Transactional(readOnly = true) public List<SysResourceVo> listParents() { MpLambdaQueryWrapper<SysResource> wrapper = new MpLambdaQueryWrapper<>(); //查找type不能等于2,即没有按钮类型 wrapper.ne(SysResource::getType, Constants.SysResourceType.BTN); //按照type升序,再按照id降序 wrapper.orderByAsc(SysResource::getType).orderByDesc(SysResource::getId); return Streams.map(baseMapper.selectList(wrapper), MapStructs.INSTANCE::po2vo); } }
角色模块
列表展示功能
-
vo/req/page下新增模糊查询的的SysRolePageReqVo
@EqualsAndHashCode(callSuper = true) @Data public class SysRolePageReqVo extends KeywordPageReqVo { }
-
SysRoleController
@RestController @RequestMapping("/sysRoles") public class SysRoleController extends BaseController<SysRole, SysRoleReqVo> { @Autowired private SysRoleService service; @Override protected IService<SysRole> getService() { return service; } //分页查询方法 @GetMapping public PageJsonVo<SysRoleVo> list(SysRolePageReqVo reqVo) { return JsonVos.ok(service.list(reqVo)); } @Override protected Function<SysRoleReqVo, SysRole> getFunction() { return MapStructs.INSTANCE::reqVo2po; } }
-
service层
public interface SysRoleService extends IService<SysRole> { PageVo<SysRoleVo> list(SysRolePageReqVo reqVo); } @Service @Transactional public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService { @Override @Transactional(readOnly = true) public PageVo<SysRoleVo> list(SysRolePageReqVo reqVo) { //1. 单表查询直接使用MpLambdaQueryWrapper MpLambdaQueryWrapper<SysRole> wrapper = new MpLambdaQueryWrapper<>(); wrapper.like(reqVo.getKeyword(),SysRole::getName); wrapper.orderByDesc(SysRole::getId); //2. 将查询的po类型结果通过po2vo转化为Vo类型 return baseMapper.selectPage(new MpPage<>(reqVo),wrapper).buildVo(MapStructs.INSTANCE::po2vo); } }
添加功能
-
添加角色页面如下
- 分析
-
添加页面需要获取项目的所有资源列表,而且资源列表的数据是树状数据
[ { "id": 1, "title": "系统", "spread": true, "children": [ { "id": 5, "title": "用户", "spread": true, "children": [ { "id": 21, "title": "查询", "spread": true } ... ] }, { "id": 6, "title": "角色", "spread": true, "children": [...] ... } ] }, ... ]
-
点击添加时,要变更2张表:角色列表表、角色资源表
-
- 实现步骤
-
新增SysResourceTreeVo,用于返回树状接口数据的Vo
//com.zh.jk.pojo.vo.list @Data public class SysResourceTreeVo { private Short id; private String title; //默认是否展开 private boolean spread = true; //子资源 private List<SysResourceTreeVo> children; }
-
SysResourceController新增获取树状数据资源接口
listTree
@GetMapping("/listTree") public DataJsonVo<List<SysResourceTreeVo>> listTree() { return JsonVos.ok(service.listTree()); }
-
service层
//SysResourceService List<SysResourceTreeVo> listTree(); //SysResourceServiceImpl @Override @Transactional(readOnly = true) public List<SysResourceTreeVo> listTree() { // 里面存放的是树状结构的VO(最外层是目录类型的资源对象) List<SysResourceTreeVo> vos = new ArrayList<>(); // 存放已经从po转换成功的vo //这里巧妙使用hashmap作为集合,将vo的id作为key,vo作为value存储 Map<Short, SysResourceTreeVo> doneVos = new HashMap<>(); //1. 查询出全量数据 MpLambdaQueryWrapper<SysResource> wrapper = new MpLambdaQueryWrapper<>(); //按照类型升序 wrapper.orderByAsc(SysResource::getType); // 里面存放的是从数据库中查出来的PO List<SysResource> pos = baseMapper.selectList(wrapper); //2. 将数据组装成树状结构 for (SysResource po : pos) { // po转vo SysResourceTreeVo vo = po2treeVo(po); // 将vo存放到doneVos中 doneVos.put(vo.getId(), vo); Short type = po.getType(); if (type == Constants.SysResourceType.DIR) { // 目录 vos.add(vo); } else { // 菜单、按钮 // 从doneVos中取出父VO SysResourceTreeVo parentVo = doneVos.get(po.getParentId()); List<SysResourceTreeVo> children = parentVo.getChildren(); if (children == null) { parentVo.setChildren(children = new ArrayList<>()); } children.add(vo); } } return vos; } //手动实现po转vo private SysResourceTreeVo po2treeVo(SysResource po) { SysResourceTreeVo treeVo = new SysResourceTreeVo(); treeVo.setId(po.getId()); treeVo.setTitle(po.getName()); return treeVo; }
-
新增角色保存SysRoleReqVo模型类
@Data public class SysRoleReqVo { /** * 主键 */ private Short id; /** * 角色名称 */ @NotBlank(message = "名称不能为空") private String name; //资源id【多个id之间用逗号,隔开】 private String resourceIds; }
- SysRoleController新增save接口
- 重写父类的save方法
//重写父类save方法,自定义实现保存功能 @Override public JsonVo save(@Valid SysRoleReqVo reqVo) { if (service.saveOrUpdate(reqVo)) { return JsonVos.ok(CodeMsg.SAVE_OK); } else { return JsonVos.raise(CodeMsg.SAVE_ERROR); } }
-
service层
//SysRoleService boolean saveOrUpdate(SysRoleReqVo reqVo); //SysRoleServiceImpl //用于更改角色资源表 @Autowired private SysRoleResourceService roleResourceService; @Override public boolean saveOrUpdate(SysRoleReqVo reqVo) { //1. 更新角色表sys_role // 转成PO,用于保存角色数据 SysRole po = MapStructs.INSTANCE.reqVo2po(reqVo); // 调用mybaits自封装方法,保存角色信息 if (!saveOrUpdate(po)) return false; //2. 更新角色资源表sys_role_resource Short id = reqVo.getId(); // 删除当前角色对应的所有资源信息 roleResourceService.removeByRoleId(id); // 保存角色信息 String resourceIdsStr = reqVo.getResourceIds(); if (Strings.isEmpty(resourceIdsStr)) return true; String[] resourceIds = resourceIdsStr.split(","); List<SysRoleResource> roleResources = new ArrayList<>(); Short roleId = po.getId(); for (String resourceId : resourceIds) { // 构建SysUserRole对象 SysRoleResource roleResource = new SysRoleResource(); roleResource.setRoleId(roleId); roleResource.setResourceId(Short.parseShort(resourceId)); roleResources.add(roleResource); } //调用mybaits自封装方法批量保存 return roleResourceService.saveBatch(roleResources); }
-
角色资源的service层需要新增删除接口
//SysRoleResourceService boolean removeByRoleId(Short roleId); //SysRoleResourceServiceImpl @Override public boolean removeByRoleId(Short roleId) { if (roleId == null || roleId <= 0) return false; MpLambdaQueryWrapper<SysRoleResource> wrapper = new MpLambdaQueryWrapper<>(); wrapper.eq(SysRoleResource::getRoleId, roleId); return baseMapper.delete(wrapper) > 0; }
-
编辑功能
- 用户点击编辑的时候,才会去调用接口
- 后台接口只需要返回某个角色选中的资源ids即可,其他由前端自己完成
-
SysResourceController控制器添加根据角色id获取资源ids接口
//根据角色id获取设置的资源id @GetMapping("/ids") public DataJsonVo<List<Short>> ids(Integer roleId) { return JsonVos.ok(service.listIds(roleId)); }
-
service层
//SysResourceService List<Short> listIds(Integer roleId); //SysResourceServiceImpl @Autowired private SysRoleResourceMapper roleResourceMapper; /*通过角色id查询资源ids*/ @Override @Transactional(readOnly = true) public List<Short> listIds(Integer roleId) { if (roleId == null || roleId <= 0) return null; //查询sys_role_source表 MpLambdaQueryWrapper<SysRoleResource> wrapper = new MpLambdaQueryWrapper<>(); wrapper.select(SysRoleResource::getResourceId); wrapper.eq(SysRoleResource::getRoleId, roleId); List<Object> ids = roleResourceMapper.selectObjs(wrapper); return Streams.map(ids, (id) -> ((Integer) id).shortValue()); }