首页 > 技术文章 > 人事管理系统

antaia11 2021-08-02 22:55 原文

人力资源管理系统

一、登录细节整理

前端

Login.vue

<template>
  <div>
    <el-form :rules="rules" ref="loginForm" :model="loginForm" class="loginContainer">
      <h3 class="loginTitle">系统登录</h3>
      <el-form-item prop="username">
        <el-input type="text" v-model="loginForm.username" auto-complete="off" placeholder="请输入用户名"/>
      </el-form-item>
      <el-form-item prop="password">
        <el-input type="text" v-model="loginForm.password" auto-complete="off" placeholder="请输入密码" @keydown.enter.native="submitLogin"/>
      </el-form-item>
      <el-checkbox v-model="checked" class="loginRemember">记住我</el-checkbox>
      <el-button type="primary" style="width: 100%" @click="submitLogin" >登录</el-button>
    </el-form>
  </div>
</template>

<script>
export default {
  name: "Login",
  data(){
    return{
      checked:true,
      loginForm:{
        username:"admin",
        password:"123"
      },
      rules:{
        username:[ {required:true,message:"请输入用户名!",trigger:"blur"}],
        password:[ {required:true,message:"请输入密码!",trigger:"blur"}]
      }
    }
  },
  methods:{
    submitLogin(){
      //校验表单信息
      this.$refs["loginForm"].validate((valid) => {
        if (valid) {
          //做登录处理
          this.postKeyValueRequest("/doLogin",this.loginForm).then(resp=>{
            if (resp){
               //将当前用户信息保存在SESSION中
              window.sessionStorage.setItem("user",JSON.stringify(resp.obj));
              //进行页面跳转
              this.$router.replace("/home");
            }
          });
        } else {
          this.$message.error('请输入所有字段!');
          return false;
        }
      });
    }
  }
}
</script>

<style>
  .loginContainer {
    border-radius: 15px;
    background-clip: padding-box;
    margin: 180px auto;
    width: 350px;
    padding: 15px 35px 15px 35px;
    background: #fff;
    border: 1px solid #eaeaea;
    box-shadow: 0 0 25px #cac6c6;
  }

  .loginTitle{
    margin: 15px auto 20px auto;
    text-align: center;
    color: #505458;
  }

  .loginRemember{
    text-align: left;
    margin: 0px 0px 20px 0px;
  }

</style>
1.axios请求封装

api.js

import axios from "axios";
import {Message} from 'element-ui'

//服务端响应拦截器
axios.interceptors.response.use(result =>{
    //如果出现此种状况.说明是业务上的错误.[登录失败]
    if (result.status && result.status == 200 && result.data.status == 500){
        Message.error({message:result.data.msg});
        return;
    }
    if (result.data.msg){
        Message.success({message:result.data.msg});
    }
    return result.data;//返回响应数据给调用处
},error => {
    if (error.response.status == 504 || error.response.status == 404){
        Message.error({message:"服务器被我吃掉咯~!!!(๑´ㅂ`๑)"})
    }else if (error.response.status == 403){
        Message.error({message:"权限不足!"})
    }else if (error.response.status == 401){
        Message.error({message:"尚未登录,请先登录!"})
    }else{
        if (error.response.data.msg){
            Message.error({message:error.response.data.msg})
        }else{
            Message.error({message:"未知错误!"})
        }
    }
    return;
});

let base = '';
export const postKeyValueRequest = (url,params) => {
    return axios({
        method:"post",
        url:`${base}${url}`,
        data:params,
        transformRequest:[function (data){
            let ret = '';
            for(let i in data){
                ret += encodeURIComponent(i) + "=" + encodeURIComponent(data[i])+"&";
            }
            return ret;
        }],
        headers:{
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    })
}

export const postRequest = (url, params) => {
    return axios({
        method: 'post',
        url: `${base}${url}`,
        data: params,
    });
}

export const putRequest = (url, params) => {
    return axios({
        method: 'put',
        url: `${base}${url}`,
        data: params,
    });
}

export const deleteRequest = (url) => {
    return axios({
        method: 'delete',
        url: `${base}${url}`
    });
}
export const getRequest = (url) => {
    return axios({
        method: 'get',
        url: `${base}${url}`
    });
}
2.将请求封装为Vue插件

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

//导入封装好的请求方法,制作成插件,以后不需要导入,只需要通过this就可以直接调用了
import {deleteRequest, getRequest, postKeyValueRequest, postRequest, putRequest} from "./utils/api";
Vue.prototype.postRequest = postRequest;
Vue.prototype.postKeyValueRequest = postKeyValueRequest;
Vue.prototype.deleteRequest = deleteRequest;
Vue.prototype.putRequest = putRequest;
Vue.prototype.getRequest = getRequest;



Vue.config.productionTip = false

Vue.use(ElementUI);

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
3.router-view 渲染层级管理

router只渲染Home组件中的router-view,而不污染主页面的router-view,将要在Home中渲染的组件注册到Home的children属性中。

router.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "../views/Login";
import Home from "../views/Home";
import Test1 from "../views/Test1";
import Test2 from "../views/Test2";

Vue.use(VueRouter)

const routes = [
  {
    path: "/",
    name:"Login",
    component: Login,
    hidden:true
  },
  {
    path:"/home",
    name:"Home",
    component: Home,
    hidden:true
  },
  {
    path:"/home",
    name:"导航一",
    component: Home,
    children:[//test1,test2将会渲染Home组件里的router-view
      {
        path:"/test1",
        name:"选项1",
        component: Test1
      },
      {
        path:"/test2",
        name:"选项2",
        component: Test2
      }
    ]
  },

]

const router = new VueRouter({
  // mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router
4.遍历路由列表,动态渲染导航菜单

通过 this.$router.options.routes 获取到路由中的所有组件,从而进行遍历,避免手动添加产生的麻烦。

<!--遍历路由列表,动态渲染导航菜单-->
    <el-submenu index="1"
    :key="index"
v-for="(item,index) in this.$router.options.routes"
v-if="!item.hidden"
>
    <template slot="title">
        <i class="el-icon-location"></i>
<span>{{item.name}}</span>
</template>

<el-menu-item
:key="indexj"
:index="child.path"
v-for="(child,indexj) in item.children">
    {{child.name}}
</el-menu-item>
5.解决跨域问题

后端端口为:8080

前端端口为:8081

vue.config.js

let proxyObj = {};
proxyObj['/ws'] = {
    ws: true,
    target: "ws://localhost:8081"
};
proxyObj['/'] = {
    ws: false,
    target: 'http://localhost:8081',
    changeOrigin: true,
    pathRewrite: {
        '^/': ''
    }
}
module.exports = {
    devServer: {
        host: 'localhost',
        port: 8080,
        proxy: proxyObj
    }
}

后端

1.配置SecurityConfig

SecurityConfig.java

package com.zhuantai.voa.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhuantai.voa.model.Hr;
import com.zhuantai.voa.service.HrService;
import com.zhuantai.voa.utils.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;


/**
 * @author ANTIA1
 * @date 2021/7/12 13:10
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    HrService hrService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(hrService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()//所有的请求都要认证之后才可以访问
                .and()
                .formLogin()//表单登录
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/doLogin")//处理登录的请求路由
                .loginPage("/login")
                .successHandler(new AuthenticationSuccessHandler() {//登录成功处理
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = resp.getWriter();

                        Hr hr = (Hr) authentication.getPrincipal();//获取登录对象
                        RespBean respBean = RespBean.ok("登录成功", hr);
                        hr.setPassword(null);
                        String str = new ObjectMapper().writeValueAsString(respBean);//对象转成JSON字符串
                        writer.write(str);//写出去

                        writer.flush();
                        writer.close();
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {//登录失败处理
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = resp.getWriter();

                        RespBean respBean = RespBean.error("登录失败!");
                        if (e instanceof LockedException){
                            respBean.setMsg("账户被锁定,请联系管理员!");
                        }else if (e instanceof CredentialsExpiredException){
                            respBean.setMsg("密码过期,请联系管理员!");
                        }else if(e instanceof AccountExpiredException){
                            respBean.setMsg("账户过期,请联系管理员!");
                        }else if(e instanceof DisabledException){
                            respBean.setMsg("账户被禁用,请联系管理员!");
                        }else if(e instanceof BadCredentialsException){
                            respBean.setMsg("用户名或密码错误,请重新输入!");
                        }
                        String str = new ObjectMapper().writeValueAsString(respBean);//结果对象转成JSON字符串
                        writer.write(str);//写出去

                        writer.flush();
                        writer.close();
                    }
                })
                .permitAll()
                .and()
                .logout()
                .logoutSuccessHandler(new LogoutSuccessHandler() {//注销成功处理
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = resp.getWriter();

                        writer.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功")));

                        writer.flush();
                        writer.close();
                    }
                })
                .permitAll()
                .and()
                .csrf().disable();
    }
}

LoginController.java

package com.zhuantai.voa.controller;

import com.zhuantai.voa.utils.RespBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author ANTIA1
 * @date 2021/7/12 15:38
 */
@RestController
public class LoginController {
    @GetMapping("/login")
    public RespBean login(){
        return RespBean.error("尚未登录,请登录!");
    }
}

二、动态菜单细节整理

1.Menu表字段分析

image-20210712212541783

字段名 作用
id ID
url 权限控制
path 请求路径
component 前端组件名
name 中文名
iconCls 菜单图标
keepAlive 焦点移出后是否销毁
requireAuth 是否需要登录才能访问
parentId 父节点id
enable 是否启用
2.Menu接口设计

Menu.java

package com.zhuantai.voa.model;

import lombok.Data;
import java.util.List;

@Data
public class Menu  {
    private Integer id;

    private String url;

    private String path;

    private String component;

    private String name;

    private String iconCls;

    private Integer parentId;

    private Boolean enabled;

    private Meta meta; //额外属性  keepAlive & requireAuth 单独抽离出来封装成一个类

    private List<Menu> children; //子菜单
}

package com.zhuantai.voa.model;

import lombok.Data;

/**
 * @author ANTIA1
 * @date 2021/7/12 21:35
 */
@Data
public class Meta {
    private Boolean keepAlive;
    private Boolean requireAuth;
}

这样处理Menu对象是为了规范化前端路由配置


const routes = [
  {
    path:"/menu",
    name:"菜单",
    component: Menu,
    meta:{
        keepAlive:null,
        requireAuth,true
    }  
    children:[
      {
        path:"/xxx",
        name:"xxx",
        component: xxx
      	meta:{
           keepAlive:null,
           requireAuth,true
        }
      },
	  ...
      ...
      ...
    ]
  },

]

SystemConfigController.java & MenuService.java & MenuMapper.java

注意:这里在Service中通过Security上下文对象来获取当前登录用户的id,而不是通过前端传过来的id来进行数据查询,是为了安全考虑,

前端的数据不可以信任。

/**
 * 系统配置
 * @author ANTIA1
 * @date 2021/7/12 21:39
 */
@RestController
@RequestMapping("/system/config")
public class SystemConfigController {

    @Autowired
    MenuService menuService;

    @GetMapping("/menu")
    public List<Menu> getMenusByHrId(){
        return menuService.getMenusByHrId();
    }
}

/**
 * @author ANTIA1
 * @date 2021/7/12 21:42
 */
@Service
public class MenuService {
    @Autowired
    MenuMapper menuMapper;

    public List<Menu> getMenusByHrId(){
        Hr hr = ((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal());//获得当前登录用户的id
        return menuMapper.getMenusByHrId(hr.getId());
    }
}

MenuMapper.xml

<resultMap id="BaseResultMap" type="com.zhuantai.voa.model.Menu" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="url" property="url" jdbcType="VARCHAR" />
    <result column="path" property="path" jdbcType="VARCHAR" />
    <result column="component" property="component" jdbcType="VARCHAR" />
    <result column="name" property="name" jdbcType="VARCHAR" />
    <result column="iconCls" property="iconCls" jdbcType="VARCHAR" />
    <result column="parentId" property="parentId" jdbcType="INTEGER" />
    <result column="enabled" property="enabled" jdbcType="BIT" />
    <association property="meta" javaType="com.zhuantai.voa.model.Meta">
      <result column="keepAlive" property="keepAlive" jdbcType="BIT" />
      <result column="requireAuth" property="requireAuth" jdbcType="BIT" />
    </association>
  </resultMap>

  <resultMap id="Menus2" type="com.zhuantai.voa.model.Menu" extends="BaseResultMap">
    <collection property="children" ofType="com.zhuantai.voa.model.Menu">
      <id column="id2" property="id" jdbcType="INTEGER" />
      <result column="url2" property="url" jdbcType="VARCHAR" />
      <result column="path2" property="path" jdbcType="VARCHAR" />
      <result column="component2" property="component" jdbcType="VARCHAR" />
      <result column="name2" property="name" jdbcType="VARCHAR" />
      <result column="iconCls2" property="iconCls" jdbcType="VARCHAR" />
      <result column="parentId2" property="parentId" jdbcType="INTEGER" />
      <result column="enabled2" property="enabled" jdbcType="BIT" />
      <association property="meta" javaType="com.zhuantai.voa.model.Meta">
        <result column="keepAlive2" property="keepAlive" jdbcType="BIT" />
        <result column="requireAuth2" property="requireAuth" jdbcType="BIT" />
      </association>
    </collection>
  </resultMap>

//查询出与当前用户所具备的role相符合的menu
<select id="getMenusByHrId" resultMap="Menus2">
    SELECT
      DISTINCT m1.*,
               m2.`id` AS id2,
               m2.`component` AS component2,
               m2.`enabled` AS enabled2,
               m2.`iconCls` AS iconCls2,
               m2.`keepAlive` AS keepAlive2,
               m2.`name` AS name2,
               m2.`parentId` AS parentId2,
               m2.`requireAuth` AS requireAuth2,
               m2.`path` AS path2
    FROM
              menu m1,
              menu m2,
              hr_role hrr,
              menu_role mr
    WHERE
              m1.`id`=m2.`parentId` AND
              hrr.`hrid`=#{hrid} AND
              hrr.`rid`=mr.`rid` AND
              mr.`mid`=m2.`id` AND
              m2.`enabled`=TRUE
    ORDER BY  m1.`id`,m2.`id`
  </select>
3.Vuex

菜单项加载成功之后,在前端有几个可以存放的地方:

  1. sessionStorage //不安全
  2. localStorage //不安全
  3. Vuex

准备工作:

将菜单数据统一方在vuex中,将其作为中央仓库存储菜单数据。

store.js

import Vue from "vue";
import Vuex from "vuex"

Vue.use(Vuex)

export default new Vuex.Store({
    //定义属性
    state:{
        routes:[//路由数组,从服务端加载过来的菜单项都存放在这里

        ]
    },
    mutations:{//修改属性
        initRoutes(state,data){//可以不用传state,只传data,如果要传多个参数,需要封装一个JSON对象传进来
            state.routes = data;
        }
    },
    actions:{//提交mutation来修改属性

    }
})

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.config.productionTip = false

Vue.use(ElementUI);

//导入封装好的请求方法,制作成插件,以后不需要导入,只需要通过this就可以直接调用了
import {deleteRequest, getRequest, postKeyValueRequest, postRequest, putRequest} from "./utils/api";
Vue.prototype.postRequest = postRequest;
Vue.prototype.postKeyValueRequest = postKeyValueRequest;
Vue.prototype.deleteRequest = deleteRequest;
Vue.prototype.putRequest = putRequest;
Vue.prototype.getRequest = getRequest;

//引入Vuex使其生效<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
import store from "./store"

new Vue({
  router,
  store,//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  render: h => h(App)
}).$mount('#app')
4.Menu请求工具类封装

由于后端数据返回来的Menu数据中component属性的值为字符串,而在前端项目中使用的组件不是字符串而是对象。

image-20210712231858708

image-20210712231927899

所以需要将其字符串转换为一个引用对象。

menus.js

import {getRequest} from "./api";

export const initMenu = (router,store)=>{
    if (store.state.routes.length > 0){ //说明有菜单数据,不需要再次加载
        return;
    }
    //请求menu数据
    getRequest("/system/config/menu").then(data=>{
        if (data){
            let ftmRoutes = formatRoutes(data);
            router.addRoutes(ftmRoutes);
            store.commit("initRoutes",ftmRoutes);//更新store中的routes属性的数据
        }
    });
}

//格式化组件方法
export const formatRoutes = (routes) =>{
    let fmRoutes = [];//要返回的格式化好的数据
    routes.forEach(router=>{
        let {path, component, name, meta, iconCls, children} = router;
        if (children && children instanceof Array){ //说明是1级菜单的children
            children = formatRoutes(children);//递归调用一下
        }
        let fmRouter = {
            path:path,
            name:name,
            iconCls:iconCls,
            meta:meta,
            children:children,
            component(resolve){//动态导入组件
                if (component.startsWith("Home")){
                    require(['../views/'+component + '.vue'],resolve);//相当于导入组件
                }else if (component.startsWith("Emp")){
                    require(['../views/emp//'+component + '.vue'],resolve);//相当于导入组件
                }else if (component.startsWith("Per")){
                    require(['../views/per/'+component + '.vue'],resolve);//相当于导入组件
                }else if (component.startsWith("Sal")){
                    require(['../views/sal/'+component + '.vue'],resolve);//相当于导入组件
                }else if (component.startsWith("Sta")){
                    require(['../views/sta/'+component + '.vue'],resolve);//相当于导入组件
                }else if (component.startsWith("Sys")){
                    require(['../views/sys/'+component + '.vue'],resolve);//相当于导入组件
                }
            }
        }
        fmRoutes.push(fmRouter);
    });
    return fmRoutes;
}
5.路由导航守卫

为了避免用户在页面按F5或浏览器上的刷新按钮刷新页面后,导致菜单数据没有加载。使用全局前置守卫。类似于后端的过滤器,对我从哪来,我要去哪里中添加条件逻辑。

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.config.productionTip = false

Vue.use(ElementUI);//引用element-ui

//导入封装好的请求方法,制作成插件,以后不需要导入,只需要通过this就可以直接调用了
import {deleteRequest, getRequest, postKeyValueRequest, postRequest, putRequest} from "./utils/api";
Vue.prototype.postRequest = postRequest;
Vue.prototype.postKeyValueRequest = postKeyValueRequest;
Vue.prototype.deleteRequest = deleteRequest;
Vue.prototype.putRequest = putRequest;
Vue.prototype.getRequest = getRequest;


import {initMenu} from "./utils/menus";
/**
 * 在页面每次跳转之前
 * @to 去哪里
 * @from 从哪来
 */
router.beforeEach((to, from, next) => {
    if (to.path == "/"){
        next();
    }else{
      initMenu(router,store);//加载菜单数据
      next();
    }
});

//引入Vuex使其生效
import store from "./store"

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
6.动态加载左边导航菜单

Home.vue

<template>
  <div>
    <el-container>

      <el-header class="homeHeader">
        <!--头部内容-->
        <div class="title">人力资源管理系统</div>
        <el-dropdown class="userInfo" @command="commandHandler">
          <span class="el-dropdown-link">
           {{user.name}}<i><img :src="user.userface"></i>
          </span>

          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item command="userinfo">个人中心</el-dropdown-item>
            <el-dropdown-item command="setting">设置</el-dropdown-item>
            <el-dropdown-item command="logout" divided>注销登录</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
      </el-header>

      <!--左侧导航栏内容-->
      <el-container>
        <el-aside width="200px">
          <el-col>
            <el-menu router unique-opened>
              <!--遍历路由列表,动态渲染导航菜单-->
              >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>  
              <el-submenu :index="index+''"
                          :key="index"
                          v-for="(item,index) in routes"
                          v-if="!item.hidden"
              >
              >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>  
                <template slot="title">
                  <i :class="item.iconCls" style="color: #409eff;margin-right: 6px"></i>
                  <span>{{item.name}}</span>
                </template>

                <el-menu-item
                    :key="indexj"
                    :index="child.path"
                    v-for="(child,indexj) in item.children">
                  {{child.name}}
                </el-menu-item>

              </el-submenu>
            </el-menu>
          </el-col>
        </el-aside>

        <el-main>
        >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>      
          <el-breadcrumb separator-class="el-icon-arrow-right" v-if="this.$router.currentRoute.path != '/home'">
            <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
            <el-breadcrumb-item>{{this.$router.currentRoute.name}}</el-breadcrumb-item>
          </el-breadcrumb>
          <div v-if="this.$router.currentRoute.path == '/home'">欢迎</div>
          <router-view></router-view>
        </el-main>
		>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>  
      </el-container>

    </el-container>
  </div>
</template>

<script>
export default {
  name: "Home",
  data(){
    return {
      user:JSON.parse(window.sessionStorage.getItem("user"))
    }
  },
  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>  
  computed:{
      routes(){
        return this.$store.state.routes;
      }
  },
  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>      
  methods:{
    commandHandler(cmd){
      if(cmd == 'logout'){
        this.$confirm('此操作将注销登录, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.getRequest("/logout");
          window.sessionStorage.removeItem("user");//注销后清除用户信息
          >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>  
          this.$store.commit('initRoutes',[]);//注销后清楚菜单信息
          >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>  
          this.$router.replace("/")
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消操作'
          });
        });
      }
    },
  }
}
</script>

<style>
  .homeHeader{
    background-color: #409eff;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0px 15px;
    box-sizing: border-box;
  }

  .homeHeader .title{
    font-size: 30px;
    font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
    color: #eaeaea;
  }

  .homeHeader .userInfo{
    cursor: pointer;
  }

  .el-dropdown-link img{
    width: 48px;
    height: 48px;
    border-radius: 24px;
    margin-left: 5px;
  }
  .el-dropdown-link{
    display: flex;
    align-items: center;
    color: white;
    font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;;
  }
</style>

三、前后端分离权限管理

1.思路

​ 在传统的前后端不分的开发中,权限管理主要通过过滤器或者拦截器来进行(权限管理框架本身也是通过过滤器来实现功能),如果用户不具备某一个角色或者某一个权限,则无法访问某一个页面。

但是在前后端分离中,页面的跳转统统交给前端去做,后端只提供数据,这种时候,权限管理不能再按照之前的思路来。

首先要明确一点,前端是展示给用户看的,所有的菜单显示或者隐藏目的不是为了实现权限管理,而是为了给用户一个良好的体验,不能依靠前端隐藏控件来实现权限管理,即数据安全不能依靠前端。

这点就像普通的表单提交一样,前端做数据校验是为了提高效率,提高用户体验,后端才是真正的确保数据完整性。

所以,真正的数据安全管理是在后端实现的,后端在接口设计的过程中,就要确保每一个接口都是在满足某种权限的基础上才能访问,也就是说,不怕将后端数据接口地址暴露出来,即使暴露出来,只要你没有相应的角色,也是访问不了的。

前端为了良好的用户体验,才需要将用户不能访问的接口或者菜单隐藏起来。

有人说,如果用户直接在地址拦输入某一个页面的路径,怎么办?此时,如果没有做任何额外的处理的话,用户确实可以通过直接输入某一个路径进入到系统中的某一个页面中,但是,不用担心数据泄露问题,因为没有相关的角色,就无法访问相关的接口。

但是,如果用户非这样操作,进入到一个空白的页面,用户体验不好,此时,我们可以使用 Vue 中的前置路由导航守卫,来监听页面跳转,如果用户想要去一个未获授权的页面,则直接在前置路由导航守卫中将之拦截下来,重定向到登录页,或者直接就停留在当前页,不让用户跳转,也可以顺手再给用户一点点未获授权的提示信息。

总而言之一句话,前端的所有操作,都是为了提高用户体验,不是为了数据安全,真正的权限校验要在后端来做,后端如果是 SSM 架构,建议使用 Shiro ,如果是 Spring Boot + 微服务,建议使用 Spring Security 。

2.后端接口权限设计

image-20210713115230209

步骤:

(1)发起一个HTTP请求,拿到请求地址后,和数据库url字段中的数据进行匹配

(2)匹配到后,拿着id去menu_role中查看这个菜单需要哪些角色能访问

(3)看当前登录的用户是否具备所需要的角色

不给一级菜单设置权限,假如当前用户具备查看基本资料或高级资料或基本资料和高级资料都可以查看,那么就可以显示员工资料,如果基本资料和高级资料都没有权限,那么后端接口就不用返回员工资料这个菜单了。角色不分配给一级菜单,只分配给二级菜单。一级菜单的作用是分类二级菜单,本身没有实际的接口。

CustomFilterInvocationSecurityMetadataSource.java

拿到请求地址,去数据库匹配,找到菜单需要哪些角色

package com.zhuantai.voa.config;

import com.zhuantai.voa.model.Menu;
import com.zhuantai.voa.model.Role;
import com.zhuantai.voa.service.MenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;
import java.util.List;

/**
 * 提供权限访问,根据传来的请求地址,分析出请求所需要的角色
 * @author ANTIA1
 * @date 2021/7/13 12:10
 */
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    MenuService menuService;

    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //获取请求地址
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        //获取所有的菜单数据
        List<Menu> menus = menuService.getAllMenusWithRole();
        //比较请求地址和menus中的url是否一致
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getUrl(), requestUrl)) {//如果匹配上的话
                List<Role> roles = menu.getRoles();//当前请求菜单所需要的的角色
                String[] str = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    str[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(str);
            }
        }
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

