新增员工:
1:新增员工的接口设计:
请求路径:/admin/employee
请求方式:POST
数据发送格式:json(新增员工肯定要新增员工的每个属性,就用一个json格式的数据来封装)
数据返回格式:Result
2:具体的代码实现:
接收前端传送过来的数据:
接收前端发送回来的数据:DTO。
接收数据,可以用我们封装的employee实体类,不过仔细观察前端传送过来的数据和这个实体类属性差别很大
(这个属性差别很大是什么意思呢,就是实体类中有很多属性,前端不会传过来,我们需要自己去设置)
这个时候可以考虑吧用employeeDTO来接收数据。
注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据
Controller层代码:
这里提一下这个PostMapping的路径我已经写到类上了,所以这里不需要指定。
/**
* 新增员工操作
* @param employeeDTO
* @return
*/
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO){
log.info("新增员工:{}",employeeDTO);
employeeService.insert(employeeDTO);
return Result.success();
}
Service层代码:
/**
* 新增员工操作
* @param employeeDTO
* @return
*/
@Override
public void insert(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//1:进行对象拷贝:BeanUtils
BeanUtils.copyProperties(employeeDTO,employee);
//2:设置账号的状态,这里的小技巧是引用常量类StatusConstant,这样可以避免把变量写死
employee.setStatus(StatusConstant.ENABLE);
//设置密码,密码这里同样用到了密码常量类,这里还需要进行MD5加密
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//设置当前记录人id和修改人id 因为这个地方以后要进行优化,所以用一个TODO
// TODO 后期需要优化当前登录用户的id
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
//调用持久层
employeeMapper.insert(employee);
}
这里用到的一些填充多余变量的一些技巧:
1:对对象进行拷贝:BeanUtils.copyProperties(employeeDTO,employee);
用到了BeanUtils的API,将employeeDTO传给employee
2:设置状态和默认密码,这里没有把这两个值写死,用了一个常量类。
并且对密码进行了MD5加密。
3:TODO,对后期需要优化的地方进行用TODO进行标记。
Mapper层代码:
/*
插入员工数据
*/
@Insert("insert into sky_take_out.employee(name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user) " +
"values " +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser})")
void insert(Employee employee);
这里的注意点就是:()里面的变量名字是数据库中的字段名
#{}里面的是,Employee实体类中的变量名。
3:功能测试:
功能测试方式:
通过接口文档测试
通过前后端联调测试
注意:由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,
导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。
功能测试时遇到的问题,
如果我们直接在Swagger上进行测试:
如图:
系统调试会报401错误,原因就是,我们在代码中设置了拦截器interceptor,我们没有登录,没有获取到jwt令牌,我们无法新增员工。
解决办法:
现在登录接口获得一个jwt令牌:
然后在文档管理中的全局参数管理保存一个:
结果:
响应返回200
并且这里可以观察到那个请求头部,有一个红点,点进去就能看到我们刚刚设置的那个token。
数据库中的结果:
因为这个新增员工的操作写在查询员工之前,前端页面还展示不出来,所以这里没有进行前后端联调。
4:代码优化:
程序存在的问题:
1:录入的用户名已存在,抛出异常后没有处理 新增员工时,
如图当我们再次插入张三的时候:
就会报500,原因就是我们没有处理这个异常
解决方法:
在GlobalExceptionHandler中添加异常方法:
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'zhangsan' for key 'employee.idx_username
String msg = ex.getMessage();
if(msg.contains("Duplicate entry")){
String[] s = msg.split(" ");
String username = s[2];
return Result.error(username+ MessageConstant.ALREAD_ERROR);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
这里的SQLIntegrityConstraintViolationException和
//Duplicate entry 'zhangsan' for key 'employee.idx_username
都是从控制台复制过来的。
具体的处理过程就如上面代码所示。
这里有一个点需要提一下就是这个返回的提示信息,我们进来不要自己去写,用常量类里面错误提示信息。
结果:
2:创建人id和修改人id设置为了固定值
//设置当前记录人id和修改人id 因为这个地方以后要进行优化,所以用一个TODO
// TODO 后期需要优化当前登录用户的id
employee.setCreateUser(10L);
employee.setUpdateUser(10L);
这一块代码这里的用户id是写死的,这在程序中一般不会出现,所以我们现在要想一个办法解决这个问题。
解决办法:
这里想要解决这个问题,我们需要引入一个新的知识点:ThreadLocal。
ThreadLocal:
我们首先要知道:
在Java中,每个请求不一定都是一个线程,但通常情况下,每个请求会被分配一个独立的线程来处理。而且,线程之间确实可以共享一些变量,但这也可能导致线程安全的问题。
这个是GPT的原话,虽然他说每个请求不一定对应一个线程,不过我们在这个项目中,可以先这么理解。
ThreadLocal概念:
ThreadLocal是Java中的一个类,它提供了线程局部变量的功能。每个ThreadLocal对象都可以维护一个独立的变量副本,每个线程都可以访问和修改自己的这个副本,而不会影响其他线程的变量副本。这样可以确保在多线程环境下,每个线程都拥有自己的变量副本,从而避免了线程间的数据共享和竞争条件。
线程安全指的是在多线程环境下,对共享资源的操作能够保证正确和一致的结果。使用ThreadLocal能够实现线程安全,因为每个线程都操作自己的变量副本,不存在线程间的竞争条件。
ThreadLocal变量副本:
这一段话差不多就是对ThreadLocal的解释,我想再解释一下这个:
每个ThreadLocal对象都可以维护一个独立的变量副本这一句话:
每个ThreadLocal实例确实只能维护一个变量副本,但这个变量可以是任意类型的对象,包括数组、集合等。因此,你可以通过ThreadLocal来存储任意类型的值,不仅仅是一个简单的值。例如,你可以将一个复杂的对象或者一个集合存储在ThreadLocal中,每个线程都可以独立地访问和修改自己的副本,而不会影响其他线程的副本。
ok,解释到这里差不多这个概念就解释的差不多了,然后我们再讨论一下如何解决上述获取用户id的方法:
通过JWT令牌:
校验这个JWT令牌的时候,我们可以设置一下这个ThreadLocal的值,就是这个变量副本,然后我们在save()方法中再取出来就好了。
登录接口具体代码:
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
//设置线程中的用户id
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
Service层代码:
//设置当前记录人id和修改人id 因为这个地方以后要进行优化,所以用一个TODO
//后期需要优化当前登录用户的id
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
查询员工:
1:查询员工的接口设计:
查询员工
请求路径:/admin/employee/page
请求方式:GET
数据发送格式:Query
name:员工姓名
page:页码
pagesize:每页记录数
数据返回格式:json
2:具体的代码实现:
接收前端传送过来的数据:
接收前端发送回来的数据:EmployeePageQueryDTO。
@Data
public class EmployeePageQueryDTO implements Serializable {
//员工姓名
private String name;
//页码
private int page;
//每页显示记录数
private int pageSize;
}
返回给前端的数据:Result(PageResult):
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
Controller层代码:
/**
* 分页查询员工操作
* @param employeePageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分页查询员工")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("分页查询员工:{}",employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
这里提两个注意点把
1:就是注意路径:需要在后面+/page
2:注意返回的参数调用接口所传的参数(因为返回值有点套娃了)
Service层:
/**
* 分页查询员工
* @param employeePageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//select * from employee limit 0,10
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total,records);
}
这里的注意点就是这个PageHelper插件:
pagehelper是mybatis的一个插件,其作用是更加方便地进行分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
这一行主要是用来设置分页的页数和每页数量。相当于SQL语句中limit后面的两个参数。
注意:只有紧跟着PageHelper.startPage()的sql语句才被pagehelper起作用
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
使用了PageHelper返回值就得使用这个插件自己定义的返回值Page。
然后调用两个方法获取这个数量的总数和返回的数据。
long total = page.getTotal();
List<Employee> records = page.getResult();
Service层:
/**
* 分页查询员工
* @param employeePageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//select * from employee limit 0,10
PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
long total = page.getTotal();
List<Employee> records = page.getResult();
return new PageResult(total,records);
}
Mapper层及动态SQL:
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<mapper namespace="com.sky.mapper.EmployeeMapper">
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from sky_take_out.employee
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
</where>
order by create_time desc
</select>
</mapper>
这里的的动态SQL:
需要回顾一下之前的Mybatis的动态SQL的知识:
动态SQL:
随着用户的输入或外部条件的变化而变化的SQL语句,我们称为 动态SQL。
if标签:
<if>:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。
先来看第一个代码案例:
<select id="list" resultType="com.springbootcrud.Pojo.Emp">
select *
from mybatis.emp
where
<if test="name!=null">
name like concat('%', #{name}, '%')
</if>
<if test="gender!=null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
where
order by update_time desc
</select>
上面这段代码就可以解决查询的问题:比如你给我的测试条件是:
List<Emp> empList = empMapper.list("张", (short) 1, LocalDate.of(2010, 1, 1), LocalDate.of(2020, 1, 1));
每个属性都有,那肯定没什么问题。
List<Emp> empList = empMapper.list("张",null,null,null);
但如果是下面这种,三个条件中只有一个存在,如果没有用if判断语句的话就会报错。
where标签:
<where>:where 元素只会在子元素有内容的情况下才插入where子句。而且会自动去除子句的开头的AND 或OR
第二个案例:
基于上面的if标签,我们已经解决了一部分的问题:
不过还是不够全面,仔细看
<if test="name!=null">
name like concat('%', #{name}, '%')
</if>
<if test="gender!=null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
</if>
中间有很多的and,这样会导致什么情况呢:
List<Emp> empList = empMapper.list(null,(short)1,null,null);
因为第一个条件不存在,所以导致后面的(short)1这个条件根本查不到,所以,也会报错
所以我们引入<where>标签,作用就是能去除开头的and和or,还有一个作用,如果where条件中的所有if都不成立,那这个where标签就不会定义
比如List<Emp> empList = empMapper.list(null,null,null,null);这种情况就会查询出所有的记录。
3:功能测试:
这里的功能测试还是主要用Swagger方式来测试
小插曲:
这个小插曲的原因和JWT令牌过期有关系:
代码没有问题的情况下报了401的响应码,原因就是因为JWT令牌的时间过期了。
所以这个时候我们需要重新获取一个JWT令牌。
查询结果:
4:代码优化:
程序存在的问题:
仔细看前端页面展示的结果-----最后操作时间那一栏
时间的格式不太对,下面就来优化这个问题。
解决方法:
在 WebMvcConfiguration 中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理
扩展Spring MVC的消息转换器:
Spring MVC框架的消息转换器是用来处理请求和响应的数据格式转换的组件.
在SpringMVC中,java对象转JSON对象是通Jackson实现的,涉及到SpringMVC中的消息转换器MappingJackson2HttpMessageConverter.
当请求和响应的数据格式不匹配时,就需要对消息转换器功能进行拓展.
我们首先要在我们编写的 WebMvcConfiguration 类中继承 WebMvcConfigurationSupport(这个应该是写web相关程序都要继承的类,应该是SpringMVC那一块的东西)
/**
* 扩展Spring MVC的消息转化器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象转换成json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转化器加入到容器中,并设置优先级
converters.add(0,converter);
}
在WebMvcConfiguration 类中重写 extendMessageConverters 方法:
1:创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); 2:需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象转换成json数据 converter.setObjectMapper(new JacksonObjectMapper()); 3:将自己的消息转化器加入到容器中,并设置优先级(0) converters.add(0,converter);
对象转换器:
这个对象转换器是我们自己定义的:(比较固定的代码)
/**
* 对象映射器:基于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_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
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(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);
}
}
这里主要提供的功能就是序列化和反序列化。
启用禁用员工账号:
1:启用禁用员工账号的接口设计:
请求路径:/admin/employee/status/{status}
请求方式:POST
数据请求参数:Headers
Query:id
数据返回格式:json
业务规则:
可以对状态为“启用” 的员工账号进行“禁用”操作
可以对状态为“禁用”的员工账号进行“启用”操作
状态为“禁用”的员工账号不能登录系统
2:具体的代码实现:
Controller层:
/**
* 启用禁用员工账号
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result StartOrStop(@RequestParam Long id,@PathVariable(value = "status") Integer status){
log.info("启用禁用员工账号:{},{}",id,status);
employeeService.StartOrStop(id,status);
return Result.success();
}
这里需要注意的就是两个参数的传递:
status是路径参数:用@PathVariable 来接收
id其实是前端的请求参数。我在这里加了@RequestParam,就是表明这个是前端请求参数)
这里其实也可以不用加,为什么呢?
因为前端传送过来的参数名字和id是一样的,然后我做了一个实验,将前端传过来的参数名称改为idd,结果不会报错,不过因为idd的值是null,所以不会在数据库中有什么体现。
Service层代码:
/**
* 启用禁用员工账号
*/
@Override
public void StartOrStop(Long id, Integer status) {
//update employee set status = ? where id = ?
Employee employee = new Employee();
employee.setId(id);
employee.setStatus(status);
//调用mapper接口
employeeMapper.StartOrStop(employee);
}
service层的代码就很中规中矩了。
有一个和之前不同的点是:在我第一次写crud的时候,如果碰到参数只有两个,我可能会直接传进去,不过听了老师的话,封装成一个集合传送。
Mapper层:
void StartOrStop(Employee employee);
注解层代码:
<update id="StartOrStop" parameterType="Employee">
update sky_take_out.employee
<set>
<if test="username != null">username = #{username},</if>
<if test="name != null">name = #{name},</if>
<if test="password != null">password = #{password},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="sex != null">sex = #{sex},</if>
<if test="idNumber != null">id_Number = #{isNumber},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="createUser != null">create_user = #{createUser},</if>
<if test="updateUser != null">update_user = #{updateUser},</if>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
</update>
注解层代码也够坑,因为employee中的变量名和mysql中的字段名不同导致改了半天,下次注意就好。
编辑员工操作 :
1:编辑员工的接口设计:
编辑员工功能涉及到两个接口:
(1)根据id查询员工信息
请求路径:/admin/employee/{id}
请求方式:GET
数据发送格式:路径参数
数据返回格式:Result
(2)编辑员工信息
请求路径:/admin/employee
请求方式:PUT
数据发送格式:json(包括要修改员工的基础属性值)
数据返回格式:Result
2:具体的代码实现:
Controller层代码:
根据id查询员工:
/**
* 根据id查询员工
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询员工")
public Result<Employee> GetById(@PathVariable long id){
log.info("根据id查询员工:{}",id);
Employee emp = employeeService.GetById(id);
return Result.success(emp);
}
这里的注意点就是注意一下这个返回值需要将Employee对象封装到Result中。
编辑员工信息:
/**
* 修改员工信息
* @param employeeDTO
* @return
*/
@PutMapping
@ApiOperation("修改员工信息")
public Result UpdateEmp(@RequestBody EmployeeDTO employeeDTO){
log.info("修改员工信息:{}",employeeDTO);
employeeService.UpdateEmp(employeeDTO);
return Result.success();
}
Service层代码:
根据id查询员工:
/**
* 根据id查询员工
* @param id
* @return
*/
@Override
public Employee GetById(long id) {
Employee employee = employeeMapper.SelectById(id);
employee.setPassword("********");//对密码进一步进行加密
return employee;
}
这里对这个密码加了一层回显,提高了安全性。
编辑员工信息:
/**
* 修改员工信息
* @param employeeDTO
* @return
*/
@Override
public void UpdateEmp(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
//1:对对象进行拷贝
BeanUtils.copyProperties(employeeDTO,employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.UpdateEmp(employee);
}
Mapper层代码:
根据id查询员工:
/**
* 根据id查询员工
* @param id
* @return
*/
@Select("select * from sky_take_out.employee where id = #{id}")
Employee SelectById(long id);
编辑员工信息:
/**
* 修改员工信息
* @param
* @return
*/
void UpdateEmp(Employee employee);
<update id="UpdateEmp" parameterType="Employee">
update sky_take_out.employee
<set>
<if test="username != null">username = #{username},</if>
<if test="name != null">name = #{name},</if>
<if test="password != null">password = #{password},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="sex != null">sex = #{sex},</if>
<if test="idNumber != null">id_Number = #{idNumber},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateUser != null">update_user = #{updateUser},</if>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
</update>