瑞吉外卖02-员工管理

本笔记源自黑马程序员的视频课程——《瑞吉外卖》,总结了课程笔记、相关知识点以及可能遇到的问题解决方案,并且增加了课程中未实现的功能,供读者参考。笔记全面且条理清晰,希望帮助读者学习和理解这个外卖项目。
本项目全部笔记见:外卖项目笔记合集

1. 完善登录功能

1.1 问题分析

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

这种设计并不合理,我们希望看到的效果应该是,只有登录后才能访问后台管理页面如果没有登录,则跳转到登录页面。

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

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

1.2 代码实现

1.2.1 实现思路

  1. 创建自定义过滤器LoginCheckFilter
  2. 在启动类上加入注解@ServletComponentScan
  3. 完善过滤器的处理逻辑

1.2.2 具体实现

1、创建filter包,在其中创建LoginCheckFilter类,实现Filter接口,并重写doFilter()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 登录检查过滤器(也可以用拦截器interceptor实现,此项目中用过滤器实现)
* 检查用户是否已经完成登录
*/
@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); // 放行
}
}

说明:

  1. @WebFilter替代web.xml配置过滤器。filterName指定过滤器名称(随意);urlPatterns指定拦截的请求。urlPatterns = "/*"表示工程路径下所有请求路径都拦截。
  2. 继承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、完善过滤器的处理逻辑

处理逻辑如下:

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

1

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
/**
* @ClassName LoginCheckFilter
* @Description 登录检查过滤器(也可以用拦截器interceptor实现,此项目中用过滤器实现)
* 检查用户是否已经完成登录
*/
@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;

//1、获取本次请求的URI
String requestURI = request.getRequestURI();
log.info("拦截到请求:{}", requestURI);

//2、判断本次请求是否需要处理
//不需要处理的请求路径:
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
boolean check = check(urls, requestURI);

//3、如果不需要处理,则直接放行
if(check){
log.info("本次请求{}无需处理", requestURI); // {}是占位符,由后面的参数填充
chain.doFilter(request, response); // 放行
return; // 放行之后,后面代码不需要执行了,直接return结束
}

//4-1、判断登录状态,如果已登录,则直接放行
HttpSession session = request.getSession();
if(session.getAttribute("employee") != null){ //判断后台员工登录状态
//已登录
log.info("用户已登录,用户id为:{}", session.getAttribute("employee")); //session中,"employee"记录的是登录用户的id
chain.doFilter(request, response); //放行
return;
}

