微人事项目学习笔记

1. 准备工作

为了能完全按照视频教程完成项目,避免出现版本不一致导致的问题,所选择的开发环境按照视频开发环境准备。松哥完整的项目地址:微人事,其中包含了所有需要的文件。教学视频想具体详细了解内容的,请关注微信公众号:江南一点雨。

我自己最终完成的微人事项目地址(内含有我个人理解的注释)

1.1 数据库准备

由于我电脑中原来安装的 MySQL 的版本是8.0版本,但是视频中的 MySQL 版本是5.7版本。为了减少自己手动卸载安装麻烦,我选择在 Linux 虚拟机中使用 docker 安装(docker 真好用!)。

首先,在 docker 中添加并启动一个 MySQL 5.7 容器。

 docker run --name promyql -p 3306:3306 -d -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7

在宿主机中使用 Navicat 软件进行远程连接 MySQL数据库。
img-cyTnD9zN-1629599678657

最后,运行数据库脚本,导入数据库数据。
img-fITAOXt5-1629599678659]

1.2 Vue 准备

在微人事项目中,采用的 vue 版本是 vue 2.x ,vue cli 版本是 3.x 。截止目前 2021年 vue 已经出到了 vue 3.x ,vue cli 4.x。这里还是选择用相同版本进行开发,原因是菜鸟,你以为我没有试过用最新版本来写吗?因为版本差异出现的问题太多了。所以第一次还是老老实实跟着教程写吧。

创建 vue 项目的方式有很多种,默认已经装完所有 vue 需要的环境。为了加深理解,我选择通过命令行的方式进行创建 vue 工程。

首先,在将要创建项目的文件夹中打开 cmd 窗口(在目录中输入 cmd)。

其次,在命令行窗口中输入以下命令:

# proName 表示项目的名称
vue create proName

根据命令提示,创建vue cli 3版本。

1.3 Spring Boot 准备

创建一个 Spring Boot项目,导入以下依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.2.0</version>
</dependency>
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid-spring-boot-starter</artifactId>
  <version>1.1.10</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
  <version>5.1.28</version>
</dependency>

关于 Mybatis 逆向工程创建 mapper 等文件有很多种方式,这里使用 松哥提供的文件进行生成。

链接:https://pan.baidu.com/s/1LSbxo9Q369tuc-CQx7FNjw
提取码:rm2m

首先,将压缩包进行解压,并修改 generator.xml 文件内容。主要修改内容为 数据库连接地址,用户和密码以及生成文件夹路径。
img-cP0aPZi0-1629599678660
最后,双击 123.bat文件就会自动生成需要的文件。

2. 前后端配置

2.1 后端返回消息对象定义

public class RespBean {
    private Integer status;
    private String msg;
    private Object obj;

    public static RespBean success(String msg) {
        return new RespBean(200, msg, null);
    }

    public static RespBean success(String msg, Object obj) {
        return new RespBean(200, msg, obj);
    }


    public static RespBean error(String msg) {
        return new RespBean(500, msg, null);
    }

    public static RespBean error(String msg, Object obj) {
        return new RespBean(500, msg, obj);
    }


    private RespBean() {
    }

    private RespBean(Integer status, String msg, Object obj) {
        this.status = status;
        this.msg = msg;
        this.obj = obj;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }
}

2.2 Spring Security 设置统一返回数据

分别定义successHandlerfailureHandlerlogoutSuccessHandler 响应体内容以便前端获取数据。

@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 out = resp.getWriter();
                    // 从 authentication 中获取用户信息
                    Hr hr = (Hr) authentication.getPrincipal();
                    // 设置返回信息的用户密码为 null,避免被恶意拦截
                    hr.setPassword("");
                    // 设置返回信息
                    RespBean success = RespBean.success("登录成功", hr);
                    // 将对象转成字符串
                    String s = new ObjectMapper().writeValueAsString(success);
                    out.write(s);
                    out.flush();
                    out.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 out = 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("用户名或密码输入错误,请重新输入!");
                    }
                    out.write(new ObjectMapper().writeValueAsString(respBean));
                    out.flush();
                    out.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 out = resp.getWriter();
                    Hr hr = (Hr) authentication.getPrincipal();
                    RespBean respBean = RespBean.success("注销成功!", hr);
                    out.write(new ObjectMapper().writeValueAsString(respBean));
                    out.flush();
                    out.close();
                }
            })
            .permitAll()
            .and()
            .csrf().disable();
}

2.3 前端统一拦截响应请求内容

比如请求失败信息统一处理,不在各个页面单独处理。

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

// 拦截服务器响应体
axios.interceptors.response.use(success => {
  // 如果响应体的状态为 200 并且返回消息状态码为 500,表示请求失败
  if (success.status && success.status == 200 && success.data.status == 500) {
    Message.error({message: success.data.msg})
    // 失败信息就不再放行响应体
    return ;
  }
  // 如果响应体中有消息
  if (success.data.msg) {
    Message.success({message: success.data.msg})
  }
  return success.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 ;
});

