本笔记源自黑马程序员的视频课程——《瑞吉外卖》,总结了课程笔记、相关知识点以及可能遇到的问题解决方案,并且增加了课程中未实现的功能,供读者参考。笔记全面且条理清晰,希望帮助读者学习和理解这个外卖项目。
本项目全部笔记见:外卖项目笔记合集
1. 完善登录功能
1.1 问题分析
前面我们已经完成了后台系统的员工登录功能开发,但是还存在一个问题:用户如果不登录,直接访问系统首页面,照样可以正常访问。
这种设计并不合理,我们希望看到的效果应该是,只有登录后才能访问后台管理页面,如果没有登录,则跳转到登录页面。
那么,具体应该怎么实现呢?
答案就是使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。
1.2 代码实现
1.2.1 实现思路
- 创建自定义过滤器LoginCheckFilter
- 在启动类上加入注解@ServletComponentScan
- 完善过滤器的处理逻辑
1.2.2 具体实现
1、创建filter包,在其中创建LoginCheckFilter类,实现Filter接口,并重写doFilter()方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@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); } }
|
说明:
- @WebFilter替代web.xml配置过滤器。filterName指定过滤器名称(随意);urlPatterns指定拦截的请求。
urlPatterns = "/*"
表示工程路径下所有请求路径都拦截。
- 继承Filter时不要导错包,import javax.servlet.*
2、在启动类上加入注解@ServletComponentScan
加上这个注解后,才会扫描@WebFilter注解,使得过滤器生效。
1 2 3 4 5 6 7 8 9
| @Slf4j @SpringBootApplication @ServletComponentScan public class ReggieApplication { public static void main(String[] args) { SpringApplication.run(ReggieApplication.class,args); log.info("项目启动成功"); } }
|
3、完善过滤器的处理逻辑
处理逻辑如下:
- 获取本次请求的URI
- 判断本次请求是否需要处理
- 如果不需要处理,则直接放行
- 判断登录状态,如果已登录,则直接放行
- 如果未登录,则返回未登录结果

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
@Slf4j @WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*") public class LoginCheckFilter implements Filter {
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestURI = request.getRequestURI(); log.info("拦截到请求:{}", requestURI);
String[] urls = new String[]{ "/employee/login", "/employee/logout", "/backend/**", "/front/**" }; boolean check = check(urls, requestURI);
if(check){ log.info("本次请求{}无需处理", requestURI); chain.doFilter(request, response); return; }
HttpSession session = request.getSession(); if(session.getAttribute("employee") != null){ log.info("用户已登录,用户id为:{}", session.getAttribute("employee")); chain.doFilter(request, response); return; }
log.info("用户未登录"); response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
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是员工的登录账号,必须是唯一的。

2.3 代码开发
2.3.1 程序执行过程
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
在“添加员工”页面填写信息后,点击“保存”按钮,会发请求,可以通过浏览器调试工具(按F12;或者在浏览器点右键,选择“检查”,切换到“Network”)查看。请求如下:

2、服务端Controller接收页面提交的数据,并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据到employee表
2.3.2 程序开发
首先,我们需要编写controller方法,处理/employee请求,method=POST。controller方法需要接收页面提交的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@PostMapping public R<String> save(HttpServletRequest request, @RequestBody Employee employee){ log.info("新增员工,员工信息:{}",employee.toString()); employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now());
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId); employee.setUpdateUser(empId);
employeeService.save(employee);
return R.success("新增员工成功"); }
|
说明:
返回类型是R是因为,保存员工时,前端只需要code这个数据,不需要data,所以用一个简单的String即可。
2.3.3 完善全局异常处理器
前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对username字段加入了唯一约束,此时程序会抛出异常:

此时需要我们的程序进行异常捕获,通常有两种处理方式:
1、在Controller方法中加入try-catch进行异常捕获

2、使用异常处理器进行全局异常捕获
我们使用第2种方法。在common包下创建GlobalExceptionHandler类。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody @Slf4j public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class) public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){ log.error(ex.getMessage()); if(ex.getMessage().contains("Duplicate entry")){ String[] split = ex.getMessage().split(" "); String msg = split[2] + "已存在"; return R.error(msg); } return R.error("未知错误"); } }
|
总结:
1、根据产品原型明确业务需求
2、重点分析数据的流转过程和数据格式
3、通过debug断点调试跟踪程序执行过程
3. 员工信息分页查询
3.1 需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

