Day04——权限认证-基础

目录

  • [1 目标](#1 目标)
  • [2 RBAC模型](#2 RBAC模型)
    • [2.1 权限系统运行演示](#2.1 权限系统运行演示)
    • [2.2 ER图与关系梳理](#2.2 ER图与关系梳理)
  • [3 部门管理](#3 部门管理)
    • [3.1 需求分析](#3.1 需求分析)
    • [3.2 表结构分析](#3.2 表结构分析)
    • [3.3 接口分析](#3.3 接口分析)
      • [3.3.1 部门列表](#3.3.1 部门列表)
      • [3.3.2 部门树形结构接口](#3.3.2 部门树形结构接口)
      • [3.3.3 部门添加](#3.3.3 部门添加)
      • [3.3.4 部门修改](#3.3.4 部门修改)
      • [3.3.5 启用-禁用](#3.3.5 启用-禁用)
      • [3.3.6 删除部门](#3.3.6 删除部门)
    • [3.4 功能实现](#3.4 功能实现)
      • [3.4.1 权限模块环境说明](#3.4.1 权限模块环境说明)
      • [3.4.2 部门列表](#3.4.2 部门列表)
      • [3.4.2 部门树形结构](#3.4.2 部门树形结构)
      • [3.4.4 部门添加](#3.4.4 部门添加)
  • [4 岗位管理](#4 岗位管理)
    • [4.1 需求分析](#4.1 需求分析)
    • [4.2 表结构分析](#4.2 表结构分析)
    • [4.3 接口分析](#4.3 接口分析)
      • [4.3.1 岗位添加](#4.3.1 岗位添加)
      • [4.3.2 岗位修改](#4.3.2 岗位修改)
      • [4.3.3 岗位分页](#4.3.3 岗位分页)
      • [4.3.4 删除岗位](#4.3.4 删除岗位)
    • [4.4 功能实现](#4.4 功能实现)
  • [5 Spring Cache](#5 Spring Cache)
    • [5.1 介绍](#5.1 介绍)
    • [5.2 常用注解](#5.2 常用注解)
    • [5.3 入门案例](#5.3 入门案例)
      • [5.3.1 环境准备](#5.3.1 环境准备)
      • [5.3.2 @CachePut注解](#5.3.2 @CachePut注解)
      • [5.3.3 @Cacheable注解](#5.3.3 @Cacheable注解)
      • [5.3.4 @CacheEvict注解](#5.3.4 @CacheEvict注解)
      • [5.3.5 @Caching注解](#5.3.5 @Caching注解)
    • [5.4 部门模块添加缓存](#5.4 部门模块添加缓存)
      • [5.4.1 环境集成](#5.4.1 环境集成)
      • [5.4.2 代码优化](#5.4.2 代码优化)
  • [6 作业](#6 作业)
    • [6.1 部门管理](#6.1 部门管理)
    • [6.2 岗位管理](#6.2 岗位管理)

1 目标

前几天,我们已经完成了项目中的一些基础功能的开发,接下来的三天,我们会讲解比较重要的一个大模块,权限。在后台管理系统中,几乎每个项目都会使用到权限认证,所以我们项目中会重点讲解这些内容,让大家掌握其核心原理,增加自己的核心竞争力。

我们今天主要开发的业务是权限相关的知识,主要就包含了以下几个内容:

  • 能够熟悉RBAC权限模型的整体业务流程及涉及到的表结构
  • 能够独立完成部门管理的所有接口
  • 能够独立完成岗位管理的所有接口
  • 能够掌握SpringCache的特点,并且能够在项目中应用

2 RBAC模型

在企业系统中,通过配置用户的功能权限可以解决不同的人分管不同业务的需求,基于RBAC模型,RBAC(Role Based Access Control)模型,它的中文是基于角色的访问控制,主要是将功能组合成角色,再将角色分配给用户,也就是说角色是功能的合集

比如:

企业A总共有12个功能,需要创建100个用户。这些用户包括财务管理、人事管理、销售管理等等。如果不引入基于角色的访问控制(RBAC)模型,我们就需要每创建一个用户就要分配一次功能,这将至少需要进行100次操作(每个用户只能拥有一个功能)。如果用户数量增加到1000甚至10000,并且一个用户可能会拥有多个功能,操作将会变得非常繁琐。如图:

经过多次操作后发现,有些人被分配了相同的功能。例如,A、B等10个用户都被分配了客户管理、订单管理和供应商管理这几个模块。我们是否可以将这几个功能模块组合成一个包,然后将整个包分配给需要的用户呢?这个包被称为角色。由于角色和功能之间的对应关系相对稳定,在分配权限时只需分配角色即可,如下图所示:如图所示:

基于RBAC授权模式后,我们可以达到以下2个目标:

  • 解耦用户和功能,降低操作错误率
  • 降低功能权限分配的繁琐程度

2.1 权限系统运行演示

我们可以通过运行权限系统,来理解RBAC。

2.2 ER图与关系梳理

在一个核心业务系统中,我们通常通过业务分析,从而抽离出数据库表,表确定之后我们会进一步分析表中应该有的字段,下面我先看下业务ER图:

上图中清楚的描述用户、角色、资源、职位、部门之间的关系,同时我们进一步推导出以下结果:

  • 用户与职位是N:1关系
  • 用户与部门是N:1关系
  • 用户与角色是N:N关系,则它们之间必然有一个中间表
  • 角色与部门是N:N关系,则它们之间必然有一个中间
  • 角色与资源是N:N关系,则它们之间必然有一个中间表

下图就展示了权限认证涉及到的所有的表结构,共8张表

具体的表字段大家可以查看提供的数据库,我们在做业务的时候,会再次梳理这些表中的字段

3 部门管理

我们先来完成部门管理这个模块,因为这个模块跟其他业务的耦合度相对较高,几乎权限所有的模块都要关联部门

我们再局部的说明一下部门跟其他业务的关系

三者之间的关系

  • 部门与用户是一对多的关系
  • 岗位与用户是一对多的关系
  • 部门与岗位是一对多的关系

3.1 需求分析

我们可以原型图进行说明,这个是功能列表页

  • 可以按照条件进行查询,由于一个公司的部门的数量是有限的,列表查询不需要分页
  • 对于每一行都可以进行"删除","禁用","编辑","新增部门"
  • 点击新增部门按钮,则会弹窗,如下图

3.2 表结构分析

表名:sys_dept

字段说明:

字段 类型 说明
id bigint 部门id
parent_dept_no varchar(20) 父部门编号
dept_no varchar(20) 部门编号
dept_name varchar(30) 部门名称
sort_no int 排序
data_state char(1) 数据状态(0正常 1停用)
create_time datetime 创建时间
update_time datetime 更新时间
create_by bigint 创建者
update_by bigint 更新者
leader_id bigint 负责人Id
remark varchar(500) 备注

表中parent_dept_no和dept_no是用于构建树形结构的基础,通过这2个字段定义资源的上下级关系,通常添加部门,我们通过程序自动生成编号,生成的编号满足以下规则:

  • 1级:100000000000000
  • 2级:100001000000000
  • 3级:100001001000000
  • 4级:100001001001000
  • 5级:100001001001001

当我们在需要查询当前1级节点以下所有节点时,就不用再递归查询,使用like "dept_no%"即可。

举个例子:

想要查询100001001000000下所有的部门,我们的查询方式为:

sql 复制代码
select * from sys_dept where dept_no like '100001001%'

这样就可以查询到100001001000000部门下所有的部门了

对应的实体类:

java 复制代码
@Data
public class Dept extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 父部门编号
     */
    private String parentDeptNo;

    /**
     * 部门编号:
     */
    private String deptNo;

    /**
     * 部门名称
     */
    private String deptName;

    /**
     * 排序
     */
    private Integer sortNo;

    /**
     * 数据状态(0正常 1停用)
     */
    private String dataState;

    /**
     * 负责人Id
     */
    private Long leaderId;

}

3.3 接口分析

依据我们刚才分析的部门的需求,在部门模块中共有5个接口,分别是:

  • 添加部门
  • 修改部门
  • 启用-禁用
  • 部门列表
  • 删除部门

下面我们就来具体分析每个接口包含的四要素(路径、请求方式、入参、出参)

3.3.1 部门列表

接口地址 :/dept/list

请求方式 :POST

请求示例:

javascript 复制代码
{
  "parentDeptNo": "", //父部门编号
  "dataState": "", //状态
  "deptName": "" //部门名称
}

响应示例:

javascript 复制代码
{
    "code": 200,
    "msg": "操作成功",
    "data": [
        {
            "id": "1671445634122588250",
            "createTime": "2023-07-09 16:00:13",
            "createDay": "2023-07-09",
            "updateTime": "2023-08-25 20:21:56",
            "createBy": "1",
            "updateBy": "1671403256519078006",
            "dataState": "0",
            "remark": "大沙发的",
            "parentDeptNo": "100001000000000",
            "deptNo": "100001030000000",
            "deptName": "静测试部",
            "sortNo": 0,
            "level": 4
        },
        {
            "id": "1671445634122588247",
            "createTime": "2023-07-07 18:18:59",
            "createDay": "2023-07-07",
            "updateTime": "2023-07-28 17:19:19",
            "createBy": "1671403256519078006",
            "updateBy": "1671403256519078006",
            "dataState": "0",
            "remark": "",
            "parentDeptNo": "100001000000000",
            "deptNo": "100001029000000",
            "deptName": "大管家",
            "sortNo": 0,
            "leaderId": "1671403256519078076",
            "leaderName": "你是是",
            "level": 4
        }
    ],
    "operationTime": "2023-08-27 11:25:01"
}

3.3.2 部门树形结构接口

由于是初始化部门的树形结构,这个接口需要在部门中去定义

接口地址 :/dept/tree

请求方式 :POST

请求示例:

响应示例:

javascript 复制代码
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "items": [
      {
        "id": "100001000000000",
        "label": "智慧养老院",
        "children": [
          {
            "id": "100001001000000",
            "label": "院长办公室"
          },
          {
            "id": "100001002000000",
            "label": "财务部",
            "children": [
              {
                "id": "100001002001000",
                "label": "会计组"
              },
              {
                "id": "100001002002000",
                "label": "结算组"
              }
            ]
          }
        ]
      }
    ]
  }
}

3.3.3 部门添加

接口地址 :/dept

请求方式 :PUT

接口描述:

部门添加

请求示例:

javascript 复制代码
{
  "dataState": "",//部门状态
  "deptName": "",//部门名称
  "leaderId": 0, // 部门负责人id
  "parentDeptNo": "", //上级部门编号
  "remark": "", //部门说明
  "sortNo": 0   //排序
}

响应示例:

javascript 复制代码
{
	"code": 0,
	"data": true,
	"msg": "",
	"operationTime": ""
}

3.3.4 部门修改

接口地址 :/dept

请求方式 :PATCH

请求示例:

javascript 复制代码
{
  "dataState": "",//部门状态
  "deptName": "",//部门名称
  "leaderId": 0, // 部门负责人id
  "parentDeptNo": "", //上级部门编号
  "remark": "", //部门说明
  "sortNo": 0,   //排序
  "id":1671445634122588232   // 主键
}

响应示例:

javascript 复制代码
{
	"code": 0,
	"data": true,
	"msg": "",
	"operationTime": ""
}

3.3.5 启用-禁用

接口地址 :/dept/is_enable

请求方式 :PATCH

请求示例:

javascript 复制代码
{
  "dataState": "",  //状态
  "id": 1671445634122588232
}

响应示例:

javascript 复制代码
{
	"code": 0,
	"data": true,
	"msg": "",
	"operationTime": ""
}

3.3.6 删除部门

接口地址 :/dept/{deptId}

请求方式 :DELETE

请求参数:

参数名称 参数说明 请求类型 数据类型
deptId 部门id path(路径上的参数) string

响应示例:

json 复制代码
{
	"code": 0,
	"data": 1,
	"msg": "",
	"operationTime": ""
}

3.4 功能实现

下面,我们就依据我们刚才分析的接口来完成具体的功能,由于这些也都是增删改查的代码,我们在课堂上一起来写一些比较有代表的功能,剩下的接口需要大家自己实现

我们课堂上的需要实现的接口有:

  • 部门列表 需要有上下级的关系,按照父部门编号查询子部门列表
  • 部门树形 在新增或是后边的岗位查询都需要展示部门的属性结构,查询没有条件,把所有数据组装成树形结构
  • 部门添加 需要设计部门编号,讲解一种通用的编号生成方案

需要大家自己实现的接口:

  • 部门修改
  • 启用-禁用
  • 部门删除

3.4.1 权限模块环境说明

权限模块是比较通用的模块,在很多项目中都会存在,它具有独立性,所以我们可以把它单独使用一个工程模块来管理。

在zzyl父工程中使用zzyl-security模块,如下图

3.4.2 部门列表

这个部门列表不需要分页,依据我们刚才的接口分析,我们直接来编码

1)定义接口

新增类DeptController中定义新的方法

java 复制代码
package com.zzyl.controller;

/**
 * @author sjqn
 */
@RestController
@RequestMapping("/dept")
@Api(tags = "部门管理")
public class DeptController {

    @PostMapping("/list")
    @ApiOperation("部门列表")
    @ApiImplicitParam(name = "deptDto",value = "部门DTO对象",required = true,dataType = "DeptDto")
    @ApiOperationSupport(
            includeParameters = {"deptDto.dataState","deptDto.deptName","deptDto.parentDeptNo"}
    )
    public ResponseResult<List<DeptVo>> deptList(@RequestBody DeptDto deptDto){
       
        return null;
    }
}

其中的ApiOperationSupport是可以在swagger接口文档中说明参数包含了哪些字段,比如你定义的dto是10个字段,但是当前接口只需要6个字段,就可以把这6个字段声明出来,这样做的好处就是dto可以在多个接口中复用

2)持久层mapper

在DeptMapper中新增方法

java 复制代码
package com.zzyl.mapper;

/**
 * @author sjqn
 */
@Mapper
public interface DeptMapper {
    /**
     * 查询列表
     * @param deptDto
     * @return
     */
    List<DeptVo> selectList(DeptDto deptDto);
}

对应的xml映射文件

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzyl.mapper.DeptMapper">
    <resultMap id="BaseResultMap" type="com.zzyl.entity.Dept">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="parent_dept_no" jdbcType="VARCHAR" property="parentDeptNo"/>
        <result column="dept_no" jdbcType="VARCHAR" property="deptNo"/>
        <result column="dept_name" jdbcType="VARCHAR" property="deptName"/>
        <result column="sort_no" jdbcType="INTEGER" property="sortNo"/>
        <result column="data_state" jdbcType="CHAR" property="dataState"/>
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
        <result column="create_by" jdbcType="BIGINT" property="createBy"/>
        <result column="update_by" jdbcType="BIGINT" property="updateBy"/>
        <result column="leader_id" jdbcType="BIGINT" property="leaderId"/>
        <result column="leader_name" jdbcType="BIGINT" property="leaderName"/>
    </resultMap>
    <resultMap id="BaseResultVoMap" type="com.zzyl.vo.DeptVo">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="parent_dept_no" jdbcType="VARCHAR" property="parentDeptNo"/>
        <result column="dept_no" jdbcType="VARCHAR" property="deptNo"/>
        <result column="dept_name" jdbcType="VARCHAR" property="deptName"/>
        <result column="sort_no" jdbcType="INTEGER" property="sortNo"/>
        <result column="data_state" jdbcType="CHAR" property="dataState"/>
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
        <result column="create_by" jdbcType="BIGINT" property="createBy"/>
        <result column="update_by" jdbcType="BIGINT" property="updateBy"/>
        <result column="leader_id" jdbcType="BIGINT" property="leaderId"/>
        <result column="role_id" jdbcType="BIGINT" property="roleId"/>
        <result column="create_day" jdbcType="BIGINT" property="createDay"/>
    </resultMap>
    <sql id="Base_Column_List">
        id
        , parent_dept_no, dept_no, dept_name, sort_no, data_state, create_time, update_time,
    create_by, update_by, remark, leader_id
    </sql>
   
    <select id="selectList" parameterType="com.zzyl.dto.DeptDto" resultMap="BaseResultVoMap">
        select
        d.id, d.parent_dept_no, d.dept_no, d.dept_name, d.sort_no, d.data_state, d.create_time, d.update_time,
        d.create_by, d.update_by, d.remark, d.leader_id
        , u.real_name as leader_name,DATE_FORMAT(d.create_time,'%Y-%m-%d') as create_day
        from sys_dept d
        left join sys_user u on u.id = leader_id
        <where>
            <if test="deptName!=null and deptName!=''">
                and d.dept_name like concat('%',#{deptName},'%')
            </if>
            <if test="parentDeptNo!=null and parentDeptNo!=''">
                and d.parent_dept_no like concat(#{parentDeptNo},'%')
            </if>
            <if test="dataState!=null and dataState!=''">
                and d.data_state=#{dataState}
            </if>
        </where>
        order by d.sort_no asc, d.create_time desc
    </select>
</mapper>

由于在部门列表的展示中,需要展示部门的负责人,所以关联了sys_user用户表来查询负责人姓名,虽然现在我们还没有开发用户模块,但是用户表已经有了,在这里可以进行关联查询

  • 在sys_dept表和实体类中并没有一个叫做leader_name的字段,我们需要扩展一下,只需要在Dept实体类中新增一个leaderName字段即可,并且把对应的xml映射文件中的BaseResultVoMap中定义这个字段
  • 在返回的日期中,前端要求返回的是创建的天,比如是:2023-10-31,我们需要使用mysql的日期函数单独处理
    • 格式化
      • select DATE_FORMAT(new(),'%Y-%m-%d %H:%i:%s')--->返回字符串:2023-10-31 01:21:00
      • select str_to_DATE('2023-10-31 01:21:00','%Y-%m-%d %H:%i:%s')--->返回日期类型

3)业务层

在DeptService中定义新的方法

java 复制代码
package com.zzyl.service;

/**
 * @author sjqn
 */
public interface DeptService {

    /**
     * 多条件查询部门
     * @param deptDto
     * @return
     */
    List<DeptVo> deptList(DeptDto deptDto);
}

实现类

java 复制代码
package com.zzyl.service.impl;

/**
 * @author sjqn
 * @date 2023/10/30
 */
@Service
public class DeptServiceImpl implements DeptService {

    @Autowired
    private DeptMapper deptMapper;

    /**
     * 多条件查询部门
     * @param deptDto
     * @return
     */
    @Override
    public List<DeptVo> deptList(DeptDto deptDto) {
        return deptMapper.selectList(deptDto);
    }
}

4)控制层代码补全

java 复制代码
/**
 * @Description:部门前端控制器
 */
@Slf4j
@Api(tags = "部门管理")
@RestController
@RequestMapping("dept")
public class DeptController {

    @Autowired
    DeptService deptService;

    /***
     * @description 多条件查询部门列表
     * @param deptDto 部门DTO对象
     * @return List<DeptVo>
     */
    @PostMapping("list")
    @ApiOperation(value = "部门列表",notes = "部门列表")
    @ApiImplicitParam(name = "deptDto",value = "部门DTO对象",required = true,dataType = "DeptDto")
    @ApiOperationSupport(includeParameters = {"deptDto.dataState","deptDto.deptName","deptDto.parentDeptNo"})
    public ResponseResult<List<DeptVo>> deptList(@RequestBody DeptDto deptDto) {
        List<DeptVo> deptVoList = deptService.findDeptList(deptDto);
        return ResponseResult.success(deptVoList);
    }
}

5)测试

同样打开前端项目进行测试

3.4.2 部门树形结构

  1. 定义接口

在DeptController中定义新的方法,如下代码:

java 复制代码
@PostMapping("/tree")
@ApiOperation("部门树形")
public ResponseResult<TreeVo> deptTreeVo(){
    return null;
}

返回值中的treeVo需要单独定义,代码如下:

java 复制代码
package com.zzyl.vo;

import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

/**
 * @description: 资源树显示类
 */
@Data
@NoArgsConstructor
public class TreeVo implements Serializable {

	@ApiModelProperty(value = "tree数据")
	private List<TreeItemVo> items;

	@Builder
	public TreeVo(List<TreeItemVo> items) {
		this.items = items;
	}
}

TreeItemVo

java 复制代码
package com.zzyl.vo;

import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * @Description:资源树结构体
 */
@Data
@NoArgsConstructor
public class TreeItemVo implements Serializable {

    @ApiModelProperty(value = "节点ID")
    public String id;

    @ApiModelProperty(value = "显示内容")
    public String label;

    @ApiModelProperty(value = "显示内容")
    public List<TreeItemVo> children = new ArrayList<>();

    @Builder
    public TreeItemVo(String id, String label, List<TreeItemVo> children) {
        this.id = id;
        this.label = label;
        this.children = children;
    }
}

2)持久层mapper

无,使用之前查询的列表

3)业务层

在DeptService中新增方法,如下

java 复制代码
/**
 * 组织部门树形
 * @param deptDto 根节点
 * @return: deptDto
 */
public TreeVo deptTreeVo();

递归基本思路

代码逻辑:

实现类中的方法:

java 复制代码
/**
     * 组织部门树形
     * @return
     */
@Override
public TreeVo deptTreeVo() {
    //获取根节点树形
    String parentDeptNo = SuperConstant.ROOT_DEPT_PARENT_ID;

    //构建查询条件
    DeptDto param = DeptDto.builder()
        .dataState(SuperConstant.DATA_STATE_0)
        .parentDeptNo(NoProcessing.processString(parentDeptNo))
        .build();
    //查询部门列表数据
    List<DeptVo> deptList =  deptMapper.selectList(param);

    if(EmptyUtil.isNullOrEmpty(deptList)){
        throw new BaseException("部门数据没有定义");
    }

    //找根节点
    DeptVo rootDept = deptList.stream().filter(d -> SuperConstant.ROOT_DEPT_PARENT_ID.equals(d.getParentDeptNo())).collect(Collectors.toList()).get(0);

    //返回的部门数据
    List<TreeItemVo> treeItemVoList = new ArrayList<>();

    //递归调用
    recursionTreeItem(treeItemVoList,rootDept,deptList);

    return TreeVo.builder().items(treeItemVoList).build();
}

/**
     * 递归调用拼装数据
     * @param treeItemVoList  封装返回的数据
     * @param rootDept  当前部门
     * @param deptList  部门列表(全部数据)
     */
private void recursionTreeItem(List<TreeItemVo> treeItemVoList, DeptVo rootDept, List<DeptVo> deptList) {
    //构建item对象
    TreeItemVo treeItemVo = TreeItemVo.builder().id(rootDept.getDeptNo()).label(rootDept.getDeptName()).build();
    //获得当前部门下的子部门
    List<DeptVo> childrenDept = deptList.stream()
        .filter(n -> n.getParentDeptNo().equals(rootDept.getDeptNo()))
        .collect(Collectors.toList());
    //如果子部门不为空,则继续递归调用
    if(!EmptyUtil.isNullOrEmpty(childrenDept)){

        ArrayList<TreeItemVo> listChildren = Lists.newArrayList();
        //子部门列表
        childrenDept.forEach(dept -> {
            this.recursionTreeItem(listChildren,dept,deptList);
        });
        treeItemVo.setChildren(listChildren);
    }

    treeItemVoList.add(treeItemVo);
}

4)控制层

java 复制代码
@PostMapping("/tree")
@ApiOperation("部门树形")
public ResponseResult<TreeVo> deptTreeVo(){
    TreeVo treeVo = deptService.deptTreeVo();
    return ResponseResult.success(treeVo);
}

5)测试

启动前端项目,然后在岗位模块中查看左侧的部门树是否成功展示

3.4.4 部门添加

依据我们分析的接口,我们还是按照步骤 进行开发

1)定义接口

java 复制代码
@PutMapping
@ApiOperation(value = "部门添加",notes = "部门添加")
@ApiImplicitParam(name = "deptDto",value = "部门DTO对象",required = true,dataType = "DeptDto")
@ApiOperationSupport(includeParameters = {
    "deptDto.dataState",
    "deptDto.deptName",
    "deptDto.leaderId",
    "deptDto.remark",
    "deptDto.sortNo",
    "deptDto.parentDeptNo"})
public ResponseResult<DeptVo> createDept(@RequestBody DeptDto deptDto) {
  
    return null;
}

2)持久层mapper

java 复制代码
@Mapper
public interface DeptMapper {

    int insert(Dept record);
}

DeptMapper.xml映射文件

xml 复制代码
 <insert id="insert" parameterType="com.zzyl.entity.Dept">
     <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
         SELECT LAST_INSERT_ID()
     </selectKey>
     insert into sys_dept
     <trim prefix="(" suffix=")" suffixOverrides=",">
         <if test="parentDeptNo != null">
             parent_dept_no,
         </if>
         <if test="deptNo != null">
             dept_no,
         </if>
         <if test="deptName != null">
             dept_name,
         </if>
         <if test="sortNo != null">
             sort_no,
         </if>
         <if test="dataState != null">
             data_state,
         </if>
         <if test="createTime != null">
             create_time,
         </if>
         <if test="updateTime != null">
             update_time,
         </if>
         <if test="createBy != null">
             create_by,
         </if>
         <if test="updateBy != null">
             update_by,
         </if>
         <if test="leaderId != null">
             leader_id,
         </if>
         <if test="remark != null">
             remark,
         </if>
     </trim>
     <trim prefix="values (" suffix=")" suffixOverrides=",">
         <if test="parentDeptNo != null">
             #{parentDeptNo,jdbcType=VARCHAR},
         </if>
         <if test="deptNo != null">
             #{deptNo,jdbcType=VARCHAR},
         </if>
         <if test="deptName != null">
             #{deptName,jdbcType=VARCHAR},
         </if>
         <if test="sortNo != null">
             #{sortNo,jdbcType=INTEGER},
         </if>
         <if test="dataState != null">
             #{dataState,jdbcType=CHAR},
         </if>
         <if test="createTime != null">
             #{createTime,jdbcType=TIMESTAMP},
         </if>
         <if test="updateTime != null">
             #{updateTime,jdbcType=TIMESTAMP},
         </if>
         <if test="createBy != null">
             #{createBy,jdbcType=BIGINT},
         </if>
         <if test="updateBy != null">
             #{updateBy,jdbcType=BIGINT},
         </if>
         <if test="leaderId != null">
             #{leaderId,jdbcType=BIGINT},
         </if>
         <if test="remark != null">
             #{remark,jdbcType=VARCHAR}
         </if>
     </trim>
</insert>

3)业务层

DeptService

java 复制代码
/**
 * @Description:部门表服务类
 */
public interface DeptService {

    /**
     * @Description 创建部门表
     * @param deptDto 对象信息
     * @return DeptVo
     */
    Boolean createDept(DeptDto deptDto);

}

实现类

java 复制代码
/**
     * 添加部门
     * @param deptDto
     */
@Override
public void createDept(DeptDto deptDto) {

    Dept dept = BeanUtil.toBean(deptDto, Dept.class);

    //根据父部门编号,构建部门编号
    String deptNo = createDeptNo(deptDto.getParentDeptNo());
    dept.setDeptNo(deptNo);

    //保存
    int flag = deptMapper.insert(dept);

    if(flag != 1){
        throw new BaseException("保存部门失败");
    }
}

/**
     * 构建部门编号
     * @param parentDeptNo
     * @return
     */
private String createDeptNo(String parentDeptNo) {

    //部门层级不能超过4级
    if(NoProcessing.processString(parentDeptNo).length() /3 == 5){
        throw new BaseException("部门创建不能超过4级");
    }

    //构建部门编号,有两种情况,当前父部门编号,有子部门:在已有的子部门的基础上累加  |  没有子部门:新增子部门编号
    DeptDto deptDto = DeptDto.builder().parentDeptNo(parentDeptNo).build();
    List<DeptVo> deptVoList = deptMapper.selectList(deptDto);
    //无下属节点则创建下属节点
    if(EmptyUtil.isNullOrEmpty(deptVoList)){
        return NoProcessing.createNo(parentDeptNo,false);
    }else {
        //有下属节点则累加下属节点
        Long deptNo = deptVoList.stream().map(dept -> {
            return Long.valueOf(dept.getDeptNo());
        }).max(Comparator.comparing(i -> i)).get();
        return NoProcessing.createNo(String.valueOf(deptNo),true);
    }
}

4)控制层

java 复制代码
@PutMapping
@ApiOperation(value = "部门添加",notes = "部门添加")
@ApiImplicitParam(name = "deptDto",value = "部门DTO对象",required = true,dataType = "DeptDto")
@ApiOperationSupport(includeParameters = {
    "deptDto.dataState",
    "deptDto.deptName",
    "deptDto.leaderId",
    "deptDto.remark",
    "deptDto.sortNo",
    "deptDto.parentDeptNo"})
public ResponseResult<DeptVo> createDept(@RequestBody DeptDto deptDto) {
    return ResponseResult.successdeptService.createDept(deptDto);
}

5)测试

权限后面的前端代码不需要大家编写,大家可以直接启动前端项目来测试功能

4 岗位管理

我们再来回顾一下刚才的表关系,大家还是看这个图

部门和岗位的关系是一对多

4.1 需求分析

我们先来看岗位中的第一个页面

这个页面中包含了两部分内容,第一个是左侧的部门结构树,第二个是岗位列表

并且是这个页面中也包含了很多的按钮,分别是:"新增职位"、"删除"、"禁用"、"编辑"

当我们点击了"新增岗位"按钮之后,会弹窗提示,如下效果

首先选中了一个部门,然后点击"新增职位"按钮,在弹窗之后,会自动回显选择的部门(置灰不可改)

4.2 表结构分析

表名:sys_post

字段说明:

字段 类型 说明
id bigint 岗位id
dept_no varchar(20) 部门编号
post_no varchar(20) 岗位编码:父部门编号+01【2位】
post_name varchar(50) 岗位名称
sort_no int 排序
data_state char(1) 数据状态(0正常 1停用)
create_time datetime 创建时间
update_time datetime 更新时间
create_by bigint 创建者
update_by bigint 更新者
remark varchar(500) 备注

对应实体类:

java 复制代码
package com.zzyl.entity;

import com.zzyl.base.BaseEntity;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Post extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 部门编号
     */
    private String deptNo;

    /**
     * 岗位编码:父部门编号+01【2位】
     */
    private String postNo;

    /**
     * 岗位名称
     */
    private String postName;

    /**
     * 显示顺序
     */
    private Integer sortNo;

    /**
     * 数据状态(0正常 1停用)
     */
    private String dataState;
}

4.3 接口分析

依据刚才的需求分析,岗位接口如下:

  • 岗位列表查询
  • 岗位添加
  • 岗位修改
  • 岗位删除

4.3.1 岗位添加

接口地址 :/post

请求方式 :PUT

请求示例:

javascript 复制代码
{
  "deptNo": "", //部门编号
  "remark": "", //备注
  "dataState": "",//是否启用(0:启用,1:禁用)
  "postName": "" //岗位名称
}

响应示例:

javascript 复制代码
{
	"code": 0,
	"data": true,
	"msg": "",
	"operationTime": ""
}

4.3.2 岗位修改

其中的"编辑"、"启用"、"禁用" 三个按钮共用这个接口

接口地址 :/post

请求方式 :PATCH

请求示例:

javascript 复制代码
{
  "id": 0,  //主键
  "deptNo": "", //部门编号
  "remark": "", //备注
  "dataState": "",//是否启用(0:启用,1:禁用)
  "postName": "" //岗位名称
}

响应示例:

javascript 复制代码
{
	"code": 0,
	"data": true,
	"msg": "",
	"operationTime": ""
}

4.3.3 岗位分页

接口地址 :/post/page/{pageNum}/{pageSize}

请求方式 :POST

请求示例:

javascript 复制代码
{
  "deptNo": "",//部门编号
  "dataState": "",//岗位状态
  "postName": ""//岗位名称
}

响应示例:

javascript 复制代码
{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "total": "2",
        "pageSize": 10,
        "pages": "1",
        "page": 1,
        "records": [
            {
                "id": "1671446178337726569",
                "createTime": "2023-07-11 10:30:25",
                "createDay": "2023-07-11",
                "createBy": "1671403256519078006",
                "dataState": "0",
                "remark": "",
                "deptNo": "100001005000000",
                "postNo": "100001005002000",
                "postName": "财务主管",
                "deptVo": {
                    "id": "1671445634122588164",
                    "createTime": "2023-06-22 11:42:36",
                    "updateTime": "2023-06-30 09:14:16",
                    "createBy": "1671362878457892866",
                    "updateBy": "1",
                    "dataState": "0",
                    "remark": "啊实打实打阿萨德阿萨德按时",
                    "parentDeptNo": "100001000000000",
                    "deptNo": "100001005000000",
                    "deptName": "财务部",
                    "sortNo": 2,
                    "leaderId": "1671403256519077890",
                    "level": 4
                }
            },
            {
                "id": "1671446178337726568",
                "createTime": "2023-07-11 10:30:14",
                "createDay": "2023-07-11",
                "createBy": "1671403256519078006",
                "dataState": "0",
                "deptNo": "100001005000000",
                "postNo": "100001005001000",
                "postName": "结算员",
                "deptVo": {
                    "id": "1671445634122588164",
                    "createTime": "2023-06-22 11:42:36",
                    "updateTime": "2023-06-30 09:14:16",
                    "createBy": "1671362878457892866",
                    "updateBy": "1",
                    "dataState": "0",
                    "remark": "啊实打实打阿萨德阿萨德按时",
                    "parentDeptNo": "100001000000000",
                    "deptNo": "100001005000000",
                    "deptName": "财务部",
                    "sortNo": 2,
                    "leaderId": "1671403256519077890",
                    "level": 4
                }
            }
        ]
    },
    "operationTime": "2023-08-27 02:53:24"
}

4.3.4 删除岗位

接口地址 :/post/{postIds}

请求方式 :DELETE

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
postIds postIds path true string

响应示例:

javascript 复制代码
{
	"code": 0,
	"data": {},
	"msg": "",
	"operationTime": ""
}

4.4 功能实现

基于我们刚才分析的接口,有两部分,第一就是部门的树形结构,第二个是关于岗位的

我们在课堂上一起来实现部门的树形结构这个接口

关于岗位管理的接口,基于大家的经验来看,如果大家能够独立完成前几天和今天上午的作业代码,那么对于岗位模块的所有接口,大家也一定能够胜任,所以,关于岗位的所有的接口的开发工作全部当做今天的作业

5 Spring Cache

在我们查询部门数据的时候,特别是树形结构,要把所有的属性结构数据都展示出来,这个是会对数据库的访问造成一定的压力,并且从数据库查询效率也不是很高,所以我们通常都会添加缓存来提升效率

添加缓存的基本逻辑:

关于缓存的添加,我们可以直接使用redis来管理缓存,不过一般企业开发中,我们更多的是使用统一管理缓存的框架来实现,主要是更加的低耦合,并且切换缓存的成本的很低

5.1 介绍

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:

  • EHCache
  • Caffeine
  • Redis(常用)

5.2 常用注解

在SpringCache中提供了很多缓存操作的注解,常见的是以下几个:

注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类上
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除
@Caching 缓存的结合体,可以组合以上注解在一个方法中使用,比如有新增,有删除

在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。

5.3 入门案例

5.3.1 环境准备

**导入基础工程:**底层已使用Redis缓存实现

基础环境的代码,在我们今天的资料中已经准备好了, 大家只需要将这个工程导入进来就可以了。导入进来的工程结构如下:

数据库准备:

创建名为cache_db数据库,执行以下sql脚本来创建用户表

sql 复制代码
CREATE TABLE "user" (
  "id" bigint NOT NULL AUTO_INCREMENT,
  "name" varchar(45) DEFAULT NULL,
  "age" int DEFAULT NULL,
  PRIMARY KEY ("id")
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

引导类上加@EnableCaching用于开启缓存

java 复制代码
package com.itheima;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@Slf4j
@SpringBootApplication
@EnableCaching//开启缓存注解功能
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class,args);
        log.info("项目启动成功...");
    }
}

5.3.2 @CachePut注解

@CachePut 说明:

  • 作用: 将方法返回值,放入缓存
  • value: 缓存的名称, 每个缓存名称下面可以有很多key
  • key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

在save方法上加注解@CachePut

当前UserController的save方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上添加注解 @CachePut,用法如下:

java 复制代码
	/**
	* CachePut:将方法返回值放入缓存
	* value:缓存的名称,每个缓存名称下面可以有多个key
	* key:缓存的key
	*/
	@PostMapping
    @CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

说明: key的写法如下

  • #user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;
  • #result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;
  • #p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key
  • #a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key
  • #root.args[0].id:#root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;

启动服务,通过swagger接口文档测试,访问UserController的save()方法

因为id是自增,所以不需要设置id属性

我们现在分别查看用户表和缓存中的数据,如果都能保存,则说明集成成功

5.3.3 @Cacheable注解

作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中

  • value: 缓存的名称,每个缓存名称下面可以有多个key
  • key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

在UserController上找到getById,添加注解@Cacheable

java 复制代码
	/**
	* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,	  *调用方法并将方法返回值放到缓存中
	* value:缓存的名称,每个缓存名称下面可以有多个key
	* key:缓存的key
	*/
	@GetMapping
    @Cacheable(cacheNames = "userCache",key="#id")
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }

重启服务,通过swagger接口文档测试,访问UserController的getById()方法

第一次访问,会请求我们controller的方法,查询数据库。后面再查询相同的id,就直接从Redis中查询数据,不用再查询数据库了,就说明缓存生效了。

测试步骤:

  • 提前在redis中手动删除掉id=${id}的用户数据
  • 查看控制台sql语句:说明从数据库查询的用户数据
  • 查看Redis中的缓存数据:说明已成功缓存
  • 再次查询相同id的数据时,直接从redis中直接获取,不再查询数据库。

多条件使用案例

java 复制代码
@Cacheable(value = "userCache",key="#userDto.hashCode()",unless = "#result.size() == 0")
public List<User> getList(UserDto userDto){
    List<User> list = userMapper.getList("%" + userDto.getName() + "%", userDto.getAge());
    return list;
}

如果返回结果为空,则不缓存unless = "#result == null"unless = "#result.size() == 0"

5.3.4 @CacheEvict注解

  • 作用: 清理指定缓存
  • value: 缓存的名称,每个缓存名称下面可以有多个key
  • key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

在 delete 方法上加注解@CacheEvict

java 复制代码
	@DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

	@DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
    public void deleteAll(){
        userMapper.deleteAll();
    }

重启服务,通过swagger接口文档测试,访问UserController的deleteAll()方法

查看数据库和缓存是否删除了数据

5.3.5 @Caching注解

作用: 组装其他缓存注解

  • cacheable 组装一个或多个@Cacheable注解
  • put 组装一个或多个@CachePut注解
  • evict 组装一个或多个@CacheEvict注解

我们可以修改getById方法,下面就使用@Caching组装使用了多种注解

java 复制代码
@Caching(
        cacheable = {
                @Cacheable(value = "userCache",key = "#id")
        },
        put = {
                @CachePut(value = "userCache",key = "#result.name"),
                @CachePut(value = "userCache",key = "#result.age")
        }
)
public User getById(Long id){
    User user = userMapper.getById(id);
    if(user == null){
        throw new RuntimeException("用户不存在");
    }
    return user;
}

当调用getById方法之后,首先到缓存中根据id查询数据,如果查询不成功还是会到数据库中找数据,同时会再往redis中set两个缓存,key分别是name和age

5.4 部门模块添加缓存

我们现在已经熟悉了spring cache的基本使用方式了,现在我们就可以把它集成我们项目中的部门模块

5.4.1 环境集成

参考spring cache入门案例

5.4.2 代码优化

我们可以把所有spring cache的注解都在deptService中去定义,最终代码如下:

java 复制代码
package com.zzyl.service.impl;

/**
 * @Description:部门表服务实现类
 */
@Service
public class DeptServiceImpl implements DeptService {

    @Autowired
    DeptMapper deptMapper;

    /**
     * @param deptDto 对象信息
     * @return DeptVo
     * @Description 创建部门表
     */
    @Transactional
    @Override
    @Caching(evict = {@CacheEvict(value = DeptCacheConstant.LIST,allEntries = true),
            @CacheEvict(value = DeptCacheConstant.TREE,allEntries = true)})
    public Boolean createDept(DeptDto deptDto) {
       
    }

    /**
     * @param deptDto 对象信息
     * @return Boolean
     * @Description 修改部门表
     */
    @Transactional
    @Caching(evict = {@CacheEvict(value = DeptCacheConstant.LIST,allEntries = true),
            @CacheEvict(value = DeptCacheConstant.TREE,allEntries = true)})
    @Override
    public Boolean updateDept(DeptDto deptDto) {
        
    }

    /**
     * @param deptDto 查询条件
     * @description 多条件查询部门表列表
     * @return: List<DeptVo>
     */
    @Cacheable(value = DeptCacheConstant.LIST,key ="#deptDto.hashCode()")
    @Override
    public List<DeptVo> findDeptList(DeptDto deptDto) {
       
    }


    @Override
    public List<DeptVo> findDeptInDeptNos(List<String> deptNos) {
        List<Dept> depts = deptMapper.findDeptInDeptNos(deptNos);
        return BeanConv.toBeanList(depts, DeptVo.class);
    }

    @Override
    public List<DeptVo> findDeptVoListInRoleId(List<Long> roleIdSet) {
        return deptMapper.findDeptVoListInRoleId(roleIdSet);
    }

    @Transactional
    @Caching(evict = {@CacheEvict(value = DeptCacheConstant.LIST,allEntries = true),
            @CacheEvict(value = DeptCacheConstant.TREE,allEntries = true)})
    @Override
    public int deleteDeptById(String deptId) {
       
    }

    /**
     * 启用-禁用部门
     *
     * @param deptDto
     * @return
     */
    @Caching(evict = {@CacheEvict(value = DeptCacheConstant.LIST,allEntries = true),
            @CacheEvict(value = DeptCacheConstant.TREE,allEntries = true)})
    @Override
    public Boolean isEnable(DeptDto deptDto) {

    }

    /**
     * 组织部门树形
     * @return: deptDto
     */
    @Override
    @Cacheable(value = DeptCacheConstant.TREE)
    public TreeVo deptTreeVo() {
        先到缓存中查询数据
            没有就到数据库中查询数据,同时缓存到redis
    }
}

6 作业

6.1 部门管理

需要完成的接口

  • 部门修改
  • 启用-禁用
  • 部门删除

6.2 岗位管理

需要完成的接口:

  • 岗位添加

  • 岗位修改

  • 岗位分页查询

    注意:在查询岗位的时候,需要展示岗位所在的部门名称,需关联部门表

  • 岗位删除

相关推荐
倔强的石头_4 小时前
kingbase备份与恢复实战(二)—— sys_dump库级逻辑备份与恢复(Windows详细步骤)
数据库
阿巴斯甜9 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker10 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952711 小时前
Andorid Google 登录接入文档
android
黄林晴12 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android