2.4 前端登录接口数据处理

由于后端 SpringSecurity 的登录接口默认采用 key/value的形式,而 vue 默认传参的格式为 JSON,所以要求前端传过来的数据格式也要是 key/value 格式。

// 登录请求单独封装,原因是登录接口接收的参数类型为 key/value 形式
export const postKeyValueRequest = (url, params) => {
  return axios({
    method: 'post',
    // 通过引用进行拼接
    url: `${base}${url}`,
    data: params,
    // 将JSON格式转换成 key/value 格式
    transformRequest: [function (data) {
      let ret = '';
      for (let i in data) {
        ret += encodeURIComponent(i) + '=' + encodeURIComponent(data[i]) + '&'
      }
      return ret;
    }],
    headers: {
      'context-Tupe': 'application/x-www-form-urlencoded'
    }
  })
};

2.4 跨域问题

解决跨域问题有很多种方式,具体解决方法请看之前的笔记。

由于微人事是一个前后端分离项目,而 vue 项目端口使用的是 8080 ,而 Spring Boot 项目端口为 8081。这就导致出现了跨域问题,但是这并不是真正的跨域,这只是在开发环境中存在的跨域,当我们把项目部署到 Nginx 后,这个问题就不在存在了。解决这个问题可以直接在 Spring Boot 项目中进行配置,也可以通过 node.js 代理解决。node.js 代理 解决思路是,前端将请求发送到 node 中,node 再将请求转发到后端,反之亦然。

具体实现:

在前端根目录下创建 vue.config.js 文件,添加完以下配置文件后这个问题就解决了。至于具体代码含义我也不太清楚,毕竟我没学过 nodejs,等以后有时间在学吧。

let proxyObj = {};
// 注意 /api 表示当前端请求路径中带有 api 是对后端接口发起的请求,将通过 node 代理获取后台服务器接口数据。
// 踩过的坑: 当 proxyObj['/api'] 为 proxyObj[''] 时 表示在页面中所有的路径请求都通过 node 代理去寻找后端接口,导致无法通过访问路径的方式去访问 vue router 中提供的页面数据。报 404 。  
proxyObj['/api'] = { 
  ws: false,
  target: 'http://localhost:8081',
  changeOrigin: true,
  pathRewrite: {
    '^/api': ''
  }
};
module.exports = {
  devServer: {
    host: 'localhost',
    port: '8080',
    proxy: proxyObj
  }
};

3. 接口调用

3.1 请求接口封装

针对 post、put、get、delete 请求进行封装,

let base = 'api';

// 请求方法封装
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 getRequest = (url, params) => {
  return axios({
    method: 'get',
    url: `${base}${url}`,
    data: params
  })
};

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

3.2 关于动态路由菜单的实现

通过不同的用户具有不同的用户权限,在 home 主页为用户展示不同权限菜单,可以提升用户使用的舒适度。比如 admin 角色具有整个管理系统所有的权限,那么当他进行登录时,home 主页则显示出所有的功能菜单。而比如普通用户他只具备员工信息查询、和工资信息查询等几项功能,那么当他进行登录是,home 主页则显示出他具备权限访问的功能菜单。

要实现这个功能,在实际开发中并不可能为每个角色创建不同的角色菜单。可以通过以下这种思路实现:

  1. 前端在登录时向后端接口请求角色菜单数据。
  2. 后端根据数据查询出来的角色权限菜单返回给前端。
  3. 前端通过 vue router 进行动态渲染菜单。

实现这一过程,主要注意几个问题。

  • 数据库设计与查询
  • 后端返回数据格式
  • 前端针对数据进行动态渲染
3.2.1 数据库设计

主要涉及到 menu 、hr_role、menu_role 这三张表。

