文章目录
🌕博客x主页:己不由心王道长🌕!
🌎文章说明:SpringBoot项目-瑞吉外卖【day02】员工管理业务开发🌎
✅系列专栏:SpringBoot项目
🌴本篇内容:对黑马的瑞吉外卖项目的day02进行笔记和项目实现🌴
☕️每日一语:人有退路,就有些许安全感。等到哪一天,你真没了退路,你就发现眼前哪条路都能走,也能通。☕️
🚩 交流社区:己不由心王道长(优质编程社区)
前言
今天是项目开发的第二天。当然,我不是第二天就写好了相应的功能,毕竟能力有限。照猫画虎也得自己思考思考再起笔吧!
员工管理业务开发
完善登录功能
问题分析
前面我们已经完成了后台系统的员工登录功能开发,但是还存在一个问题:用户如果不登录,直接访问系统首页面,照样可以正常访问。
这种设计并不合理,我们希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面。
那么,具体应该怎么实现呢?
答案就是使用过滤器或者拦截器,在过流器成者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。
那我们选择过滤器还是拦截器呢?现在不能全都要,所以我这里选择用的是过滤器。在后面优化的时候我们再试试拦截器。
代码实现
实现步骤如下:
一、创建自定义过滤器LoginCheckFilter
见名知意嘛,登录检查过滤器:
package com.example.filter;
/**
* @author 不止于梦想
* @date 2022/11/13 17:23
*/
public class LoginCheckFilter {
}
二、在启动类上加入注解@ServletComponentScan:
SpringBootApplication 上使用@ServletComponentScan 注解后
Servlet可以直接通过@WebServlet注解自动注册
Filter可以直接通过@WebFilter注解自动注册
Listener可以直接通过@WebListener 注解自动注册
其实就是组件扫描,而@ServletComponentScan顾名思义就是扫描Servlet技术相关的注解进行注册,并加载成bean。
在这里要提醒以下,SpringBoot的启动类要在所有其他包的同层或者父层,这样才能扫描到,不然是扫描不到的。
三、完善过滤器处理逻辑
1、获取本次请求的URI
2、判断本次请求是否需要处理
3、如果不需要处理,直接放行
4、需要处理的则判断登录状态,如果已经登录,则直接放行
5、如果未登录则返回登录结果
上面第五步返回登录结果时不能直接返回,还得看前端代码:
// 响应拦截器
service.interceptors.response.use(res => {
console.log('---响应拦截器---',res)
// 未设置状态码则默认成功状态
const code = res.data.code;
// 获取错误信息
const msg = res.data.msg
console.log('---code---',code)
if (res.data.code === 0 && res.data.msg === '未登录') {// 返回登录页面
// MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
// confirmButtonText: '重新登录',
// cancelButtonText: '取消',
// type: 'warning'
// }
// ).then(() => {
// })
console.log('---/backend/page/login/login.html---',code)
localStorage.removeItem('userInfo')
window.top.location.href = '/backend/page/login/login.html'
} else {
return res.data
}
},
上面是一个前端响应拦截器,就是我们发请求,后台处理给的响应信息会被响应拦截器截取。当我们未登录返回登录结果时,应当按照它给的要求格式返回,这样前端的代码才能正确处理并执行正确的操作(好鸡肋,感觉严重耦合在一起)。
这个过程中的难点是判断是否需要处理。那么我们来说说什么情况下需要处理,什么情况下需要放行。
一、前端向后台的controller层发送的请求需要处理。这个毋庸置疑。
二、静态资源应该放行。什么静态资源?所有静态资源,这样岂不是让别人都能看到你的资源了?看到就看到呗,真正有用的数据都要走后端的controller获取,他看到你的页面也没事,你的数据并不会被看到
三、退出、登录请求放行。要退出就已经说明人已经登录了,要登录当然放行,不然就死循环了。
上面分析已经做好了,现在应该把代码整一整
package com.example.filter;
import com.alibaba.fastjson.JSON;
import com.example.commons.R;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.util.AntPathMatcher;
import javax.management.modelmbean.RequiredModelMBean;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
/**
* @author 不止于梦想
* @date 2022/11/13 17:23
*/
@WebFilter(filterName = "loginFilter" ,urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
//先把请求和响应转换为http格式的,因为这是协议
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
// 1、获取本次请求的URI
String requestURI = httpServletRequest.getRequestURI();
// 2、定义不需要处理的请求路径
String[] urls = new String[]{//这里定义需要放行的urls
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// 3、如果不需要处理,直接放行
if (urlCheck(requestURI,urls)) {
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
// 4、需要处理的则判断登录状态,如果已经登录,则直接放行
if (httpServletRequest.getSession().getAttribute("employee")!=null) {
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
// 5、如果未登录则返回登录结果
httpServletResponse.getWriter().write(JSON.toJSONString(R.error("未登录")));
}
public boolean urlCheck(String url,String[] urls){
for (String s : urls) {
if (PATH_MATCHER.match(s,url)) {
return true;
}
}//如果请求与需要放行的请求不匹配,则返回false。
return false;
}
}
在第五步,是我们要重点注意的,因为前端的响应拦截器需要的信息是这样的
统一格式中的error中的code都统一为了0,msg是我们自己可以设置的,前端需要什么,我们就给他返回什么。
说明:
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
这是一个路径匹配器,我们在设置"/backend/**",这样的路径过滤时,如果遇到/backend/index.html时,路径并不能匹配上,/ ** 是拦截所有的文件夹及里面的子文件夹,但是当前文件夹有静态页面时则不会拦截,而路径匹配器则会让器拦截所有属于它的东西。
功能测试
在每一步的后面根据实际情况进行日志输出,这样更能观察我们的程序到底走了哪些操作。
现在我不登录直接进行index.html页面试试:
可以清晰的看到,我只是发了一个index.html页面。我们在进行未登录时直接访问index.html页面,它应该给我判断未登录,然后返回登录界面。但是由于我们开发了所有的静态资源,所以不需要处理,但是在index界面上会自动发一个获取page的controller请求,这时候会判断用户是否已经登录,没有登录则回退到登录界面。
这里逻辑全部正确,就是视图并没有进行跳转,算是一个败笔,暂时没有找出解决办法。
新增员工
需求分析
我们在系统中可以管理员工的信息,可以通过新增员工来添加后台系统的用户。当我们点击【添加员工】按钮则视图进行相应跳转:
当我们输入数据以后,会在前端先进行一个格式校验,如手机号码和省份证号:
手机号码必须是11位数字,身份证则是18位。在以上信息都输入正确以后,点击保存按钮即可,如果保存以后还要继续添加,则点击保存并继续添加。
在保存、取消等按钮的后面都绑定了单击事件,我们看看保存的单击事件是什么样的。
过程我已经梳理了大概,所以主角是谁?是addEmployee,在我们单击保存后,由于表单已经绑定了这个事件,那么表单会作为一个数据去调用方法,传给这个方法。
可以看出来,这个函数其实也是发送一个axios请求,不同的是这个方法带有参数,就是把表单填的数据传给后端的controller,路径是/employee,类型是post。
数据模型
分析好了需求以后,我们看看数据模型,为什么呢?先看再说!!!
这里没有拿出id,因为id是利用雪花算法自动生成的。
上面的由用户填写的是name(用户名)、username(员工姓名)、phone(电话号码)、sex(性别)、id_number(身份证号)。
其他都是在添加用户时,由后端自动生成的、密码是统一的,后面由员工根据自己的需求进行更改。
代码开发
根据分析和模型,我们现在可以编写相应的方法了:
@PostMapping
public R<String> addEmployee(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());
//获取当前登录人员信息
Long empId = (Long) request.getSession().getAttribute("employee");
//添加创建者信息
employee.setCreateUser(empId);
//设置更新人员信息
employee.setUpdateUser(empId);
//调用添加方法
employeeService.save(employee);
//返回结果
return R.success("新增员工成功");
}
功能测试
可以看到,我们已经测试通过了,但是上面还是存在一些问题的,什么问题?异常的问题
统一处理异常
报的是后台500错误,我们查看后台
什么意思,不知道,百度查查:
说的是username重复了,原来是我们加入数据的时候,用户名重复了,这是不允许的。
所以要处理这种情况,这时候就需要我们的异常处理上场了。我们可以一个一个异常处理,也可以一类异常处理,当然是选择多的啦。
package com.example.commons;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* @author 不止于梦想
* @date 2022/11/14 18:27
*/
@ControllerAdvice(annotations = {Controller.class, RestController.class})
@ResponseBody
@Slf4j
public class GlobalExcption {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
//后台输出异常信息
log.info(ex.getMessage());
//Duplicate entry '123' for key 'employee.idx_username'
//如果异常信息包括Duplicate entry,则可以确定是用户名字重复
if(ex.getMessage().contains("Duplicate entry")){
//以空格为分隔符分割异常信息
String[] s = ex.getMessage().split(" ");
//取出数组中的用户名,返回给客户端,提升他名字重复
return R.error(s[2]+"已存在");
}
//否则,返回未知名错误
return R.error("未知名错误,请重新输入信息");
}
}
验证:
要说明的是,这里的功能还十分的不完善,比如手机号肯定不能重复,如果重复应该提示,该手机号已经绑定,身份证肯定不能重复吧,员工姓名可以重复,同名的人多了去了。这是后台的处理逻辑,那前台的呢?哪里出错你应该在哪提示,并且删除表格里的数据吧?只能说我们不是前端的,不能瞎动前端代码,但是这些代码漏洞很多。
员工信息分页查询
需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
首先我们要看的是,当我们点击登录并且进入index.html之后,页面会发送一个controller的请求查询page,还记得吧?
并且是通过这个请求走过滤器的controller判断路径判断是否已经登录,没有登录则回退到登录页面,但是现在不研究这个,要研究的是它发送的page请求。
无非就是下面几种情况
而它们都会走/employee/page这个controller路径。
需要注意的是,我们发送请求时,不一定会按条件查,比如我们不一定输入员工姓名。但是这个请求发送的时候,对应的页码和每页的条数是一定存在的,你不指定也有默认值。
整个过程梳理:
接下来看看前端的代码:
当我们进入到index界面,并且是在员工管理界面时,会自动加载一个页面,页面url如图,我们跟进去。
创建了一个vue,并且绑定了member-app,并且初始化一个init方法。
我们看看这个init方法
方法就是这个getMemberList,一样的,跟过去看看它要干嘛。
放松一个axios请求,方式是get方式,传入params参数,路径是/employee/page。
params参数有page、pageSize、name。
梳理思路:
员工管理界面,当我们进去的时候会有默认提供的参数请求后台并且查询数据,我们也可以手动的选择我们的查询条件,比如按姓名并且同时有每页多少条记录,或者直接点击第几页,输入每页多少条数。总得来说,就是按姓名不一定有,但是第几页和每页多少条记录是必然存在的。所以编写代码的时候要判断是否有姓名。
代码开发
上面已经把前端请求需要的东西和思路都理清了一遍,现在编写代码。
因为这里用的分页,而我们使用的MP为我们提供了分页插件,我们需要编写一个拦截器去拦截分页请求
package com.example.config;
/**
* @author 不止于梦想
* @date 2022/11/14 20:15
*/
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//交给Spring容器管理
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//创建MybatisPlus拦截器
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//添加分页拦截器
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//返回分页拦截器
return mybatisPlusInterceptor;
}
}
有了分页插件,现在我们使用分页方法就会被拦截,并进行处理
先创建分页构造器
代码如下:
@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);
}
这里在页面展示顺序的时候,是按照更新时间降序输出的
功能测试
看看,不指定姓名能不能查询:
pageSize是10,但是明显不对,这应该是前端的问题,我们再看看。
不对的时候记得清理一下缓存就好了,啊哈哈。
不过缓存清理了,可能你的页面图片又没有了,好迷茫。
启用/禁用员工
需求分析
在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。
需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户警录系统后启用禁用按钮不显示。
管理员admin登录系统可以对所有员工账号进行启用、禁用操作如果某个员工账号状态为正常,则按钮显示为“禁用”,如果员工账号状态为已禁用,则按钮显示为“启用(只有禁用了你才需要启用,只有启用的状态你才能禁用)。
看看前端代码吧:
当点击后面的禁用/启用按钮时,先调用一个statusHandle函数,并传入scope.row作为参数,在传参前前端判断你的账号是否时admin用户,是的话则弹出一个对话框,就判断你当前要调整的用户的状态,当前的用户是启用的,那么参数传的就是禁用,与之对应的是当前用户已经被禁用,那么参数就是启用(只有已经被禁用的用户需要启用),然后执行函数:
弹出一个选择框:
你选择了确定,那么久根据你传的参数对该用户进行相应的禁用和启用:
重点是enableOrDisableEmployee方法和它传入的参数:
这里通过传入被操作对象的id和被操作对象的状态的取反结果。
然后执行以下function:
发送一个put请求,并且路径是/employee,带传入的参数。
返回值是相应的状态码,我们的controller返回值应该是R
代码实现
前面忘了说,其实我们能不能看见后面操作有编辑,前端已经帮我们判断了,不是admin用户,你看都看不到。
@PutMapping
public R<String> updateStatus(HttpServletRequest request,@RequestBody Employee employee) {
/**
* 就是一个判断语句,当id值相等时,把数据库的status,
* 修改为参数的status,参数的status已经跟原本的status取反了。
* 这里要注意的是,我们每次修改信息的时候,都会把修改人的信息和修改时间也进行更新,所以需要HttpServletRequest
* 来获取当前操作人的session,从而获取修改人的id。
*/
log.info(employee.toString());
//获取操作人id
Long empId = (Long) request.getSession().getAttribute("employee");
//设置修改人
employee.setUpdateUser(empId);
//设置修改时间
employee.setUpdateTime(LocalDateTime.now());
//调用方法,修改user
boolean b = employeeService.saveOrUpdate(employee);
//判断修改是否成功
if(b){
return R.success("员工信息修改成功");
}
return R.error("员工信息修改失败");
测试
测试没有报错,但是有问题,什么问题?看到Parameters,是不是看起来跟我们的原有数据不一样?
因为长度太长了,前端处理时丢失精度了。怎么解决?
我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串,
这里直接引用官方的
JacksonObjectMapper:
package com.example.commons;
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);
}
}
有了对象转换器之后,在mvc配置文件种配置即可:
@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);
}
add(0,messageConverter);表示我们自定义的转换器放在转换器首部位置,优先使用我们自定义的转换器。
再测试:
可以看到id已经正常的传过来了,至于其中原理,以后再慢慢深究吧,会用再说。
编辑员工信息
需求分析
当我们点击编辑时,程序应该通过被修改的用户的id,去后台调用查询方法然后在上面的页面进行回显。当我们点击保存的时候,其实就是一个更新方法,前面我们已经写过一个通用的更新方法。就是update方法。所以这里我们只需要编写一个通过id查询用户的方法即可。我们看看前端代码:
下面是函数:st是判断当前要走的方法,如果st不等于add,则进行修改操作。在add.html上并且携带被修改对象的id。
当我们点击编辑的时候,走的应该是修改员工,所以这个方法不用写了。
需要写的时通过id查询员工。
点击编辑跳出以下信息,说明我们需要回显的数据是通过request方法,并且携带参数的。
代码实现
上面已经分析清楚了,现在把代码完善:
selectById
@RequestMapping("/{id}")
public R<Employee> selectById(@PathVariable Long id){
log.info("查询用户id");
Employee employee = employeeService.getById(id);
if(employee!=null){
return R.success(employee);
}
return R.error("没有查询到用户");
}
功能测试
当我们点击修改大朗时,跳转页面:
现在我更改其手机号码:13512345678
注意看:这个选项…
ok兄弟们,看它看它,已经完成了,证明我们的程序没有错误。
总结
这是本次项目的第二天,由于各种关系,其实我没有在第二天就写好,而是推迟了一天,在第三天晚上才把第二天的内容整理好,为什么这么慢呢?一是时间不充裕,尤其是现在临近期末,也得为期末早做打算。还有就是,咱写代码,总得自己思考吧。不能把老师的抄了就是自己的啦。
在做这个项目中,可以发现,基本上都是crud,但是有的地方又需要想象力。比如全局处理异常,要想想出现什么情况下用到它,这些都是需要经验的,而项目就是积累经验的过程,所以兄弟们,咱项目得好好做。