Bootstrap

学习【瑞吉外卖②】SpringBoot单体项目_员工管理业务开发



  • 本文章发布在CSDN上,一是方便博主自己线上阅览,二是巩固自己所学知识。
  • 博客内容主要参考上述视频和资料。

  • 若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。


上一篇学习【瑞吉外卖①】SpringBoot单体项目https://blog.csdn.net/yanzhaohanwei/article/details/124981530


0.总目录



1.完善登录功能


1.1.问题分析


前面我们已经完成了后台系统的员工登录功能开发,但是还存在一个问题:用户如果不登录,直接访问系统首页面,照样可以正常访问。

这种设计并不合理,我们希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面。

那么,具体应该怎么实现呢?

答案就是使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。


1.2.代码实现


  • 实现步骤
    • 创建自定义过滤器 LoginCheckFilter
    • 在启动类上加入注解 @ServletComponentScan
    • 完善过滤器的处理逻辑

1.2.1.自定义过滤器


创建自定义过滤器 LoginCheckFilter

com/itheima/reggie/filter/LoginCheckFilter.java

package com.itheima.reggie.filter;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 检查用户是否已经完成登录
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
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);
    }
}

1.2.2.@ServletComponentScan


2.在启动类上加入注解 @ServletComponentScan

com/itheima/reggie/ReggieApplication.java

@ServletComponentScan
  • SpringBootApplication 上使用 @ServletComponentScan 注解后
  • Servlet 可以直接通过 @WebServlet 注解自动注册
  • Filter 可以直接通过 @WebFilter 注解自动注册
  • Listener 可以直接通过 @WebListener 注解自动注册

1.2.3.完善过滤器的处理逻辑


完善过滤器的逻辑处理图

  • 过滤器具体的逻辑处理如下:
    • 1.获取本次请求的URI
    • 2.判断本次请求是否需要处理
    • 3.如果不需要处理,则直接放行
    • 4.判断登录状态,如果已登录,则直接放行
    • 5.如果未登录则返回未登录结果

com/itheima/reggie/filter/LoginCheckFilter.java

package com.itheima.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.itheima.raggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 检查用户是否已经完成登录
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    //Spring 提供的路径匹配器,支持通配符
    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(urls, requestURI);

        //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;
        }

        log.info("用户未登录");

        //5.如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }

    /**
     * 路径匹配,检查本次请求是否需要放行
     *
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls, String requestURI) {
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if (match) {
                return true;
            }
        }
        return false;
    }
}

2.新增员工


2.1.需求分析


后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击“添加员工“”按钮跳转到新增页面。

新增员工页面展示


2.2.数据模型


新增员工,其实就是将我们新增页面录入的员工数据插入到 employee 表。

需要注意的是,employee表中对 username 字段加入了唯一约束,因为 username 是员工的登录账号,必须是唯一的。

username字段的唯一约束

employee 表中的 status 字段已经设置了默认值 1,表示状态正常。

employee表


2.3.代码开发


2.3.1.思路整理


  • 梳理一下整个程序的执行过程
    • 页面发送 ajax 请求,将新增员工页面中输入的数据以 json 的形式提交到服务端
    • 服务端 Controller 接收页面提交的数据并调用 Service 将数据进行保存
    • Service 调用 Mapper 操作数据库,保存数据

员工添加表单输入的数据


2.3.2.添加员工方法


com/itheima/reggie/controller/EmployeeController.java

/**
 * 新增员工
 *
 * @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("新增员工成功");
}

2.3.3.编写全局异常处理器


关于前面的程序,在我们新增员工时输入已存在的账号时,程序会抛异常。

因为 employee 表中对该字段加入了唯一约束。

java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'heniang' for key 'idx_username'

此时需要我们的程序进行异常捕获,通常有两种处理方式:

  1. Controller 方法中加入 try { } catch { } 进行异常捕获

com/itheima/reggie/controller/EmployeeController.java

try {
	employeeService.save(employee);
} catch {
	R.error("新增员工失败");
}

return R.success("新增员工成功");
  1. 使用异常处理器进行全局异常捕获

com/itheima/reggie/common/GlobalExceptionHandler.java

package com.itheima.reggie.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 1. 全局异常处理
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 异常处理方法
     *
     * @param ex
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        log.error(ex.getMessage());

        if (ex.getMessage().contains("Duplicate entry")) {
        	//split() 主要是用于对一个字符串切割,分成多个字符串数组
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }

        return R.error("未知错误");
    }
}
  • ControllerAdvice 本质上是一个 Component ,因此也会被当成组件扫描
  • 作用:1.处理全局异常、2.预设全局数据、3.请求参数预处理
  • @ControllerAdvice
    • @Target({ElementType.TYPE})
    • @Retention(RetentionPolicy.RUNTIME)
    • @Documented
    • @Component
  • 详细情况可以参考这篇博客【@ControllerAdvice 的介绍及三种用法】

2.4.总结


  1. 根据产品原型明确业务需求

  2. 重点分析数据的流转过程和数据格式

  3. 通过 debug 断点调试跟踪程序执行过程

请求响应式


3.员工信息分页查询


3.1.需求分析


系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看。

所以一般的系统中都会以分页的方式来展示列表数据。

员工分页列表展示数据


3.2.代码开发


3.2.1.思路整理


  • 在开发代码之前,需要梳理一下整个程序的执行过程:
    • 页面发送 ajax 请求,将分页查询参数(page.pageSizename)提交到服务端
    • 服务端 Controller 接收页面提交的数据并调用 Service 查询数据
    • Service 调用 Mapper 操作数据库,查询分页数据
    • Controller 将查询到的分页数据响应给页面
    • 页面接收到分页数据并通过 ElementUITable 组件展示到页面上

分页查询的数据


3.2.2.配置MP的分页插件


com/itheima/reggie/config/MybatisPlusConfig.java

package com.itheima.reggie.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置 MP 的分页插件
 */
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

