问题分析
前面我们已经完成了后台系统的员工登录功能开发,但是目前还存在一个问题,接下来我们来说明一个这个问题, 以及如何处理。
1). 目前现状
用户如果不登录,直接访问系统首页面,照样可以正常访问。
2). 理想效果
上述这种设计并不合理,我们希望看到的效果应该 是,只有登录成功后才可以访问系统中的页面,如果没有登录, 访问系统中的任何界面都直接跳转到登录页面。
那么,具体应该怎么实现呢?
可以使用我们之前讲解过的过滤器、拦截器来实现,在过滤器、拦截器中拦截前端发起的请求,判断用户是否已经完成登录,如果没有登录则返回提示信息,跳转到登录页面。
1.2 思路分析
过滤器具体的处理逻辑如下:
A. 获取本次请求的URI
B. 判断本次请求, 是否需要登录, 才可以访问
C. 如果不需要,则直接放行
D. 判断登录状态,如果已登录,则直接放行
E. 如果未登录, 则返回未登录结果
如果未登录,我们需要给前端返回什么样的结果呢? 这个时候, 我们可以去看看前端是如何处理的 ?
1.3 代码实现
拦截器与过滤器的区别:
1 拦截器是属于springmvc体系的,只能拦截controller的请求
2 过滤器是属于servlet体系,可以拦截所有的请求。
1). 定义登录校验过滤器
自定义一个过滤器 LoginCheckFilter 并实现 Filter 接口, 在doFilter方法中完成校验的逻辑。 那么接下来, 我们就根据上述分析的步骤, 来完成具体的功能代码实现
所属包: com.mywork.reggie.filter
package com.mywork.reggie.filters;import com.alibaba.fastjson.JSON;
import com.mywork.reggie.common.R;
import com.sun.org.apache.bcel.internal.generic.RETURN;
import org.springframework.util.AntPathMatcher;import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter{//该类专门用于匹配请求路径的。private AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {//1. 由于request对象需要使用到子类特有的方法,所以强制类型转换HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;//2. 获取本次请求的urlString requestURI = request.getRequestURI(); //http://localhost:8080/backend/index.html//3. 定义一个数组存储需要放行的url,判断本次请求是否需要登录权限的。String[] urls = {"/backend/**","/front/**","/employee/login"};boolean flag = checkUrl(urls,requestURI);if(flag){//直接放行filterChain.doFilter(request,response);return;}//4. 如果需要登录权限的,那么从session中取出登录成功标记检查HttpSession session = request.getSession();if(session.getAttribute("employee")!=null){//如果用户已经登录,也可以直接放行filterChain.doFilter(request,response);return;}//5. 如果没有登录,返回数据前端,让前端发生跳转/*目前你使用的是Filter,不是springmvc ,所有如果你需要返回json数据,需要自己转换,*/String json = JSON.toJSONString(R.error("NOTLOGIN"));response.getWriter().write(json);}/*检查本次请求路径是否属于直接放行的资源*/private boolean checkUrl(String[] urls, String requestURI) {//遍历所有放行的路径,是否与本次请求的路径匹配for (String url : urls) {if(antPathMatcher.match(url,requestURI)){return true;}}return false;}}
2). 开启组件扫描
需要在引导类上, 加上Servlet组件扫描的注解, 来扫描过滤器配置的@WebFilter注解, 扫描上之后, 过滤器在运行时就生效了
package com.mywork.reggie;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;@SpringBootApplication
@MapperScan(basePackages = "com.mywork.reggie.dao")
@ServletComponentScan(basePackages = "com.mywork.reggie.filters")
public class ReggieApplication {public static void main(String[] args) {SpringApplication.run(ReggieApplication.class, args);}
}
@ServletComponentScan 的作用:
在SpringBoot项目中, 在引导类/配置类上加了该注解后, 会自动扫描项目中(当前包及其子包下)的@WebServlet , @WebFilter , @WebListener 注解, 自动注册Servlet的相关组件 ;
功能测试
代码编写完毕之后,我们需要将工程重启一下,然后在浏览器地址栏直接输入系统管理后台首页,然后看看是否可以跳转到登录页面即可。我们也可以通过debug的形式来跟踪一下代码执行的过程。
对于前端的代码, 也可以进行debug调试。
F12打开浏览器的调试工具, 找到我们前面提到的request.js, 在request.js的响应拦截器位置打上断点
2. 新增员工
2.1 需求分析
后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击[添加员工]按钮跳转到新增页面,如下:
当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。
2.2 数据模型
新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。employee表中的status字段已经设置了默认值1,表示状态正常。
需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的。
2.3 程序执行流程
在开发代码之前,我们需要结合着前端页面发起的请求, 梳理一下整个程序的执行过程:
A. 点击"保存"按钮, 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端, 请求方式POST, 请求路径 /employee
B. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
C. Service调用Mapper操作数据库,保存数据
2.4 代码实现
在EmployeeController中增加save方法, 用于保存用户员工信息。
- EmployeeController 获取登陆者与创建修改时间
package com.mywork.reggie.controller;import com.mywork.reggie.common.R;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;@RestController
@RequestMapping("/employee")
public class EmployeeController {@Autowiredprivate EmployeeService employeeService;/*** 作用:员工添加*/@PostMappingpublic R save(@RequestBody Employee employee, HttpSession session){//1. 先从session中取出当前登录的用户Long empId = (Long) session.getAttribute("employee");//2. 补全员工的创建人与修改人employee.setCreateUser(empId);employee.setUpdateUser(empId);//3. 调用service方法进行添加employeeService.save(employee);return R.success("添加成功");}}
EmployeeService
package com.mywork.reggie.service;import com.mywork.reggie.common.R;
import com.mywork.reggie.entity.Employee;public interface EmployeeService {//添加员工void save(Employee employee);
}
EmployeeServiceImpl
package com.mywork.reggie.service.impl;import com.mywork.reggie.common.R;
import com.mywork.reggie.dao.EmployeeDao;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;import java.time.LocalDateTime;@Service
public class EmployeeServiceImpl implements EmployeeService{@Autowired(required = false)private EmployeeDao employeeDao;/*添加员工*/@Overridepublic void save(Employee employee) {//1.补全用户的数据employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));//2. 补全创建时间与修改时间、状态employee.setStatus(1);//默认是启用的状态employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());employeeDao.save(employee);}
}
EmployeeMapper
用#号代表映射的是实体类里的属性名
package com.mywork.reggie.dao;import com.mywork.reggie.entity.Employee;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;public interface EmployeeDao {@Insert("insert into employee values(null,#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})")void save(Employee employee);
}
2.5 功能测试
代码编写完毕之后,我们需要将工程重启, 完毕之后直接访问管理系统首页, 点击 “员工管理” 页面中的 “添加员工” 按钮, 输入员工基本信息, 然后点击 “保存” 进行数据保存, 保存完毕后, 检查数据库中是否录入员工数据。
当我们在测试中,添加用户时, 输入了一个已存在的用户名时,前端界面出现错误提示信息:
而此时,服务端已经报错了, 报错信息如下:
出现上述的错误, 主要就是因为在 employee 表结构中,我们针对于username字段,建立了唯一索引,添加重复的username数据时,违背该约束,就会报错。但是此时前端提示的信息并不具体,用户并不知道是因为什么原因造成的该异常,我们需要给用户提示详细的错误信息
2.6 全局异常处理
2.6.1 思路分析
要想解决上述测试中存在的问题,我们需要对程序中可能出现的异常进行捕获,通常有两种处理方式:
A. 在Controller方法中加入 try…catch 进行异常捕获
如果采用这种方式,虽然可以解决,但是存在弊端,需要我们在保存其他业务数据时,也需要在Controller方法中加上try…catch进行处理,代码冗余,不通用。
B. 使用异常处理器进行全局异常捕获
采用这种方式来实现,我们只需要在项目中定义一个通用的全局异常处理器,就可以解决本项目的所有异常。
2.6.2 全局异常处理器
在项目中自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 @ExceptionHandler 来指定拦截的是那一类型的异常。
- 自定义一个异常类,名字已存在的异常类
package com.mywork.reggie.exception;public class NameExistsException extends RuntimeException {public NameExistsException(String message) {super(message);}
}
修改EmployeeServiceImpl添加方法,添加之前先检查username是否已经存在
package com.mywork.reggie.service.impl;import com.mywork.reggie.common.R;
import com.mywork.reggie.dao.EmployeeDao;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.exception.NameExistsException;
import com.mywork.reggie.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;import java.time.LocalDateTime;@Service
public class EmployeeServiceImpl implements EmployeeService{@Autowired(required = false)private EmployeeDao employeeDao;/*添加员工*/@Overridepublic void save(Employee employee) {//查询员工的名字是否存在Employee dbEmp = employeeDao.findByName(employee);//如果不为空if(dbEmp!=null){throw new NameExistsException(employee.getUsername());}//1.补全用户的数据employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));//2. 补全创建时间与修改时间、状态employee.setStatus(1);//默认是启用的状态employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());employeeDao.save(employee);}
}
自定义 了全局异常处理器
package com.mywork.reggie.exception;import com.mywork.reggie.common.R;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*@RestControllerAdvice 表明该类是一个全局异常处理器。*/
@RestControllerAdvice
public class GlobalExceptionHandler {/*@ExceptionHandler : 用于指定这个方法处理那些类型的异常的。*/@ExceptionHandler(NameExistsException.class)public R exceptionHandler(NameExistsException e){e.printStackTrace(); //打印异常信息,否则控制台没有任何的内容return R.error(e.getMessage()+"已经存在");}/*@ExceptionHandler : 用于指定这个方法处理那些类型的异常的。*/@ExceptionHandler(Exception.class)public R exceptionHandler(Exception e){e.printStackTrace(); //打印异常信息,否则控制台没有任何的内容return R.error("目前访问人数过多,请稍后!");}
}
2.6.3 测试
全局异常处理器编写完毕之后,我们需要将项目重启, 完毕之后直接访问管理系统首页, 点击 “员工管理” 页面中的 “添加员工” 按钮。当我们在测试中,添加用户时, 输入了一个已存在的用户名时,前端界面出现如下错误提示信息:
3. 员工分页查询
3.1 需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。而在我们的分页查询页面中, 除了分页条件以外,还有一个查询条件 “员工姓名”。
- 请求参数
- 搜索条件: 员工姓名(模糊查询)
- 分页条件: 每页展示条数 , 页码
- 响应数据
- 总记录数
- 结果列表
3.2 程序执行流程
3.2.1 页面流程分析
在开发代码之前,需要梳理一下整个程序的执行过程。
A. 点击菜单,打开员工管理页面时,执行查询:
B. 搜索栏输入员工姓名,回车,执行查询:
1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
3). Service调用Dao操作数据库,查询分页数据
4). Controller将查询到的分页数据, 响应给前端页面
5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
3.2.2 前端代码介绍
1). 访问员工列表页面/member/list.html时, 会触发Vuejs中的钩子方法, 在页面初始化时调用created方法
从上述的前端代码中我们可以看到, 执行完分页查询, 我们需要给前端返回的信息中需要包含两项 : records 中封装结果列表, total中封装总记录数 。
而在组装请求参数时 , page、pageSize 都是前端分页插件渲染时的参数;
2). 在getMemberList方法中, 通过axios发起异步请求
最终发送给服务端的请求为 : GET请求 , 请求链接 /employee/page?page=1&pageSize=10&name=xxx
3.3 代码实现
pageHelper
介绍
PageHelper是国内非常优秀的一款开源的mybatis分页插件,它支持基本主流与常用的数据库, 例如mysql、 oracle、mariaDB、 DB2、 SQLite、Hsqldb等。
本项目在 github 的项目地址: https://github.com/pagehelper/Mybatis-PageHelper
本项目在 gitosc 的项目地址: http://git.oschina.net/free/Mybatis_PageHelper
自己实现分页的弊端:
- (1)每一种数据库分页语句不同,我们实现分页需要针对每一种数据库定制符合其语法的SQL,如果更换数据库,需要修改分页SQL。limit top rownum
- (2)除了这些以外,我们还要定义PageBean,封装分页参数,PageHelper提供了PageInfo对象给你们去使用,这里的PageInfo对象就是你们以前定义pageBean.
3.3.1 mybatis分页插件-pageHelper的使用
导入依赖(已完成)
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.4.0</version>
</dependency>
在application.yml文件配置信息
pagehelper:#使用的数据库helper-dialect: mysql#合理化分页#如果查询页数小于实际页数则查询第一页,如果超出则查询最后一页reasonable: true
测试(pageHelper的基本使用)
EmployeeDao(添加一个findByName方法)
package com.mywork.reggie.dao;import com.mywork.reggie.entity.Employee;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;import java.util.List;public interface EmployeeDao {/*分页的时候name的参数是不一定传递的,所以我们需要使用动态sql,一旦使用动态sql,即使只有一个参数也需要使用参数的名字*/List<Employee> findByPage(@Param("name") String name);}
EmployeeMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.mywork.reggie.dao.EmployeeDao"><select id="findByPage" resultType="employee">select * from employee<where><if test="name!=null and name!=''">name like concat('%',#{name},'%')</if></where></select>
- 测试类
package com.mywork.reggie.test;import com.github.pagehelper.PageHelper;import com.github.pagehelper.PageInfo;import com.mywork.reggie.dao.EmployeeDao;import com.mywork.reggie.entity.Employee;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTestpublic class PageTest {@Autowiredprivate EmployeeDao employeeDao;/*pageHelper的用法步骤:1. 设置当前页与页面大小2. 查询页面的数据3. 创建PageInfo对象,PageINfo封装了页面的所有数据,相当于以前的PageBean对象*/@Testpublic void test01(){// 1. 设置当前页与页面大小PageHelper.startPage(1,2);// 2. 查询页面的数据 limit (curPgae-1)*pageSize , pagesizeList<Employee> empList = employeeDao.findByPage(null);// 3. 创建PageInfo对象,PageINfo封装了页面的所有数据,相当于以前的PageBean对象PageInfo<Employee> pageInfo = new PageInfo<>(empList);System.out.println("当前页:"+pageInfo.getPageNum());System.out.println("页面大小:"+pageInfo.getPageSize());System.out.println("总记录数:"+pageInfo.getTotal());System.out.println("总页数:"+pageInfo.getPages());System.out.println("页面的数据:"+pageInfo.getList());}}
pagehelp小结:
1.设置当前页与页面大小
2.查询页面的数据
3.创建PageInfo对象,把List对象传入即可
3.3.2 分页查询实现
在上面我们已经分析了,页面在进行分页查询时, 具体的请求信息如下:
请求 说明 请求方式 GET 请求路径 /employee/page 请求参数 page , pageSize , name
那么查询完毕后我们需要给前端返回什么样的结果呢?
在上述我们也分析了, 查询返回的结果数据data中应该封装两项信息, 分别为: records 封装分页列表数据, total 中封装符合条件的总记录数。 那我们则需要自定义一个Page类必须包含records 与total两个属性的
Page实体类的编写
package com.mywork.reggie.entity;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Page<T> {protected List<T> records;protected long total;protected long pageSize;protected long page;}
EmployeeController控制器
package com.mywork.reggie.controller;import com.github.pagehelper.PageInfo;
import com.mywork.reggie.common.R;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.entity.Page;
import com.mywork.reggie.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;@RestController
@RequestMapping("/employee")
public class EmployeeController {@Autowiredprivate EmployeeService employeeService;/*** 作用:员工列表分页*/@GetMapping("/page")public R page(@RequestParam(defaultValue = "1") Integer page,@RequestParam(defaultValue = "10") Integer pageSize,String name){Page<Employee> pageBean = employeeService.findByPage(page,pageSize,name);return R.success(pageBean);}}
EmployeeService接口
package com.mywork.reggie.service;import com.mywork.reggie.common.R;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.entity.Page;public interface EmployeeService {/*员工分页列表*/Page<Employee> findByPage(Integer page, Integer pageSize, String name);
}
EmployeeServiceImpl接口实现类
推荐安装使用GeneralAllSetter,在类中按alt+enter使用
package com.mywork.reggie.service.impl;import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.mywork.reggie.common.R;
import com.mywork.reggie.dao.EmployeeDao;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.entity.Page;
import com.mywork.reggie.exception.NameExistsException;
import com.mywork.reggie.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;import java.time.LocalDateTime;
import java.util.List;@Service
public class EmployeeServiceImpl implements EmployeeService{@Autowired(required = false)private EmployeeDao employeeDao;/*员工分页列表*/@Overridepublic Page<Employee> findByPage(Integer page, Integer pageSize, String name) {//1. 设置当前页与页面大小PageHelper.startPage(page,pageSize);//2. 查询当前页的数据List<Employee> employeeList = employeeDao.findByPage(name);//3. 创建PageInfo对象,把List传入。PageInfo<Employee> pageInfo = new PageInfo<>(employeeList);//4. 创建Page对象,然后把PageInfo的数据封装到page对象。Page<Employee> pageResult = new Page<>(); //alt+enterpageResult.setRecords(pageInfo.getList()) ;//页面的数据pageResult.setTotal(pageInfo.getTotal()); //总记录数pageResult.setPageSize(pageInfo.getPageSize()); //页面大小pageResult.setPage(pageInfo.getPageNum()); //当前页return pageResult;}
}
EmployeeMapper文件
package com.mywork.reggie.dao;import com.mywork.reggie.entity.Employee;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;import java.util.List;public interface EmployeeDao {/*分页的时候name的参数是不一定传递的,所以我们需要使用动态sql,一旦使用动态sql,即使只有一个参数也需要使用参数的名字*/List<Employee> findByPage(@Param("name") String name);}
EmployeeMapper.xml映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.mywork.reggie.dao.EmployeeDao"><select id="findByPage" resultType="employee">select * from employee<where><if test="name!=null and name!=''">name like concat('%',#{name},'%')</if></where></select></mapper>
3.4 功能测试
代码编写完毕之后,我们需要将工程重启, 完毕之后直接访问管理系统首页, 默认就会打开员工管理的列表页面, 我们可以查看列表数据是否可以正常展示, 也可以通过分页插件来测试分页功能, 及员工姓名的模糊查询功能。
在进行测试时,可以使用浏览器的监控工具查看页面和服务端的数据交互细节。 并借助于debug的形式, 根据服务端参数接收及逻辑执行情况。
测试过程中可以发现,对于员工状态字段(status)服务端返回的是状态码(1或者0),但是页面上显示的则是“正常”或者“已禁用”,这是因为页面中在展示数据时进行了处理。
4. 启用/禁用员工账号
4.1 需求分析
在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 “禁用”,如果员工账号状态为已禁用,则按钮显示为"启用"。
需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
A. admin 管理员登录
B. 普通用户登录
4.2 程序执行流程
4.2.1 页面按钮动态展示
在上述的需求中,我们提到需要实现的效果是 : 只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示 , 页面中是怎么做到只有管理员admin能够看到启用、禁用按钮的?
1). 在列表页面(list.html)加载时, 触发钩子函数created, 在钩子函数中, 会从localStorage中获取到用户登录信息, 然后获取到用户名
2). 在页面中, 通过Vue指令v-if进行判断,如果登录用户为admin将展示 启用/禁用 按钮, 否则不展示
4.2.2 执行流程分析
1). 当管理员admin点击 “启用” 或 “禁用” 按钮时, 调用方法statusHandle
2). statusHandle方法中进行二次确认, 然后发起ajax请求, 传递id、status参数
最终发起异步请求, 请求服务端, 请求信息如下:
请求 说明
请求方式 PUT
请求路径 /employee
请求参数 {“id”:xxx,“status”:xxx}
{…params} : 三点是ES6中出现的扩展运算符。作用是遍历当前使用的对象能够访问到的所有属性,并将属性放入当前对象中。
4.3 代码实现
在开发代码之前,需要梳理一下整个程序的执行过程:
1). 页面发送ajax请求,将参数(id、status)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service更新数据
3). Service调用Dao操作数据库
启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作。在Controller中创建update方法,此方法是一个通用的修改员工信息的方法。
代码实现
- EmployeeController
package com.mywork.reggie.controller;import com.github.pagehelper.PageInfo;
import com.mywork.reggie.common.R;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.entity.Page;
import com.mywork.reggie.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;@RestController
@RequestMapping("/employee")
public class EmployeeController {@Autowiredprivate EmployeeService employeeService;/*** 作用:修改员工信息*/@PutMappingpublic R updateEmp(@RequestBody Employee employee,HttpSession session){//补全修改人//1.先从session中取出当前登录的用户Long empId = (Long) session.getAttribute("employee");//2. 补全员工的创建人与修改人employee.setUpdateUser(empId);employeeService.updateEmp(employee);return R.success("修改成功");}}
EmployeeService
package com.mywork.reggie.service;import com.mywork.reggie.common.R;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.entity.Page;public interface EmployeeService {/*修改员工信息*/void updateEmp(Employee employee);
}
EmployeeServiceImp
package com.mywork.reggie.service.impl;import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.mywork.reggie.common.R;
import com.mywork.reggie.dao.EmployeeDao;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.entity.Page;
import com.mywork.reggie.exception.NameExistsException;
import com.mywork.reggie.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;import java.time.LocalDateTime;
import java.util.List;@Service
public class EmployeeServiceImpl implements EmployeeService{@Autowired(required = false)private EmployeeDao employeeDao;@Overridepublic void updateEmp(Employee employee) {employee.setUpdateTime(LocalDateTime.now());employeeDao.updateEmp(employee);}
}
EmployeeMapper
package com.mywork.reggie.dao;import com.mywork.reggie.entity.Employee;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;import java.util.List;public interface EmployeeDao {/*修改员工信息 ,不管你是修改员工的状态还是修改员工的基本信息都可以使用这个方法,由于修改不同信息传递的字段不一样,所以这里使用动态sql*/void updateEmp(Employee employee);
}
EmployeeMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.mywork.reggie.dao.EmployeeDao"><!--set标签的作用: 1.帮你添加set的关键字,2.去除最后一个逗号--><update id="updateEmp">update employee<set><if test="username!=null and username!=''">username=#{username},</if><if test="name!=null and name!=''">name=#{name},</if><if test="password!=null and password!=''">password=#{password},</if><if test="phone!=null and phone!=''">phone=#{phone},</if><if test="sex!=null and sex!=''">sex=#{sex},</if><if test="idNumber!=null and idNumber!=''">id_number=#{idNumber},</if><if test="status!=null">status=#{status},</if><if test="updateTime!=null">update_time=#{updateTime},</if><if test="updateUser!=null">update_user=#{updateUser},</if></set>where id=#{id}</update>
</mapper>
4.4 功能测试
代码编写完毕之后,我们需要将工程重启。 然后访问前端页面, 进行 “启用” 或 “禁用” 的测试
测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。但是从控制台输出的日志, 可以看出确实没有更新成功。
而在我们的数据库表结构中, 并不存在该ID, 数据库中 风清扬 对应的ID为 1420038345634918401
4.5 代码修复
4.5.1 原因分析
通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致,这是怎么回事呢?
在分页查询时,服务端会将返回的R对象进行json序列化,转换为json格式的数据,而员工的ID是一个Long类型的数据,而且是一个长度为 19 位的长整型数据, 该数据返回给前端是没有问题的。
那么具体的问题出现在哪儿呢?
问题实际上, 就出现在前端JS中, js在对长度较长的长整型数据进行处理时, 会损失精度, 从而导致提交的id和数据库中的id不一致。 这里,我们也可以做一个简单的测试,代码如下:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title><script>alert(1420038345634918401);</script>
</head>
<body>
</body>
</html>
4.5.2 解决方案
要想解决这个问题,也很简单,我们只需要让js处理的ID数据类型为字符串类型即可, 这样就不会损失精度了。同样, 大家也可以做一个测试:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title><script>alert("1420038345634918401");</script>
</head>
<body>
</body>
</html>
那么在我们的业务中, 我们只需要让分页查询返回的json格式数据库中, long类型的属性, 不直接转换为数字类型, 转换为字符串类型就可以解决这个问题了 , 最终返回的结果为 :
4.5.3 代码修复
由于在SpringMVC中, 将Controller方法返回值转换为json对象, 是通过jackson来实现的, 涉及到SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter, 所以我们要解决这个问题, 就需要对该消息转换器的功能进行拓展。
具体实现步骤:
1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
2). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
1). 引入JacksonObjectMapper
package com.mywork.reggie.config;import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;/*** 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]*/
public class JacksonObjectMapper extends ObjectMapper {public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";public JacksonObjectMapper() {super();//收到未知属性时不报异常this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);//反序列化时,属性不存在的兼容处理this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);SimpleModule simpleModule = new SimpleModule().addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))).addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))).addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))).addSerializer(BigInteger.class, ToStringSerializer.instance).addSerializer(Long.class, ToStringSerializer.instance).addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))).addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))).addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));//注册功能模块 例如,可以添加自定义序列化器和反序列化器this.registerModule(simpleModule);}
}
该自定义的对象转换器, 主要指定了, 在进行json数据序列化及反序列化时, LocalDateTime、LocalDate、LocalTime的处理方式, 以及BigInteger及Long类型数据,直接转换为字符串。
2). 在WebMvcConfig中重写方法extendMessageConverters
/*** 扩展mvc框架的消息转换器* @param converters*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {log.info("扩展消息转换器...");//创建消息转换器对象MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();//设置对象转换器,底层使用Jackson将Java对象转为jsonmessageConverter.setObjectMapper(new JacksonObjectMapper());//将上面的消息转换器对象追加到mvc框架的转换器集合中converters.add(0,messageConverter);
}
5. 编辑员工信息
5.1 需求分析
在员工管理列表页面点击 “编辑” 按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击 “保存” 按钮完成编辑操作。
那么从上述的分析中,我们可以看出当前实现的编辑功能,我们需要实现两个方法:
A. 根据ID查询, 用于页面数据回显
B. 保存修改
5.2 程序执行流程
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1). 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2). 在add.html页面获取url中的参数[员工id]
3). 发送ajax请求,请求服务端,同时提交员工id参数
4). 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5.3 代码实现
5.3.1 根据ID查询
经过上述的分析,我们看到,在根据ID查询员工信息时,请求信息如下:
代码实现:
- 在EmployeeController中增加方法, 根据ID查询员工信息。
package com.mywork.reggie.controller;import com.mywork.reggie.common.R;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.entity.Page;
import com.mywork.reggie.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;@RestController
@RequestMapping("/employee")
public class EmployeeController {@Autowiredprivate EmployeeService employeeService;/*** 作用:根据id查询员工的信息*/@GetMapping("/{id}")public R findById(@PathVariable Long id){Employee employee = employeeService.findById(id);return R.success(employee);}}
EmployeeService
package com.mywork.reggie.service;import com.mywork.reggie.common.R;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.entity.Page;public interface EmployeeService {/*** 根据id查找员工* @param id* @return*/Employee findById(Long id);
}
EmployeeServiceImpl
package com.mywork.reggie.service.impl;import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.mywork.reggie.common.R;
import com.mywork.reggie.dao.EmployeeDao;
import com.mywork.reggie.entity.Employee;
import com.mywork.reggie.entity.Page;
import com.mywork.reggie.exception.NameExistsException;
import com.mywork.reggie.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;import java.time.LocalDateTime;
import java.util.List;@Service
public class EmployeeServiceImpl implements EmployeeService{@Autowired(required = false)private EmployeeDao employeeDao;/*** 根据id查找员工* @param id* @return*/@Overridepublic Employee findById(Long id) {Employee employee = employeeDao.findById(id);return employee;}
}
EmployeeMapper
package com.mywork.reggie.dao;import com.mywork.reggie.entity.Employee;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;import java.util.List;public interface EmployeeDao {@Select("select * from employee where id = #{id}")Employee findById(Long id);
}
5.3.2 修改员工(已完成不需要再写)
经过上述的分析,我们看到,在修改员工信息时,请求信息如下:
5.4 功能测试
代码编写完毕之后,我们需要将工程重启。 然后访问前端页面, 按照前面分析的操作流程进行测试,查看数据是否正常修改即可。