笔者采用前后端分离项目开发自定义权限功能模块有一段时间了,今天这部分的收尾篇了。在这个系列的文章里笔者后端采用一个开源的springboot
项目blog-server
,前端采用基于vue
和element-ui
技术栈的开源项目vue-element-admin
先后实现了「根据当前登录用户角色动态加载左侧菜单、用户分页查询和给用户授予角色」等功能的实现。本文则是这个权限功能的扫尾部分,笔者带领大家来继续实现「角色的增删改和给角色分配路由资源」这部分功能,以后有时间的化还会继续补角色-按钮级别的权限控制。为了利于笔者和我的读者朋友往高级开发和架构师方向发展,后面发文的重点将放在redis、rabbitmq、rocketmq和springcloud
等分布式技术栈的学习和实践上。
图 1 角色列表页
图 2 编辑角色界面
图 3 角色分配路由资源界面
这个效果如是笔者最终实现的效果图,鉴于前端水平有限,没有对界面样式进行特别的美化调整,还请读者们将就着看,我们实现功能即可,界面的美化后续可以继续通过调样式实现。
从效果图中,我们可以整理出需要开发的接口主要有「查询全量角色、新增角色、修改角色、删除角色、给角色添加路由资源」等5个接口。由于新建角色时增加了「角色描述」一列,所以笔者给roles
表添加了一列description
, sql如下:
alter table roles add (description varchar(50));
「Dao层编码实现」
RolesMapper.java
List<Role> getAllRole();
RolesMapper.xml
<select id="getAllRole" resultType="org.sang.pojo.Role">
select id, role_code as roleCode,role_name as roleName,description
from roles where 1=1
</select>
「Servcie层代码实现」
RoleService.java
@Autowired
private RolesMapper rolesMapper;
public List<Role> getAllRoles(){
List<Role> roles = rolesMapper.getAllRole();
return roles;
}
「Controller层代码实现」
RoleController.java
@Autowired
private RoleService roleService;
@GetMapping("/allRoles")
@ApiOperation(value = "getAllRoles", notes = "获取所有角色列表", produces = "application/json", consumes = "application/json", response = RespBean.class)
public RespBean<List<Role>> getAllRoles() {
List<Role> roles = roleService.getAllRoles();
RespBean<List<Role>> respBean = new RespBean<>(200, "success");
respBean.setData(roles);
return respBean;
}
「Dao层编码实现」
RolesMapper.java
int addRole(Role role);
RoleMapper.xml
<insert id="addRole" keyProperty="id" useGeneratedKeys="true" parameterType="org.sang.pojo.Role">
insert into roles(role_code,role_name,description)
values(#{roleCode,jdbcType=VARCHAR},
#{roleName,jdbcType=VARCHAR},
#{description,jdbcType=VARCHAR})
</insert>
「Service层编码实现」
RoleService.java
@Autowired
private RolesMapper rolesMapper;
public int updateRole(Role role) {
int count = rolesMapper.updateRole(role);
return count;
}
「Controller层编码实现」
RoleController.java
@Autowired
private RoleService roleService;
privatestaticfinal Logger logger = LoggerFactory.getLogger(RoleController.class);
@PostMapping(path = "/addRole")
@ApiOperation(value = "addRole", notes = "添加角色", produces = "application/json",
consumes = "application/json", response = RespBean.class)
@ApiImplicitParam(name="role", value = "角色对象", dataTypeClass = Role.class, paramType="body", required = true)
public RespBean<Integer> addRole(@RequestBody Role role) {
logger.info("roleCode={},roleName={}",role.getRoleCode(),role.getRoleName());
int addCount = roleService.addRole(role);
RespBean<Integer> respBean = new RespBean<>(200, "success");
respBean.setData(addCount);
return respBean;
}
「Dao层代码实现」
RolesMapper.java
int updateRole(Role role);
<update id="updateRole" parameterType="org.sang.pojo.Role">
update roles set role_code = #{roleCode,jdbcType=VARCHAR},
role_name = #{roleName,jdbcType=VARCHAR},
description = #{description,jdbcType=VARCHAR}
where id = #{id, jdbcType=INTEGER}
</update>
「Service层代码实现」
RoleService.java
public int updateRole(Role role) {
int count = rolesMapper.updateRole(role);
return count;
}
「Controller层代码实现」
RoleController
@Autowired
private RoleService roleService;
private static final Logger logger = LoggerFactory.getLogger(RoleController.class);
@PostMapping(path = "/addRole")
@ApiOperation(value = "addRole", notes = "添加角色", produces = "application/json",
consumes = "application/json", response = RespBean.class)
@ApiImplicitParam(name="role", value = "角色对象", dataTypeClass = Role.class, paramType="body", required = true)
public RespBean<Integer> addRole(@RequestBody Role role) {
logger.info("roleCode={},roleName={}",role.getRoleCode(),role.getRoleName());
int addCount = roleService.addRole(role);
RespBean<Integer> respBean = new RespBean<>(200, "success");
respBean.setData(addCount);
return respBean;
}
删除角色接口涉及到多个与角色关联表中的数据也要一并删除,尤其以角色ID为外键的表中的记录,必须先删除以关联表中的记录,才能成功删除角色,否则直接删除角色时会导致删除失败,所以这个接口稍微复杂一点,在Service层方法中还要加上声明式事务注解。
「Dao 层代码实现」
RolesMapper.java
// 根据角色ID删除角色
int delRoleById(Integer roleId);
// 删除角色-用户表中与角色关联的记录
int delRoleUserByRoleId(Integer roleId);
RolesMapper.xml
<delete id="delRoleById" parameterType="java.lang.Integer">
delete from roles where id = #{roleId, jdbcType=INTEGER}
</delete>
<delete id="delRoleUserByRoleId" parameterType="java.lang.Integer">
delete from roles_user where rid=#{roleId,jdbcType=INTEGER}
</delete>
RoleRouterMapper.java
//角色-路由资源表中删除与角色关联的记录
Integer delRoleResourceByRoleId(Integer roleId);
RoleRouterMapper.xml
<delete id="delRoleResourceByRoleId" parameterType="java.lang.Integer">
delete from role_resources where role_id = #{roleId, jdbcType=INTEGER}
</delete>
「Service层代码实现」
RoleService.java
@Autowired
private RolesMapper rolesMapper;
@Autowired
private RoleRouterMapper roleRouterMapper;
@Transactional(rollbackFor = Exception.class)
public int delRoleById(Integer roleId){
rolesMapper.delRoleUserByRoleId(roleId);
roleRouterMapper.delRoleResourceByRoleId(roleId);
return rolesMapper.delRoleById(roleId);
}
「Controller层代码实现」
RoleController.java
@DeleteMapping("/delRole/{roleId}")
@ApiOperation(value = "delRoleById", notes = "删除角色", produces = "application/json",
consumes = "application/json", response = RespBean.class)
@ApiImplicitParam(name="roleId", value = "角色ID", required = true, paramType = "path", dataType = "java.lang.Integer")
public RespBean<Integer> delRoleById(@PathVariable Integer roleId){
logger.info("roleId={}", roleId);
RespBean<Integer> respBean = new RespBean<>(200,"success");
Integer count = roleService.delRoleById(roleId);
respBean.setData(count);
return respBean;
}
「Dao层代码实现」
RoleRouterMapper.java
Integer addRouteIdsForRole(List<Integer> roleIds, Integer roleId);
RoleRouterMapper.xml
<insert id="addRouteIdsForRole" useGeneratedKeys="true">
insert into role_resources(role_id,resource_id,created_by,created_time,last_updated_by,last_updated_time)
values
<foreach collection="param1" item="routeId" separator=",">
(#{param2, jdbcType=INTEGER}, #{routeId, jdbcType=INTEGER}, 'heshengfu', now(), 'heshengfu', now())
</foreach>
</insert>
「Service层代码实现」
RoleRouterService.java
@Transactional(rollbackFor = Exception.class)
public Integer addRouteIdsForRole(List<Integer> routeIds, Integer roleId){
// 删除原来的角色-路由资源关系
roleRouterMapper.delRoleResourceByRoleId(roleId);
Integer count = roleRouterMapper.addRouteIdsForRole(routeIds, roleId);
return count;
}
「Controller层代码实现」
RouterResourceController.java
privatestaticfinal Logger logger = LoggerFactory.getLogger(RouterResourceController.class);
@Autowired
private RoleRouterService roleRouterService;
@PostMapping("/addRouteIds")
@ApiOperation(value = "addRouteIdsForRole", notes = "给角色添加路由资源", response = RespBean.class)
public RespBean<Integer> addRouteIdsForRole(@RequestBody List<Integer> routeIds, @RequestParam("roleId") Integer roleId){
logger.info("http request addRouteIds start");
logger.info("roleId={}",roleId);
RespBean<Integer> respBean = new RespBean<>(200, "success");
Integer count = roleRouterService.addRouteIdsForRole(routeIds, roleId);
respBean.setData(count);
return respBean;
}
接口开发完毕,可借助postman
接口UI工具或者启动项目后在进入接口文档页面http://localhost:8081/blog/doc.html#/home
, 找到对应的接口界面进入起调试界面输入相关参数后对接口进行测试。详情可参考笔者之前发过的文章SpringBoot项目集成knif4j,从此告别手写Api文档
更具需求我们可以整理出前端要做的工作就是绘制一个展示角色列表的页面、增加或修改角色信息的弹出框及给角色分配路由资源的树形控件对话框。同时还要通过axios请求调用后台接口拿到5个后台接口的数据后,将数据在页面渲染。本文功能的实现在修改vue-element-admin
开源项目中src/views/permission/role.vue
组件的基础上进行。
api/role.js
文件中添加暴露调用后台接口方法
// 获取全量角色
exportfunction getAllRoles() {
return request({
url: '/role/allRoles',
method: 'get',
headers: {
'Content-Type': 'x-www-form-urlencoded'
}
})
}
//添加角色
exportfunction addRole(data) {
return request({
url: '/role/addRole',
method: 'post',
data
})
}
// 修改角色
exportfunction updateRole(data) {
return request({
url: `/role/updateRole`,
method: 'post',
data
})
}
// 删除角色
exportfunction deleteRole(roleId) {
return request({
url: `/role/delRole/${roleId}`,
method: 'delete'
})
}
// 给角色分配路由资源
exportfunction addRouteIdsForRole(routeIds, roleId) {
return request({
url: `/routerResource/addRouteIds?roleId=${roleId}`,
method: 'post',
data: routeIds
})
}
role.vue
<template>
<div class="app-container">
<el-button type="primary" @click="handleAddRole">新增角色</el-button>
<el-table :data="rolesList" style="width: 100%;margin-top:30px;" border>
<el-table-column align="center" label="角色代码" width="220">
<template slot-scope="scope">
{{ scope.row.roleCode}}
</template>
</el-table-column>
<el-table-column align="center" label="角色名称" width="220">
<template slot-scope="scope">
{{ scope.row.roleName}}
</template>
</el-table-column>
<el-table-column align="center" label="角色描述">
<template slot-scope="scope">
{{ scope.row.description }}
</template>
</el-table-column>
<el-table-column align="center" label="操作">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="handleEdit(scope)">修改角色</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope)">删除角色</el-button>
<el-button type="primary" size="small" @click="allocateRoutes(scope)">分配路由</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog :visible.sync="dialogVisible" :title="dialogType==='edit'?'修改角色':'添加角色'">
<el-form :model="activeRole" label-width="80px" label-position="left">
<el-form-item label="角色编码">
<el-input v-model="activeRole.roleCode" placeholder="角色编码" />
</el-form-item>
<el-form-item label="角色名称">
<el-input v-model="activeRole.roleName" placeholder="角色名称" />
</el-form-item>
<el-form-item label="角色描述">
<el-input
v-model="activeRole.description"
type="text"
placeholder="角色描述"
/>
</el-form-item>
</el-form>
<div style="text-align:right;">
<el-button type="danger" @click="dialogVisible=false">取消</el-button>
<el-button type="primary" @click="confirmRole">确认</el-button>
</div>
</el-dialog>
<el-dialog :visible.sync="treeVisible" width="500px" :title="treeTitle" class="tree-dialog">
<el-tree
ref="tree"
:data="routesData"
show-checkbox
node-key="id"
:default-checked-keys="checkedKeys"
class="permission-tree"
/>
<div style="text-align:right;">
<el-button type="danger" @click="treeVisible=false">取消</el-button>
<el-button type="primary" @click="confirmRoutes">确认</el-button>
</div>
</el-dialog>
</div>
</template>
role.vue
<script>
import { asyncRoutes } from'@/router/index'
import { getRouteIds, getAllRoles, addRole, deleteRole, updateRole, addRouteIdsForRole} from'@/api/role'
exportdefault {
data() {
return {
routesData: [],
rolesList: [],
dialogVisible: false,
treeVisible: false,
dialogType: 'new',
activeRole: '',
treeTitle: '',
checkedKeys: [],
defaultProps: {
children: 'children',
label: 'label'
}
}
},
created() {
// Mock: get all routes and roles list from server
this.getRoles()
},
methods: {
// 获取路由数据方法
getRoutes() {
this.routesData = this.generateRoutes(asyncRoutes)
},
//获取全部角色方法
getRoles() {
getAllRoles().then(res=>{
if(res.status===200 && res.data.status===200){
this.rolesList = res.data.data
}else{
this.$message({
message: 'get getAllRoles error:'+res.data.msg,
type: 'error'
})
}
}).catch(err=>{
this.$message({
message: 'get getAllRoles error:'+err,
type: 'error'
})
})
},
// 生产树形路由数据
generateRoutes(routes) {
let routeDisplayData = []
for(let i=0;i<routes.length;i++){
let routeItem = routes[i]
let cateItem = {id: parseInt(routeItem.id), label: routeItem.meta && routeItem.meta.title? routeItem.meta.title: routeItem.name, children: []}
routeDisplayData.push(cateItem)
if(routeItem.children && routeItem.children.length>0){
cateItem.children = this.generateRoutes(routeItem.children)
}
}
return routeDisplayData
},
// 打开添加角色对话框
handleAddRole() {
this.dialogType = 'new'
this.dialogVisible = true
// 当前选中角色信息清空
this.activeRole = {};
},
//打开修改角色对话框
handleEdit(scope) {
this.dialogType = 'edit'
this.dialogVisible = true
this.checkStrictly = true
//变换当前选中角色
this.activeRole = scope.row;
},
//删除角色
handleDelete(scope) {
this.$confirm('删除角色将一并删除角色用户关系表及角色资源关系表中与该角色关联的记录,是否确定删除?', 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
const roleId = scope.row.id
deleteRole(roleId).then(res=>{
if(res.status===200 && res.data.status===200){
this.$message({
type: 'success',
message: '删除角色成功'
})
this.getRoles()
}else{
this.$message({
type: 'error',
message: '删除角色失败'
})
}
})
})
.catch(err => { console.error(err) })
},
// 打开分配路由资源对话框
allocateRoutes(scope) {
this.treeVisible = true;
this.activeRole = scope.row
this.treeTitle = this.activeRole.roleName+'分配路由资源'
const roleId = scope.row.id;
if(this.routesData.length===0){
this.routesData = this.generateRoutes(asyncRoutes)
}
this.checkedKeys = []
getRouteIds(roleId).then(res=>{
if(res.status===200 && res.data.status===200){
const chekedRouteIds = res.data.data
for(let i=0;i<chekedRouteIds.length;i++){
this.checkedKeys.push(parseInt(chekedRouteIds[i]))
// 设置角色已有的路由资源
this.$refs.tree.setCheckedKeys(this.checkedKeys)
}
}else{
this.$message({
type: 'error',
message: 'getRouteIds error: ' + res.msg
})
}
})
},
// 确认提交添加或修改角色
confirmRole() {
const isEdit = this.dialogType === 'edit'
// this.role.routes = this.generateTree(deepClone(this.serviceRoutes), '/', checkedKeys)
if (isEdit) {
const role = this.activeRole
updateRole(role).then(res=>{
if(res.status===200 && res.data.status===200){
this.$message({
type: 'success',
message: '修改角色成功'
})
this.getRoles()
} else {
this.$message({
type: 'error',
message: '修改角色失败'
})
}
})
this.dialogVisible = false
} else {
const role = this.activeRole
addRole(role).then(res=>{
if(res.status===200 && res.data.status===200){
this.$message({
type: 'success',
message: '添加角色成功'
})
this.getRoles()
}else{
this.$message({
type: 'error',
message: '添加角色失败'
})
}
})
this.dialogVisible = false
}
},
// 确认提交添加路由资源列表
confirmRoutes(){
const roleId = this.activeRole.id
const routeIds = this.$refs.tree.getCheckedKeys()
if(routeIds.length===0){
this.$message({
type: 'warning',
message: '选中的路由ID不能为空'
})
this.treeVisible = false
return
}
addRouteIdsForRole(routeIds, roleId).then(res=>{
if(res.status===200 && res.data.status===200){
this.$message({
type: 'success',
message: '添加路由资源成功'
})
}else{
this.$message({
type: 'error',
message: '添加路由资源失败'
})
}
})
this.treeVisible = false
},
}
}
</script>
role.vue
为了让界面卡看起来稍微美观一点,保持树形控件长度超过一定高度后显示垂直滚动条,方便操作人员在弹出的对话框界面看到确认和取消按钮,需要调整对话框的样式进行部分调整,修改后的样式代码如下。
<style lang="scss" scoped>
.app-container {
.roles-table {
margin-top: 30px;
}
.el-dialog_header .el-dialog__title{
font-size: 20px;
}
.tree-dialog {
margin-left: 200px;
}
.permission-tree {
margin-bottom: 30px;
height: 320px;
overflow-y: auto;
}
}
</style>
启动后台springboot项目blog-server
服务后,然后在开发环境下启动vue-element-admin
项目(在vue-element-admin项目根目录右键->Git Bash Here进入控制台输入命令npm run dev
后回车即可)
前后端项目启动成功后在谷歌浏览器中输入网址: http://localhost:3000/
回车后重定向到登录界面,输入用户名和密码登录成功后点击右侧的「权限管理->角色管理」菜单即可进入角色管理操作界面测试本文开发的各项功能,感兴趣的读者可从笔者的代码仓库克隆下来后在本地跑起来然后亲自体验一番点击页面及各个按钮的效果,所有功能都经过了笔者的测试并通过,效果图在「效果预览」部分已经给出,其他就不再一一贴图了。
https://gitee.com/heshengfu1211/blogserver.git
https://gitee.com/heshengfu1211/vue-element-admin.git
【1】 https://element.eleme.cn/#/zh-CN/component/dialog
【2】 https://element.eleme.cn/#/zh-CN/component/message-box
【3】 https://element.eleme.cn/#/zh-CN/component/tree