3.2.3.分页查询方法


com/itheima/reggie/controller/EmployeeController.java

/**
 * 员工信息分页查询
 *
 * @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);

    //构造分页构造器
    Page pageInfo = new Page(page, pageSize);

    //构造条件构造器
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
    //添加过滤条件
    queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
    //添加排序条件
    queryWrapper.orderByDesc(Employee::getUpdateTime);

    //执行查询
    employeeService.page(pageInfo, queryWrapper);

    return R.success(pageInfo);
}

4.启用/禁用员工账号


4.1.需求分析


在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。

需要注意,只有管理员(admin 用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。


员工管理页面图_管理员


员工管理页面图_普通员工


4.2.代码实现


4.2.1.分析页面按钮动态显示效果


  • 页面中是怎么做到只有管理员 admin 才能看到启用、禁用按钮的?

分析页面按钮动态显示效果


4.2.2.思路整理


在开发代码之前,需要梳理一下整个程序的执行过程

  1. 页面发送 ajax 请求,将参数(idstatus)提交到服务端
  2. 服务端 Controller 接收页面提交的数据并调用 Service 更新数据
  3. Service 调用 Mapper 操作数据库

更改用户状态信息传递的数据


4.2.3.分析页面ajax请求发送过程


  • 页面中的 ajax 请求是如何发送的?

分析页面ajax请求发送过程


4.2.4.代码开发


启用、禁用员工账号,本质上就是一个更新操作,也就是对 status 状态字段进行操作。

Controller 中创建 update 方法,此方法是一个通用的修改员工信息的方法。

com/itheima/reggie/controller/EmployeeController.java

/**
 * 根据 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("员工信息修改成功");
}

测试过程中没有报错,但是功能没有实现,查看数据库中的数据也没有变化。

观察控制台输出的SQL,发现更新数为 0

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@682eff44] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@68a2736d] will not be managed by Spring
==>  Preparing: UPDATE employee SET status=?, update_time=?, update_user=? WHERE id=?
==> Parameters: 0(Integer), 2022-06-01T08:45:06.492(LocalDateTime), 1(Long), 1530928460934004700(Long)
<==    Updates: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@682eff44]

实际上,数据库中对应记录的 id1530928460934004737


4.2.5.代码修复


前面我们已经发现问题,并找到了原因,即 jslong 型数据进行处理时丢失精度,导致提交的 id 和数据库中的 id 不一致。

如何解决该问题?

我们可以在服务端给页面响应 json 数据时进行处理,将 long 型数据统一转换为 String 字符串。

  • 具体实现步骤

  • 提供对象转换器 JacksonObjectMapper ,基于 Jackson 进行 Java 对象到 json 数据的转换。

  • WebMvcConfig配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到 json 数据的转换

com/itheima/reggie/common/JacksonObjectMapper.java

package com.itheima.reggie.common;

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);
    }
}

com/itheima/reggie/config/WebMvcConfig.java

/**
  * 扩展 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);
 }

5.编辑员工信息


5.1.需求分析


在员工管理列表页面点击编辑按钮,跳转到编辑页面。

在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作。

员工信息编辑页面


5.2.代码实现


5.2.1.思路整理


  • 在开发代码之前需要梳理一下操作过程和对应的程序的执行流程
    • 点击编辑按钮时,页面跳转到 add.html ,并在 url 中携带参数[员工 id]
    • add.html 页面获取 url 中的参数[员工 id]
    • 发送 ajax 请求,请求服务端,同时提交员工id参数
    • 服务端接收请求,根据员工 id 查询员工信息,将员工信息以 json 形式响应给页面
    • 页面接收服务端响应的 json 数据,通过VUE的数据绑定进行员工信息回显
    • 点击保存按钮,发送 ajax 请求,将页面中的员工信息以 json 方式提交给服务端
    • 服务端接收员工信息,并进行处理,完成后给页面响应
    • 页面接收到服务端响应信息后进行相应处理

  • 注意
    • add.html 页面为公共页面。新增员工和编辑员工都是在此页面操作。
    • 故该代码部分与之前添加员工代码对应,不需要重写。

5.2.2.代码开发


该代码部分与之前添加员工代码对应,此处无需重写。

故只需写查询方法(根据id查询)即可。

com/itheima/reggie/controller/EmployeeController.java

/**
 * 根据 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("没有查询到对应员工信息");
}

下一篇学习【瑞吉外卖③】SpringBoot单体项目https://blog.csdn.net/yanzhaohanwei/article/details/125110127


悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;