- 本文章发布在CSDN上,一是方便博主自己线上阅览,二是巩固自己所学知识。
- 博客内容主要参考上述视频和资料。
- 若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
上一篇:学习【瑞吉外卖①】SpringBoot单体项目:https://blog.csdn.net/yanzhaohanwei/article/details/124981530
0.总目录
- 学习【瑞吉外卖①】SpringBoot单体项目
- 软件开发流程、瑞吉外卖项目介绍、环境搭建、后台登录功能、后台退出功能
- 学习【瑞吉外卖②】SpringBoot单体项目(后台)
- 完善登录功能、 新增员工功能、员工信息分页查询功能、启用 / 禁用员工账号功能、编辑员工信息功能
- 学习【瑞吉外卖③】SpringBoot单体项目(后台)
- 公共字段自动填充功能、新增分类功能、分类信息分页查询功能、删除分类功能、修改分类功能
- 学习【瑞吉外卖④】SpringBoot单体项目(后台)
- 文件上传下载功能、新增菜品功能、菜品信息分页查询功能、修改菜品功能
- 其他功能:删除菜品(单个 / 批量)功能、停售 / 启售菜品(单个 / 批量)功能。
- 学习【瑞吉外卖⑤】SpringBoot单体项目(后台)
- 新增套餐功能、套餐分页查询功能、删除套餐功能(单个 / 批量)
- 其他功能:停售 / 启售(批量 / 单个)套餐功能、修改套餐功能
- 学习【瑞吉外卖⑥】SpringBoot单体项目(移动端)
- 手机验证码登录功能(短信发送、手机验证码登录)
- 学习【瑞吉外卖⑦】SpringBoot单体项目(移动端)
- 用户地址簿功能:增删改查功能,设置、查看默认地址功能。
- 菜品展示功能(套餐展示功能也包含在其中)
- 购物车功能:购物车中增加/减少 套餐/菜品的功能,菜品/套餐 在购物车中的展示功能,购物车中 菜品/套餐 清空的功能
- 订单功能:用户下单功能、用户查看订单功能、用户再来一单功能。
- 用户登出功能
- 学习【瑞吉外卖⑧】SpringBoot单体项目(后台)
- 订单展示功能、订单状态修改功能
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 是员工的登录账号,必须是唯一的。
employee 表中的 status 字段已经设置了默认值 1,表示状态正常。
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'
此时需要我们的程序进行异常捕获,通常有两种处理方式:
- 在 Controller 方法中加入 try { } catch { } 进行异常捕获
com/itheima/reggie/controller/EmployeeController.java
try {
employeeService.save(employee);
} catch {
R.error("新增员工失败");
}
return R.success("新增员工成功");
- 使用异常处理器进行全局异常捕获
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.总结
-
根据产品原型明确业务需求
-
重点分析数据的流转过程和数据格式
-
通过 debug 断点调试跟踪程序执行过程
3.员工信息分页查询
3.1.需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看。
所以一般的系统中都会以分页的方式来展示列表数据。
3.2.代码开发
3.2.1.思路整理
- 在开发代码之前,需要梳理一下整个程序的执行过程:
- 页面发送 ajax 请求,将分页查询参数(page.pageSize、name)提交到服务端
- 服务端 Controller 接收页面提交的数据并调用 Service 查询数据
- Service 调用 Mapper 操作数据库,查询分页数据
- Controller 将查询到的分页数据响应给页面
- 页面接收到分页数据并通过 ElementUI 的 Table 组件展示到页面上
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.思路整理
在开发代码之前,需要梳理一下整个程序的执行过程
- 页面发送 ajax 请求,将参数(id、status)提交到服务端
- 服务端 Controller 接收页面提交的数据并调用 Service 更新数据
- Service 调用 Mapper 操作数据库
4.2.3.分析页面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]
实际上,数据库中对应记录的 id 是 1530928460934004737。
4.2.5.代码修复
前面我们已经发现问题,并找到了原因,即 js 对 long 型数据进行处理时丢失精度,导致提交的 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