CustomUrlDecisionManager.java

看当前用户所具备的权限,是否满足所需要的权限

package com.zhuantai.voa.config;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * 当前用户具备哪些角色
 * @author ANTIA1
 * @date 2021/7/13 12:38
 */
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {

    /**
     * 只要不抛异常,请求继续往下执行
     * @param authentication 当前用户的信息
     * @param o
     * @param collection MyFilter.java 中的返回值,返回的是所需要的角色
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : collection) {
            String needRole = configAttribute.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {//说明登录之后就能访问
                if (authentication instanceof AnonymousAuthenticationToken) {//如果authentication是一个匿名用户的实例,说明没登录
                    throw new AccessDeniedException("尚未登录,请登录!");
                }else {
                    return;
                }
            }
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();//获取当前用户的角色
            for (GrantedAuthority authority : authorities) {
                //比较当前用户的角色和所需要的的角色
                if (authority.getAuthority().equals(needRole)) {//说明具备角色
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足,请联系管理员!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

SecurityConfig.java

将以上两项配置到Security中来

package com.zhuantai.voa.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhuantai.voa.model.Hr;
import com.zhuantai.voa.service.HrService;
import com.zhuantai.voa.utils.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;


/**
 * @author ANTIA1
 * @date 2021/7/12 13:10
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    HrService hrService;

    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    @Autowired
    CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;

    @Autowired
    CustomUrlDecisionManager customUrlDecisionManager;
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(hrService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
//                .anyRequest().authenticated()//所有的请求都要认证之后才可以访问
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(customUrlDecisionManager);
                        o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        return o;
                    }
                })
            >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                .and()
                .formLogin()//表单登录
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/doLogin")//处理登录的请求路由
                .loginPage("/login")
                .successHandler(new AuthenticationSuccessHandler() {//登录成功处理
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = resp.getWriter();

                        Hr hr = (Hr) authentication.getPrincipal();//获取登录对象
                        RespBean respBean = RespBean.ok("登录成功", hr);
                        hr.setPassword(null);
                        String str = new ObjectMapper().writeValueAsString(respBean);//对象转成JSON字符串
                        writer.write(str);//写出去

                        writer.flush();
                        writer.close();
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {//登录失败处理
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = resp.getWriter();

                        RespBean respBean = RespBean.error("登录失败!");
                        if (e instanceof LockedException){
                            respBean.setMsg("账户被锁定,请联系管理员!");
                        }else if (e instanceof CredentialsExpiredException){
                            respBean.setMsg("密码过期,请联系管理员!");
                        }else if(e instanceof AccountExpiredException){
                            respBean.setMsg("账户过期,请联系管理员!");
                        }else if(e instanceof DisabledException){
                            respBean.setMsg("账户被禁用,请联系管理员!");
                        }else if(e instanceof BadCredentialsException){
                            respBean.setMsg("用户名或密码错误,请重新输入!");
                        }
                        String str = new ObjectMapper().writeValueAsString(respBean);//结果对象转成JSON字符串
                        writer.write(str);//写出去

                        writer.flush();
                        writer.close();
                    }
                })
                .permitAll()
                .and()
                .logout()
                .logoutSuccessHandler(new LogoutSuccessHandler() {//注销成功处理
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = resp.getWriter();

                        writer.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功")));

                        writer.flush();
                        writer.close();
                    }
                })
                .permitAll()
                .and()
                .csrf().disable();
    }
}

Hr.java

package com.zhuantai.voa.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Hr implements UserDetails {
    private Integer id;

    private String name;

    private String phone;

    private String telephone;

    private String address;

    private Boolean enabled;

    private String username;

    private String password;

    private String userface;

    private String remark;

    private List<Role> roles;


    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

}

HrService.java

package com.zhuantai.voa.service;

import com.zhuantai.voa.mapper.MenuMapper;
import com.zhuantai.voa.model.Hr;
import com.zhuantai.voa.model.Menu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/12 21:42
 */
@Service
public class MenuService {
    @Autowired
    MenuMapper menuMapper;

    public List<Menu> getMenusByHrId(){
        Hr hr = ((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal());//获得当前登录用户的id
        return menuMapper.getMenusByHrId(hr.getId());
    }

//    @Cacheable
    public List<Menu> getAllMenusWithRole(){
        return menuMapper.getAllMenusWithRole();
    }
}

HrMapper.xml

 <select id="getHrRolesById" resultType="com.zhuantai.voa.model.Role">
    SELECT r.* FROM role r,hr_role hrr WHERE hrr.rid = r.`id` AND hrr.hrid = #{id}
</select>
3.权限管理细节完善

问题:

如果我们直接在浏览器地址栏想要访问/sys/basic 会被重定向到登录页,那么我能不能在被重定向到登录也后完成登录时,再给我跳转到我想要访问的页面/sys/basic/而不是首页呢?

解决方案:

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import 'font-awesome/css/font-awesome.min.css';//导入图标库

Vue.config.productionTip = false

Vue.use(ElementUI);//引用element-ui

//导入封装好的请求方法,制作成插件,以后不需要导入,只需要通过this就可以直接调用了
import {deleteRequest, getRequest, postKeyValueRequest, postRequest, putRequest} from "./utils/api";
Vue.prototype.postRequest = postRequest;
Vue.prototype.postKeyValueRequest = postKeyValueRequest;
Vue.prototype.deleteRequest = deleteRequest;
Vue.prototype.putRequest = putRequest;
Vue.prototype.getRequest = getRequest;


import {initMenu} from "./utils/menus";
/**
 * 在页面跳转之前
 * @to 去哪里
 * @from 从哪来
 */
router.beforeEach((to, from, next) => {
    if (to.path == "/"){
        next();
    }else{
      if (window.sessionStorage.getItem("user")){
          initMenu(router,store);//加载菜单数据
          next();
      }else{
          >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
          next("/?redirect="+to.path);//在地址栏中记录一下要去的页面
          >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
      }
    }
});

//引入Vuex使其生效
import store from "./store"

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

Login.vue

<template>
  <div>
    <el-form :rules="rules" ref="loginForm" :model="loginForm" class="loginContainer">
      <h3 class="loginTitle">系统登录</h3>
      <el-form-item prop="username">
        <el-input type="text" v-model="loginForm.username" auto-complete="off" placeholder="请输入用户名"/>
      </el-form-item>
      <el-form-item prop="password">
        <el-input type="text" v-model="loginForm.password" auto-complete="off" placeholder="请输入密码" @keydown.enter.native="submitLogin"/>
      </el-form-item>
      <el-checkbox v-model="checked" class="loginRemember">记住我</el-checkbox>
      <el-button type="primary" style="width: 100%" @click="submitLogin" >登录</el-button>
    </el-form>
  </div>
</template>

<script>
export default {
  name: "Login",
  data(){
    return{
      checked:true,
      loginForm:{
        username:"admin",
        password:"123"
      },
      rules:{
        username:[ {required:true,message:"请输入用户名!",trigger:"blur"}],
        password:[ {required:true,message:"请输入密码!",trigger:"blur"}]
      }
    }
  },
  methods:{
    submitLogin(){
      //校验表单信息
      this.$refs["loginForm"].validate((valid) => {
        if (valid) {
          //做登录处理
          this.postKeyValueRequest("/doLogin",this.loginForm).then(resp=>{
            if (resp){
               //将当前用户信息保存在SESSION中
              window.sessionStorage.setItem("user",JSON.stringify(resp.obj));
              >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
              //进行页面跳转
              let path = this.$route.query.redirect;//获取到地址栏中设置的页面路径
              this.$router.replace((path=="/"||path==undefined)?"/home":path);//跳转
              >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
            }
          });
        } else {
          this.$message.error('请输入所有字段!');
          return false;
        }
      });
    }
  }
}
</script>

<style>
  .loginContainer {
    border-radius: 15px;
    background-clip: padding-box;
    margin: 180px auto;
    width: 350px;
    padding: 15px 35px 15px 35px;
    background: #fff;
    border: 1px solid #eaeaea;
    box-shadow: 0 0 25px #cac6c6;
  }

  .loginTitle{
    margin: 15px auto 20px auto;
    text-align: center;
    color: #505458;
  }

  .loginRemember{
    text-align: left;
    margin: 0px 0px 20px 0px;
  }
</style>

四、[业务] 基础信息设置

1.职位管理

PosMana.vue

<template>
  <div>
    <div>
      <el-input
          size="small"
          class="addPosInput"
          placeholder="添加职位..."
          prefix-icon="el-icon-plus"
          @keydown.enter.native="addPosition"
          v-model="pos.name">
      </el-input>
      <el-button type="primary" size="small" icon="el-icon-plus" @click="addPosition">添加</el-button>
    </div>
    <div class="posManaMain">
      <el-table
          :data="positions"
          border
          stripe
          size="small"
          ref="multipleTable"
          @selection-change="handleSelectionChange"
          style="width: 70%">
        <el-table-column
            type="selection"
            width="55">
        </el-table-column>
        <el-table-column
            prop="id"
            label="编号"
            width="55">
        </el-table-column>
        <el-table-column
            prop="name"
            label="职位名称"
            width="120">
        </el-table-column>
        <el-table-column
            prop="createDate"
            label="创建时间">
        </el-table-column>
        <el-table-column label="操作">
          <template slot-scope="scope">
            <el-button
                size="mini"
                @click="showEditView(scope.$index, scope.row)">编辑</el-button>
            <el-button
                size="mini"
                type="danger"
                @click="handleDelete(scope.$index, scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-button type="danger" size="small" style="margin-top: 8px" :disabled="multipleSelection.length == 0" @click="deleteMany">批量删除</el-button>
    </div>
    <el-dialog
        title="修改职位"
        :visible.sync="dialogVisible"
        width="30%" >
      <div>
        <el-tag>职位名称</el-tag>
        <el-input class="updatePosInput" size="small" v-model="updatePos.name"/>
      </div>
      <span slot="footer" class="dialog-footer">
    <el-button size="small" @click="dialogVisible = false">取 消</el-button>
    <el-button size="small" type="primary" @click="doUpdate">保 存</el-button>
  </span>
    </el-dialog>
  </div>
</template>

<script>
export default {
  name: "PosMana",
  data(){
    return{
      dialogVisible:false,
      pos:{
        name:''
      },
      updatePos:{
        name:''
      },
      positions:[],
      multipleSelection: []
    }
  },
  mounted() {
    this.initPositions();
  },
  methods:{
    initPositions(){
      this.getRequest("/system/basic/pos/").then((resp)=>{
        if (resp){
          this.positions = resp;
        }
      })
    },
    deleteMany(){
      this.$confirm('此操作将永久删除这【'+this.multipleSelection.length+'】条记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        let ids = '?';
        this.multipleSelection.forEach(item=>{
          ids += 'ids='+ item.id + '&';
        });
        this.deleteRequest("/system/basic/pos/"+ids).then(resp=>{
          if (resp){
            this.initPositions();
          }
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    },
    handleSelectionChange(val) {
      this.multipleSelection = val;
      console.log(this.multipleSelection)
    },
    showEditView(index,row){
      this.dialogVisible = true;
      Object.assign(this.updatePos,row);
    },
    doUpdate(){
      this.putRequest("/system/basic/pos/",this.updatePos).then(resp=>{
        if (resp){
          this.initPositions();
          this.updatePos.name = '';
          this.dialogVisible = false;
        }
      });
    },
    handleDelete(index,row){
      this.$confirm('此操作将永久删除【'+row.name+'】职位, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.deleteRequest("/system/basic/pos/"+row.id).then(resp=>{
          if (resp){
            this.initPositions();
          }
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    },
    addPosition(){
      if (this.pos.name){
        this.postRequest("/system/basic/pos/",this.pos).then(resp=>{
          if (resp){
            this.initPositions();//刷新表格
            this.pos.name = '';
          }
        })
      }else{
        this.$message.error("职位名称不可以为空!");
      }
    }
  }

}
</script>

<style>
  .addPosInput{
    width: 300px;
    margin-right: 6px;
  }

  .posManaMain{
    margin-top: 10px;
  }

  .updatePosInput{
    width: 200px;
    margin-left: 8px;
  }
</style>

PosController.java

package com.zhuantai.voa.controller.system.basic;

import com.zhuantai.voa.model.Position;
import com.zhuantai.voa.service.PositionService;
import com.zhuantai.voa.utils.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;
import java.util.List;

/**
 * 职位管理
 * @author ANTIA1
 * @date 2021/7/13 18:22
 */
@RestController
@RequestMapping("/system/basic/pos")
public class PositionController {

    @Autowired
    PositionService positionService;

    @GetMapping("/")
    public List<Position> getAllPositions(){
        return positionService.getAllPositions();
    }

    @PostMapping("/")
    public RespBean addPosition(@RequestBody Position position){
        if (positionService.addPosition(position) == 1){
            return RespBean.ok("添加成功!");
        }
        return RespBean.error("添加失败");
    }

    @PutMapping("/")
    public RespBean updatePositions(@RequestBody Position position){
        if (positionService.updatePositions(position) == 1){
            return RespBean.ok("更新成功!");
        }
        return RespBean.error("更新失败");
    }

    @DeleteMapping("/{id}")
    public RespBean deletePosition(@PathVariable Integer id){
        if (positionService.deletePosition(id) == 1){
            return RespBean.ok("删除成功!");
        }
        return RespBean.error("删除失败!");
    }

    @DeleteMapping("/")
    public RespBean deletePositionsByIds(Integer [] ids){
        if (positionService.deletePositionsByIds(ids) == ids.length){
            return RespBean.ok("删除成功!");
        }
        return RespBean.error("删除失败!");
    }
}

PosService.java

package com.zhuantai.voa.service;

import com.zhuantai.voa.mapper.PositionMapper;
import com.zhuantai.voa.model.Position;
import org.apache.ibatis.annotations.Param;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/13 18:24
 */
@Service
public class PositionService {

    @Autowired
    PositionMapper positionMapper;

    public List<Position> getAllPositions(){
        return positionMapper.getAllPositions();
    }

    public int addPosition(Position position) {
        position.setCreateDate(new Date());
        position.setEnabled(true);
        return positionMapper.insertSelective(position);
    }

    public int updatePositions(Position position) {
        return positionMapper.updateByPrimaryKeySelective(position);
    }


    public int deletePosition(Integer id) {
        return positionMapper.deleteByPrimaryKey(id);
    }

    public int deletePositionsByIds(@Param("ids") Integer[] ids) {
        return positionMapper.deletePositionsByIds(ids);
    }
}

PosMapper.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.zhuantai.voa.mapper.PositionMapper" >
  <resultMap id="BaseResultMap" type="com.zhuantai.voa.model.Position" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="name" property="name" jdbcType="VARCHAR" />
    <result column="createDate" property="createDate" jdbcType="TIMESTAMP" />
    <result column="enabled" property="enabled" jdbcType="BIT" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, name, createDate, enabled
  </sql>
  
  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  <select id="getAllPositions" resultMap="BaseResultMap">
    select * from position
  </select>

  <delete id="deletePositionsByIds">
    delete from position where id in
    <foreach collection="ids" item="id" separator="," open="(" close=")">
      #{id}
    </foreach>
  </delete>
  >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

  
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from position
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from position
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.zhuantai.voa.model.Position" >
    insert into position (id, name, createDate, 
      enabled)
    values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{createDate,jdbcType=TIMESTAMP}, 
      #{enabled,jdbcType=BIT})
  </insert>
  <insert id="insertSelective" parameterType="com.zhuantai.voa.model.Position" >
    insert into position
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="name != null" >
        name,
      </if>
      <if test="createDate != null" >
        createDate,
      </if>
      <if test="enabled != null" >
        enabled,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="name != null" >
        #{name,jdbcType=VARCHAR},
      </if>
      <if test="createDate != null" >
        #{createDate,jdbcType=TIMESTAMP},
      </if>
      <if test="enabled != null" >
        #{enabled,jdbcType=BIT},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.zhuantai.voa.model.Position" >
    update position
    <set >
      <if test="name != null" >
        name = #{name,jdbcType=VARCHAR},
      </if>
      <if test="createDate != null" >
        createDate = #{createDate,jdbcType=TIMESTAMP},
      </if>
      <if test="enabled != null" >
        enabled = #{enabled,jdbcType=BIT},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.zhuantai.voa.model.Position" >
    update position
    set name = #{name,jdbcType=VARCHAR},
      createDate = #{createDate,jdbcType=TIMESTAMP},
      enabled = #{enabled,jdbcType=BIT}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>
2.全局异常处理

在做职位删除的时候发现有些职位在数据库有外键关联,所以在删除的时候回出现异常,这里做全局统一异常处理,让错误信息显示的更加友好。

GlobalExceptionHandler.java

package com.zhuantai.voa.exception;

import com.zhuantai.voa.utils.RespBean;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理
 * @author ANTIA1
 * @date 2021/7/13 19:28
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(SQLException.class)
    public RespBean sqlException(SQLException e){
        if (e instanceof SQLIntegrityConstraintViolationException){
            return RespBean.error("该数据有关联数据,操作失败!");
        }
        return RespBean.error("数据库异常,操作失败!");
    }
}
3.职称管理

JobLevelMana.vue

<template>
  <div>
    <div>
      <el-input
          size="small"
          class="addJobLevelInput"
          placeholder="添加职称..."
          prefix-icon="el-icon-plus"
          @keydown.enter.native="addJobLevel"
          v-model="jobLevel.name">
      </el-input>
      <el-select v-model="jobLevel.titleLevel" placeholder="职称等级" size="small" style="margin-right: 6px">
        <el-option
            v-for="item in options"
            :key="item.value"
            :label="item.label"
            :value="item.value">
        </el-option>
      </el-select>
      <el-button type="primary" size="small" icon="el-icon-plus" @click="addJobLevel">添加</el-button>
    </div>
    <div class="JobLevelManaMain">
      <el-table
          :data="jobLevels"
          border
          stripe
          size="small"
          ref="multipleTable"
          @selection-change="handleSelectionChange"
          style="width: 70%">
        <el-table-column
            type="selection"
            width="55">
        </el-table-column>
        <el-table-column
            prop="id"
            label="编号"
            width="55">
        </el-table-column>
        <el-table-column
            prop="name"
            label="职称名称"
            width="120">
        </el-table-column>
        <el-table-column
            prop="titleLevel"
            label="职称级别"
            width="120">
        </el-table-column>
        <el-table-column
            prop="createDate"
            label="创建时间">
        </el-table-column>
        <el-table-column
            label="是否启用">
          <template slot-scope="scope">
            <el-tag v-if="scope.row.enabled" type="success">已启用</el-tag>
            <el-tag v-else type="danger">未启用</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作">
          <template slot-scope="scope">
            <el-button
                size="mini"
                @click="showEditView(scope.$index, scope.row)">编辑</el-button>
            <el-button
                size="mini"
                type="danger"
                @click="handleDelete(scope.$index, scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-button type="danger" size="small" style="margin-top: 8px" :disabled="multipleSelection.length == 0" @click="deleteMany">批量删除</el-button>
    </div>
    <el-dialog
        title="修改职称"
        :visible.sync="dialogVisible"
        width="30%" >
      <div>
        <table>
          <tr>
            <td><el-tag>职称名称</el-tag></td>
            <td><el-input class="updateJobLevelInput" size="small" v-model="updateJobLevel.name"/></td>
          </tr>
          <tr>
            <td><el-tag>职称级别</el-tag></td>
            <td>
              <el-select class="updateJobLevelInput" v-model="updateJobLevel.titleLevel" placeholder="请选择" size="small">
                <el-option
                    v-for="item in options"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value">
                </el-option>
              </el-select>
            </td>
          </tr>
          <tr>
            <td>
              <el-tag>是否启用</el-tag>
            </td>
            <td>
              <el-switch
                  v-model="updateJobLevel.enabled"
                  active-text="启用"
                  inactive-text="禁用"
                  active-color="#13ce66"
                  inactive-color="#ff4949"
              >
              </el-switch>
            </td>
          </tr>
        </table>
      </div>
      <span slot="footer" class="dialog-footer">
    <el-button size="small" @click="dialogVisible = false">取 消</el-button>
    <el-button size="small" type="primary" @click="doUpdate">保 存</el-button>
  </span>
    </el-dialog>
  </div>
</template>

<script>
export default {
  name: "JobLevelMana",
  data(){
    return {
      jobLevel:{
        name:'',
        titleLevel:''
      },
      jobLevels:[],
      updateJobLevel:{
        name:'',
        titleLevel:'',
        enabled:null,
      },
      dialogVisible:false,
      multipleSelection:[],
      options: [{
        value: '正高级',
        label: '正高级'
      }, {
        value: '副高级',
        label: '副高级'
      },{
          value: '初级',
          label: '初级'
      },{
        value: '中级',
        label: '中级'
      }, {
        value: '员级',
        label: '员级'
      }],
    }
  },
  mounted() {
    this.initJobLevels();
  },
  methods:{
    initJobLevels(){
      this.getRequest("/system/basic/job/").then(resp=>{
        if (resp){
          this.jobLevels = resp;
        }
      })
    },
    addJobLevel(){
      if (this.jobLevel.name && this.jobLevel.titleLevel){
        this.postRequest("/system/basic/job/",this.jobLevel).then(resp=>{
          if (resp){
            this.initJobLevels();
            this.jobLevel = {name:'',titleLevel:''};
          }
        })
      }else{
        this.$message.error("职称名称或职称级别不能为空!");
      }
    },
    doUpdate(){
      if (this.updateJobLevel.name && this.updateJobLevel.titleLevel){
        this.putRequest("/system/basic/job/",this.updateJobLevel).then(resp=>{
          if (resp){
            this.initJobLevels();
            this.dialogVisible = false;
            this.updateJobLevel = {name:'',titleLevel:'',enabled: null};
          }
        })
      }else{
        this.$message.error("职称名称或职称级别不能为空!");
      }
    },
    deleteMany(){
      this.$confirm('此操作将永久删除这【'+this.multipleSelection.length+'】条记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        let ids = '?';
        this.multipleSelection.forEach(item=>{
          ids += 'ids='+ item.id + '&';
        });
        this.deleteRequest("/system/basic/job/"+ids).then(resp=>{
          if (resp){
            this.initJobLevels();
          }
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    },
    handleDelete(index,row){
      this.$confirm('此操作将永久删除【'+row.name+'】职称, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.deleteRequest("/system/basic/job/"+row.id).then(resp=>{
          if (resp){
            this.initJobLevels();
          }
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    },
    showEditView(index,row){
      this.dialogVisible = true;
      Object.assign(this.updateJobLevel,row);
    },
    handleSelectionChange(val) {
      this.multipleSelection = val;
    }
  }
}
</script>

<style>
  .addJobLevelInput{
    width: 200px;
    margin-right: 6px;
  }
  .JobLevelManaMain{
    margin-top: 10px;
  }

  .updateJobLevelInput{
    width: 200px;
    margin-left: 8px;
  }
</style>

JobLevelController.java

package com.zhuantai.voa.controller.system.basic;

import com.zhuantai.voa.model.JobLevel;
import com.zhuantai.voa.service.JobLevelService;
import com.zhuantai.voa.utils.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 职称管理
 * @author ANTIA1
 * @date 2021/7/14 0:06
 */
@RestController
@RequestMapping("/system/basic/job")
public class JobLevelController {

    @Autowired
    JobLevelService jobLevelService;

    @GetMapping("/")
    public List<JobLevel> getAllJobLevels(){
        return jobLevelService.getAllJobLevels();
    }

    @PostMapping("/")
    public RespBean addJobLevel(@RequestBody JobLevel jobLevel){
        if (jobLevelService.addJobLevel(jobLevel) == 1){
            return RespBean.ok("添加成功!");
        }
        return RespBean.error("添加失败!");
    }

    @PutMapping("/")
    public RespBean updateJobLevel(@RequestBody JobLevel jobLevel){
        if (jobLevelService.updateJobLevel(jobLevel) == 1){
            return RespBean.ok("更新成功!");
        }
        return RespBean.error("更新失败!");
    }

    @DeleteMapping("/{id}")
    public RespBean deleteJobLevel(@PathVariable Integer id){
        if (jobLevelService.deleteJobLevel(id) == 1){
            return RespBean.ok("删除成功!");
        }
        return RespBean.error("删除失败!");
    }

    @DeleteMapping("/")
    public RespBean deleteJobLevelsByIds(String[] ids){
        if(jobLevelService.deleteJobLevelsByIds(ids) == ids.length){
            return RespBean.ok("删除成功!");
        }
        return RespBean.error("删除失败!");
    }
}

JobLevelService.java

package com.zhuantai.voa.service;

import com.zhuantai.voa.mapper.JobLevelMapper;
import com.zhuantai.voa.model.JobLevel;
import com.zhuantai.voa.utils.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/14 13:47
 */
@Service
public class JobLevelService {

    @Autowired
    JobLevelMapper jobLevelMapper;

    public List<JobLevel> getAllJobLevels() {
        return jobLevelMapper.getAllJobLevels();
    }

    public Integer deleteJobLevelsByIds(String[] ids) {
        return jobLevelMapper.deleteJobLevelsByIds(ids);
    }

    public Integer deleteJobLevel(Integer id) {
        return jobLevelMapper.deleteByPrimaryKey(id);
    }

    public int addJobLevel(JobLevel jobLevel) {
        jobLevel.setEnabled(true);
        jobLevel.setCreateDate(new Date());
        return jobLevelMapper.insertSelective(jobLevel);
    }

    public int updateJobLevel(JobLevel jobLevel){
        return jobLevelMapper.updateByPrimaryKeySelective(jobLevel);
    }
}
4.解决element-ui的table表格控件表头与内容列不对齐问题

将以下样式代码添加到index.html、或app.vue中(必须是入口文件,起全局作用!

App.vue

<template>
  <div id="app">
    <router-view/>
  </div>
</template>
<style>
/* 解决element-ui的table表格控件表头与内容列不对齐问题 */
.el-table th.gutter{
  display: table-cell!important;
}
</style>
5.把扁平数据转成树形数据
function treeData(source, id, parentId, children){   
    let cloneData = JSON.parse(JSON.stringify(source))
    return cloneData.filter(father=>{
        let branchArr = cloneData.filter(child => father[id] == child[parentId]);
        branchArr.length>0 ? father[children] = branchArr : ''
        return father[parentId] == 0        // 如果第一层不是parentId=0,请自行修改
    })
}
// 调用时,字段名以字符串的形式传参,如treeData(source, 'id', 'parentId', 'children')

test.vue

<template>
  <el-tree
    :data="treeData"
    :props="defaultProps"
    accordion
    @node-click="handleNodeClick">
  </el-tree>
</template>
 
<script>
    export default {
        name: "Test",
      data(){
        return {
          data : [
            {id:1,parentId:0,name:"一级菜单A",rank:1},
            {id:2,parentId:0,name:"一级菜单B",rank:1},
            {id:3,parentId:0,name:"一级菜单C",rank:1},
            {id:4,parentId:1,name:"二级菜单A-A",rank:2},
            {id:5,parentId:1,name:"二级菜单A-B",rank:2},
            {id:6,parentId:2,name:"二级菜单B-A",rank:2},
            {id:7,parentId:4,name:"三级菜单A-A-A",rank:3},
            {id:8,parentId:7,name:"四级菜单A-A-A-A",rank:4},
            {id:9,parentId:8,name:"五级菜单A-A-A-A-A",rank:5},
            {id:10,parentId:9,name:"六级菜单A-A-A-A-A-A",rank:6},
            {id:11,parentId:10,name:"七级菜单A-A-A-A-A-A-A",rank:7},
            {id:12,parentId:11,name:"八级菜单A-A-A-A-A-A-A-A",rank:8},
            {id:13,parentId:12,name:"九级菜单A-A-A-A-A-A-A-A-A",rank:9},
            {id:14,parentId:13,name:"十级菜单A-A-A-A-A-A-A-A-A-A",rank:10},
          ],
          defaultProps: {
            children: 'children',
            label: 'name'
          }
        }
      },
      computed:{
        treeData(){
          let cloneData = JSON.parse(JSON.stringify(this.data))    // 对源数据深度克隆
          return cloneData.filter(father=>{               
            let branchArr = cloneData.filter(child=>father.id == child.parentId)    //返回每一项的子级数组
            branchArr.length>0 ? father.children = branchArr : ''   //如果存在子级,则给父级添加一个children属性,并赋值
            return father.parentId==0;      //返回第一层
          });
        }
      },
      methods:{
        handleNodeClick(data){
          // console.log(data)
          console.log(this.treeData)
        }
      },
      mounted(){
      }
    }
</script>
 
<style scoped>
 
</style>
6.树形数据转成扁平数据

利用递归的方法循环树形数组,当遇到有children的对象再次调用递归函数循环children数组,每次循环的数据放入一个提前声明好的数组里,等所有递归函数执行完,这个数组即是想要得到的扁平数据数组。

let res = []
const fn = (source)=>{
    source.forEach(el=>{
        res.push(el)
        el.children && el.children.length>0 ? fn(el.children) : ""
    })
}

test.js

let res = []        // 用于存储递归结果(扁平数据)
// 递归函数
const fn = (source)=>{
    source.forEach(el=>{
        res.push(el)
        el.children && el.children.length>0 ? fn(el.children) : ""        // 子级递归
    })
}
 
// 树形数据
const arr = [
    { id: "1", rank: 1 },
    { id: "2", rank: 1,
        children:[ 
            { id: "2.1", rank: 2 },
            { id: "2.2", rank: 2 } 
        ] 
    },
    { id: "3", rank:1,
        children:[ 
            { id: "3.1", rank:2, 
                children: [ 
                    { id:'3.1.1', rank:3,
                        children:[ 
                            { id: "3.1.1.1", rank: 4, 
                                children:[
                                    { id: "3.1.1.1.1", rank: 5 }
                                ] 
                            } 
                        ] 
                    } 
                ] 
            } 
        ] 
    }
]
fn(arr)             // 执行递归函数
console.log(res)    // 查看结果
7.权限组

Permiss.vue

<template>
  <div>
    <div >
      <el-input class="permissManaTool" size="small" placeholder="请输入角色英文名" v-model="role.name">
        <template slot="prepend">ROLE_</template>
      </el-input>
      <el-input  class="permissManaTool" size="small" placeholder="请输入角色中文名" v-model="role.nameZh" @keydown.enter.native="doAddRole">
      </el-input>
      <el-button type="primary" size="small" icon="el-icon-plus" @click="doAddRole" >添加角色</el-button>
    </div>
    <div style="margin-top: 10px;width: 700px">
      <el-collapse accordion @change="change" v-model="activeName">
        <el-collapse-item :title="role.nameZh" :name="role.id" v-for="(role,index) in roles" :key="index">
          <el-card class="box-card">
            <div slot="header" class="clearfix">
              <span>可访问资源</span>
              <el-button @click="deleteRole(role)" style="float: right; padding: 3px 0;color: #ff0000" icon="el-icon-delete" type="text"></el-button>
            </div>
            <div>
              <el-tree :data="menus"
                       ref="tree"
                       :props="props"
                       show-checkbox
                       node-key="id"
                       :key="index"
                       :default-checked-keys="selectedMenus">
              </el-tree>
              <div style="display: flex;justify-content: flex-end">
                <el-button size="mini" @click="cancelUpdate">取消修改</el-button>
                <el-button size="mini" type="primary" @click="doUpdate(role.id,index)">确认修改</el-button>
              </div>
            </div>
          </el-card>
        </el-collapse-item>
      </el-collapse>
    </div>
  </div>
</template>

<script>
export default {
  name: "PermissMana",
  data(){
    return {
      role:{
        name:'',
        nameZh:''
      },
      roles:[],
      menus:[],
      props: {
        label: 'name',
        children: 'children'
      },
      selectedMenus:[],
      activeName:-1
    }
  },
  mounted() {
    this.initRoles();
  },
  methods:{
    initRoles(){
      this.roles = [];
      this.getRequest("/system/basic/per/").then(resp=>{
        this.roles = resp;
      })
    },
    initMenus(){
      this.getRequest("/system/basic/per/menus").then(resp=>{
        this.menus = resp;
      })
    },
    initSelectedMenus(rid){
      this.getRequest("/system/basic/per/mids/"+rid).then(resp=>{
        if (resp){
          this.selectedMenus = resp;
        }
      })
    },
    change(rid){
      if (rid){
        this.initMenus();
        this.initSelectedMenus(rid);
      }
    },
    doUpdate(rid,index){
      let tree = this.$refs.tree[index]; //获取当前展开的tree
      let checkedKeys = tree.getCheckedKeys(true);//只返回叶子节点
      let url = "/system/basic/per/?rid="+rid;
      checkedKeys.forEach(key=>{
        url += "&mids="+key;
      });
      this.putRequest(url).then(resp=>{
        if (resp){
          this.initRoles();
          this.activeName = -1;
        }
      });
    },
    cancelUpdate(){
      this.activeName = -1;
    },
    doAddRole(){
      if (this.role.name && this.role.nameZh){
        this.postRequest("/system/basic/per/",this.role).then(resp=>{
          if (resp){
            this.initRoles();
            this.role = {name:"",nameZh: ""};
          }
        });
      }else{
        this.$message.error("添加字段不能为空!");
      }
    },
    deleteRole(role){
      this.$confirm('此操作将永久删除这【'+role.nameZh+'】条记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.deleteRequest("/system/basic/per/"+role.id).then(resp=>{
          if (resp){
            this.initRoles();
          }
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    }
  }

}
</script>

<style>
  .permissManaTool{
    width: 300px;
    margin-right: 6px;
  }

</style>

PermissController.java

package com.zhuantai.voa.controller.system.basic;

import com.zhuantai.voa.model.Menu;
import com.zhuantai.voa.model.Role;
import com.zhuantai.voa.service.MenuService;
import com.zhuantai.voa.service.RoleService;
import com.zhuantai.voa.utils.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/14 15:54
 */
@RestController
@RequestMapping("/system/basic/per")
public class PermissController {

    @Autowired
    RoleService roleService;

    @Autowired
    MenuService menuService;

    @GetMapping("/")
    public List<Role> getAllRoles(){
        return roleService.getAllRoles();
    }

    @GetMapping("/menus")
    public List<Menu> getAllMenus(){
        return menuService.getAllMenus();
    }

    @GetMapping("/mids/{rid}")
    public List<Integer> getMidsByRid(@PathVariable Integer rid){
        return menuService.getMidsByRid(rid);
    }

    @PutMapping("/")
    public RespBean updateMenuRole(Integer rid,Integer[] mids){
        if (menuService.updateMenuRole(rid, mids)) {
            return RespBean.ok("更新成功!");
        }
        return RespBean.ok("更新失败!");
    }

    @PostMapping("/")
    public RespBean addRole(@RequestBody Role role){
        if (roleService.addRole(role)==1) {
            return RespBean.ok("添加成功!");
        }
        return RespBean.error("添加失败!");
    }

    @DeleteMapping("/{rid}")
    public RespBean deleteRoleById(@PathVariable Integer rid){
        if (roleService.deleteRoleById(rid) == 1){
            return RespBean.ok("删除成功!");
        }
        return RespBean.error("删除失败!");
    }
}

RoleService.java

package com.zhuantai.voa.service;

import com.zhuantai.voa.mapper.MenuMapper;
import com.zhuantai.voa.mapper.MenuRoleMapper;
import com.zhuantai.voa.mapper.RoleMapper;
import com.zhuantai.voa.model.MenuRole;
import com.zhuantai.voa.model.Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/14 15:56
 */
@Service
public class RoleService {

    @Autowired
    RoleMapper roleMapper;

    @Autowired
    MenuRoleMapper menuRoleMapper;

    public List<Role> getAllRoles(){
        return roleMapper.getAllRoles();
    }

    public int addRole(Role role) {
        if (!role.getName().startsWith("ROLE_")){
            role.setName("ROLE_"+role.getName());
        }
        return roleMapper.insert(role);
    }

    @Transactional
    public int deleteRoleById(Integer rid) {
        menuRoleMapper.deleteByRid(rid);
        return roleMapper.deleteByPrimaryKey(rid);
    }
}

MenuService.java

package com.zhuantai.voa.service;

import com.zhuantai.voa.mapper.MenuMapper;
import com.zhuantai.voa.mapper.MenuRoleMapper;
import com.zhuantai.voa.model.Hr;
import com.zhuantai.voa.model.Menu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/12 21:42
 */
@Service
public class MenuService {
    @Autowired
    MenuMapper menuMapper;

    @Autowired
    MenuRoleMapper menuRoleMapper;

    public List<Menu> getMenusByHrId(){
        Hr hr = ((Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal());//获得当前登录用户的id
        return menuMapper.getMenusByHrId(hr.getId());
    }

//    @Cacheable
    public List<Menu> getAllMenusWithRole(){
        return menuMapper.getAllMenusWithRole();
    }

    public List<Menu> getAllMenus() {
        return menuMapper.getAllMenus();
    }

    public List<Integer> getMidsByRid(Integer rid) {
        return menuMapper.getMidsByRid(rid);
    }

    @Transactional
    public boolean updateMenuRole(Integer rid, Integer[] mids) {
        menuRoleMapper.deleteByRid(rid);
        Integer res = menuRoleMapper.insertRecord(rid,mids);
        return res == mids.length;
    }
}
8.SESSION过期后,前端与后端会断开连接的问题

服务端重启,或者SESSION过期后页面会无法使用功能,只有手动注销再重新登录才可以继续访问,为了更好的用户体验,SESSION过期后用户在前端如果操作了页面,会自动跳转到登录页面让用户重新登录。

SecurityController.java

package com.zhuantai.voa.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhuantai.voa.model.Hr;
import com.zhuantai.voa.service.HrService;
import com.zhuantai.voa.utils.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;


/**
 * @author ANTIA1
 * @date 2021/7/12 13:10
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    HrService hrService;

    @Autowired
    CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;

    @Autowired
    CustomUrlDecisionManager customUrlDecisionManager;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(hrService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
//                .anyRequest().authenticated()//所有的请求都要认证之后才可以访问
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(customUrlDecisionManager);
                        o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        return o;
                    }
                })
                .and()
                .formLogin()//表单登录
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/doLogin")//处理登录的请求路由
                .loginPage("/login")
                .successHandler(new AuthenticationSuccessHandler() {//登录成功处理
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = resp.getWriter();

                        Hr hr = (Hr) authentication.getPrincipal();//获取登录对象
                        RespBean respBean = RespBean.ok("登录成功", hr);
                        hr.setPassword(null);
                        String str = new ObjectMapper().writeValueAsString(respBean);//对象转成JSON字符串
                        writer.write(str);//写出去

                        writer.flush();
                        writer.close();
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {//登录失败处理
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = resp.getWriter();

                        RespBean respBean = RespBean.error("登录失败!");
                        if (e instanceof LockedException){
                            respBean.setMsg("账户被锁定,请联系管理员!");
                        }else if (e instanceof CredentialsExpiredException){
                            respBean.setMsg("密码过期,请联系管理员!");
                        }else if(e instanceof AccountExpiredException){
                            respBean.setMsg("账户过期,请联系管理员!");
                        }else if(e instanceof DisabledException){
                            respBean.setMsg("账户被禁用,请联系管理员!");
                        }else if(e instanceof BadCredentialsException){
                            respBean.setMsg("用户名或密码错误,请重新输入!");
                        }
                        String str = new ObjectMapper().writeValueAsString(respBean);//结果对象转成JSON字符串
                        writer.write(str);//写出去

                        writer.flush();
                        writer.close();
                    }
                })
                .permitAll()
                .and()
                .logout()
                .logoutSuccessHandler(new LogoutSuccessHandler() {//注销成功处理
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = resp.getWriter();

                        writer.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功")));

                        writer.flush();
                        writer.close();
                    }
                })
                .permitAll()
                .and()
                .csrf().disable().
                //没有认证时,在这里处理结果,不要重定向
                exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
                @Override
                public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                    resp.setContentType("application/json;charset=utf-8");
                    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                    resp.setStatus(401);//添加状态码,让前端的拦截器去拦截
                    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                    PrintWriter writer = resp.getWriter();
                    RespBean respBean = RespBean.error("访问失败!");
                    if (e instanceof InsufficientAuthenticationException){
                        respBean.setMsg("请求失败,请联系管理员!");
                    }
                    String str = new ObjectMapper().writeValueAsString(respBean);//结果对象转成JSON字符串
                    writer.write(str);//写出去

                    writer.flush();
                    writer.close();
                }
            });
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/login");
    }
}

api.js

import axios from "axios";
import {Message} from 'element-ui'
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
import router from "../router";
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>


//服务端响应拦截器
axios.interceptors.response.use(result =>{
    //如果出现此种状况.说明是业务上的错误.[登录失败]
    if (result.status && result.status == 200 && result.data.status == 500){
        Message.error({message:result.data.msg});
        return;
    }
    if (result.data.msg){
        Message.success({message:result.data.msg});
    }
    return result.data;//返回响应数据给调用处
},error => {
    if (error.response.status == 504 || error.response.status == 404){
        Message.error({message:"服务器被我吃掉咯~!!!(๑´ㅂ`๑)"})
    }else if (error.response.status == 403){
        Message.error({message:"权限不足!"})
    }else if (error.response.status == 401){
        Message.error({message:"尚未登录,请先登录!"})
        >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        router.replace("/");//跳转到登录页
        >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    }else{
        if (error.response.data.msg){
            Message.error({message:error.response.data.msg})
        }else{
            Message.error({message:"未知错误!"})
        }
    }
    return;
});

let base = '';
export const postKeyValueRequest = (url,params) => {
    return axios({
        method:"post",
        url:`${base}${url}`,
        data:params,
        transformRequest:[function (data){
            let ret = '';
            for(let i in data){
                ret += encodeURIComponent(i) + "=" + encodeURIComponent(data[i])+"&";
            }
            return ret;
        }],
        headers:{
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    })
}

export const postRequest = (url, params) => {
    return axios({
        method: 'post',
        url: `${base}${url}`,
        data: params,
    });
}

export const putRequest = (url, params) => {
    return axios({
        method: 'put',
        url: `${base}${url}`,
        data: params,
    });
}

export const deleteRequest = (url) => {
    return axios({
        method: 'delete',
        url: `${base}${url}`
    });
}
export const getRequest = (url) => {
    return axios({
        method: 'get',
        url: `${base}${url}`
    });
}
9.部门管理

DepMana.vue

<template>
<div style="width: 500px">
  <el-input
      prefix-icon="el-icon-search"
      size="small"
      placeholder="输入部门名称进行搜索"
      v-model="filterText">
  </el-input>

  <el-tree
      class="filter-tree"
      :data="departments"
      :props="defaultProps"
      default-expand-all
      :expand-on-click-node="false"
      :filter-node-method="filterNode"
      ref="tree">

     <span class="custom-tree-node" style="display: flex;justify-content: space-between;width: 100%" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <el-button
              class="depBtn"
              type="primary"
              size="mini"
              @click="showAddDepView(data)">
            添加
          </el-button>
          <el-button
              class="depBtn"
              type="danger"
              size="mini"
              @click="deleteDept(data)">
            删除
          </el-button>
        </span>
      </span>


  </el-tree>

  <el-dialog
      title="添加部门"
      :visible.sync="dialogVisible"
      width="30%"
      >
    <span>
      <table>
        <tr>
          <td><el-tag>上级部门</el-tag></td>
          <td><span>{{pname}}</span></td>
        </tr>
        <tr>
          <td><el-tag>部门名称</el-tag></td>
          <td><el-input v-model="dept.name" size="small" placeholder="请输入部门名称..." @keydown.enter.native="addDept"></el-input></td>
        </tr>
      </table>
      <div>
      </div>
    </span>
    <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false" size="small">取 消</el-button>
    <el-button type="primary" @click="addDept" size="small">确 定</el-button>
  </span>
  </el-dialog>
</div>
</template>

<script>
export default {
  name: "DepMana",
  watch: {
    filterText(val) {
      this.$refs.tree.filter(val);
    }
  },
  data(){
    return {
      filterText:'',
      defaultProps:{
        label:"name",
        children:"children"
      },
      departments:[],
      dialogVisible:false,
      dept:{
        name:"",
        parentId:-1
      },
      pname:''
    }
  },
  methods: {
    initDepartments(){
      this.getRequest("/system/basic/department/").then(resp=>{
        this.departments = resp;
      });
    },
    filterNode(value, data) {
      if (!value) return true;
      return data.name.indexOf(value) !== -1;
    },
    showAddDepView(data){
      this.pname = data.name;
      this.dept.parentId = data.id;
      this.dialogVisible = true;
    },
    deleteDept(data){
      if (data.isParent){
        this.$message.error("父部门无法直接删除!");
      }else{
        this.$confirm('此操作将永久删除【'+data.name+'】部门, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.deleteRequest("/system/basic/department/"+data.id).then(resp=>{
            if (resp){
              this.initDepartments();
            }
          })
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });
        });
      }
    },
    addDept(){
      if (this.dept.name){
        this.postRequest("/system/basic/department/",this.dept).then(resp=>{
          if (resp){
            this.initDepartments();
            this.dialogVisible = false;
            this.dept = {name:'',parentId: -1};
            this.pname = '';
          }
        })
      }
    }
  },
  mounted() {
    this.initDepartments();
  }

}
</script>

<style>
  .depBtn{
    padding: 2px;
  }
</style>

DepartmentController.java

package com.zhuantai.voa.controller.system.basic;

import com.zhuantai.voa.model.Department;
import com.zhuantai.voa.service.DepartmentService;
import com.zhuantai.voa.utils.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/15 18:09
 */
@RestController
@RequestMapping("/system/basic/department")
public class DepartmentController {

    @Autowired
    DepartmentService departmentService;

    @GetMapping("/")
    public List<Department> getAllDepartments(){
        return departmentService.getAllDepartments();
    }

    @PostMapping("/")
    public RespBean addDept(@RequestBody Department department){
        departmentService.addDept(department);
        if (department.getResult() == 1){
            return RespBean.ok("添加成功!",department);
        }
        return RespBean.error("添加失败!");
    }

    @DeleteMapping("/{id}")
    public RespBean deleteDeptById(@PathVariable Integer id){
        Department department = new Department();
        department.setId(id);
        departmentService.deleteDeptById(department);
        if (department.getResult()==-2) {
            return RespBean.error("该部门下有子部门,删除失败!");
        }else if (department.getResult()==-1){
            return RespBean.error("该部门下有员工,删除失败!");
        }else if (department.getResult()==1) {
            return RespBean.ok("删除成功!");
        }
        return RespBean.error("未知原因,删除失败!");
    }

}

DepartmentService.java

package com.zhuantai.voa.service;

import com.zhuantai.voa.mapper.DepartmentMapper;
import com.zhuantai.voa.model.Department;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/15 18:12
 */
@Service
public class DepartmentService {

    @Autowired
    DepartmentMapper departmentMapper;

    public List<Department> getAllDepartments(){
        return departmentMapper.departmentMapperByParentId(-1);
    }

    public void addDept(Department department) {
        department.setEnabled(true);
        departmentMapper.addDept(department);
    }

    public void deleteDeptById(Department department) {
        departmentMapper.deleteDepById(department);
    }
}

DepartmentMapper.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.zhuantai.voa.mapper.DepartmentMapper" >
  <resultMap id="BaseResultMap" type="com.zhuantai.voa.model.Department" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="name" property="name" jdbcType="VARCHAR" />
    <result column="parentId" property="parentId" jdbcType="INTEGER" />
    <result column="depPath" property="depPath" jdbcType="VARCHAR" />
    <result column="enabled" property="enabled" jdbcType="BIT" />
    <result column="isParent" property="isParent" jdbcType="BIT" />
  </resultMap>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  <resultMap id="DepartmentWithChildren" type="com.zhuantai.voa.model.Department" extends="BaseResultMap">
    <collection property="children" ofType="com.zhuantai.voa.model.Department"
                select="com.zhuantai.voa.mapper.DepartmentMapper.departmentMapperByParentId"
                column="id"
    >
      <id column="id" property="id" jdbcType="INTEGER" />
      <result column="name" property="name" jdbcType="VARCHAR" />
      <result column="parentId" property="parentId" jdbcType="INTEGER" />
      <result column="depPath" property="depPath" jdbcType="VARCHAR" />
      <result column="enabled" property="enabled" jdbcType="BIT" />
      <result column="isParent" property="isParent" jdbcType="BIT" />
    </collection>
  </resultMap>
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  <sql id="Base_Column_List" >
    id, name, parentId, depPath, enabled, isParent
  </sql>

  <select id="departmentMapperByParentId" resultMap="DepartmentWithChildren">
    select * from department where parentId = #{pid}
  </select>

  <select id="addDept" statementType="CALLABLE">
    call addDep
        (
          #{name,mode=IN,jdbcType=VARCHAR},
          #{parentId,mode=IN,jdbcType=INTEGER},
          #{enabled,mode=IN,jdbcType=BOOLEAN},
          #{result,mode=OUT,jdbcType=INTEGER},
          #{id,mode=OUT,jdbcType=INTEGER}
        )
  </select>

  <select id="deleteDepById" statementType="CALLABLE">
    call deleteDep(#{id,mode=IN,jdbcType=INTEGER},#{result,mode=OUT,jdbcType=INTEGER})
  </select>

  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from department
    where id = #{id,jdbcType=INTEGER}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
    delete from department
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" parameterType="com.zhuantai.voa.model.Department" >
    insert into department (id, name, parentId, 
      depPath, enabled, isParent)
    values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{parentId,jdbcType=INTEGER}, 
      #{depPath,jdbcType=VARCHAR}, #{enabled,jdbcType=BIT}, #{isParent,jdbcType=BIT})
  </insert>
  <insert id="insertSelective" parameterType="com.zhuantai.voa.model.Department" >
    insert into department
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="name != null" >
        name,
      </if>
      <if test="parentId != null" >
        parentId,
      </if>
      <if test="depPath != null" >
        depPath,
      </if>
      <if test="enabled != null" >
        enabled,
      </if>
      <if test="isParent != null" >
        isParent,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=INTEGER},
      </if>
      <if test="name != null" >
        #{name,jdbcType=VARCHAR},
      </if>
      <if test="parentId != null" >
        #{parentId,jdbcType=INTEGER},
      </if>
      <if test="depPath != null" >
        #{depPath,jdbcType=VARCHAR},
      </if>
      <if test="enabled != null" >
        #{enabled,jdbcType=BIT},
      </if>
      <if test="isParent != null" >
        #{isParent,jdbcType=BIT},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.zhuantai.voa.model.Department" >
    update department
    <set >
      <if test="name != null" >
        name = #{name,jdbcType=VARCHAR},
      </if>
      <if test="parentId != null" >
        parentId = #{parentId,jdbcType=INTEGER},
      </if>
      <if test="depPath != null" >
        depPath = #{depPath,jdbcType=VARCHAR},
      </if>
      <if test="enabled != null" >
        enabled = #{enabled,jdbcType=BIT},
      </if>
      <if test="isParent != null" >
        isParent = #{isParent,jdbcType=BIT},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.zhuantai.voa.model.Department" >
    update department
    set name = #{name,jdbcType=VARCHAR},
      parentId = #{parentId,jdbcType=INTEGER},
      depPath = #{depPath,jdbcType=VARCHAR},
      enabled = #{enabled,jdbcType=BIT},
      isParent = #{isParent,jdbcType=BIT}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>
10.部门树中的递归实现

可以通过Mybatis自带的递归操作完成

  <resultMap id="DepartmentWithChildren" type="com.zhuantai.voa.model.Department" extends="BaseResultMap">
    <collection property="children" ofType="com.zhuantai.voa.model.Department"
                select="com.zhuantai.voa.mapper.DepartmentMapper.departmentMapperByParentId"
                column="id"
    >
      <id column="id" property="id" jdbcType="INTEGER" />
      <result column="name" property="name" jdbcType="VARCHAR" />
      <result column="parentId" property="parentId" jdbcType="INTEGER" />
      <result column="depPath" property="depPath" jdbcType="VARCHAR" />
      <result column="enabled" property="enabled" jdbcType="BIT" />
      <result column="isParent" property="isParent" jdbcType="BIT" />
    </collection>
  </resultMap>

五.[业务]操作员管理

SysHr.vue

<template>
  <div>
      <div style="margin-top:10px;display: flex;justify-content: center">
          <el-input
                    size="small"
                    style="width: 400px;margin-right: 10px;"
                    v-model="keywords"
                    @keydown.enter.native="doSearch(keywords)"
                    placeholder="通过用户名搜索用户..."
                    prefix-icon="el-icon-search">
          </el-input>
        <el-button icon="el-icon-search" type="primary" size="small" @click="doSearch(keywords)">搜索</el-button>
      </div>

    <div class="hrContainer" >
      <el-card class="hr-card" v-for="(hr,index) in hrs" :key="index">
        <div slot="header" class="clearfix">
          <span>{{ hr.name }}</span>
          <el-button style="float: right; padding: 3px 0;color: red" type="text" icon="el-icon-delete" @click="doDelete(hr)"></el-button>
          <div class="roleContainer">
            <el-tag style="margin-right: 5px;margin-top: 5px" type="warning" size="mini" effect="dark" :key="index" v-for="(item,index) in hr.roles">
              {{item.nameZh}}
            </el-tag>
            <el-popover
                placement="right"
                title="角色列表"
                width="200"
                @show="showPop(hr)"
                @hide="hidePop(hr)"
                trigger="click">
              <el-select v-model="selectedRoles" multiple placeholder="请选择">
                <el-option
                    v-for="(r,indexk) in allroles"
                    :key="indexk"
                    :label="r.nameZh"
                    :value="r.id">
                </el-option>
              </el-select>
              <el-button slot="reference" icon="el-icon-more" circle type="text" size="mini" title="修改角色"></el-button>
            </el-popover>
          </div>

        </div>
        <div>
          <div class="img-container">
            <img :src="hr.userface" :alt="hr.name" :title="hr.name" class="userface-img">
          </div>
          <div class="userinfo-container">
            <table>
              <tr>
                <td><el-tag>用&nbsp;&nbsp;户&nbsp;&nbsp;名</el-tag></td>
                <td>{{ hr.name }}</td>
              </tr>
              <tr>
                <td><el-tag>手机号码</el-tag></td>
                <td>{{ hr.phone }}</td>
              </tr>
              <tr>
                <td><el-tag>电话号码</el-tag></td>
                <td>{{ hr.telephone }}</td>
              </tr>
              <tr>
                <td><el-tag>地&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;址</el-tag></td>
                <td>{{ hr.address }}</td>
              </tr>
              <tr>
                <td><el-tag>用户状态</el-tag></td>
                <td>
                  <el-switch
                      @change="enabledChange(hr)"
                      style="display: block"
                      v-model="hr.enabled"
                      active-color="#13ce66"
                      inactive-color="#ff4949"
                      active-text="启用"
                      inactive-text="禁用">
                  </el-switch>
                </td>
              </tr>
              <tr>
                <td><el-tag>备&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;注</el-tag></td>
                <td>{{ hr.remark }}</td>
              </tr>

            </table>
          </div>
        </div>
      </el-card>
    </div>
  </div>
</template>

<script>
export default {
  name: "SysHr",
  data(){
    return{
      hrs:[],
      keywords:'',
      allroles:[],
      selectedRoles:[]
    }
  },
  mounted() {
    this.initHrs();
  },
  methods:{
    initHrs(){
      this.getRequest("/system/hr/?keywords="+this.keywords).then(resp=>{
        if (resp){
          this.hrs = resp;
        }
      })
    },
    initAllRoles(){
      this.getRequest("/system/hr/roles").then(resp=>{
        if (resp){
          this.allroles = resp;
        }
      })
    },
    doSearch(){
      this.initHrs();
    },
    showPop(hr){
      this.initAllRoles();
      this.selectedRoles = [];
      hr.roles.forEach(r=>{
        this.selectedRoles.push(r.id);
      });
    },
    hidePop(hr){
      let roles = [];
      Object.assign(roles, hr.roles);
      let flag = false;
      if (roles.length != this.selectedRoles.length) {
        flag = true;
      } else {
        for (let i = 0; i < roles.length; i++) {
          let role = roles[i];
          for (let j = 0; j < this.selectedRoles.length; j++) {
            let sr = this.selectedRoles[j];
            if (role.id == sr) {
              roles.splice(i, 1);
              i--;
              break;
            }
          }
        }
        if (roles.length != 0) {
          flag = true;
        }
      }
      if (flag) {
        let url = '/system/hr/role?hrid=' + hr.id;
        this.selectedRoles.forEach(sr => {
          url += '&rids=' + sr;
        });
        this.putRequest(url).then(resp => {
          if (resp) {
            this.initHrs();
          }
        });
      }
    },
    enabledChange(hr){
      delete hr.roles;
      this.putRequest("/system/hr/",hr).then(resp=>{
        if (resp){
          this.initHrs();
        }
      })
    },
    doDelete(hr){
      this.$confirm('此操作将永久删除【'+hr.name+'】, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.deleteRequest("/system/hr/"+hr.id).then(resp=>{
          if (resp){
            this.initHrs();
          }
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    }
  }
}
</script>

<style>
  .hr-card{
    width: 350px;
    margin-bottom: 20px;
  }
  .hrContainer{
    margin-top: 10px;
    display: flex;/*水平布局*/
    flex-wrap:wrap;/*自动换行*/
    justify-content: space-around;/*空隙平均分配*/
  }

  .userface-img{
    width: 72px;
    height: 72px;
    border-radius: 72px;/*变成圆型*/
  }
  .img-container{
    width: 100%;
    display: flex;
    justify-content: center;
  }
  .userinfo-container{
    margin-top: 20px;
    font-size: 14px;
  }

  .roleContainer{
    width: 100%;
    height: 100%;
    margin-top: 10px;
    display: flex;
    flex-wrap:wrap;/*自动换行*/
  }
</style>

HrController.java

package com.zhuantai.voa.controller.system;

import com.zhuantai.voa.model.Hr;
import com.zhuantai.voa.model.Role;
import com.zhuantai.voa.service.HrService;
import com.zhuantai.voa.service.RoleService;
import com.zhuantai.voa.utils.RespBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/15 22:50
 */
@RestController
@RequestMapping("/system/hr")
public class HrController {

    @Autowired
    HrService hrService;
    @Autowired
    RoleService roleService;

    @GetMapping("/")
    public List<Hr> getAllHrs(String keywords){
        return hrService.getAllHrs(keywords);
    }

    @PutMapping("/")
    public RespBean updateHr(@RequestBody Hr hr){
        if (hrService.updateHr(hr) == 1){
            return RespBean.ok("更新成功!");
        }
        return RespBean.error("更新失败!");
    }

    @GetMapping("/roles")
    public List<Role> getAllRoles(){
        return roleService.getAllRoles();
    }

    @PutMapping("/role")
    public RespBean updateHrRole(Integer hrid,Integer[] rids){
        if (hrService.updateHrRole(hrid,rids)){
            return RespBean.ok("更新成功!");
        }
        return RespBean.error("更新失败!");
    }

    @DeleteMapping("/{id}")
    public RespBean deleteHrById(@PathVariable Integer id){
        if (hrService.deleteHrById(id) == 1){
            return RespBean.ok("删除成功!");
        }
       return  RespBean.error("删除失败!");
    }

}

HrService.java

package com.zhuantai.voa.service;

import com.zhuantai.voa.mapper.HrMapper;
import com.zhuantai.voa.mapper.HrRoleMapper;
import com.zhuantai.voa.model.Hr;
import com.zhuantai.voa.utils.HrUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/12 13:07
 */
@Service
public class HrService implements UserDetailsService {
    @Autowired
    HrMapper hrMapper;

    @Autowired
    HrRoleMapper hrRoleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Hr hr = hrMapper.loadUserByUsername(username);
        if (hr == null){
            throw new UsernameNotFoundException("用户名不存在!");
        }

        hr.setRoles(hrMapper.getHrRolesById(hr.getId()));

        return hr;
    }

    public List<Hr> getAllHrs(String keywords) {
        return hrMapper.getAllHrs(HrUtils.getCurrentHr().getId(),keywords);
    }

    public Integer updateHr(Hr hr) {
        return hrMapper.updateByPrimaryKeySelective(hr);
    }

    @Transactional
    public boolean updateHrRole(Integer hrid, Integer[] rids) {
        hrRoleMapper.deleteByHrId(hrid);
        return hrRoleMapper.addRole(hrid,rids) == rids.length;
    }

    public Integer deleteHrById(Integer id) {
        return hrMapper.deleteByPrimaryKey(id);
    }
}

六.[业务]员工资料

1.员工管理

EmpBasic.vue

<template>
    <div>
      <div style="display: flex;justify-content: space-between;">
        <div>
          <el-input size="small"
                    @keydown.enter.native="initEmps"
                    v-model="keywords"
                    clearable
                    @clear="initEmps"
                    style="width: 300px; margin-right: 10px"
                    placeholder="通过员工名搜索员工..."
                    prefix-icon="el-icon-search"/>
          <el-button type="primary" size="small" icon="el-icon-search" @click="initEmps">搜索</el-button>
          <el-button type="primary" size="small">
            <i class="fa fa-angle-double-down" aria-hidden="true"></i>
            高级搜索
          </el-button>
        </div>
        <div>
          <el-button type="success" size="small">
            <i class="fa fa-level-down" aria-hidden="true"></i>
            导入数据
          </el-button>
          <el-button type="success" size="small">
            <i class="fa fa-level-up" aria-hidden="true"></i>
            导出数据
          </el-button>
          <el-button size="small" type="primary" icon="el-icon-plus" @click="showAddEmpView">
            添加员工
          </el-button>
        </div>
      </div>
      <div style="margin-top: 10px">
        <el-table
            v-loading="loading"
            element-loading-text="加载中..."
            size="small"
            :data="emps"
            stripe
            border
            style="width: 100%">
          <el-table-column
              type="selection"
              width="55">
          </el-table-column>
          <el-table-column
              prop="name"
              fixed
              align="left"
              label="姓名"
              width="90">
          </el-table-column>
          <el-table-column
              prop="workID"
              label="工号"
              align="left"
              width="85">
          </el-table-column>
          <el-table-column
              prop="gender"
              label="性别"
              align="left"
              width="85">
          </el-table-column>
          <el-table-column
              prop="birthday"
              width="95"
              align="left"
              label="出生日期">
          </el-table-column>
          <el-table-column
              prop="idCard"
              width="150"
              align="left"
              label="身份证号码">
          </el-table-column>
          <el-table-column
              prop="wedlock"
              width="70"
              label="婚姻状况">
          </el-table-column>
          <el-table-column
              prop="nation.name"
              width="50"
              label="民族">
          </el-table-column>
          <el-table-column
              prop="nativePlace"
              width="80"
              label="籍贯">
          </el-table-column>
          <el-table-column
              prop="politicsstatus.name"
              label="政治面貌">
          </el-table-column>
          <el-table-column
              prop="email"
              width="180"
              align="left"
              label="电子邮件">
          </el-table-column>
          <el-table-column
              prop="phone"
              width="100"
              align="left"
              label="电话号码">
          </el-table-column>
          <el-table-column
              prop="address"
              width="220"
              align="left"
              label="联系地址">
          </el-table-column>
          <el-table-column
              prop="department.name"
              width="100"
              align="left"
              label="所属部门">
          </el-table-column>
          <el-table-column
              prop="position.name"
              width="100"
              label="职位">
          </el-table-column>
          <el-table-column
              prop="jobLevel.name"
              width="100"
              label="职称">
          </el-table-column>
          <el-table-column
              prop="engageForm"
              width="100"
              align="left"
              label="聘用形式">
          </el-table-column>
          <el-table-column
              prop="tiptopDegree"
              width="80"
              align="left"
              label="最高学历">
          </el-table-column>
          <el-table-column
              prop="specialty"
              width="150"
              align="left"
              label="专业">
          </el-table-column>
          <el-table-column
              prop="school"
              width="150"
              align="left"
              label="毕业院校">
          </el-table-column>
          <el-table-column
              prop="beginDate"
              width="95"
              align="left"
              label="入职日期">
          </el-table-column>
          <el-table-column
              prop="conversionTime"
              width="95"
              align="left"
              label="转正日期">
          </el-table-column>
          <el-table-column
              prop="beginContract"
              width="95"
              align="left"
              label="合同起始日期">
          </el-table-column>
          <el-table-column
              prop="endContract"
              width="95"
              align="left"
              label="合同截止日期">
          </el-table-column>
          <el-table-column
              width="100"
              align="left"
              label="合同期限">
            <template slot-scope="scope">
              <el-tag>{{scope.row.contractTerm}}</el-tag>
              年
            </template>
          </el-table-column>
          <el-table-column
              fixed="right"
              width="200"
              label="操作">
            <template slot-scope="scope">
              <el-button @click="showEditEmpView(scope.row)" type="success" style="padding: 3px" size="mini">编辑</el-button>
              <el-button style="padding: 3px" size="mini" type="primary">查看高级资料</el-button>
              <el-button @click="deleteEmp(scope.row)" style="padding: 3px" size="mini" type="danger">删除
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <div style="display: flex;justify-content: flex-end;margin-top: 10px;">
          <el-pagination
              background
              @current-change="currentChange"
              @size-change="sizeChange"
              layout="sizes, prev, pager, next, jumper, ->, total, slot"
              :total="total">
          </el-pagination>
        </div>
        <el-dialog
            :title="title"
            :visible.sync="dialogVisible"
            width="80%">
          <div>
            <el-form :model="emp" :rules="rules" ref="empForm">
              <el-row>
                <el-col :span="6">
                  <el-form-item label="姓名:" prop="name">
                    <el-input size="mini" style="width: 150px" prefix-icon="el-icon-edit" v-model="emp.name"
                              placeholder="请输入员工姓名"></el-input>
                  </el-form-item>
                </el-col>
                <el-col :span="5">
                  <el-form-item label="性别:" prop="gender">
                    <el-radio-group v-model="emp.gender">
                      <el-radio label="男">男</el-radio>
                      <el-radio label="女">女</el-radio>
                    </el-radio-group>
                  </el-form-item>
                </el-col>
                <el-col :span="6">
                  <el-form-item label="出生日期:" prop="birthday">
                    <el-date-picker
                        v-model="emp.birthday"
                        size="mini"
                        type="date"
                        value-format="yyyy-MM-dd"
                        style="width: 150px;"
                        placeholder="出生日期">
                    </el-date-picker>
                  </el-form-item>
                </el-col>
                <el-col :span="7">
                  <el-form-item label="政治面貌:" prop="politicId">
                    <el-select v-model="emp.politicId" placeholder="政治面貌" size="mini" style="width: 200px;">
                      <el-option
                          v-for="item in politicsstatus"
                          :key="item.id"
                          :label="item.name"
                          :value="item.id">
                      </el-option>
                    </el-select>
                  </el-form-item>
                </el-col>
              </el-row>
              <el-row>
                <el-col :span="6">
                  <el-form-item label="民族:" prop="nationId">
                    <el-select v-model="emp.nationId" placeholder="民族" size="mini" style="width: 150px;">
                      <el-option
                          v-for="item in nations"
                          :key="item.id"
                          :label="item.name"
                          :value="item.id">
                      </el-option>
                    </el-select>
                  </el-form-item>
                </el-col>
                <el-col :span="5">
                  <el-form-item label="籍贯:" prop="nativePlace">
                    <el-input size="mini" style="width: 120px" prefix-icon="el-icon-edit"
                              v-model="emp.nativePlace" placeholder="请输入籍贯"></el-input>
                  </el-form-item>
                </el-col>
                <el-col :span="6">
                  <el-form-item label="电子邮箱:" prop="email">
                    <el-input size="mini" style="width: 150px" prefix-icon="el-icon-message"
                              v-model="emp.email" placeholder="请输入电子邮箱"></el-input>
                  </el-form-item>
                </el-col>
                <el-col :span="7">
                  <el-form-item label="联系地址:" prop="address">
                    <el-input size="mini" style="width: 200px" prefix-icon="el-icon-edit"
                              v-model="emp.address" placeholder="请输入联系地址"></el-input>
                  </el-form-item>
                </el-col>
              </el-row>
              <el-row>
                <el-col :span="6">
                  <el-form-item label="职位:" prop="posId">
                    <el-select v-model="emp.posId" placeholder="职位" size="mini" style="width: 150px;">
                      <el-option
                          v-for="item in positions"
                          :key="item.id"
                          :label="item.name"
                          :value="item.id">
                      </el-option>
                    </el-select>
                  </el-form-item>
                </el-col>
                <el-col :span="5">
                  <el-form-item label="职称:" prop="jobLevelId">
                    <el-select v-model="emp.jobLevelId" placeholder="职称" size="mini" style="width: 150px;">
                      <el-option
                          v-for="item in jobLevels"
                          :key="item.id"
                          :label="item.name"
                          :value="item.id">
                      </el-option>
                    </el-select>
                  </el-form-item>
                </el-col>
                <el-col :span="6">
                  <el-form-item label="所属部门:" prop="departmentId">
                      <el-popover
                          placement="right"
                          title="请选择部门"
                          width="200"
                          trigger="manual"
                          v-model="popVisible">
                      <el-tree default-expand-all :data="allDeps" :props="defaultProps" :expand-on-click-node="false"
                               @node-click="handleNodeClick"></el-tree>
                      <div slot="reference"
                           style="width: 150px;display: inline-flex;font-size: 13px;border: 1px solid #dedede;height: 26px;border-radius: 5px;cursor: pointer;align-items: center;padding-left: 8px;box-sizing: border-box"
                           @click="showDepView">{{inputDepName}}
                      </div>
                    </el-popover>
                  </el-form-item>
                </el-col>
                <el-col :span="7">
                  <el-form-item label="电话号码:" prop="phone">
                    <el-input size="mini" style="width: 200px" prefix-icon="el-icon-phone"
                              v-model="emp.phone" placeholder="电话号码"></el-input>
                  </el-form-item>
                </el-col>
              </el-row>
              <el-row>
                <el-col :span="6">
                  <el-form-item label="工号:" prop="workID">
                    <el-input size="mini" style="width: 150px" prefix-icon="el-icon-edit"
                              v-model="emp.workID" placeholder="工号" disabled></el-input>
                  </el-form-item>
                </el-col>
                <el-col :span="5">
                  <el-form-item label="学历:" prop="tiptopDegree">
                    <el-select v-model="emp.tiptopDegree" placeholder="学历" size="mini"
                               style="width: 150px;">
                      <el-option
                          v-for="item in tiptopDegrees"
                          :key="item"
                          :label="item"
                          :value="item">
                      </el-option>
                    </el-select>
                  </el-form-item>
                </el-col>
                <el-col :span="6">
                  <el-form-item label="毕业院校:" prop="school">
                    <el-input size="mini" style="width: 150px" prefix-icon="el-icon-edit"
                              v-model="emp.school" placeholder="毕业院校名称"></el-input>
                  </el-form-item>
                </el-col>
                <el-col :span="7">
                  <el-form-item label="专业名称:" prop="specialty">
                    <el-input size="mini" style="width: 200px" prefix-icon="el-icon-edit"
                              v-model="emp.specialty" placeholder="请输入专业名称"></el-input>
                  </el-form-item>
                </el-col>
              </el-row>
              <el-row>
                <el-col :span="6">
                  <el-form-item label="入职日期:" prop="beginDate">
                    <el-date-picker
                        v-model="emp.beginDate"
                        size="mini"
                        type="date"
                        value-format="yyyy-MM-dd"
                        style="width: 130px;"
                        placeholder="入职日期">
                    </el-date-picker>
                  </el-form-item>
                </el-col>
                <el-col :span="6">
                  <el-form-item label="转正日期:" prop="conversionTime">
                    <el-date-picker
                        v-model="emp.conversionTime"
                        size="mini"
                        type="date"
                        value-format="yyyy-MM-dd"
                        style="width: 130px;"
                        placeholder="转正日期">
                    </el-date-picker>
                  </el-form-item>
                </el-col>
                <el-col :span="6">
                  <el-form-item label="合同起始日期:" prop="beginContract">
                    <el-date-picker
                        v-model="emp.beginContract"
                        size="mini"
                        type="date"
                        value-format="yyyy-MM-dd"
                        style="width: 130px;"
                        placeholder="合同起始日期">
                    </el-date-picker>
                  </el-form-item>
                </el-col>
                <el-col :span="6">
                  <el-form-item label="合同终止日期:" prop="endContract">
                    <el-date-picker
                        v-model="emp.endContract"
                        size="mini"
                        type="date"
                        value-format="yyyy-MM-dd"
                        style="width: 150px;"
                        placeholder="合同终止日期">
                    </el-date-picker>
                  </el-form-item>
                </el-col>
              </el-row>
              <el-row>
                <el-col :span="8">
                  <el-form-item label="身份证号码:" prop="idCard">
                    <el-input size="mini" style="width: 180px" prefix-icon="el-icon-edit"
                              v-model="emp.idCard" placeholder="请输入身份证号码"></el-input>
                  </el-form-item>
                </el-col>
                <el-col :span="8">
                  <el-form-item label="聘用形式:" prop="engageForm">
                    <el-radio-group v-model="emp.engageForm">
                      <el-radio label="劳动合同">劳动合同</el-radio>
                      <el-radio label="劳务合同">劳务合同</el-radio>
                    </el-radio-group>
                  </el-form-item>
                </el-col>
                <el-col :span="8">
                  <el-form-item label="婚姻状况:" prop="wedlock">
                    <el-radio-group v-model="emp.wedlock">
                      <el-radio label="已婚">已婚</el-radio>
                      <el-radio label="未婚">未婚</el-radio>
                      <el-radio label="离异">离异</el-radio>
                    </el-radio-group>
                  </el-form-item>
                </el-col>
              </el-row>
            </el-form>
          </div>
          <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false" size="small">取 消</el-button>
    <el-button type="primary" @click="doAddEmp" size="small">确 定</el-button>
  </span>
        </el-dialog>
      </div>
    </div>
</template>

<script>
export default {
  name: "EmpBasic",
  data(){
    return {
      defaultProps: {
        label:'name',
        children:'children'
      },
      title: '',
      dialogVisible:false,
      emps:[],
      total : 0,
      page:1,
      size:10,
      loading:false,
      keywords:'',
      emp: {
        name: "zhuantai1111",
        gender: "男",
        birthday: "1989-12-31",
        idCard: "610122199001011256",
        wedlock: "已婚",
        nationId: 1,
        nativePlace: "陕西",
        politicId: 13,
        email: "laowang@qq.com",
        phone: "18565558897",
        address: "深圳市南山区",
        departmentId: null,
        jobLevelId: 9,
        posId: 29,
        engageForm: "劳务合同",
        tiptopDegree: "本科",
        specialty: "信息管理与信息系统",
        school: "深圳大学",
        beginDate: "2017-12-31",
        workState: "在职",
        workID: "00000057",
        contractTerm: 2,
        conversionTime: "2018-03-31",
        notworkDate: null,
        beginContract: "2017-12-31",
        endContract: "2019-12-31",
        workAge: null
      },
      rules: {
        name: [{required: true, message: '请输入用户名', trigger: 'blur'}],
        gender: [{required: true, message: '请输入性别', trigger: 'blur'}],
        birthday: [{required: true, message: '请输入出生日期', trigger: 'blur'}],
        idCard: [{required: true, message: '请输入身份证号码', trigger: 'blur'}, {
          pattern: /(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/,
          message: '身份证号码格式不正确',
          trigger: 'blur'
        }],
        wedlock: [{required: true, message: '请输入婚姻状况', trigger: 'blur'}],
        nationId: [{required: true, message: '请输入您组', trigger: 'blur'}],
        nativePlace: [{required: true, message: '请输入籍贯', trigger: 'blur'}],
        politicId: [{required: true, message: '请输入政治面貌', trigger: 'blur'}],
        email: [{required: true, message: '请输入邮箱地址', trigger: 'blur'}, {
          type: 'email',
          message: '邮箱格式不正确',
          trigger: 'blur'
        }],
        phone: [{required: true, message: '请输入电话号码', trigger: 'blur'}],
        address: [{required: true, message: '请输入员工地址', trigger: 'blur'}],
        departmentId: [{required: true, message: '请输入部门名称', trigger: 'blur'}],
        jobLevelId: [{required: true, message: '请输入职称', trigger: 'blur'}],
        posId: [{required: true, message: '请输入职位', trigger: 'blur'}],
        engageForm: [{required: true, message: '请输入聘用形式', trigger: 'blur'}],
        tiptopDegree: [{required: true, message: '请输入学历', trigger: 'blur'}],
        specialty: [{required: true, message: '请输入专业', trigger: 'blur'}],
        school: [{required: true, message: '请输入毕业院校', trigger: 'blur'}],
        beginDate: [{required: true, message: '请输入入职日期', trigger: 'blur'}],
        workState: [{required: true, message: '请输入工作状态', trigger: 'blur'}],
        workID: [{required: true, message: '请输入工号', trigger: 'blur'}],
        contractTerm: [{required: true, message: '请输入合同期限', trigger: 'blur'}],
        conversionTime: [{required: true, message: '请输入转正日期', trigger: 'blur'}],
        notworkDate: [{required: true, message: '请输入离职日期', trigger: 'blur'}],
        beginContract: [{required: true, message: '请输入合同起始日期', trigger: 'blur'}],
        endContract: [{required: true, message: '请输入合同结束日期', trigger: 'blur'}],
        workAge: [{required: true, message: '请输入工龄', trigger: 'blur'}],
      },
      nations:[],
      politicsstatus:[],
      jobLevels:[],
      allDeps:[],
      tiptopDegrees: ['本科', '大专', '硕士', '博士', '高中', '初中', '小学', '其他'],
      positions:[],
      popVisible:false,
      inputDepName: '所属部门',
    }

  },
  mounted() {
    this.initEmps();
    this.initData();
  },
  methods:{
    initPositions() {
      this.getRequest("/emp/basic/positions").then(resp=>{
        if (resp){
          this.positions = resp;
        }
      });
    },
    initData(){
      if (!window.sessionStorage.getItem("nations")){
        this.getRequest("/emp/basic/nations").then(resp=>{
          if (resp){
            this.nations = resp;
            window.sessionStorage.setItem("nations",JSON.stringify(resp));
          }
        });
      }else{
        this.nations = JSON.parse(window.sessionStorage.getItem("nations"));
      }
      if (!window.sessionStorage.getItem("politicsstatus")){
        this.getRequest("/emp/basic/politicsstatus").then(resp=>{
          if (resp){
            this.politicsstatus = resp;
            window.sessionStorage.setItem("politicsstatus",JSON.stringify(resp));
          }
        });
      }else{
        this.politicsstatus = JSON.parse(window.sessionStorage.getItem("politicsstatus"));
      }
      if (!window.sessionStorage.getItem("jobLevels")){
        this.getRequest("/emp/basic/jobLevels").then(resp=>{
          if (resp){
            this.jobLevels = resp;
            window.sessionStorage.setItem("jobLevels",JSON.stringify(resp));
          }
        });
      }else{
        this.jobLevels = JSON.parse(window.sessionStorage.getItem("jobLevels"));
      }
      if (!window.sessionStorage.getItem("deps")){
        this.getRequest("/emp/basic/deps").then(resp=>{
          if (resp){
            this.allDeps = resp;
            window.sessionStorage.setItem("deps",JSON.stringify(resp));
          }
        });
      }else{
        this.allDeps = JSON.parse(window.sessionStorage.getItem("deps"));
      }
    },
    initEmps(){
      this.loading = true;
      this.getRequest("/emp/basic/?page="+this.page+"&size="+this.size+"&keywords="+this.keywords).then(resp=>{
        this.loading = false;
        if (resp){
          this.emps = resp.data;
          this.total = resp.total;
        }
      })
    },
    currentChange(currentPage){
      this.page = currentPage;
      this.initEmps();
    },
    sizeChange(size){
      this.size = size;
      this.initEmps();
    },
    showAddEmpView(){
      this.emptyEmp();
      this.dialogVisible = true;
      this.initPositions();
      this.initData();
      this.getMaxWorkID();
      this.title="添加员工";
    },
    getMaxWorkID(){
      this.getRequest("/emp/basic/maxWorkID").then(resp=>{
        if (resp){
          this.emp.workID = resp.obj;
        }
      })
    },
    showDepView(){
        this.popVisible = !this.popVisible;
    },
    handleNodeClick(data){
        this.popVisible = !this.popVisible;
        this.inputDepName  = data.name;
        this.emp.departmentId = data.id;
    },
    doAddEmp(){
      if (this.emp.id) {
        this.$refs['empForm'].validate(valid => {
          if (valid) {
            this.putRequest("/emp/basic/", this.emp).then(resp => {
              if (resp) {
                this.dialogVisible = false;
                this.initEmps();
              }
            })
          }
        });
      } else {
        this.$refs['empForm'].validate(valid => {
          if (valid) {
            this.postRequest("/emp/basic/", this.emp).then(resp => {
              if (resp) {
                this.dialogVisible = false;
                this.initEmps();
              }
            })
          }
        });
      }
    },
    deleteEmp(row){
      this.$confirm('此操作将永久删除这【'+row.name+'】条记录, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.deleteRequest("/emp/basic/"+row.id).then(resp=>{
          if (resp){
            this.initEmps();
          }
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    },
    emptyEmp() {
      this.emp = {
        name: "",
        gender: "",
        birthday: "",
        idCard: "",
        wedlock: "",
        nationId: 1,
        nativePlace: "",
        politicId: 13,
        email: "",
        phone: "",
        address: "",
        departmentId: null,
        jobLevelId: 9,
        posId: 29,
        engageForm: "",
        tiptopDegree: "",
        specialty: "",
        school: "",
        beginDate: "",
        workID: "",
        contractTerm: 2,
        conversionTime: "",
        notworkDate: null,
        beginContract: "",
        endContract: "",
        workAge: null
      }
      this.inputDepName = '';
    },
    showEditEmpView(data) {
      this.initPositions();
      this.title = '编辑员工信息';
      this.emp = data;
      this.inputDepName = data.department.name;
      this.dialogVisible = true;
    },
  },


}
</script>

<style>

/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.slide-fade-enter-active {
  transition: all .8s ease;
}
.slide-fade-leave-active {
  transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
  /* .slide-fade-leave-active for below version 2.1.8 */
{
  transform: translateX(10px);
  opacity: 0;
}

</style>

EmpBasicController.java

package com.zhuantai.voa.controller.emp;

import com.zhuantai.voa.model.*;
import com.zhuantai.voa.service.*;
import com.zhuantai.voa.utils.RespBean;
import com.zhuantai.voa.utils.RespPageBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/16 15:54
 */
@RestController
@RequestMapping("/emp/basic")
public class EmpBasicController {

    @Autowired
    EmployeeService employeeService;

    @Autowired
    NationService nationService;

    @Autowired
    PoliticsstatusService politicsstatusService;

    @Autowired
    JobLevelService jobLevelService;

    @Autowired
    PositionService positionService;

    @Autowired
    DepartmentService departmentService;

    @GetMapping("/")
    public RespPageBean getEmployeeByPage(@RequestParam(defaultValue = "1") Integer page,
                                          @RequestParam(defaultValue = "10")Integer size,
                                          String keywords
                                          )
    {
        return employeeService.getEmployeeByPage(page,size,keywords);
    }

    @PostMapping("/")
    public RespBean addEmp(@RequestBody Employee employee){
        if (employeeService.addEmp(employee) != 1) {
            return RespBean.error("添加失败!");
        }
        return RespBean.ok("添加成功!");
    }

    @GetMapping("/nations")
    public List<Nation> getAllNations(){
        return nationService.getAllNations();
    }

    @GetMapping("/politicsstatus")
    public List<Politicsstatus> getAllPoliticsstatus(){
        return politicsstatusService.getAllPoliticsstatus();
    }

    @GetMapping("/jobLevels")
    public List<JobLevel> getAllJobLevels(){
        return jobLevelService.getAllJobLevels();
    }

    @GetMapping("/positions")
    public List<Position> getAllPositions(){
        return positionService.getAllPositions();
    }

    @GetMapping("/maxWorkID")
    public RespBean maxWorkID(){
        return RespBean.build().setStatus(200).setObj(String.format("%08d",employeeService.maxWorkID()+1));
    }

    @GetMapping("/deps")
    public List<Department> getAllDepartments(){
        return departmentService.getAllDepartments();
    }

    @DeleteMapping("/{id}")
    public RespBean deleteEmpByEid(@PathVariable Integer id){
        if (employeeService.deleteEmpByEid(id) == 1){
            return RespBean.ok("删除成功!");
        }
        return RespBean.error("删除失败!");
    }

    @PutMapping("/")
    public RespBean updateEmp(@RequestBody Employee employee){
        if (employeeService.updateEmp(employee) == 1){
            return RespBean.ok("更新成功!");
        }
        return RespBean.error("更新失败!");
    }
}
2.自动计算合同期限问题

EmpBasicService.java

package com.zhuantai.voa.service;

import com.zhuantai.voa.mapper.EmployeeMapper;
import com.zhuantai.voa.mapper.EmployeeecMapper;
import com.zhuantai.voa.model.Employee;
import com.zhuantai.voa.utils.RespPageBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/16 15:57
 */
@Service
public class EmployeeService {
    @Autowired
    EmployeeMapper employeeMapper;

    SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
    SimpleDateFormat monthFormat = new SimpleDateFormat("MM");
    DecimalFormat decimalFormat = new DecimalFormat("##.00");

    public RespPageBean getEmployeeByPage(Integer page, Integer size, String keywords) {
        if (page != null && size != null){
            page = (page -1) * size;
        }
        List<Employee> employeeList = employeeMapper.getEmployeeByPage(page,size,keywords);
        Long total = employeeMapper.getTotal(keywords);
        RespPageBean respPageBean = new RespPageBean();
        respPageBean.setData(employeeList);
        respPageBean.setTotal(total);
        return respPageBean;
    }
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    public Integer addEmp(Employee employee) {
        Date beginContract = employee.getBeginContract();
        Date endContract = employee.getEndContract();
        double month = (Double.parseDouble(yearFormat.format(endContract)) - Double.parseDouble(yearFormat.format(beginContract)))
                * 12
                + (Double.parseDouble(monthFormat.format(endContract)) - Double.parseDouble(monthFormat.format(beginContract)));

        employee.setContractTerm(Double.parseDouble(decimalFormat.format(month/12)));
        return employeeMapper.insertSelective(employee);
    }
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    public Integer maxWorkID() {
        return employeeMapper.maxWorkID();
    }

    public Integer deleteEmpByEid(Integer id) {
        return employeeMapper.deleteByPrimaryKey(id);
    }
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    public Integer updateEmp(Employee employee) {
        Date beginContract = employee.getBeginContract();
        Date endContract = employee.getEndContract();
        double month = (Double.parseDouble(yearFormat.format(endContract)) - Double.parseDouble(yearFormat.format(beginContract)))
                * 12
                + (Double.parseDouble(monthFormat.format(endContract)) - Double.parseDouble(monthFormat.format(beginContract)));

        employee.setContractTerm(Double.parseDouble(decimalFormat.format(month/12)));
        return employeeMapper.updateByPrimaryKeySelective(employee);
    }
}
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

3.员工资料导出

使用POI

<!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
</dependency>

POIUtils.java

package com.zhuantai.voa.utils;

import com.zhuantai.voa.model.Employee;
import org.apache.poi.hpsf.DocumentSummaryInformation;
import org.apache.poi.hpsf.SummaryInformation;
import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/17 20:10
 */
public class POIUtils {
    public static ResponseEntity<byte[]> employeeToExcel(List<Employee> list) {
        //1.创建excel文档
        HSSFWorkbook workbook = new HSSFWorkbook();
        //2.创建文档摘要
        workbook.createInformationProperties();
        //3.配置文档摘要信息
        DocumentSummaryInformation docInfo = workbook.getDocumentSummaryInformation();
        docInfo.setCategory("员工信息");//设置文档类别
        docInfo.setManager("朱安泰");//文档管理员
        docInfo.setCompany("antia11.top");//设置公司名
        //4.获取文档摘要信息
        SummaryInformation summaryInformation = workbook.getSummaryInformation();
        summaryInformation.setTemplate("员工信息表");//这种文档标题
        summaryInformation.setAuthor("zhuantai");//设置作者
        summaryInformation.setComments("本文档由 朱安泰 提供");

        //5.创建样式
        CellStyle headerCellStyle = workbook.createCellStyle();
        headerCellStyle.setFillForegroundColor(IndexedColors.YELLOW.index);
        headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //6.配置表单信息
        HSSFSheet sheet = workbook.createSheet("员工信息表");//表单


        HSSFRow titleRow = sheet.createRow(0);//标题行
        String [] cname = {"编号" , //标题行列名
                "姓名" ,
                "工号" ,
                "性别" ,
                "出生日期" ,
                "身份证号" ,
                "婚姻状况" ,
                "民族" ,
                "籍贯" ,
                "政治面貌" ,
                "电子邮件" ,
                "电话号码" ,
                "联系地址" ,
                "所属部门" ,
                "职位" ,
                "职称" ,
                "聘用形式" ,
                "最高学历" ,
                "专业" ,
                "毕业院校" ,
                "入职日期" ,
                "转正日期" ,
                "合同起始日期" ,
                "合同截止日期" ,
                "合同期限"};


        //配置列信息
        //日期类列样式
        HSSFCellStyle dateCellStyle = workbook.createCellStyle();
        dateCellStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat("m/d/yy"));


        for (int i = 0; i < cname.length; i++) {
            HSSFCell c = titleRow.createCell(i);//第1行第1列
            sheet.autoSizeColumn(i);//设置列宽度自适应
            c.setCellValue(cname[i]);
            c.setCellStyle(headerCellStyle);

        }

        for (int j = 0; j < list.size(); j++) {
            Employee emp = list.get(j);
            HSSFRow row = sheet.createRow(j + 1);
            row.createCell(0).setCellValue(emp.getId());
            row.createCell(1).setCellValue(emp.getName());
            row.createCell(2).setCellValue(emp.getWorkID());
            row.createCell(3).setCellValue(emp.getGender());
            HSSFCell cell4 = row.createCell(4);
            cell4.setCellStyle(dateCellStyle);
            cell4.setCellValue(emp.getBirthday());
            row.createCell(5).setCellValue(emp.getIdCard());
            row.createCell(6).setCellValue(emp.getWedlock());
            row.createCell(7).setCellValue(emp.getNation().getName());
            row.createCell(8).setCellValue(emp.getNativePlace());
            row.createCell(9).setCellValue(emp.getPoliticsstatus().getName());
            row.createCell(10).setCellValue(emp.getEmail());
            row.createCell(11).setCellValue(emp.getPhone());
            row.createCell(12).setCellValue(emp.getAddress());
            row.createCell(13).setCellValue(emp.getDepartment().getName());
            row.createCell(14).setCellValue(emp.getPosition().getName());
            row.createCell(15).setCellValue(emp.getJobLevel().getName());
            row.createCell(16).setCellValue(emp.getEngageForm());
            row.createCell(17).setCellValue(emp.getTiptopDegree());
            row.createCell(18).setCellValue(emp.getSpecialty());
            row.createCell(19).setCellValue(emp.getSchool());
            HSSFCell cell20 = row.createCell(20);
            cell20.setCellStyle(dateCellStyle);
            cell20.setCellValue(emp.getBeginDate());

            HSSFCell cell21 = row.createCell(21);
            cell21.setCellStyle(dateCellStyle);
            cell21.setCellValue(emp.getConversionTime());

            HSSFCell cell22 = row.createCell(22);
            cell22.setCellStyle(dateCellStyle);
            cell22.setCellValue(emp.getBeginContract());

            HSSFCell cell23 = row.createCell(23);
            cell23.setCellStyle(dateCellStyle);
            cell23.setCellValue(emp.getEndContract());
            row.createCell(24).setCellValue(emp.getContractTerm());
        }


        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HttpHeaders headers = new HttpHeaders();
        try {
            headers.setContentDispositionFormData("attachment", new String("员工表.xls".getBytes("UTF-8"), "ISO-8859-1"));
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            workbook.write(baos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ResponseEntity<byte[]>(baos.toByteArray(), headers, HttpStatus.CREATED);
    }
}
4.员工资料导入

POIUtils.java

package com.zhuantai.voa.utils;

import com.zhuantai.voa.model.*;
import org.apache.poi.hpsf.DocumentSummaryInformation;
import org.apache.poi.hpsf.SummaryInformation;
import org.apache.poi.hssf.usermodel.*;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * @author ANTIA1
 * @date 2021/7/17 20:10
 */
public class POIUtils {
    public static ResponseEntity<byte[]> employeeToExcel(List<Employee> list) {
        //1.创建excel文档
        HSSFWorkbook workbook = new HSSFWorkbook();
        //2.创建文档摘要
        workbook.createInformationProperties();
        //3.配置文档摘要信息
        DocumentSummaryInformation docInfo = workbook.getDocumentSummaryInformation();
        docInfo.setCategory("员工信息");//设置文档类别
        docInfo.setManager("朱安泰");//文档管理员
        docInfo.setCompany("antia11.top");//设置公司名
        //4.获取文档摘要信息
        SummaryInformation summaryInformation = workbook.getSummaryInformation();
        summaryInformation.setTemplate("员工信息表");//这种文档标题
        summaryInformation.setAuthor("zhuantai");//设置作者
        summaryInformation.setComments("本文档由 朱安泰 提供");

        //5.创建样式
        CellStyle headerCellStyle = workbook.createCellStyle();
        headerCellStyle.setFillForegroundColor(IndexedColors.YELLOW.index);
        headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        //6.配置表单信息
        HSSFSheet sheet = workbook.createSheet("员工信息表");//表单


        HSSFRow titleRow = sheet.createRow(0);//标题行
        String [] cname = {
                "编号" , //标题行列名
                "姓名" ,
                "工号" ,
                "性别" ,
                "出生日期" ,
                "身份证号" ,
                "婚姻状况" ,
                "民族" ,
                "籍贯" ,
                "政治面貌" ,
                "电话号码" ,
                "联系地址" ,
                "所属部门" ,
                "职称" ,
                "职位" ,
                "聘用形式" ,
                "最高学历" ,
                "专业" ,
                "毕业院校" ,
                "入职日期" ,
                "在职状态" ,
                "邮箱" ,
                "合同期限(年)" ,
                "合同起始日期",
                "合同终止日期",
                "转正日期"
        };


        //配置列信息
        //日期类列样式
        HSSFCellStyle dateCellStyle = workbook.createCellStyle();
        dateCellStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat("m/d/yy"));


        for (int i = 0; i < cname.length; i++) {
            HSSFCell c = titleRow.createCell(i);//第1行第1列
            sheet.autoSizeColumn(i);//设置列宽度自适应
            c.setCellValue(cname[i]);
            c.setCellStyle(headerCellStyle);

        }

        for (int j = 0; j < list.size(); j++) {
            Employee emp = list.get(j);
            HSSFRow row = sheet.createRow(j + 1);
            row.createCell(0).setCellValue(emp.getId());
            row.createCell(1).setCellValue(emp.getName());
            row.createCell(2).setCellValue(emp.getWorkID());
            row.createCell(3).setCellValue(emp.getGender());
            HSSFCell cell4 = row.createCell(4);
            cell4.setCellStyle(dateCellStyle);
            cell4.setCellValue(emp.getBirthday());
            row.createCell(5).setCellValue(emp.getIdCard());
            row.createCell(6).setCellValue(emp.getWedlock());
            row.createCell(7).setCellValue(emp.getNation().getName());
            row.createCell(8).setCellValue(emp.getNativePlace());
            row.createCell(9).setCellValue(emp.getPoliticsstatus().getName());
            row.createCell(10).setCellValue(emp.getPhone());
            row.createCell(11).setCellValue(emp.getAddress());
            row.createCell(12).setCellValue(emp.getDepartment().getName());
            row.createCell(13).setCellValue(emp.getJobLevel().getName());
            row.createCell(14).setCellValue(emp.getPosition().getName());
            row.createCell(15).setCellValue(emp.getEngageForm());
            row.createCell(16).setCellValue(emp.getTiptopDegree());
            row.createCell(17).setCellValue(emp.getSpecialty());
            row.createCell(18).setCellValue(emp.getSchool());
            HSSFCell cell19 = row.createCell(19);
            cell19.setCellStyle(dateCellStyle);
            cell19.setCellValue(emp.getBeginDate());
            row.createCell(20).setCellValue(emp.getWorkState());
            row.createCell(21).setCellValue(emp.getEmail());
            row.createCell(22).setCellValue(emp.getContractTerm());
            HSSFCell cell23 = row.createCell(23);
            cell23.setCellStyle(dateCellStyle);
            cell23.setCellValue(emp.getBeginContract());
            HSSFCell cell24 = row.createCell(24);
            cell24.setCellStyle(dateCellStyle);
            cell24.setCellValue(emp.getEndContract());
            HSSFCell cell25 = row.createCell(25);
            cell25.setCellStyle(dateCellStyle);
            cell25.setCellValue(emp.getConversionTime());
        }


        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HttpHeaders headers = new HttpHeaders();
        try {
            headers.setContentDispositionFormData("attachment", new String("员工表.xls".getBytes("UTF-8"), "ISO-8859-1"));
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            workbook.write(baos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ResponseEntity<byte[]>(baos.toByteArray(), headers, HttpStatus.CREATED);
    }

    /**
     * excel解析成员工集合
     * @param file
     * @param allNations
     * @param allPoliticsstatus
     * @param allPositions
     * @param allJobLevels
     */
    public static List<Employee> excelToEmployee(MultipartFile file,
                                       List<Nation> allNations,
                                       List<Politicsstatus> allPoliticsstatus,
                                       List<Department> allDepartments,
                                       List<Position> allPositions,
                                       List<JobLevel> allJobLevels)
    {
        List<Employee> list = new ArrayList<>();
        Employee employee = null;

        try {
            //1,创建一个workbook对象
            HSSFWorkbook workbook = new HSSFWorkbook(file.getInputStream());
            //2. 获取 workbook 表单中的数量
            int numberOfSheets = workbook.getNumberOfSheets();
            for (int i = 0; i < numberOfSheets; i++) {
                //3.获取表单
                HSSFSheet sheet = workbook.getSheetAt(i);
                //4.获取表单中的行数
                int rowCount = sheet.getPhysicalNumberOfRows();//获取行数
                for (int j = 0; j < rowCount; j++) {
                    if (j == 0){
                        continue;//跳过标题行
                    }
                    //6.获取行
                    HSSFRow row = sheet.getRow(j);
                    if (row == null){
                        continue;//防止中间有空行
                    }
                    //7.获取表单里面的列
                    int cellCount = row.getPhysicalNumberOfCells();
                    employee = new Employee();
                    for (int k = 0; k < cellCount; k++) {
                        HSSFCell cell = row.getCell(k);
                        switch (cell.getCellType()) {
                            case STRING:
                                String cellValue = cell.getStringCellValue();
                                switch (k) {
                                    case 1:
                                        employee.setName(cellValue);
                                        break;
                                    case 2:
                                        employee.setWorkID(cellValue);
                                        break;
                                    case 3:
                                        employee.setGender(cellValue);
                                        break;
                                    case 5:
                                        employee.setIdCard(cellValue);
                                        break;
                                    case 6:
                                        employee.setWedlock(cellValue);
                                        break;
                                    case 7:
                                        int nationIndex = allNations.indexOf(new Nation(cellValue));
                                        employee.setNationId(allNations.get(nationIndex).getId());
                                        break;
                                    case 8:
                                        employee.setNativePlace(cellValue);
                                        break;
                                    case 9:
                                        int politicstatusIndex = allPoliticsstatus.indexOf(new Politicsstatus(cellValue));
                                        employee.setPoliticId(allPoliticsstatus.get(politicstatusIndex).getId());
                                        break;
                                    case 10:
                                        employee.setPhone(cellValue);
                                        break;
                                    case 11:
                                        employee.setAddress(cellValue);
                                        break;
                                    case 12:
                                        int departmentIndex = allDepartments.indexOf(new Department(cellValue));
                                        employee.setDepartmentId(allDepartments.get(departmentIndex).getId());
                                        break;
                                    case 13:
                                        int jobLevelIndex = allJobLevels.indexOf(new JobLevel(cellValue));
                                        employee.setJobLevelId(allJobLevels.get(jobLevelIndex).getId());
                                        break;
                                    case 14:
                                        int positionIndex = allPositions.indexOf(new Position(cellValue));
                                        employee.setPosId(allPositions.get(positionIndex).getId());
                                        break;
                                    case 15:
                                        employee.setEngageForm(cellValue);
                                        break;
                                    case 16:
                                        employee.setTiptopDegree(cellValue);
                                        break;
                                    case 17:
                                        employee.setSpecialty(cellValue);
                                        break;
                                    case 18:
                                        employee.setSchool(cellValue);
                                        break;
                                    case 20:
                                        employee.setWorkState(cellValue);
                                        break;
                                    case 21:
                                        employee.setEmail(cellValue);
                                        break;
                                }
                                break;
                            default: {
                                switch (k) {
                                    case 4:
                                        employee.setBirthday(cell.getDateCellValue());
                                        break;
                                    case 19:
                                        employee.setBeginDate(cell.getDateCellValue());
                                        break;
                                    case 23:
                                        employee.setBeginContract(cell.getDateCellValue());
                                        break;
                                    case 24:
                                        employee.setEndContract(cell.getDateCellValue());
                                        break;
                                    case 22:
                                        employee.setContractTerm(cell.getNumericCellValue());
                                        break;
                                    case 25:
                                        employee.setConversionTime(cell.getDateCellValue());
                                        break;
                                }
                            }
                            break;
                        }
                    }
                    list.add(employee);
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
        return list;
    }
}

七、搭建邮件服务

1.准备消息中间件

通过DOCKER安装RabbitMQ

docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672  rabbitmq:3-management

安装好后启动。

2.配置文件

application.properties

server.port=8082

spring.mail.host=smtp.qq.com
spring.mail.protocol=smtp
spring.mail.default-encoding=UTF-8
spring.mail.password=XXXXXXX
spring.mail.username=xxxx@qq.com
spring.mail.port=587
spring.mail.properties.mail.stmp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.properties.maill.debug=true


spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=ip
spring.rabbitmq.port=5672

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit-test</artifactId>
    <scope>test</scope>
</dependency>
3.编写邮件模板

mail.html

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
    
    <title>入职欢迎邮件</title>
</head>
<body>
    欢迎 <span th:text="${name}"></span> 加入本公司,您的入职信息如下:
<table border="1">
    <tr>
        <td>姓名</td>
        <td th:text="${name}"></td>
    </tr>
    <tr>
        <td>职称</td>
        <td th:text="${jobLevelName}"></td>
    </tr>
    <tr>
        <td>职位</td>
        <td th:text="${posname}"></td>
    </tr>
    <tr>
        <td>部门</td>
        <td th:text="${depname}"></td>
    </tr>
</table>

<p>希望在未来的日子里,携手共进!</p>
</body>
</html>
4.配置QUEUE_BEAN
@Bean
Queue queue(){
    return new Queue("zhuantai.mail.welcome");
}
5.配置Receiver

MailRecelver.java

package com.zhuantai.mailserver.receiver;

import com.zhuantai.voa.model.Employee;
import com.zhuantai.voa.model.Hr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.Date;

/**
 * @author ANTIA1
 * @date 2021/7/20 15:35
 */
@Component
public class MailReceiver {

    public static final Logger logger = LoggerFactory.getLogger(MailReceiver.class);

    @Autowired
    JavaMailSender javaMailSender;

    @Autowired
    MailProperties mailProperties;

    @Autowired
    TemplateEngine templateEngine;



    @RabbitListener(queues = "zhuantai.mail.welcome")
    public void handler(Employee employee){
        logger.info(employee.toString());
        //收到消息,发送邮件
        MimeMessage msg = javaMailSender.createMimeMessage();
        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(msg);
        try {
            mimeMessageHelper.setTo(employee.getEmail());
            mimeMessageHelper.setFrom(mailProperties.getUsername());
            mimeMessageHelper.setSubject("入职欢迎邮件");
            mimeMessageHelper.setSentDate(new Date());
            Context context = new Context();
            context.setVariable("name",employee.getName());
            context.setVariable("jobLevelName",employee.getDepartment().getName());
            context.setVariable("posname",employee.getPosition().getName());
            context.setVariable("depname",employee.getDepartment().getName());
            String mail = templateEngine.process("mail", context);
            mimeMessageHelper.setText(mail,true);

            javaMailSender.send(msg);
        } catch (MessagingException e) {
            logger.error("邮件发送失败:"+e.getMessage());
        }
    }
}
6.调用

在添加员工时调用,发送邮件到员工邮箱

EmployeeService.java

@Autowired
RabbitTemplate rabbit;

public Integer addEmp(Employee employee) {
    Date beginContract = employee.getBeginContract();
    Date endContract = employee.getEndContract();
    double month = (Double.parseDouble(yearFormat.format(endContract)) - Double.parseDouble(yearFormat.format(beginContract)))
        * 12
        + (Double.parseDouble(monthFormat.format(endContract)) - Double.parseDouble(monthFormat.format(beginContract)));

    employee.setContractTerm(Double.parseDouble(decimalFormat.format(month/12)));
    int i = employeeMapper.insertSelective(employee);
    if (i == 1){
        Employee emp = employeeMapper.getEmployeeById(employee.getId());
        >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        rabbit.convertAndSend("zhuantai.mail.welcome",emp);
        >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        LOGGER.info(emp.toString());
    }
    return i;
}

八、在线聊天功能

推荐阅读