//5、如果未登录,向浏览器响应数据,返回未登录结果
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
/* 通过输出流,向浏览器响应数据,给R.error传回msg ——"NOTLOGIN"。
在request.js页面的前端响应拦截器处,会拦截服务器给的响应。如果code==0,且msg显示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.2 数据模型

新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的。

3

2.3 代码开发

2.3.1 程序执行过程

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

1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端

在“添加员工”页面填写信息后,点击“保存”按钮,会发请求,可以通过浏览器调试工具(按F12;或者在浏览器点右键,选择“检查”,切换到“Network”)查看。请求如下:

4

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
/**
* 添加员工
* @param employee 待被添加的员工
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
//设置初始密码(需要md5加密处理)
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());

//status在入库时默认值1,此处不用设置

//没使用mybatisplus的"自动填充公共字段"功能之前,自己设置创建/更新人
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字段加入了唯一约束,此时程序会抛出异常:

5

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

1、在Controller方法中加入try-catch进行异常捕获

6

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
/**
* @ClassName GlobalExceptionHandler
* @Description 全局异常处理类,捕获处理各种可能出现的异常。把处理异常的方法封装到此类中,简化代码
* 通过aop,拦截到save等控制器方法,在此处理
* 并返回错误信息给前端
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})// 指定拦截哪些controller
// 加了@RestController注解或@Controller注解的方法,会被该拦截器拦截下来处理
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* 一旦标注了@RestController注解或@Controller注解的控制器方法,抛SQLIntegrityConstraintViolationException异常,
* 就会被这个exceptionHandler()方法拦截到,在这里处理
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class) //处理哪个异常的方法
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());//异常信息:SQLIntegrityConstraintViolationException:Duplicate entry 'zhangsan' for key 'employee.idx_username'
if(ex.getMessage().contains("Duplicate entry")){ //说明违反了数据表的UNIQUE约束
String[] split = ex.getMessage().split(" ");//将错误信息按空格划分开,数组内索引=2的元素是username信息
String msg = split[2] + "已存在"; //xxx(username)已存在
return R.error(msg);//给前端页面返回
}
return R.error("未知错误");//给前端页面返回
}
}

总结:

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

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

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

3. 员工信息分页查询

3.1 需求分析

系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

7

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 {

//配置MybatisPlus的分页插件:
@Bean //让spring管理它
public MybatisPlusInterceptor mybatisPlusInterceptor(){ //配置MybatisPlusInterceptor类型的bean
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());//加入拦截器。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
/**
* 员工信息的分页查询
* @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);
//构造条件构造器:如果name传过来,还需要过滤条件(where name= ?)
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件:只要name不为空,就在Employee表中,根据name字段查询,要查的值是传来的参数name
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);//like方法的参数:执行条件,字段,值。相当于:LIKE '%值%'
//添加排序条件:按照Employee表里的更新时间 倒序排列,即最近更新的排在前面(orderBy)
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo, queryWrapper); //处理好会封装到Page类型的pageInfo中,pageInfo中含有数据列表records、总记录数total等信息

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 功能测试

8

4. 启用/禁用员工账号

4.1 需求分析

管理员admin登录系统后,可以在员工管理列表页面,对某个员工账号进行启用、禁用操作。

如果某个员工账号状态为正常,则按钮显示为 “禁用“,如果员工账号状态为已禁用,则按钮显示为 “启用”。

注意只有管理员可以看到“禁用”/“启用”按钮(如下图),普通用户看不到,只能看到“编辑”按钮。

9

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

10

4.2 代码开发

4.2.1 程序执行过程

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

1、点击“禁用”/“启用”按钮后,页面发送ajax请求,将参数(id、 status)提交到服务端

2、服务端Controller接收页面提交的数据,并调用Service更新数据

3、Service调用Mapper操作数据库

页面发出的ajax请求:

11

页面中的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。

12

请求发出后,需要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
/**
* 根据id修改员工信息
* 从修改员工页面,修改信息后,点击"确定",向http://localhost:8080/employee发送put请求
* @param employee
* @return
*/
@PutMapping("")
public R<String> update(@RequestBody Employee employee, HttpServletRequest request){
log.info(employee.toString());// Employee(id=1625874579766513700, username=null, name=null, password=null, phone=null, sex=null, idNumber=null, status=0, createTime=null, updateTime=null, createUser=null, updateUser=null)

//没使用mybatisplus的"自动填充公共字段"功能之前,自己设置创建/更新人/时间。(后续会讲"自动填充公共字段")
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。表示没有匹配到记录,没有更新成功。

13

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

14

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

15

16

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
/**
* 对象映射器:基于jackson,将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*
* 继承的ObjectMapper是导入的jackson包中的。可以将java对象转成json数据,也可以把json数据转成java对象
*/
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) //序列化器:将Long转换成String
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))//格式化LocalDateTime型数据
.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
/**
* 扩展自己的消息转换器
* 以便于将Long型的id转为String型,不丢失精度
* @param converters the list of configured converters to extend
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {//mvc框架默认有几个转换器,放在converters中
log.info("扩展消息转换器...");
//创建消息转换器对象。将controller方法的返回结果R对象,转成json,再通过输出流响应给页面
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();

//设置对象转换器,底层使用jackson,将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());//JacksonObjectMapper是粘贴过来的对象映射器

//将上面的消息转化器对象 追加到mvc框架的转换器集合中
converters.add(0, messageConverter); //把自定义的转换器追加到converters中,并放在最前面优先用,否则可能会先用别的转换器
}

5. 编辑员工信息

5.1 需求分析

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

17

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
/**
* 根据id查询员工
* 为了在修改时,可以回显信息
* 修改时,浏览器向服务器发送请求:http://localhost:8080/employee/1626479515129643009
* 处理之后,需要传回code、data、msg等数据
* @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);//把查出的employee转成json发回前端
}else {
return R.error("没有查询到该员工");
}
}

5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显

6、点击“保存”按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端

7、服务端接收员工信息,并进行处理,完成后给页面响应

8、页面接收到服务端响应信息后进行相应处理

注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作。所以点“保存后”,修改员工信息的操作由之前的update()方法可以完成,不需要重写。

5.3 功能测试

启动服务,测试功能即可。