外卖-Day02
课程内容
-
完善登录功能
-
新增员工
-
员工信息分页查询
-
启用/禁用员工账号
-
编辑员工信息
1. 完善登录功能
1.1 问题分析
前面我们已经完成了后台系统的员工登录功能开发,但是目前还存在一个问题,接下来我们来说明一个这个问题, 以及如何处理。
1). 目前现状
用户如果不登录,直接访问系统首页面,照样可以正常访问。
2). 理想效果
上述这种设计并不合理,我们希望看到的效果应该 是,只有登录成功后才可以访问系统中的页面,如果没有登录, 访问系统中的任何界面都直接跳转到登录页面。
那么,具体应该怎么实现呢?
可以使用我们之前讲解过的 过滤器、拦截器来实现,在过滤器、拦截器中拦截前端发起的请求,判断用户是否已经完成登录,如果没有登录则返回提示信息,跳转到登录页面。
1.2 思路分析
过滤器具体的处理逻辑如下:
A. 获取本次请求的URI
B. 判断本次请求, 是否需要登录, 才可以访问
C. 如果不需要,则直接放行
D. 判断登录状态,如果已登录,则直接放行
E. 如果未登录, 则返回未登录结果
如果未登录,我们需要给前端返回什么样的结果呢? 这个时候, 我们可以去看看前端是如何处理的 ?
1.3 代码实现
1). 定义登录校验过滤器
自定义一个过滤器 LoginCheckFilter 并实现 Filter 接口, 在doFilter方法中完成校验的逻辑。 那么接下来, 我们就根据上述分析的步骤, 来完成具体的功能代码实现:
其实就是javaWeb基础的过滤器,也就是Servlet提供的很原生的过滤器: javaWeb核心05-Filter(JSON模块有后端输出流直接响应数据的方式)
(注解里多写了一个名字)
所属包: cn.whu.reggie.filter
/**
* 检查用户是否已经完成登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = {"/*"})
@Slf4j //要习惯用日志而非 System.out.println();
public class LoginCheckFilter implements Filter {//还是用的javaWeb基础的过滤器
//Spring提供的工具类: 路径匹配器,支持通配符匹配
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
// 实际使用中 实现doFilter一个方法就OK啦
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//A. 获取本次请求的URI
String requestURI = request.getRequestURI(); //URI即可 不需要URL全路径 也没办法用通配符匹配啊
log.info("拦截到请求:{}",requestURI);
String[] urls = {
"/employee/login", // 注意开头的/不能少,得和URI匹配上
"/employee/logout",
"/front/**", //静态资源直接放行 只拦截Controller即可 页面让你看 但是没有数据啊,看了也白看
"/backend/**" // 注意此处是AntPathMatcher类的通配符匹配规则 /* 和 /** 匹配规则不同
};//放行路径
//B. 判断本次请求, 是否需要登录, 才可以访问
boolean check = check(urls, requestURI);
//C. 如果不需要,则直接放行
if(check){
log.info("本次请求{} 不需要处理",requestURI);
chain.doFilter(request,response);
return; // 变不需要执行了 直接return (否则过滤器链 原始controller执行完毕 会返回到这里继续往下执行的)
}
//D. 判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employeeId")!=null){
log.info("用户已经登录,直接放行,用户id为{}",request.getSession().getAttribute("employeeId"));
chain.doFilter(request,response);//已经登录上了 直接放行
return;
}
log.info("用户未登录");
//E. 如果未登录, 则返回未登录结果
// 访问的是需要登录的资源,而且没有登录
// 前端也有拦截器 专门拦截服务端给前端页面的响应,所以response得出马了
// 非Controller里如何给前端响应数据? 就是普通Servlet的方式,输出流即可: response.getWriter().write("xx");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
//原始方式给前端返回json串 所以底层API还是要学学的 不能啥都靠框架,自己也得会
return; //直接return 不能放行 (不写也return也 写了逻辑更清晰)
//log.info("拦截到请求:{}",request.getRequestURL()); // log'{}'占位符语法 // URI短 URL长
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] patterns,String requestURI){
for (String pattern : patterns) {
boolean match = PATH_MATCHER.match(pattern, requestURI);//match不是matchStart 代码提示注意一下,别选错了
if(match){//人家的编码习惯 不要求短 要求稳
return true;
}
}
return false;
}
}
AntPathMatcher 拓展:
介绍: Spring中提供的路径匹配器 ;
通配符规则:
符号 含义 ? 匹配一个字符 * 匹配0个或多个字符 ** 匹配0个或多个目录/字符
2). 开启组件扫描
需要在引导类上, 加上Servlet组件扫描的注解, 来扫描过滤器配置的@WebFilter注解, 扫描上之后, 过滤器在运行时就生效了。
@Slf4j
@SpringBootApplication
@ServletComponentScan
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功...");
}
}
@ServletComponentScan 的作用:
在SpringBoot项目中, 在引导类/配置类上加了该注解后, 会自动扫描项目中(当前包及其子包下)的@WebServlet , @WebFilter , @WebListener 注解, 自动注册Servlet的相关组件 ;
1.4 功能测试
代码编写完毕之后,我们需要将工程重启一下,然后在浏览器地址栏直接输入系统管理后台首页,然后看看是否可以跳转到登录页面即可。我们也可以通过debug的形式来跟踪一下代码执行的过程。
对于前端的代码, 也可以进行debug调试。
F12打开浏览器的调试工具, 找到我们前面提到的request.js, 在request.js的响应拦截器位置打上断点。
其中request.js文件相对路径不行,改成了绝对路径
改完千万记得ctrl+shift+delete 清空一下浏览器缓存,否则不会生效的
浏览器打断点:
注意:request.js文件有问题,修改了下,但是一直不生效,原来是浏览器缓存了。所以千万注意修改了纯.js文件一定要先清空浏览器缓存
最后:注意日志的使用,
@Slf4j
log.info("本次请求{} 不需要处理",requestURI);
调试多方便啊:
2. 新增员工
2.1 需求分析
后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击[添加员工]按钮跳转到新增页面,如下:
当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。
2.2 数据模型
新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。employee表中的status字段已经设置了默认值1,表示状态正常。
需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的。
2.3 程序执行流程
在开发代码之前,我们需要结合着前端页面发起的请求, 梳理一下整个程序的执行过程:
A. 点击"保存"按钮, 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端, 请求方式POST, 请求路径 /employee
B. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
C. Service调用Mapper操作数据库,保存数据
2.4 代码实现
在EmployeeController中增加save方法, 用于保存用户员工信息。
A. 在新增员工时, 按钮页面原型中的需求描述, 需要给员工设置初始默认密码 123456, 并对密码进行MD5加密。
B. 在组装员工信息时, 还需要封装创建时间、修改时间,创建人、修改人信息(从session中获取当前登录用户)。
我的版本:
/**
* 新增员工
* @param request
* @param employe
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employe){
log.info("新增员工,员工信息: {}",employe.toString());
//此日志可以很方便看出哪些字段为空,然后去数据库看哪些已经有默认值,剩下没有默认值的就是我们要在此处给出默认值的
//1. username字段必须唯一
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
lqw.eq(Employee::getUsername,employe.getUsername());
Employee usernameEmploye = employeeService.getOne(lqw);
if(usernameEmploye!=null){
return R.error("用户名已经被使用");
}
//2. 在新增员工时, 按钮页面原型中的需求描述, 需要给员工设置初始默认密码 123456, 并对密码进行MD5加密。
employe.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
//3. 在组装员工信息时, 还需要封装创建时间、修改时间,创建人、修改人信息(从session中获取当前登录用户).
employe.setCreateTime(LocalDateTime.now());
employe.setUpdateTime(LocalDateTime.now());
Long id = (Long) request.getSession().getAttribute("employeeId");
employe.setCreateUser(id);
employe.setUpdateUser(id);
boolean save = employeeService.save(employe);
if(save){
return R.success("添加成功");
}else {
return R.error("添加失败");
}
}
教程版本:
/**
* 新增员工
* @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.5 功能测试
代码编写完毕之后,我们需要将工程重启, 完毕之后直接访问管理系统首页, 点击 “员工管理” 页面中的 “添加员工” 按钮, 输入员工基本信息, 然后点击 “保存” 进行数据保存, 保存完毕后, 检查数据库中是否录入员工数据。
当我们在测试中,添加用户时, 输入了一个已存在的用户名时,前端界面出现错误提示信息:
教材版本:
我的版本:(做了用户名校验就不会了)
而此时,服务端已经报错了, 报错信息如下:
出现上述的错误, 主要就是因为在 employee 表结构中,我们针对于username字段,建立了唯一索引,添加重复的username数据时,违背该约束,就会报错。但是此时前端提示的信息并不具体,用户并不知道是因为什么原因造成的该异常,我们需要给用户提示详细的错误信息 。
故意不校验只是为了演示不可避免的全局异常该如何处理。
转全局异常处理确实更好,也更具有通用性,因为可以处理所有的唯一字段约束异常提示
故而:这里舍弃我的版本,改用教程版本
改成如下:
/**
* 新增员工
* @param request
* @param employe
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employe){
log.info("新增员工,员工信息: {}",employe.toString());
//此日志可以很方便看出哪些字段为空,然后去数据库看哪些已经有默认值,剩下没有默认值的就是我们要在此处给出默认值的
//1. username字段必须唯一
/*LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
lqw.eq(Employee::getUsername,employe.getUsername());
Employee usernameEmploye = employeeService.getOne(lqw);
if(usernameEmploye!=null){
return R.error("用户名已经被使用");
}*/
// 改为全局异常处理了
//2. 在新增员工时, 按钮页面原型中的需求描述, 需要给员工设置初始默认密码 123456, 并对密码进行MD5加密。
employe.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
//3. 在组装员工信息时, 还需要封装创建时间、修改时间,创建人、修改人信息(从session中获取当前登录用户).
employe.setCreateTime(LocalDateTime.now());
employe.setUpdateTime(LocalDateTime.now());
Long id = (Long) request.getSession().getAttribute("employeeId");
employe.setCreateUser(id);
employe.setUpdateUser(id);
/*boolean save = employeeService.save(employe);
if(save){
return R.success("添加成功");
}else {
return R.error("添加失败");
}*/
// 不必判断,转全局异常处理
employeeService.save(employe);
return R.success("新增员工成功!");
}
2.6 全局异常处理 (写一个单独的拦截器类就行了)
2.6.1 思路分析
要想解决上述测试中存在的问题,我们需要对程序中可能出现的异常进行捕获,通常有两种处理方式:
A. 在Controller方法中加入 try…catch 进行异常捕获
形式如下:
如果采用这种方式,虽然可以解决,但是存在弊端,需要我们在保存其他业务数据时,也需要在Controller方法中加上try…catch进行处理,代码冗余,不通用。
B. 使用异常处理器进行全局异常捕获
采用这种方式来实现,我们只需要在项目中定义一个通用的全局异常处理器,就可以解决本项目的所有异常。
2.6.2 全局异常处理器
在项目中自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 @ExceptionHandler 来指定拦截的是那一类型的异常。
异常处理方法逻辑:
- 指定捕获的异常类型为 SQLIntegrityConstraintViolationException
- 解析异常的提示信息, 获取出是那个值违背了唯一约束
- 组装错误信息并返回
所有Exception全部抛到Controller统一处理,然后所有Controller的异常又用AOP增强方式一劳永逸
写一个全局异常处理器:相关博客:springMVC02-SSM整合:三、异常处理器【理解】
common包下: 全局异常处理器
cn.whu.reggie.common
@ControllerAdvice(annotations = {RestControllerAdvice.class, Controller.class})//对所有RestController或者Controller进行AOP增强
@ResponseBody //设置当前控制器方法响应内容为当前返回值,无需当做页面来解析跳转 (不是返回页面)
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* AOP:统一处理,所有Controller的SQLIntegrityConstraintViolationException异常
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){//函数名无所谓 重要的是注解
log.error("捕获到Controller的异常,异常信息为: {}",ex.getMessage());
// ex.getMessage(): Duplicate entry '张三丰' for key 'idx_username'
if(ex.getMessage().contains("Duplicate entry")){//此时一定是违反了唯一键约束
String[] split = ex.getMessage().split(" ");
String msg = split[2] + " 已存在";//Duplicate entry '张三丰' for key 'idx_username': '张三丰'下标为2
return R.error(msg);
}
return R.error("未知错误");//其他sql语句执行错误
}
}
注解说明:
上述的全局异常处理器上使用了的两个注解 @ControllerAdvice , @ResponseBody , 他们的作用分别为:
@ControllerAdvice : 指定拦截那些类型的控制器;
@ResponseBody: 将方法的返回值 R 对象转换为json格式的数据, 响应给页面;
上述使用的两个注解, 也可以合并成为一个注解 @RestControllerAdvice
(类似 @RestController = @Controller + @ResponseBody + “一些无关紧要的”)
eg:
2.6.3 测试
全局异常处理器编写完毕之后,我们需要将项目重启, 完毕之后直接访问管理系统首页, 点击 “员工管理” 页面中的 “添加员工” 按钮。当我们在测试中,添加用户时, 输入了一个已存在的用户名时,前端界面出现如下错误提示信息:
3. 员工分页查询
3.1 需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。而在我们的分页查询页面中, 除了分页条件以外,还有一个查询条件 “员工姓名”。
-
请求参数
-
搜索条件: 员工姓名(模糊查询)
-
分页条件: 每页展示条数 , 页码
-
-
响应数据
-
总记录数
-
结果列表
-
3.2 程序执行流程
3.2.1 页面流程分析
在开发代码之前,需要梳理一下整个程序的执行过程。
A. 点击菜单,打开员工管理页面时,执行查询:
B. 搜索栏输入员工姓名,回车,执行查询:
1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
3). Service调用Mapper操作数据库,查询分页数据
4). Controller将查询到的分页数据, 响应给前端页面
5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
3.2.2 前端代码介绍
1). 访问员工列表页面/member/list.html时, 会触发Vuejs中的钩子方法, 在页面初始化时调用created方法
注意: Get请求参数,他也组装成了json格式
从上述的前端代码中我们可以看到, 执行完分页查询, 我们需要给前端返回的信息中需要包含两项 : records 中封装结果列表, total中封装总记录数 。
而在组装请求参数时 , page、pageSize 都是前端分页插件渲染时的参数;
2). 在getMemberList方法中, 通过axios发起异步请求
axios发起的异步请求会被声明在 request.js 中的request拦截器拦截, 在其中对get请求进行进一步的封装处理
解开json数据,拼接到url后面,get请求得这么做(拦截器的价值)
最终发送给服务端的请求为 : GET请求 , 请求链接 /employee/page?page=1&pageSize=10&name=xxx
3.3 代码实现
3.3.1 分页插件配置
当前我们要实现的分页查询功能,而在MybatisPlus要实现分页功能,就需要用到MybatisPlus中提供的分页插件,要使用分页插件,就要在配置类中声明分页插件的bean对象。
所属包: com.itheima.reggie.config
/**
* 配置MP插件功能需要的辅助拦截器
*/
@Configuration //此类才会被扫描到
public class MyBatisPlusConfig {
@Bean //IOC容器中有MybatisPlusInterceptor类型对象 且其属性里有MP分页拦截后具体的增强操作
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
// 这里可以加入各种各样MP的内置拦截器 // 比如这里加入的是分页插件拦截器
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}
3.3.2 分页查询实现
在上面我们已经分析了,页面在进行分页查询时, 具体的请求信息如下:
请求 | 说明 |
---|---|
请求方式 | 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){//get请求 普通url参数
log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
//构造分页构造器
Page pageInfo = new Page(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
//添加过滤条件
if(name!=null) name = name.trim();//工具类没有trim 自己想trim就trim
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
MP条件构造器: https://baomidou.com/pages/10c804/#notlikeleft
我的错误版本以及修改版本:
// 我的写法 竟然忘记了 get请求参数直接封装在url里面,直接可以获取
/*@GetMapping("/page")
public R<IPage> getPage(Page<Employee> page){ // MP提供好了现成的Page类 // 不写@RequestBody 也收到了(因为这是get请求 普通name='...'参数) name怎么办?
//page没有封装toString方法 只能打断点看了 然后自己打印喽
log.info("getPage: 当前页:{} 每页数量:{}",page.getCurrent(),page.getSize());
employeeService.page(page);//查看源码,就是selectPage
log.info("查询到员工信息:{}",page.getRecords());
//要写MP拦截器,里面加上分页拦截器,否则获取得page里的内容总是有个别字段为空
return R.success(page);
}*/
//教程写法
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){//Get请求 getParamter("xxx")就可以获取 所以直接写形参即可获取
log.info("分页查询: 当前页page={}, 页面大小pageSize={}, 查询条件name={}",page,pageSize,name);
//构造分页构造器
Page<Employee> pageInfo = new Page(page, pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
//添加过滤条件
/*if(name!=null&&!"".equals(name.trim())){
lqw.eq(Employee::getName,name); // 角色名 填了就要加进来
}*/
//学过的东西又不会用了。。。 这个工具类不好用 没有trim (前端也没有trim)
if(name!=null) name = name.trim();
lqw.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序条件
lqw.orderByDesc(Employee::getUpdateTime);// 根据更新时间降序排序(新的放前面)
//执行查询
employeeService.page(pageInfo,lqw);//查询条件要传进来
//log.info("查到分页数据, total:{}, 数据:{}",pageInfo.getTotal(),pageInfo.getRecords());
return R.success(pageInfo);
}
3.4 功能测试
代码编写完毕之后,我们需要将工程重启, 完毕之后直接访问管理系统首页, 默认就会打开员工管理的列表页面, 我们可以查看列表数据是否可以正常展示, 也可以通过分页插件来测试分页功能, 及员工姓名的模糊查询功能。
在进行测试时,可以使用浏览器的监控工具查看页面和服务端的数据交互细节。 并借助于debug的形式, 根据服务端参数接收及逻辑执行情况。
类似el表达式的好东西:{{ }}
测试过程中可以发现,对于员工状态字段(status)服务端返回的是状态码(1或者0),但是页面上显示的则是“正常”或者“已禁用”,这是因为页面中在展示数据时进行了处理。
好东西,el表达式似的:
<template slot-scope="scope">
{{ String(scope.row.status) === '0' ? '已禁用' : '正常' }}
</template>
之前当前页码bug解决
最最后发现这里分页没有bug,虽然当前页page不是从后端接收的,且前端也没有看到查询完毕更新page值,但是他也没有bug:当前第4页,条件查询只有1页数据,也会自动跳转到第1页。
可见官方解决了这个bug了,将
:current-page="currentPage"
换成
:current-page.sync="currentPage"
即可
Ctrl+F5 不读缓存刷新
Ctrl+F5 会要求浏览器从网站而非缓存获取数据
但是又发现,分页查询,其实条件查询时,人家直接代码里强制将当前页修改成1了
handleQuery() {
this.page = 1;
this.init();
},
修改页面大小时并没有:
handleSizeChange (val) {
this.pageSize = val
this.init()
},
初始化,点击页码,修改当前页、条件查询,等等都要到后台分页查询数据,所以前台分页查询的方法,需要抽取出来成为一个单独的方法,以便复用~
4. 启用/禁用员工账号
4.1 需求分析
在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 “禁用”,如果员工账号状态为已禁用,则按钮显示为"启用"。
需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
A. admin 管理员登录
B. 普通用户登录
4.2 程序执行流程
4.2.1 页面按钮动态展示
在上述的需求中,我们提到需要实现的效果是 : 只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示 , 页面中是怎么做到只有管理员admin能够看到启用、禁用按钮的?
1). 在列表页面(list.html)加载时, 触发钩子函数created, 在钩子函数中, 会从localStorage中获取到用户登录信息, 然后获取到用户名
2). 在页面中, 通过Vue指令v-if进行判断,如果登录用户为admin将展示 启用/禁用 按钮, 否则不展示
4.2.2 执行流程分析
1). 当管理员admin点击 “启用” 或 “禁用” 按钮时, 调用方法statusHandle
scope.row : 获取到的是这一行的数据信息 ;
2). statusHandle方法中进行二次确认, 然后发起ajax请求, 传递id、status参数
真的就是类似后端,也分了3层,标签就是controller, 方法就是service, .js文件的function就是dao
最终发起异步请求, 请求服务端, 请求信息如下:
请求 | 说明 |
---|---|
请求方式 | PUT |
请求路径 | /employee |
请求参数 | {“id”:xxx,“status”:xxx} |
{…params} : 三点是ES6中出现的扩展运算符。作用是遍历当前使用的对象能够访问到的所有属性,并将属性放入当前对象中。
4.3 代码实现
在开发代码之前,需要梳理一下整个程序的执行过程:
1). 页面发送ajax请求,将参数(id、status)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service更新数据
3). Service调用Mapper操作数据库
启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作。在Controller中创建update方法,此方法是一个通用的修改员工信息的方法。
竟然还纠结 id、status 这两个参数怎么接收,不就是Employee这两个实体类的属性嘛,直接Employee接收就ok啦
/**
* 根据Id修改员工信息
* @param request
* @param employee
* @return
*/
// 应该专门的权限控制注解 标注这个方法
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
//log.info("禁用启用 用户: id:{} status:{}",employee.getId(),employee.getStatus());
log.info("更新 用户: {} {}",employee.toString());
// 注意: 修改者用户 修改时间也要更新
Long empId = (Long) request.getSession().getAttribute("employeeId");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);//有的值修改 没有的自动不修改
return R.success("用户信息修改成功");
}
这个方法写得太妙了,发现没有,编辑和禁用都可以复用这一个方法
4.4 功能测试
代码编写完毕之后,我们需要将工程重启。 然后访问前端页面, 进行 “启用” 或 “禁用” 的测试。
测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。但是从控制台输出的日志, 可以看出确实没有更新成功。
而在我们的数据库表结构中, 并不存在该ID, 数据库中 风清扬 对应的ID为 1420038345634918401
盲猜是Long真的当作数值去处理了,然后就出现了精度损失。
4.5 代码修复
简言之,前端发送的Long太长了,js没有那么高精度的类型,得让MVC给他转成字符串再发送到前端
4.5.1 原因分析
通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致,这是怎么回事呢?
在分页查询时,服务端会将返回的R对象进行json序列化,转换为json格式的数据,而员工的ID是一个Long类型的数据,而且是一个长度为 19 位的长整型数据, 该数据返回给前端是没有问题的。
那么具体的问题出现在哪儿呢?
问题实际上, 就出现在前端JS中, js在对长度较长的长整型数据进行处理时, js只能保证前16位精确,后3位会四舍五入,损失精度, 从而导致提交的id和数据库中的id不一致。 这里,我们也可以做一个简单的测试,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
<script>
alert(1643511557906653186);
</script>
</html>
4.5.2 解决方案 (mvc消息转换器增强)
简言之,前端发送的Long太长了,js没有那么高精度的类型,得让MVC给他转成字符串再发送到前端
要想解决这个问题,也很简单,我们只需要让js处理的ID数据类型为字符串类型即可, 这样就不会损失精度了。同样, 也可以做一个测试:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
<script>
alert("1643511557906653186");
</script>
</html>
那么在我们的业务中, 我们只需要让分页查询返回的json格式数据库中, long类型的属性, 不直接转换为数字类型, 转换为字符串类型就可以解决这个问题了 , 最终返回的结果为 :
4.5.3 代码修复
由于在SpringMVC中, 将Controller方法返回值转换为json对象, 是通过jackson来实现的, 涉及到SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter, 所以我们要解决这个问题, 就需要对该消息转换器的功能进行拓展。
具体实现步骤:
1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
2). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
1). 引入JacksonObjectMapper
common包下比较合适
cn.whu.reggie.common.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;
/**
* 对象映射器:基于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(DeserializationFeature.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)// Integer->String 序列化就是java对象->json
.addSerializer(Long.class, ToStringSerializer.instance)// Long->String 数据绑定时
.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框架的消息转换器 (controller里return的对象转json 功能增强)
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器..... (controller里return的对象转json 功能增强)");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);//index=0,将我们的转换器设置到最前面,否则用得还是他自己的转换器
}
再测试就没有问题了:
5. 编辑员工信息
5.1 需求分析
在员工管理列表页面点击 “编辑” 按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击 “保存” 按钮完成编辑操作。
那么从上述的分析中,我们可以看出当前实现的编辑功能,我们需要实现两个方法:
A. 根据ID查询, 用于页面数据回显 (虽然前端就有这个数据,但还是去数据库查,防止前端直接修改数据)
B. 保存修改
编辑操作总是分两步
5.2 程序执行流程
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1). 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2). 在add.html页面获取url中的参数[员工id]
3). 发送ajax请求,请求服务端,同时提交员工id参数
4). 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5). 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6). 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7). 服务端接收员工信息,并进行处理,完成后给页面响应
8). 页面接收到服务端响应信息后进行相应处理
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作
5.3 代码实现
5.3.1 根据ID查询
经过上述的分析,我们看到,在根据ID查询员工信息时,请求信息如下:
请求 | 说明 |
---|---|
请求方式 | GET |
请求路径 | /employee/{id} |
代码实现:
在EmployeeController中增加方法, 根据ID查询员工信息。
/**
* 根据id查询员工信息
* @param id
* @return R<Employee>
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询数据: id={}",id);
Employee employee = employeeService.getById(id);
if(employee==null) return R.error("没有查到对应员工信息");
return R.success(employee);
}
5.3.2 修改员工
经过上述的分析,我们看到,在修改员工信息时,请求信息如下:
请求 | 说明 |
---|---|
请求方式 | PUT |
请求路径 | /employee |
请求参数 | {…} json格式数据 |
代码实现:
在EmployeeController中增加方法, 根据ID更新员工信息。
其实之前禁用用户时写过这个方法,直接复用即可,不用写了
/**
* 根据Id修改员工信息
* @param request
* @param employee
* @return
*/
// 应该专门的权限控制注解 标注这个方法
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
//log.info("禁用启用 用户: id:{} status:{}",employee.getId(),employee.getStatus());
log.info("更新用户: {}",employee.toString());
// 此代码复用性很强,编辑和禁用启用都可以用这个一个方法
// 注意: 修改者用户 修改时间也要更新
Long empId = (Long) request.getSession().getAttribute("employeeId");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);//有的值修改 没有的自动不修改
return R.success("用户信息修改成功");
}
5.4 功能测试
代码编写完毕之后,我们需要将工程重启。 然后访问前端页面, 按照前面分析的操作流程进行测试,查看数据是否正常修改即可。
测试正常