SaaS系统用户权限设计
学习目标:
理解RBAC模型的基本概念及设计思路
了解SAAS-HRM中权限控制的需求及表结构分析完成组织机构的基本CRUD操作
完成用户管理的基本CRUD操作完成角色管理的基本CRUD操作
组织机构管理
需求分析
需求分析
实现企业组织结构管理,实现部门的基本CRUD操作
数据库表设计
CREATE TABLE `co_department` ( `id` varchar(40) NOT NULL, `company_id` varchar(255) NOT NULL COMMENT '企业ID', `parent_id` varchar(255) DEFAULT NULL COMMENT '父级部门ID', `name` varchar(255) NOT NULL COMMENT '部门名称', `code` varchar(255) NOT NULL COMMENT '部门编码', `category` varchar(255) DEFAULT NULL COMMENT '部门类别', `manager_id` varchar(255) DEFAULT NULL COMMENT '负责人ID', `city` varchar(255) DEFAULT NULL COMMENT '城市', `introduce` text COMMENT '介绍', `create_time` datetime NOT NULL COMMENT '创建时间', `manager` varchar(40) DEFAULT NULL COMMENT '部门负责人', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
微服务实现
抽取公共代码
(1) 在公共controller
ihrm_commoncom.模块下的 ihrm.common.controller 包下添加公共controller
package com.ihrm.common.controller; import org.springframework.web.bind.annotation.ModelAttribute; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 公共controller * 获取request,response * 获取企业id,获取企业名称 */ public class BaseController { protected HttpServletRequest request; protected HttpServletResponse response; @ModelAttribute public void setReqAndResp(HttpServletRequest request, HttpServletResponse response) { this.request = request; this.response = response; } //企业id,(暂时使用1,以后会动态获取) public String parseCompanyId() { return "1"; } public String parseCompanyName() { return "江苏传智播客教育股份有限公司"; } }
骚戴理解:@ModelAttribute是Spring MVC中的一个注释,它的作用是将HTTP请求参数绑定到指定的ModelAttribute对象并添加到ModelAndView中。 通俗地讲,它可以在请求处理程序方法之前预处理模型属性,以便在请求处理程序方法中使用。在Spring MVC中,当从浏览器提交表单时,所有表单字段的名称和值都被收集到一个名为“请求参数”的数据结构中。 @ModelAttribute注释可以将此请求参数映射到Java对象的属性中。
(2) 公共service
ihrm_commoncom.模块下的 ihrm.common.service 包下添加公共BaseService
public class BaseService<T> { protected Specification<T> getSpecification(String companyId) { return new Specification<T>() { @Override public Predicate toPredicate(Root<T> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) { return cb.equal(root.get("companyId").as(String.class),companyId); } }; } }
骚戴理解:在SpringData里面提供的findAll方法如果是没有任何参数就是获取所有的数据,如果需要查询所有符合条件的数据,那么将就需要传入一个参数Specification,Specification参数其实就是条件 ,这里的getSpecification方法是用来创建这个条件的,这里是抽出来了,这个条件的意思是查询companyId这个公司id下的所有部门(组织架构)
criteriabuilder.equal(root.get("companyid").as(string.class), companyid)中的两个companyid指的是不同的变量。
第一个companyid:表示root对象所代表的实体类中的属性名,该属性用于和后面传递进来的companyid进行比较。
第二个companyid:表示方法参数中传递进来的某个值,用于与实体的companyid属性进行比较。
简而言之,这个代码片段的作用是使用criteriabuilder构造一个查询条件,即根据传入的companyid值筛选出实体类中属性名为companyid的值等于该值的记录。
public predicate topredicate(root<t> root, criteriaquery<?> criteriaquery, criteriabuilder cb) 是一个接口方法,用于创建 criteria api 中的查询谓语(predicate),它接收三个参数:
root<t> root:代表查询的根节点,可以从中获取实体类的属性。
criteriaquery<?> criteriaquery:代表将被执行的查询对象。
criteriabuilder cb:代表 criteria api 的工厂类,用于创建各种查询条件。
该方法会返回一个 predicate 类型的查询条件,表示要在给定的查询中使用的过滤器或限制条件。predicate 是 criteria api 中与 boolean 表达式相关的基础接口,用于构造 where 子句中的条件表达式。
这个方法是通用的,可以在不同的场景下使用。例如,在 spring data jpa 中,我们可以使用它来创建基于查询方法名的动态查询。具体而言,我们可以定义一个接口方法并使用 @query 注解,以便将该方法与特定的 jpql 查询一起使用。然后,我们可以在该方法中编写自定义查询逻辑,并使用 topredicate() 方法创建基于标准的查询谓词。
(3)公共DeptListResult
package com.ihrm.common.response; import com.ihrm.domain.company.Company; import com.ihrm.domain.company.Department; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.util.List; @Getter @Setter @NoArgsConstructor public class DeptListResult { private String companyId; private String companyName; private String companyManage; private List<Department> depts; public DeptListResult(Company company,List<Department> list) { this.companyId = company.getId(); this.companyName = company.getName(); this.companyManage = company.getLegalRepresentative(); this.depts = list; } }
骚戴理解:DeptListResult这个类的作用就是封装前端需要的企业下所有组织架构(部门)信息,因为前端展示的时候不但要一个企业下所有的部门列表信息,还要企业的相关信息,所以这里就封装成了DeptListResult类
/** * 组织架构列表 */ @RequestMapping(value = "/departments", method = RequestMethod.GET) public Result findAll() throws Exception { Company company = companyService.findById(parseCompanyId()); List<Department> list = departmentService.findAll(parseCompanyId()); return new Result(ResultCode.SUCCESS,new DeptListResult(company,list)); }
实现基本CRUD操作
(1)实体类
在com.ihrm.domain.company包下创建Department实体类
package com.ihrm.domain.company; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Transient; import java.io.Serializable; import java.util.Date; import java.util.List; /** * (Department)实体类 */ @Entity @Table(name = "co_department") @Data @AllArgsConstructor @NoArgsConstructor public class Department implements Serializable { private static final long serialVersionUID = -9084332495284489553L; //ID @Id private String id; /** * 父级ID */ private String parentId; /** * 企业ID */ private String companyId; /** * 部门名称 */ private String name; /** * 部门编码,同级部门不可重复 */ private String code; /** * 负责人ID */ private String managerId; /** * 负责人名称 */ private String manager; /** * 介绍 */ private String introduce; /** * 创建时间 */ private Date createTime; }
(2)持久化层
在 com.ihrm.company.dao 包下创建DepartmentDao
package com.ihrm.company.dao; import com.ihrm.domain.company.Department; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; /** * 部门操作持久层 */ public interface DepartmentDao extends JpaRepository<Department, String>, JpaSpecificationExecutor<Department> { }
(3)业务层
在 com.ihrm.company.service 包下创建DepartmentService
package com.ihrm.company.service; import com.ihrm.common.entity.ResultCode; import com.ihrm.common.exception.CommonException; import com.ihrm.common.service.BaseService; import com.ihrm.common.utils.IdWorker; import com.ihrm.company.dao.DepartmentDao; import com.ihrm.domain.company.Department; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import java.util.Date; import java.util.List; /** * 部门操作业务逻辑层 */ @Service public class DepartmentService extends BaseService { @Autowired private IdWorker idWorker; @Autowired private DepartmentDao departmentDao; /** * 添加部门 */ public void save(Department department) { //填充其他参数 department.setId(idWorker.nextId() + ""); department.setCreateTime(new Date()); departmentDao.save(department); } /** * 更新部门信息 */ public void update(Department department) { Department sourceDepartment = departmentDao.findById(department.getId()).get(); sourceDepartment.setName(department.getName()); sourceDepartment.setPid(department.getPid()); sourceDepartment.setManagerId(department.getManagerId()); sourceDepartment.setIntroduce(department.getIntroduce()); sourceDepartment.setManager(department.getManager()); departmentDao.save(sourceDepartment); } /** * 根据ID获取部门信息 * * @param id 部门ID * @return 部门信息 */ public Department findById(String id) { return departmentDao.findById(id).get(); } /** * 删除部门 * * @param id 部门ID */ public void delete(String id) { departmentDao.deleteById(id); } /** * 获取部门列表 */ public List<Department> findAll(String companyId) { return departmentDao.findAll(getSpecification(companyId)); } }
骚戴理解:以前用mybatis的时候写的修改都是直接把新的对象穿给dao层,在dao层动态更新(动态SQL便签),这里由于用的是SpringData的API,看下面的接口可以发现这个框架并没有提供update方法,所以这里的修改都是通过save新增方法来实现的
还要注意findById方法是有一个.get()后缀的,例如 departmentDao.findById(id).get(),容易忘/漏
getSpecification方法是创建条件谓语,这个方法是来自于父类BaseService的方法
(4)控制层
在 ihrm.company.controller 创建控制器类DepartmentController
package com.ihrm.company.controller; import com.ihrm.common.controller.BaseController; import com.ihrm.common.entity.Result; import com.ihrm.common.entity.ResultCode; import com.ihrm.company.service.CompanyService; import com.ihrm.company.service.DepartmentService; import com.ihrm.domain.company.Company; import com.ihrm.domain.company.Department; import com.ihrm.domain.company.response.DeptListResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import java.util.*; import java.util.stream.Collectors; /** * 控制器层 */ @CrossOrigin @RestController @RequestMapping("/company") public class DepartmentController extends BaseController{ @Autowired private DepartmentService departmentService; @Autowired private CompanyService companyService; /** * 添加部门 */ @RequestMapping(value = "/departments", method = RequestMethod.POST) public Result add(@RequestBody Department department) throws Exception { department.setCompanyId(parseCompanyId()); departmentService.save(department); return Result.SUCCESS(); } /** * 修改部门信息 */ @RequestMapping(value = "/departments/{id}", method = RequestMethod.PUT) public Result update(@PathVariable(name = "id") String id, @RequestBody Department department) throws Exception { department.setCompanyId(parseCompanyId()); department.setId(id); departmentService.update(department); return Result.SUCCESS(); } /** * 删除部门 */ @RequestMapping(value = "/departments/{id}", method = RequestMethod.DELETE) public Result delete(@PathVariable(name = "id") String id) throws Exception { departmentService.delete(id); return Result.SUCCESS(); } /** * 根据id查询 */ @RequestMapping(value = "/departments/{id}", method = RequestMethod.GET) public Result findById(@PathVariable(name = "id") String id) throws Exception { Department department = departmentService.findById(id); return new Result(ResultCode.SUCCESS,department); } /** * 组织架构列表 */ @RequestMapping(value = "/departments", method = RequestMethod.GET) public Result findAll() throws Exception { Company company = companyService.findById(parseCompanyId()); List<Department> list = departmentService.findAll(parseCompanyId()); return new Result(ResultCode.SUCCESS,new DeptListResult(company,list)); } }
骚戴理解:控制器调用的parseCompanyId方法和parseCompanyName方法都是来自父类BaseController中
前端实现
创建模块
- 使用命令行创建module-departments模块并引入到工程中
- 在src/main.js中注册模块
import departments from '@/module-departments/' // 组织机构管理 Vue.use(departments, store)
- 在/module-departments/router/index.js配置路由
import Layout from '@/module-dashboard/pages/layout' const _import = require('@/router/import_' + process.env.NODE_ENV) export default [ { root: true, path: '/departments', component: Layout, redirect: 'noredirect', name: 'departments', meta: { title: '组织架构管理', icon: 'architecture' }, children: [ { path: 'index', component: _import('departments/pages/index'), name: 'organizations-index', meta: {title: '组织架构', icon: 'architecture', noCache: true} } ] } ]
配置请求API
在/src/api/base/创建departments.js作为组织机构管理的API公共接口方法
import {createAPI} from '@/utils/request' //查询部门列表 export const list = data => createAPI('/company/department', 'get', data) //保存部门 //data {id:“”,name:“”} export const save = data => createAPI('/company/department', 'post', data) //根据id查询部门 {id:“”} export const find = data => createAPI(`/company/department/${data.id}`, 'get', data) //根据id删除部门 {id:""} export const deleteById = data => createAPI(`/company/department/${data.id}`, 'delete', data) //根据id更新部门 {id:"",name:"",code:""} export const update = data => createAPI(`/company/department/${data.id}`, 'put', data) //保存或更新的方法 export const saveOrupdate = data => {return data.id?update(data):save(data)}
骚戴理解: {return data.id?update(data):add(data)}的意思是判断data里面有没有id,如果有的话就调update方法,没有就调用add方法
构造树形列表
(1)构造页面样式
<template> <div class="dashboard-container"> <div class="app-container"> <el-card shadow="never"> <div class='organization-index'> <div class='organization-index-top'> <div class='main-top-title'> <el-tabs v-model="activeName"> <el-tab-pane label="组织结构" name="first"></el-tab-pane> <div class="el-tabs-report"> <a class="el-button el-button--primary el-button--mini" title="导出" >导入</a> <a class="el-button el-button--primary el-button--mini" title="导出" >导出</a> </div> </el-tabs> </div> </div> <div style="overflow: scroll;white-space:nowrap" class="treBox"> <div class="treeCon clearfix"> <span> <i class="fa fa-university" aria-hidden="true"></i> <span ><strong>{{departData.companyName}}</strong></span> </span> <div class="fr"> <span class="treeRinfo"> <div class="treeRinfo"> <span>{{departData.companyManage}}</span> <span>在职 <em class="colGreen" title="在职人数">---</em> (<em class="colGreen" title="正式员工">---</em> / <em class="colRed" title="非正式员工">---</em>)</span> </div> <div class="treeRinfo"> <el-dropdown class="item"> <span class="el-dropdown-link"> 操作<i class="el-icon-arrow-down el-icon--right"></i> </span> <el-dropdown-menu slot="dropdown"> <el-dropdown-item> <el-button type="text" @click="handlAdd('')">添加子部门</el-button> </el-dropdown-item> <el-dropdown-item> <el-button type="text" @click="handleList()">查看待分配员工</el-button> </el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </span> </div> </div> <!-- 构造树形列表 叶子 <i class="fa fa-male"></i> 非叶子 展开 <i class="fa fa-minus-square-o"> 闭合 <i class="fa fa-plus-square-o"> <div class="generalClass" slot-scope="{node,data}" style="width:99%"> --> <el-tree :props="{label:'name'}" :data="depts" node-key="id" default-expand-all> <!-- node : 是否展开,是否叶子节点 data:部门对象 id,name --> <div class="generalClass" slot-scope="{node,data}" style="width:99%"> <span> <i v-if="node.isLeaf" class="fa fa-male"></i> <i v-else :class="node.expanded?'fa fa-minus-square-o':'fa fa-plus-square-o'"></i> <span>{{ node.label }}</span> </span> <div class="fr"> <span class="treeRinfo"> <div class="treeRinfo"> <span>{{departData.companyManage}}</span> <span>在职 <em class="colGreen" title="在职人数">---</em> (<em class="colGreen" title="正式员工">---</em> / <em class="colRed" title="非正式员工">---</em>)</span> </div> <div class="treeRinfo"> <el-dropdown class="item"> <span class="el-dropdown-link"> 操作<i class="el-icon-arrow-down el-icon--right"></i> </span> <el-dropdown-menu slot="dropdown"> <el-dropdown-item> <el-button type="text" @click="handlAdd(data.id)">添加子部门</el-button> </el-dropdown-item> <el-dropdown-item> <el-button type="text" @click="handUpdate(data.id)">查看部门</el-button> </el-dropdown-item> <el-dropdown-item> <el-button type="text" @click="handleList()">查看待分配员工</el-button> </el-dropdown-item> <el-dropdown-item> <el-button type="text" @click="handleDelete(data.id)">删除部门</el-button> </el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </span> </div> </div> </el-tree> </div> </div> </el-card> </div> <!--:visible.sync 是否显示 --> <!--引入组件--> <component v-bind:is="deptAdd" ref="addDept"></component> </div> </template> <!-- 引入组件 --> <script> //引入api import {list,saveOrupdate,find,deleteById} from "@/api/base/dept" import commonApi from '@/utils/common' import deptAdd from './../components/add' export default { components:{deptAdd}, data() { return { deptAdd:'deptAdd', activeName: 'first', departData:{}, depts:[] } }, methods: { //添加部门 handlAdd(parentId) { //父页面调用子组件中的内容 this.$refs.addDept.parentId = parentId; this.$refs.addDept.dialogFormVisible = true }, //查看部门 handUpdate(id) { //根据id查询部门 find({id:id}).then(res => { //数据绑定到dept对象中 this.$refs.addDept.dept = res.data.data; this.$refs.addDept.dialogFormVisible = true }) }, handleDelete(id) { this.$confirm('是否删除此条记录?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { deleteById({id:id}).then(res=> { this.$message({ message: res.data.message, type: res.data.success?'success':'error' }); if(res.data.success) { location.reload(); } }) }) }, //构造查询方法 getList() { list().then(res => { this.departData = res.data.data //将普通的数据转化为父子接口 this.depts = commonApi.transformTozTreeFormat(res.data.data.depts); console.log(this.depts) }) } }, created: function() { this.getList(); }, } </script> <style rel="stylesheet/scss" lang="scss"> .el-dropdown { color: #000000 } .el-tree-node__content>.el-tree-node__expand-icon { padding:0px; } .el-tree-node__expand-icon { color:#ffffff } .generalClassNode { padding-left: 20px; } .el-tree-node__content{ font-size: 16px; line-height: 36px; height:36px; } .custom-tree-node{ padding-left: 20px; } .objectTree { overflow: auto; z-index: 100; width: 300px; border: 1px solid #dcdfe6; margin-top: 5px; left: 70px; } .el-tabs__content { overflow: initial; } .boxpad { margin-left: -40px; } </style> <style rel="stylesheet/scss" lang="scss" scoped> .el-tree-node__expand-icon{ } .el-icon-caret-right{} .el-tree-node__content{ font-size: 14px; line-height: 36px; } .generalClass { font-size: 14px; line-height: 36px; color:#000000 } .all { position: relative; min-height: 100%; padding-bottom: 200px; } .organization-main:after, .organization-index-top:after { display: block; clear: both; content: ''; visibility: hidden; height: 0; } .organization-main { font-size: 14px; font-size: 14px; } .organization-index { padding-bottom: 20px; margin-left: 20px; } .main-top-title { padding-left: 20px; padding-top: 20px; text-align: left; } ::-webkit-scrollbar-thumb { background-color: #018ee8; height: 50px; outline-offset: -2px; outline: 8px solid #fff; -webkit-border-radius: 4px; } ::-webkit-scrollbar-track-piece { background-color: #fff; -webkit-border-radius: 0; } ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-thumb:hover { background-color: #fb4446; height: 50px; -webkit-border-radius: 4px; } .modal-total { width: 100%; height: 100%; position: fixed; top: 0; left: 0; background: #000; z-index: 90; opacity: 0.2; } .modal { width: 400px; height: 300px; background-color: #ffffff; z-index: 999; position: absolute; left: 45%; top: 20%; text-align: center; } .treBox { padding: 30px 120px 0; } .organization-index-top { position: relative; .el-tabs-report { position: absolute; top: -50px; right: 15px; } } .treeCon { border-bottom: 1px solid #cfcfcf; padding: 10px 0; margin-bottom: 10px; .el-dropdown { color: #333; } } .treeRinfo { display: inline-block; } .treeRinfo span { padding-left: 30px; } </style>
(2)树形机构列表
<!-- 构造树形列表 叶子 <i class="fa fa-male"></i> 非叶子 展开 <i class="fa fa-minus-square-o"> 闭合 <i class="fa fa-plus-square-o"> <div class="generalClass" slot-scope="{node,data}" style="width:99%"> --> <el-tree :props="{label:'name'}" :data="depts" node-key="id" default-expand-all> <!-- node : 是否展开,是否叶子节点 data:部门对象 id,name --> <div class="generalClass" slot-scope="{node,data}" style="width:99%"> <span> <i v-if="node.isLeaf" class="fa fa-male"></i> <i v-else :class="node.expanded?'fa fa-minus-square-o':'fa fa-plus-square-o'"></i> <span>{{ node.label }}</span> </span> <div class="fr"> <span class="treeRinfo"> <div class="treeRinfo"> <span>{{departData.companyManage}}</span> <span>在职 <em class="colGreen" title="在职人数">---</em> (<em class="colGreen" title="正式员工">---</em> / <em class="colRed" title="非正式员工">---</em>)</span> </div> <div class="treeRinfo"> <el-dropdown class="item"> <span class="el-dropdown-link"> 操作<i class="el-icon-arrow-down el-icon--right"></i> </span> <el-dropdown-menu slot="dropdown"> <el-dropdown-item> <el-button type="text" @click="handlAdd(data.id)">添加子部门</el-button> </el-dropdown-item> <el-dropdown-item> <el-button type="text" @click="handUpdate(data.id)">查看部门</el-button> </el-dropdown-item> <el-dropdown-item> <el-button type="text" @click="handleList()">查看待分配员工</el-button> </el-dropdown-item> <el-dropdown-item> <el-button type="text" @click="handleDelete(data.id)">删除部门</el-button> </el-dropdown-item> </el-dropdown-menu> </el-dropdown> </div> </span> </div> </div> </el-tree>
骚戴理解:
解释<el-tree :props="{label:'name'}" :data="depts" node-key="id" default-expand-all>
这是vue.js中使用的一个el-tree组件,用于显示层次结构的树形数据。以下是对代码中每个属性的解释:
:props="{label:'name'}":这个属性设置了要显示在树节点上的文本内容的属性名为“name”,即树节点将显示"data"数组中每个元素的"name"属性。 后面可以通过{{ node.label }}去获取name,也就是部门的名称
:data="depts":这个属性设置了树形数据的来源为一个名为“depts”的数据数组,即树的每个节点由数组中的一个元素表示。
node-key="id":这个属性设置了树节点的唯一标识符为"data"数组中每个元素的"id"属性,这将有助于在树中添加、删除或更新节点时进行准确定位。
default-expand-all:这个属性设置了默认情况下,所有树节点都将展开显示。
解释 <i v-if="node.isLeaf" class="fa fa-male"></i> 和<i v-else :class="node.expanded?'fa fa-minus-square-o':'fa fa-plus-square-o'"></i>
如果 node.isleaf 为真(判断是不是叶子节点),则渲染一个男性符号的图标 <i class="fa fa-male"></i>。
否则,就渲染一个可以展开和折叠的矩形图标。该矩形图标的样式取决于 node.expanded 属性的值:
如果 node.expanded 为 true(节点展开的话),则渲染带有减号的图标 <i class="fa fa-minus-square-o"></i>。
如果 node.expanded 为 false(节点关闭的话),则渲染带有加号的图标 `<i class="fa fa-plus-square-o">
解释以下代码
<span>在职 <em class="colGreen" title="在职人数">---</em> (<em class="colGreen" title="正式员工">---</em> / <em class="colRed" title="非正式员工">---</em>)</span>
<em> 标签用于在文本中强调某些词语,使其在视觉上与其他文本内容区别开来。 表示空格,实现效果如下所示