menu 表结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RTMGIIWS-1629599678662)(https://i.loli.net/2021/08/05/xM9RrqAoTmY7tVh.png)]

menu_role 表结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJQXSR0Z-1629599678663)(https://i.loli.net/2021/08/05/YG2lt4JPO17LFEX.png)]

hr_role 表结构:
image-20210805153114791
数据库查询语句:

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`
3.2.2 后端查询

实体类设计:

public class Meta implements Serializable {
    private Boolean keepAlive;

    private Boolean requireAuth;

    public Boolean getKeepAlive() {
        return keepAlive;
    }

    public void setKeepAlive(Boolean keepAlive) {
        this.keepAlive = keepAlive;
    }

    public Boolean getRequireAuth() {
        return requireAuth;
    }

    public void setRequireAuth(Boolean requireAuth) {
        this.requireAuth = requireAuth;
    }
}
public class Menu implements Serializable {
    private Integer id;

    private String url;

    private String path;

    private String component;

    private String name;

    private String iconCls;
	// 将 keepAlive requireAuth进行抽离
    private Meta meta;

    private Integer parentId;

    private Boolean enabled;
    // 子菜单
    private List<Menu> children;
    // 具有的角色
    private List<Role> roles;
    
    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

    public Meta getMeta() {
        return meta;
    }

    public void setMeta(Meta meta) {
        this.meta = meta;
    }

    public List<Menu> getChildren() {
        return children;
    }

    public void setChildren(List<Menu> children) {
        this.children = children;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getComponent() {
        return component;
    }

    public void setComponent(String component) {
        this.component = component;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getIconCls() {
        return iconCls;
    }

    public void setIconCls(String iconCls) {
        this.iconCls = iconCls;
    }

    public Integer getParentId() {
        return parentId;
    }

    public void setParentId(Integer parentId) {
        this.parentId = parentId;
    }

    public Boolean getEnabled() {
        return enabled;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }
}

**mapper.xml的 resultMap返回结果 **

<resultMap id="BaseResultMap" type="org.crc.myvhrb.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="org.crc.myvhrb.model.Meta">
    <result column="keepAlive" property="keepAlive" jdbcType="BIT" />
    <result column="requireAuth" property="requireAuth" jdbcType="BIT" />
  </association>
</resultMap>
<resultMap id="Menus3" type="org.crc.myvhrb.model.Menu" extends="BaseResultMap">
    <!-- 一对多 -->
    <collection property="children" ofType="org.crc.myvhrb.model.Menu">
      <id column="id2" property="id" jdbcType="INTEGER"/>
      <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="org.crc.myvhrb.model.Meta">
        <result column="keepAlive2" property="keepAlive" jdbcType="BIT"/>
        <result column="requireAuth2" property="requireAuth" jdbcType="BIT"/>
      </association>
    </collection>
  </resultMap>

后端返回 JSON 数据格式:

[
    {
        "id": 2,
        "url": "/",
        "path": "/home",
        "component": "Home",
        "name": "员工资料",
        "iconCls": "fa fa-user-circle-o",
        "meta": {
            "keepAlive": null,
            "requireAuth": true
        },
        "parentId": 1,
        "enabled": true,
        "children": [
            {
                "id": 7,
                "url": null,
                "path": "/emp/basic",
                "component": "EmpBasic",
                "name": "基本资料",
                "iconCls": null,
                "meta": {
                    "keepAlive": null,
                    "requireAuth": true
                },
                "parentId": 2,
                "enabled": true,
                "children": null,
                "roles": null
            },
            {
                "id": 8,
                "url": null,
                "path": "/emp/adv",
                "component": "EmpAdv",
                "name": "高级资料",
                "iconCls": null,
                "meta": {
                    "keepAlive": null,
                    "requireAuth": true
                },
                "parentId": 2,
                "enabled": true,
                "children": null,
                "roles": null
            }
        ],
        "roles": null
    }
]
3.2.3 前端渲染数据

前端将 menu 数据保存到 vuex 中。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
  // 数据存放的位置
  state: {
    // 路由菜单
    routes: []
  },
  // 相当于方法
  mutations: {
    // 这个方法在调用的时候可以只传 data 参数。保存 router 数据
    initRoutes(state, data) {
      state.routes = data;
    }
  },
  actions: {}
})

通过观察我们可以发现,后端响应给前端的 JSON 数据中 component 字段是以字符串的形式。而在 vue router 的 component 中则是一个对象,这个问题可以通过路由的懒加载解决。

import {getRequest} from "./api";

// 定义一个方法,参数1 表示router对象 ,参数2 store 对象(保存路由)
export const initMenu = (router, store) => {
  // 判断 store 中是否保存有路由信息
  if (store.state.routes.length > 0) {
    // 如果有,则直接返回
    return;
  }
  // 获取用户路由菜单
  getRequest("/system/config/menu").then(data => {
    // 如果有返回数据
    if (data) {
      // 由于后端过来的数据格式不正确。在前端路由中,component 需要的是一个实例对象,而后端传过来的则是字符串,因此我们需要对 component 属性进行转换,可以采用路由懒加载的方式实现。
      // 对路由数据进行转换
      // 将处理好的数据添加到 router 中
      let fmtRoutes = formatRoutes(data);
      router.addRoutes(fmtRoutes);
      // 同时将数据保存到 store 中,参数1 表示 store 方法的名字,参数2 表示传入的参数
      store.commit('initRoutes',fmtRoutes);
    }
  })
};

// 路由数据转换方法
export const formatRoutes = (routes) => {
  // 全部处理好数据格式的数据
  let fmRoutes = [];
  // 对原始路由数据进行遍历
  routes.forEach(router => {
    // 只需要后端返回的部分路由数据,通过赋值的方式获取
    let {
      path,
      component,
      name,
      meta,
      iconCls,
      children
    } = router;
    // 判断 children 属性是否为空,并且 是否是 Array 对象
    if (children && children instanceof Array) {
      // 满足条件,说明是一级目录,通过递归的方式继续遍历二级目录
      children = formatRoutes(children);
    }
    // 单个处理数据转换
    let fmRouter = {
      path: path,
      name: name,
      iconCls: iconCls,
      meta: meta,
      children: children,
      // 以上的数据都不需要进行任何处理,主要处理 component 属性,通过动态懒加载的方式加载
      component(resolve) {
        // startsWith() 方法用于检测字符串是否以指定的前缀开始。
        if (component.startsWith("Home")) {
          // 参数1 表示加载的路径,参数2 传入的参数
          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);
        }
      }
    };
    // 将处理好的数据 push 到 fmRoutes
    fmRoutes.push(fmRouter);
  });
  return fmRoutes;
};

可以通过前置全局路由守卫加载路由菜单数据:

// 配置全局路由守卫
router.beforeEach((to,from,next) =>{
  // 如果跳转的页面是登录页面,则直接放行
  if (to.path == '/'){
    next();
  }else {
    // 用户已经登录
    if (window.sessionStorage.getItem("user")){
      //渲染路由菜单
      initMenu(router,store);
      next();
    } else{
      // 将要跳转的路径作为参数
      next('/?redirect=' + to.path);
    }
  }
});

以上就是实现动态路由菜单的一些需要注意的地方,可能会有些零碎,最好多看源码。

3.3 后端权限控制

虽然前面我们根据了用户的角色,在页面加载不同角色列表。但是,这只是前端对角色权限的简单控制,这并不是真正的权限控制,因为用户同样可以通过输入接口路径的方式访问到不同角色接口。我们需要在后端进行权限控制。

3.3.1 数据库设计与查询

查询用户角色信息:

<select id="getHrRolesById" resultType="org.crc.myvhrb.model.Role">
      select r.* from role r,hr_role hrr where hrr.`rid`=r.`id` and hrr.`hrid`=#{id}
</select>

查询菜单权限角色:

<resultMap id="MenuWithRole" type="org.crc.myvhrb.model.Menu" extends="BaseResultMap">
    <collection property="roles" ofType="org.crc.myvhrb.model.Role">
      <result column="rid" property="id"/>
      <result column="rname" property="name"/>
      <result column="rnameZh" property="namezh"/>
    </collection>
  </resultMap>

<select id="getAllMenusWithRole" resultMap="MenuWithRole">
  select m.*,r.`id` as rid,r.`name` as rname,r.`nameZh` as rnameZh 
    from
    menu m,
    menu_role mr,
    role r
    where m.`id`=mr.`mid` and mr.`rid`=r.`id` order by m.`id`
</select>
3.3.2 Spring Boot配置

Spring Boot 实现从数据库中加载角色权限主要分为两步:

  • 根据用户的请求路径接口从数据库中查询所需的角色列表
  • 根据角色列表判断当前请求用户是否具备角色权限

首先,在用户进行登录时,根据用户信息查询到用户具有的角色信息。

创建实现 FilterInvocationSecurityMetadataSource 过滤器。

// 主要用于拦截请求路径,查询数据库路径权限信息,通过比较的方式判断请求的路径需要具备那些角色。
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;
    // 由于数据库中保存的路径格式为字符串 "/home/**" 的格式,不好与用户请求路径进行比较,因此,Spring boot 提供了一个比较方式。通过 antPathMatcher 中的 match 方法可以很快的进行比较判断
    // 用于进行路径判断
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        // 获取请求路径
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        // 查询所有路径所需要的权限角色
        List<Menu> menus = menuService.getAllMenusWithRole();
        // 遍历所有路径权限角色中是否存在与请求路径一致的权限角色
        for (Menu menu : menus) {
            // match 方法参数1表示数据库中的路径,参数2 表示请求路径
            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;
    }
}

创建实现AccessDecisionManager 接口的类,主要用于比较请求路径的用户是否具有相应的角色。

@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    /**
     * 判断 是否具有需要的角色
     *
     * @param authentication 里面保存着用户的所有信息
     * @param o
     * @param collection     FilterInvocationSecurityMetadataSource返回的信息,也就是需要的角色信息
     * @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();
            // 如果拦截器返回的角色为 ROLE_LOGIN 则表示为需要登录就可以访问
            if ("ROLE_LOGIN".equals(needRole)) {
                // 如果用户是一个匿名用户
                if (authentication instanceof AnonymousAuthenticationToken) {
                    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 类中配置:

@Autowired
CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
@Autowired
CustomUrlDecisionManager customUrlDecisionManager;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                    o.setAccessDecisionManager(customUrlDecisionManager);
                    o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                    return o;
                }
            }).
        and()
        ....

4. 邮件的自动发送

在微人事中为添加新员工实现了入职自动发送入职欢迎邮件功能,该功能主要用到了 RabbitMQ、Java Mail Sender 、thymeleaf等依赖。

4.1 准备工作

4.1.1 RabbitMQ的准备工作

由于 RabbitMQ的安装配置过于繁琐,并且很容易出问题。所以这里采用了在 Linux 虚拟机中使用 docker 进行安装 RabbitMQ

docker 安装 RabbitMQ 安装语句

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

安装完成后启动即可。

可以在宿主机中访问 http://192.168.88.101:15672/ 接口查看是否安装成功(默认账号密码为 guest)。

4.1.2 相关依赖

微人事是一个模块化的项目,在 server 模块中需要使用 rabbitTemplate 进行消息发送。因此在 server 模块中也需要导入 rabbitMQ 依赖并且进行相关配置(在这里踩过坑,弄了半天都解决,最后发现是这个原因)。

mailserver相关依赖:

<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>

myvhrserver相关依赖:

<!-- 只需要在用到 rabbitTemplate 的模块引入rabbitmq 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

application.properties文件:

# 配置 RabbitMQ
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=192.168.88.101
spring.rabbitmq.port=5672

mailserver模块和 myvhrserver模块中都进行以上配置,原因是两个模块都使用到了 rabbitMQ 。需要注意的是在我由于某个问题(就是配置文件没配完整导致的问题)而寻找解决方法时,发现 rabbitmq 的默认用户账号为 guest ,并且该账号只支持本地里登录也就是只能通过 localhost 进行登录,而不是使用远程连接。而解决这个问题的方法是:1.为 guest默认账号添加配置文件。 2.添加新的登录账号。

由于我在解决某个问题的这些方法配置都进行配置了,是不是如上文所说的我不能确定。

4.1.3 邮件服务申请以及配置

邮件服务申请:

image-20210822091954931
(img-tRCPjOI0-1629599678666

开启POP3服务,需要注意的是记得复制授权码

application.properties文件:

# 配置邮件服务
spring.mail.host=smtp.qq.com
spring.mail.protocol=smtp
spring.mail.default-encoding=UTF-8
# 授权码
spring.mail.password=bqfamonzgnnocdba 
spring.mail.username=xxxx@qq.com
# 端口号
spring.mail.port=587
spring.mail.properties.mail.stmp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.debug=true

4.2 mailserver 服务端配置

首先,在 spring 容器中注入一个消息队列。该消息队列就是用来接收邮件提交的申请。

可以在 mailserver的启动类中注入:

@SpringBootApplication
public class MailserverApplication {

    public static void main(String[] args) {
        SpringApplication.run(MailserverApplication.class, args);
    }

    @Bean
    Queue queue(){
        return new Queue("anyCcCN.mail.welcome");
    }
}

其次,要发送一封入职欢迎邮件需要一个入职邮件模板。

这里使用 thymeleaf 模板:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>入职欢迎邮件</title>
</head>
<body>
欢迎<span th:text="${name}"></span> 加入xxx公司,您的入职信息如下:
<table border="1">
  <tr>
    <td>姓名</td>
    <td th:text="${name}"></td>
  </tr>
  <tr>
    <td>职位</td>
    <td th:text="${posName}"></td>
  </tr>
  <tr>
    <td>职称</td>
    <td th:text="${joblevelName}"></td>
  </tr>
  <tr>
    <td>部门</td>
    <td th:text="${departmentName}"></td>
  </tr>
</table>
</body>
</html>

有了消息队列中的提交信息,以及邮件模板。我们只需要通过读取消息队列中的提交信息和邮件模板进行渲染成 html 字符串,最后通过 javaMailSender发送邮件。

@Component
public class MailReceiver {

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

    @Autowired
    JavaMailSender javaMailSender;
    @Autowired
    MailProperties mailProperties;
    @Autowired
    TemplateEngine templateEngine;


    @RabbitListener(queues = "anyCcCN.mail.welcome")
    public void handler(Employee employee){
        logger.info("MailReceiver:" + employee.toString());
        // 收到消息,发送邮件
        MimeMessage msg = javaMailSender.createMimeMessage();
        // 设置邮件内容
        MimeMessageHelper helper = new MimeMessageHelper(msg);
        try {
            // 发送邮箱地址
            helper.setTo(employee.getEmail());
            // 发送人
            helper.setFrom(mailProperties.getUsername());
            // 邮件主题
            helper.setSubject("入职欢迎");
            // 发送时间
            helper.setSentDate(new Date());
            // 上下文
            Context context = new Context();
            context.setVariable("name",employee.getName());
            context.setVariable("posName",employee.getPosition().getName());
            context.setVariable("joblevelName",employee.getJobLevel().getName());
            context.setVariable("departmentName",employee.getDepartment().getName());
            // 将上下文添加到邮件模板中,参数1: 邮件模板名,参数2:上下文内容
            String mail = templateEngine.process("mail", context);
            helper.setText(mail,true);
            // 发送邮件
            javaMailSender.send(msg);
            logger.info("邮件发送成功");
        } catch (MessagingException e) {
            e.printStackTrace();
            logger.error("邮件发送失败:" + e.getMessage());
        }
    }

}

以上代码中通过@RabbitListener获取保存在消息队列中的数据,数据类型就是方法的参数。

4.3 myvhrserver服务端的配置

mailserver配置好之后,在myvhrserver服务端只需要将消息内容发送到消息队列中即可。

在添加 Employee 的 service 方法中添加一下代码:

// 将员工信息发送,参数1:消息队列的名称 ,参数2:消息队列的内容
rabbitTemplate.convertAndSend("anyCcCN.mail.welcome", emp);

5. Excel 数据表格的导入导出

在微人事项目的员工数据管理中使用到了 针对员工数据进行 Excel 数据表格的导入导出。这里主要使用的技术点为 poi 。

5.1 相关依赖

依赖地址

<!--POI 实现 Excel 表格的导入导出-->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.1</version>
</dependency>

5.2 POIUtils工具类

注意 POIUtils工具类主要针对关于 微人事中的 Employee 表进行导入和导出,如果想要复用需要自己根据需求字段进行修改。

public class POIUtils {

    /**
     * 导出数据,将数据库中的员工数据转换成 Excel 表格导出
     *
     * @param list
     * @return
     */
    public static ResponseEntity<byte[]> employee2Excel(List<Employee> list) {
        // 1. 创建一个 Excel 文档
        HSSFWorkbook workbook = new HSSFWorkbook();
        // 2. 创建文档摘要
        workbook.createInformationProperties();
        // 3. 获取并配置文档信息
        DocumentSummaryInformation docInfo = workbook.getDocumentSummaryInformation();
        // 文档类别
        docInfo.setCategory("员工信息");
        // 文档管理员
        docInfo.setManager("anyCcCN");
        // 设置公司信息
        docInfo.setCompany("www.anyCcCN.org");
        // 4. 获取文档摘要信息
        SummaryInformation summInfo = workbook.getSummaryInformation();
        // 文档标题
        summInfo.setTitle("员工信息表");
        // 文档作者
        summInfo.setAuthor("anyCcCN");
        // 文档备注
        summInfo.setComments("本文档由 anyCcCN 提供");
        // 5. 创建样式,主要设置 Excel 表格的背景色,宽度等样式
        // 创建标题行的样式
        HSSFCellStyle headerStyle = workbook.createCellStyle();
        // 设置背景色为 YELLOW
        headerStyle.setFillForegroundColor(IndexedColors.YELLOW.index);
        // 模式
        headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        // 创建日期样式
        HSSFCellStyle dateCellStyle = workbook.createCellStyle();
        dateCellStyle.setDataFormat(HSSFDataFormat.getBuiltinFormat("m/d/yy"));
        // 6. 创建表
        HSSFSheet sheet = workbook.createSheet("员工信息表");
        // 创建标题行
        HSSFRow r0 = sheet.createRow(0);
        // 第一行第一列数据,以此类推
        HSSFCell c0 = r0.createCell(0);
        c0.setCellValue("编号");
        c0.setCellStyle(headerStyle);
        HSSFCell c1 = r0.createCell(1);
        c1.setCellValue("姓名");
        c1.setCellStyle(headerStyle);
        HSSFCell c2 = r0.createCell(2);
        c2.setCellValue("工号");
        c2.setCellStyle(headerStyle);
        HSSFCell c3 = r0.createCell(3);
        c3.setCellValue("性别");
        c3.setCellStyle(headerStyle);
        HSSFCell c4 = r0.createCell(4);
        c4.setCellValue("出生日期");
        c4.setCellStyle(headerStyle);
        HSSFCell c5 = r0.createCell(5);
        c5.setCellValue("身份证号码");
        c5.setCellStyle(headerStyle);
        HSSFCell c6 = r0.createCell(6);
        c6.setCellValue("婚姻状况");
        c6.setCellStyle(headerStyle);
        HSSFCell c7 = r0.createCell(7);
        c7.setCellValue("民族");
        c7.setCellStyle(headerStyle);
        HSSFCell c8 = r0.createCell(8);
        c8.setCellValue("籍贯");
        c8.setCellStyle(headerStyle);
        HSSFCell c9 = r0.createCell(9);
        c9.setCellValue("政治面貌");
        c9.setCellStyle(headerStyle);
        HSSFCell c10 = r0.createCell(10);
        c10.setCellValue("电话号码");
        c10.setCellStyle(headerStyle);
        HSSFCell c11 = r0.createCell(11);
        c11.setCellValue("联系地址");
        c11.setCellStyle(headerStyle);
        HSSFCell c12 = r0.createCell(12);
        c12.setCellValue("所属部门");
        c12.setCellStyle(headerStyle);
        HSSFCell c13 = r0.createCell(13);
        c13.setCellValue("职称");
        c13.setCellStyle(headerStyle);
        HSSFCell c14 = r0.createCell(14);
        c14.setCellValue("职位");
        c14.setCellStyle(headerStyle);
        HSSFCell c15 = r0.createCell(15);
        c15.setCellValue("聘用形式");
        c15.setCellStyle(headerStyle);
        HSSFCell c16 = r0.createCell(16);
        c16.setCellValue("最高学历");
        c16.setCellStyle(headerStyle);
        HSSFCell c17 = r0.createCell(17);
        c17.setCellValue("专业");
        c17.setCellStyle(headerStyle);
        HSSFCell c18 = r0.createCell(18);
        c18.setCellValue("毕业院校");
        c18.setCellStyle(headerStyle);
        HSSFCell c19 = r0.createCell(19);
        c19.setCellValue("入职日期");
        c19.setCellStyle(headerStyle);
        HSSFCell c20 = r0.createCell(20);
        c20.setCellValue("在职状态");
        c20.setCellStyle(headerStyle);
        HSSFCell c21 = r0.createCell(21);
        c21.setCellValue("邮箱");
        c21.setCellStyle(headerStyle);
        HSSFCell c22 = r0.createCell(22);
        c22.setCellValue("合同期限(年)");
        c22.setCellStyle(headerStyle);
        HSSFCell c23 = r0.createCell(23);
        c23.setCellValue("合同起始日期");
        c23.setCellStyle(headerStyle);
        HSSFCell c24 = r0.createCell(24);
        c24.setCellValue("合同终止日期");
        c24.setCellStyle(headerStyle);
        HSSFCell c25 = r0.createCell(25);
        c25.setCellValue("转正");
        c25.setCellStyle(headerStyle);

        // 7. 将员工数据加载到 Excel 表格中
        for (int i = 0; i < list.size(); i++) {
            Employee emp = list.get(i);
            // 第 0 行为标题行
            HSSFRow row = sheet.createRow(i + 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 {
//            // 将创建好的 Excel 表格转成 byte[] 形式
//            workbook.write(baos);
//            // 设置文件头
//            headers.setContentDispositionFormData("attachment",new String("员工表.xls".getBytes("UTF-8"),"ISO-8859-1"));
//            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
//
//        // 参数1 Excel数据,参数2 文件头数据,参数3 模式
//        return new ResponseEntity<byte[]>(baos.toByteArray(),headers, HttpStatus.CREATED);
        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 表格数据转成 List 数组,
     * 由于在数据库中的 员工表并不直接保存民族、政治面貌、部门、职位、职称等数据的名称,而是以 id 的形式保存,
     * 因此需要以下几个参数进行转换
     *
     * @param file           上传的 Excel 表格数据
     * @param allNations     所有民族数据
     * @param allPoliticss   所有政治面貌数据
     * @param allDepartments 所有部门数据
     * @param allPosition    所有职位数据
     * @param allJobLevel    所有职称数据
     * @return
     */
    public static List<Employee> excel2Employee(MultipartFile file, List<Nation> allNations,
                                                List<Politicsstatus> allPoliticss, List<Department> allDepartments,
                                                List<Position> allPosition, List<JobLevel> allJobLevel) {
        // 保存员工数据,最终 return
        List<Employee> list = new ArrayList<>();
        Employee employee = null;
        try {
            // 1. 通过 file 创建一个 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 physicalNumberOfRows = sheet.getPhysicalNumberOfRows();
                for (int j = 0; j < physicalNumberOfRows; j++) {
                    // 5. 第一行为标题行,跳过标题行
                    if (j == 0) {
                        continue;
                    }
                    // 6. 获取行
                    HSSFRow row = sheet.getRow(j);
                    // 如果为null,表示该行是空行,防止数据中间有空行
                    if (row == null) {
                        continue;
                    }
                    // 7. 获取列数
                    int physicalNumberOfCells = row.getPhysicalNumberOfCells();
                    employee = new Employee();
                    for (int k = 0; k < physicalNumberOfCells; k++) {
                        // 获取列
                        HSSFCell cell = row.getCell(k);
                        // 由于表格中保存着不同的数据类型。因此通过 switch 的方式遍历
                        // 通过类型
                        switch (cell.getCellType()) {
                            // 如果类型为 string
                            case STRING:
                                // 获取数据
                                String cellValue = cell.getStringCellValue();
                                // 通过表格列位置判断那些需要转成 string
                                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:
                                        // 在员工表中保存的是 id 而不是名称,因此需要进行以下转换
                                        // 通过民族名获取民族的下标,然后根据下标获取民族 id ,这是一种简约的方式。也可用通过民族名称查询数据库中对应的 id
                                        int index = allNations.indexOf(new Nation(cellValue));
                                        employee.setNationId(allNations.get(index).getId());
                                        break;
                                    case 8:
                                        employee.setNativePlace(cellValue);
                                        break;
                                    case 9:
                                        int indexOf = allPoliticss.indexOf(new Politicsstatus(cellValue));
                                        Integer id1 = allPoliticss.get(indexOf).getId();
                                        employee.setPoliticId(id1);
                                        break;
                                    case 10:
                                        employee.setPhone(cellValue);
                                        break;
                                    case 11:
                                        employee.setAddress(cellValue);
                                        break;
                                    case 12:
                                        int i1 = allDepartments.indexOf(new Department(cellValue));
                                        employee.setDepartmentId(allDepartments.get(i1).getId());
                                        break;
                                    case 13:
                                        int indexOf2 = allJobLevel.indexOf(new JobLevel(cellValue));
                                        employee.setJobLevelId(allJobLevel.get(indexOf2).getId());
                                        break;
                                    case 14:
                                        int indexOf3 = allPosition.indexOf(new Position(cellValue));
                                        employee.setPosId(allPosition.get(indexOf3).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;
    }
}

6. 在线聊天功能

微人事项目中实现了一对一的单人在线聊天功能,主要使用的技术点为: 服务端 websocketsecurity

6.1 后端配置

6.1.1 相关依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
6.1.2 配置内容

配置 webSocketConfig

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    // 注册端点、连接点
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 配置连接点,用户需要成功连接才能进行在线聊天
        registry.addEndpoint("/ws/ep")
                // 允许连接的域
                .setAllowedOrigins("http://localhost:8080")
                // 开启 withSockJS
                .withSockJS();
    }

    // 消息代理
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 消息队列广播的前缀,所有消息队列中以 "queue"为前缀的都进行广播
        registry.enableSimpleBroker("/queue");
    }
}

配置消息发送接口:

@RestController
public class WsController {

    @Autowired
    SimpMessagingTemplate simpMessagingTemplate;

    @MessageMapping("/ws/chat")
    public void handleMsg(Authentication authentication, ChatMsg chatMsg){
        Hr hr = ((Hr) authentication.getPrincipal());
        // 发送的用户账号
        chatMsg.setFrom(hr.getUsername());
        // 发送的用户名称
        chatMsg.setFromNickname(hr.getName());
        chatMsg.setDate(new Date());
        // 参数1:消息发送目标, 参数2: 消息队列,参数3: 消息内容
        simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(),"/queue/chat",chatMsg);
    }

}

整体流程为,前端进行登录并且成功连接到 "/ws/ep" 连接点,通过接口 "/ws/chat" 发送消息。在 WebSocketConfig配置类中由于消息队列为 "/queue/chat" 满足以 queue 为前缀,所以配置类会将消息队列的消息内容发送给指定用户。

6.2 前端配置

6.2.1 相关准备

在微人事中使用了别人写好的聊天UI (GitHub地址)。

实际效果:
img-Ax7RVDdO-1629599678667
我们只需要将以上项目代码整合到微人事项目中进行改造即可(在整合过程中缺什么补什么)。

首先,前端需要两个工具支持:stompsockjs-client

安装响应的 npm 包:

npm install sockjs-client --save
npm install stompjs --save

在需要建立 webSocket连接的组件中引入:

import SockJS from 'sockjs-client';
import Stomp from 'stompjs';
6.2.2 配置内容

注意,这里的主要配置内容都是通过使用 vuex。

建立连接与接收消息:这里的主要功能就是是通过 stompsockjs连接webSocket,连接成功后可以获取消息的回调。

// 建立连接
connect(context) {
  //  '/ws/ep' 为服务端 webSocket 配置的连接点
  context.state.stomp = Stomp.over(new SockJS('/ws/ep'));
  // 建立连接
  context.state.stomp.connect({}, success => {
    // 连接成功,可以在这里获取聊天内容,参数1 为服务端 webSocket 配置的消息队列(需要手动添加user)
    context.state.stomp.subscribe('/user/queue/chat', msg => {
      // 获取消息内容,别人发来的消息
      let receiveMsg = JSON.parse(msg.body);
    
  }, errorr => {

  })
}

消息发送

let msgObj = new Object();
// 从选中用户数据中获取发送对象名
msgObj.to = this.currentSession.username;
msgObj.content = this.content;
console.log(this.$store.state.stomp);
this.$store.state.stomp.send('/ws/chat', {}, JSON.stringify(msgObj));

总结

微人事项目采用了前后端分离的项目结构。

前端主要涉及到的技术点:

"dependencies": {
  "axios": "^0.21.1",
  "core-js": "^3.6.5",
  "element-ui": "^2.15.3",
  "font-awesome": "^4.7.0",
  "sockjs-client": "^1.5.1",
  "stompjs": "^2.3.3",
  "vue": "^2.6.11",
  "vue-router": "^3.2.0",
  "vuex": "^3.6.2"
},
"devDependencies": {
  "@vue/cli-plugin-babel": "~4.5.0",
  "@vue/cli-plugin-router": "~4.5.0",
  "@vue/cli-service": "~4.5.0",
  "node-sass": "^4.14.1",
  "sass-loader": "^8.0.2",
  "vue-template-compiler": "^2.6.11"
}

后端主要涉及到的技术点:SpringBootSpring SecurityRabbitMQJavaMailSenderThymeleafMybatisRediscachewebSocket 等。

项目预览:
img-jLAnFu40-1629599678668img-lRNKW5Yp-1629599678668
img-DqT8Q5mM-1629600196217

image-20210822102957936
image-20210822103023616
image-20210822103041574

Logo

前往低代码交流专区

更多推荐