3.2 代码开发
3.2.1 程序执行过程
在开发代码之前,需要梳理一下整个程序的执行过程:
- 页面发送ajax请求,将分页查询参数(page.pageSize、name)提交到服务端
- 服务端Controller接收页面提交的数据并调用Service查询数据
- Service调用Mapper操作数据库,查询分页数据
- Controller将查询到的分页数据响应给页面
- 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
3.2.2 配置mybatis-plus分页插件
在config包下创建MybatisPlusConfig类,如下:
1 2 3 4 5 6 7 8 9 10 11
| @Configuration public class MybatisPlusConfig {
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mybatisPlusInterceptor; } }
|
3.2.3 员工信息分页查询程序开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
@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); }
|
说明:
1、返回类型:R的泛型不能是Employee。因为页面需要服务端传回records、total等数据以显示分页页面(见list.html),这里需要使用Page类型。
2、参数:请求http://localhost:8080/employee/page?page=1&pageSize=10 中带着参数发过来,声明和请求参数同名的形参,可以接收请求参数。跳页码时,请求参数带着page和pageSize;在搜索框搜索员工姓名时,还另外带着name请求参数。
3、return R.success(pageInfo);
把pageInfo信息传回前端,list.html中的getMemberList()方法可以拿到Page对象里的records、total等信息
3.3 功能测试

4. 启用/禁用员工账号
4.1 需求分析
管理员admin登录系统后,可以在员工管理列表页面,对某个员工账号进行启用、禁用操作。
如果某个员工账号状态为正常,则按钮显示为 “禁用“,如果员工账号状态为已禁用,则按钮显示为 “启用”。
注意只有管理员可以看到“禁用”/“启用”按钮(如下图),普通用户看不到,只能看到“编辑”按钮。

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

4.2 代码开发
4.2.1 程序执行过程
在开发代码之前,需要梳理一下整个程序的执行过程:
1、点击“禁用”/“启用”按钮后,页面发送ajax请求,将参数(id、 status)提交到服务端
2、服务端Controller接收页面提交的数据,并调用Service更新数据
3、Service调用Mapper操作数据库
页面发出的ajax请求:

页面中的ajax请求是如何发送的?
点击”禁用”/“启用”按钮后,会执行statusHandle()
,并传入row参数,即这一条(行)数据封装的json对象。
如果页面中“确认调整该账号的状态?”的对话框点了“确定”,就执行then后面的回调函数,执行enableOrDisableEmployee()
方法,向/employee发送put请求,带上参数:id 和 status(以json格式传给服务器)。其中,id是在this.id = row.id
处获取到的this.id。status是通过'status': !this.status ? 1 : 0
一句算出来的,要调整成跟原来status相反。即,如果当前status为0,传给服务器的status为1;如果当前status为1,传给服务器的status为0。

请求发出后,需要controller编写方法处理该请求。
4.2.2 程序开发
“禁用”/“启用”实际上是更新操作,修改employee表的status字段。
我们现在写一个通用的修改方法,根据id修改员工信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@PutMapping("") public R<String> update(@RequestBody Employee employee, HttpServletRequest request){ log.info(employee.toString());
employee.setUpdateTime(LocalDateTime.now());
Long empId = (Long) request.getSession().getAttribute("employee"); employee.setUpdateUser(empId);
employeeService.updateById(employee); return R.success("修改成功"); }
|
说明:
返回类型:前端只需要简单的code,不需要其他数据,所以返回R类型的就可以。
参数:由于前端传来的是json形式的参数(id和status),需加上@RequestBody注解,获取请求参数。
4.3 功能测试
测试中页面没有报错,但是查看数据库发现并没有修改成功。
观察控制台的SQL语句,SQL执行后更新的数据行数为0。表示没有匹配到记录,没有更新成功。

仔细观察id的值(Long型,19位长度,以200结尾),和数据表中对应记录的id值(以226结尾)并不一致。

在页面调试工具处看,在分页查询数据时,查到数据后给页面响应的id是正常的,以226结尾;但是点“禁用”按钮后,页面发送的id就有问题了,以200结尾。


4.4 代码修复
4.4.1 问题分析
通过观察控制台输出的SQL,发现页面传递过来的员工id的值和数据库中的id值不一致,这是怎么回事呢?
分页查询时,服务端响应给页面的数据中id的值为19位数字,类型为long。
页面中js处理long型数字只能精确到前16位,所以最终通过ajax请求提交给服务端的时候id就改变了,变为以200结尾。即,js对long型数据处理时丢失精度,导致提交的id和数据库中的id不一致。
4.4.2 解决方案
我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串。
具体实现步骤:
1、提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
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); } }
|
2、在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中,使用提供的对象转换器进行Java对象到json数据的转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..."); MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
messageConverter.setObjectMapper(new JacksonObjectMapper());
converters.add(0, messageConverter); }
|
5. 编辑员工信息
5.1 需求分析
在员工管理列表页面点击编辑按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作。

5.2 代码开发
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1、点击“编辑”按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2、在add.html页面获取url中的参数[员工id]
3、发送ajax请求,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面(页面回显)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@GetMapping("/{id}") public R<Employee> getById(@PathVariable Long id){ log.info("根据id查询员工"); Employee employee = employeeService.getById(id); if(employee != null){ return R.success(employee); }else { return R.error("没有查询到该员工"); } }
|
5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6、点击“保存”按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7、服务端接收员工信息,并进行处理,完成后给页面响应
8、页面接收到服务端响应信息后进行相应处理
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作。所以点“保存后”,修改员工信息的操作由之前的update()方法可以完成,不需要重写。
5.3 功能测试
启动服务,测试功能即可。