一、完善登录功能
1.1 问题分析
**问题:**index页面不用登录直接输入url也可以访问
**理想效果:**只有登录成功后才可以访问系统中的页面,如果没有登录, 访问系统中的任何界面都直接跳转到登录页面。
**实现方式:**通过过滤器或拦截器来实现,在过滤器、拦截器中拦截前端发起的请求,判断用户是否已经完成登录,如果没有登录则返回提示信息,跳转到登录页面。
1.2 思路分析
过滤器具体的处理逻辑如下:
- 获取本次请求的URI
- 判断本次请求, 是否需要登录, 才可以访问
- 如果不需要,则直接放行
- 判断登录状态,如果已登录,则直接放行
- 如果未登录, 则返回未登录结果
如果没登录,根据前端的处理来实现后端代码
1.3 代码实现
创建过滤器
创建filter包,并创建LoginCheckFilter过滤器
代码如下:
/**
* 检查用户是否已经完成登陆
* @auther xiaochen
* @create 2022-08-05 19:58
*/
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info("拦截到请求:{}", request.getRequestURI());
filterChain.doFilter(request, response);
}
}
在启动类上加上Servlet组件扫描的注解, 来扫描过滤器配置的@WebFilter注解, 扫描上之后, 过滤器在运行时就生效了。
@ServletComponentScan 的作用:
在SpringBoot项目中, 在引导类/配置类上加了该注解后, 会自动扫描项目中(当前包及其子包下)的@WebServlet , @WebFilter , @WebListener 注解, 自动注册Servlet的相关组件 ;
测试一下:
发现过滤器是可以生效的。
AntPathMatcher 拓展:
介绍: Spring中提供的路径匹配器 ;
通配符规则:
符号 含义 ? 匹配一个字符 * 匹配0个或多个字符 ** 匹配0个或多个目录/字符
完整代码:
/**
* 检查用户是否已经完成登陆
*
* @auther xiaochen
* @create 2022-08-05 19:58
*/
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1. 获取本次请求的URI
String requestURI = request.getRequestURI();
log.info("拦截到请求:{}", requestURI);
//不需要进行处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// 2. 判断本次请求, 是否需要登录, 才可以访问
boolean check = check(requestURI, urls);
// 3. 如果不需要登录,则直接放行
if (check) {
log.info("本次请求{}不需要处理", requestURI);
filterChain.doFilter(request, response);
return;
}
// 4. 判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,用户ID为:{}", request.getSession().getAttribute("employee"));
filterChain.doFilter(request, response);
return;
}
// 5. 如果未登录, 则返回未登录结果
//通过输出流的方式向客户端响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
log.info("用户未登录");
return;
}
/**
* 路径匹配,检查本期请求是否需要放行
*
* @param requestURI
* @param urls
* @return
*/
public boolean check(String requestURI, String[] urls) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if (match) {
return true;
}
}
return false;
}
}
测试:
成功实现
二、新增员工
2.1 代码执行流程
- 点击"保存"按钮, 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端, 请求方式POST, 请求路径 /employee
- 服务端Controller接收页面提交的数据并调用Service将数据进行保存
- Service调用Mapper操作数据库,保存数据
2.2 代码实现
在EmployeeController中增加save方法, 用于保存用户员工信息。
-
在新增员工时, 按钮页面原型中的需求描述, 需要给员工设置初始默认密码 123456, 并对密码进行MD5加密。
-
在组装员工信息时, 还需要封装创建时间、修改时间,创建人、修改人信息(从session中获取当前登录用户)。
/**
* 新增员工功能
*
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee) {
log.info("新增员工,员工信息:{}", employee.toString());
//设置初始密码123456,使用md5进行加密
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
//设置创建时间
employee.setCreateTime(LocalDateTime.now());
//设置更新时间
employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
employeeService.save(employee);
//返回成功信息
return R.success("新增员工成功!");
}
测试一下,插入成功
但是这里有一个问题,如果再次插入同账号的员工
发现这里出现系统接口500异常
控制台也报错了,因为在 employee 表结构中,我们针对于username字段,建立了唯一索引,添加重复的username数据时,违背该约束,就会报错。但是此时前端提示的信息并不具体,用户并不知道是因为什么原因造成的该异常,我们需要给用户提示详细的错误信息 。
三、全局异常处理器
3.1 思路分析
要想解决上述测试中存在的问题,我们需要对程序中可能出现的异常进行捕获,通常有两种处理方式:
A. 在Controller方法中加入 try…catch 进行异常捕获
形式如下:
如果采用这种方式,虽然可以解决,但是存在弊端,需要我们在保存其他业务数据时,也需要在Controller方法中加上try…catch进行处理,代码冗余,不通用。
B. 使用异常处理器进行全局异常捕获
采用这种方式来实现,我们只需要在项目中定义一个通用的全局异常处理器,就可以解决本项目的所有异常。
3.2 全局异常处理器
在项目中自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 @ExceptionHandler 来指定拦截的是那一类型的异常。
异常处理方法逻辑:
- 指定捕获的异常类型为 SQLIntegrityConstraintViolationException
- 解析异常的提示信息, 获取出是那个值违背了唯一约束
- 组装错误信息并返回
代码实现:
/**
* 全局异常处理器
*
* @auther xiaochen
* @create 2022-08-05 21:00
*/
@ControllerAdvice(annotations = {RestController.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
log.error(ex.getMessage());
//将错误信息用空格分开Duplicate entry 'duoduo' for key 'idx_username'
if (ex.getMessage().contains("Duplicate entry")) {
String[] split = ex.getMessage().split(" ");
//获取用户名
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
注解说明:
上述的全局异常处理器上使用了的两个注解 @ControllerAdvice , @ResponseBody , 他们的作用分别为:
@ControllerAdvice : 指定拦截那些类型的控制器;
@ResponseBody: 将方法的返回值 R 对象转换为json格式的数据, 响应给页面;
上述使用的两个注解, 也可以合并成为一个注解 @RestControllerAdvice
测试:
成功检查出异常并返回错误信息
四、员工分页查询
4.1 需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。而在我们的分页查询页面中, 除了分页条件以外,还有一个查询条件 “员工姓名”。
-
请求参数
-
搜索条件: 员工姓名(模糊查询)
-
分页条件: 每页展示条数 , 页码
-
-
响应数据
-
总记录数
-
结果列表
-
4.2 程序执行流程分析
在开发代码之前,需要梳理一下整个程序的执行过程。
A. 点击菜单,打开员工管理页面时,执行查询:
B. 搜索栏输入员工姓名,回车,执行查询:
-
页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
-
服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
-
Service调用Mapper操作数据库,查询分页数据
-
Controller将查询到的分页数据, 响应给前端页面
-
页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
4.3 代码实现
要实现分页查询功能,就需要用到MybatisPlus中提供的分页插件,要使用分页插件,就要在配置类中声明分页插件的bean对象。
在config目录下新建MybatisPlusConfig类
代码如下:
/**
* 配置MP的分页插件
* @auther xiaochen
* @create 2022-08-06 8:02
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
分页查询实现
页面在进行分页查询时, 具体的请求信息如下:
请求 | 说明 |
---|---|
请求方式 | GET |
请求路径 | /employee/page |
请求参数 | page , pageSize , name |
那么查询完毕后我们需要给前端返回什么样的结果呢?
查询返回的结果数据data中应该封装两项信息, 分别为: records 封装分页列表数据, total 中封装符合条件的总记录数。
那么这个时候, 在定义controller方法的返回值类型R时, 我们可以直接将 MybatisPlus 分页查询的结果 Page 直接封装返回, 因为Page中的属性如下:
那么接下来就依据于这些已知的需求和条件完成分页查询的代码实现。 具体的逻辑如下:
A. 构造分页条件
B. 构建搜索条件 - name进行模糊匹配
C. 构建排序条件 - 更新时间倒序排序
D. 执行查询
E. 组装结果并返回
代码如下:
/**
* 员工信息分页查询功能
*
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
log.info("page = {},pageSize={},name={}", page, pageSize, name);
//1.构造分页构造器
Page pageInfo = new Page(page, pageSize);
//2.构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件,模糊查询
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
//添加排序条件,按照更新时间进行排序
queryWrapper.orderByDesc(Employee::getUpdateTime);
//3.执行查询
employeeService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
测试:
前端正常拿到数据
后端也执行了查询
五、启动、禁用员工账号
5.1 需求分析
在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 “禁用”,如果员工账号状态为已禁用,则按钮显示为"启用"。
注意:只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
5.2 程序执行流程
1 页面按钮动态展示
1). 当管理员admin点击 “启用” 或 “禁用” 按钮时, 调用方法statusHandle
scope.row : 获取到的是这一行的数据信息 ;
2). statusHandle方法中进行二次确认, 然后发起ajax请求, 传递id、status参数
最终发起异步请求, 请求服务端, 请求信息如下:
请求 | 说明 |
---|---|
请求方式 | PUT |
请求路径 | /employee |
请求参数 | {“id”:xxx,“status”:xxx} |
{…params} : 三点是ES6中出现的扩展运算符。作用是遍历当前使用的对象能够访问到的所有属性,并将属性放入当前对象中。
5.3 代码实现
程序的执行过程:
1). 页面发送ajax请求,将参数(id、status)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service更新数据
3). Service调用Mapper操作数据库
启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作。在Controller中创建update方法,此方法是一个通用的修改员工信息的方法。
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
Long empId = (Long)request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
5.4 代码修复
在前端JS中, js在对长度较长的长整型数据进行处理时, 会损失精度, 从而导致提交的id和数据库中的id不一致。
想解决这个问题,只需要让js处理的ID数据类型转为字符串类型即可, 这样就不会损失精度了。
由于在SpringMVC中, 将Controller方法返回值转换为json对象, 是通过jackson来实现的, 涉及到SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter, 所以我们要解决这个问题, 就需要对该消息转换器的功能进行拓展。
具体实现步骤:
1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
2). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
1). 引入JacksonObjectMapper
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对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
测试:
状态更改成功
六、编辑员工信息
6.1 程序执行流程
-
点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
-
在add.html页面获取url中的参数[员工id]
-
发送ajax请求,请求服务端,同时提交员工id参数
-
服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
-
页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
-
点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
-
服务端接收员工信息,并进行处理,完成后给页面响应
-
页面接收到服务端响应信息后进行相应处理
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作
6.2 代码实现
根据ID查询
经过上述的分析,我们看到,在根据ID查询员工信息时,请求信息如下:
请求 | 说明 |
---|---|
请求方式 | GET |
请求路径 | /employee/{id} |
代码实现:
在EmployeeController中增加方法, 根据ID查询员工信息。
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
修改员工
经过上述的分析,我们看到,在修改员工信息时,请求信息如下:
请求 | 说明 |
---|---|
请求方式 | PUT |
请求路径 | /employee |
请求参数 | {…} json格式数据 |
代码实现:
在EmployeeController中增加方法, 根据ID更新员工信息。
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
Long empId = (Long)request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
6.4 功能测试
数据可以正常修改
最后也欢迎大家来访问我的博客:丸了鸭的博客