1、新增员工
1)需求分析和设计
- 产品原型
- 接口设计
- 数据库设计
产品原型
接口设计
数据库设计
2)代码开发
- 需求设计与分析
- 代码开发、测试
- 代码完善
根据员工新增的数据设计 DTO
当前端提交的数据和我们所需要的数据差距过大的时候,我们需要去新建一个 DTO 来封装数据,比如上述的条件下,前端的发送来的员工信息和我们实际的员工实体类有很大的差距,我们此时可以封装一个新的 DTO 来接收传来的数据。
@Data
public class EmployeeDTO implements Serializable {
private Long id;
private String username;
private String name;
private String phone;
private String sex;
private String idNumber;
}
开发新增员工的功能
-
在 Controller 类中添加一个新增员工的方法,并且调用 Serive 中的新增员工的方法
@PostMapping //swapper 提供的注释,用于在网页上显示该方法 @ApiOperation("新增员工") public Result<String> save(@RequestBody EmployeeDTO employeeDTO) { log.info("新增员工的操作{}", employeeDTO); employeeService.save(employeeDTO); return Result.success(); }
-
添加 Service 中新增员工的方法,再调用 Mapper 中的添加到数据库的方法。
@Override public void save(EmployeeDTO employeeDTO) { Employee employee = new Employee(); //对象属性拷贝 BeanUtils.copyProperties(employeeDTO, employee); //设置用户状态 employee.setStatus(StatusConstant.ENABLE); //设置默认密码 employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); //设置创建时间和修改时间 employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); //设置当前记录创建人和修改人的 id // @TODO 设置当前记录创建人和修改人的 id employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.insert(employee); }
-
添加新增的方法在 Mapper 类中
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user, status)" + "values " + "(#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})") void insert(Employee employee);
功能测试
在我们日常开发的时候,由于很多时候前端的界面还未开发完成,无法进行前后端联调,这时我们需要通过接口文档来进行测试,也就是我们前面配置的 Swapper,正常开发的时候以接口文档测试为主。
在测试的时候,我们会发现这时的请求是无法直接发送到后端服务器的,因为当我们执行这种类型的命令的时候,后端的过滤器会对我们前端发送过来的 jwt 令牌进行校验,而我们通过这种方式发送的令牌为空,会返回一个异常。
因此我们需要通过登录方法来获取一个令牌,但需要注意的是这个令牌的存在时间为两个小时,后续还需要再次获取。
3)代码完善
解决同名异常问题
通过上面的测试,我们可以发现一个问题,就是我们的用户名在数据库设定为不能重复,当我们输入重复的数据的时候,会抛出一个异常,这个异常如果我们不去处理而去交给其自动处理的话,就会影响程序的正常运行,因此我们可以使用 MVC 中的异常处理器来处理。
/**
* 捕获 sql 异常
* @param ex 抛出的异常
* @return 处理的结果,是已经约定好的
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
String message = ex.getMessage();
if (message.contains("Duplicate entry")) {
// 利用字符串分割来得到传来的姓名
String[] s = message.split(" ");
String username = s[2];
// 将姓名和存在的字符串拼接起来
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
} else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
解决修改人员命名的问题
还有一点就是上面在记录修改员工的管理的 id 和 name 的时候设置了一个默认的值,这里来进行完善
-
先来回顾一个 jwt 的功能
-
员工登录后后端将 jwt 令牌返回给前端,这时前端的每一次请求都会去携带这个令牌,我们可以从令牌中解析到员工的 id 数据
// 1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getAdminTokenName()); // 2、校验令牌 try{ Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); // 3、通过,放行 return true; }catch (Exception ex){ // 4、不通过,响应401状态码 response.setStatus(401); return false; }
这时候我们得到了员工的 id 那应该如何传递给我们的 save 方法呢?
补充一个 ThreadLocal 的知识
ThreadLocal 并不是一个Thread,而是Thread的局部变量。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
- public void remove() 移除当前线程的线程局部变量
当服务器每次发送请求的时候,后端服务器 Tomcat 都对应一个新的线程。
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);
//3、通过,放行
BaseContext.setCurrentId(empId);
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
上面的是一个验证令牌的方法,可以看到当从令牌中取出了员工的 id 后,封装到了 ThreadLocal 中,BaseContext 是为了使用方便,将 ThreadLocal 中的方法进行了进一步的封装,这时候我们就可以从线程的空间中取出这个 id,来为上述的两个未处理的值赋值。
2、员工分页查询
- 需求设计和分析
- 代码开发
1)需求设计与分析
- 需要根据页码来展示员工的信息
- 每页显示十条数据
- 分页查询的时候要根据需要输入员工的姓名进行查询
接口设计
![-<img src="../../AppData/Roaming/Typora/typora-user-images/image-20231020115149731.png" alt="image-20231020115149731" style="zoom:50%;" />](https://img-blog.csdnimg.cn/52dd9443bb1b42d9a79e80d581f2d2f9.png)
2)代码开发
根据分页查询的接口设定相应的 DTO
和上述的问题相同,我们虽然有可以接收前端传输的内容的实体类,但是传来的数据没有包含太多实体类的数据。
@Data
public class EmployeePageQueryDTO implements Serializable {
//员工姓名
private String name;
//页码
private int page;
//每页显示记录数
private int pageSize;
}
设计 PageResult 对象
后续为了分页查询的方便,我们做了统一的规定,将返回的数据统一设定为 PageResult 对象,通过上述的接口分析,我们需要设计其包含两个属性,存储返回的数据的属性和总记录数。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
根据接口创建分页查询的方法
@GetMapping("/page")
@ApiOperation("员工的分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
log.info("员工分页查询");
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}
使用分页插件来简化开发
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
这里可以通过 PageHelper 来协助分页查询的功能,里面传入两个参数一个是当前的页码,另一个是每一页的需要的元素个数。
这时候我们再调用分页查询的方法,并且将他的返回对象设置为 Page,那当我们进行分页查询的时候,就会自动的为我们添加 limit 语句。
return new PageResult(page.getTotal(), page.getResult());
这时候我们再使用这个对象的 getTotal 和 getResult 方法就可以得到查询到的所有元素以及查询到的总页数。
后续就继续正常的使用一般的步骤,但写 sql 语句的时候不用再写 limit 语句,且注意不能加分号,该插件默认会在这个 sql 语句的后面加上 limit 修饰。
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != '' and name != null">
and name like concat('%', #{name}, '%') and name != ""
</if>
</where>
order by create_time
</select>
3) 功能测试
可以通过前后端联调和接口文档测试的方式来进行测试.
4) 代码完善
日期显示的问题
观察返回的对象 json 对象可以发现日期显示的是以上图的形式返回的,体现在数据的显示就会发现日期显示的格式有问题.
解决方式
- 方式一:在属性上加入注解,对日期进行格式化
- 方式二:在 WebMvcConfiguration 中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("开始拓展消息转换器");
// 创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//创建对象转换器,可以将 Java 对象转换为 json 字符串
converter.setObjectMapper(new JacksonObjectMapper());
// 将我们自己的转换器放入 springMVC 框架的容器中
converters.add(0, converter);
}
3、禁用和启用员工的功能
- 需求设计和分析
- 代码开发
- 功能测试
1) 需求设计分析
业务规则
- 可以对状态为启用的员工张航进行禁用操作
- 可以对状态为禁用的员工账号进行启用的操作
- 状态为禁用的员工的账号不能登录系统
接口设计
根据接口的路径信息和传输的参数来设计方法
2) 代码开发
@PostMapping("status/{status}")
@ApiOperation("启用禁用员工账号")
public Result EnableAndDisableEmp(@PathVariable Integer status,
@RequestParam(name = "id") long id) {
log.info("启用禁用员工账号{}{}", status, id);
employeeService.enableAndDisableEmp(status, id);
return Result.success();
}
@Override
public void enableAndDisableEmp(Integer status, long id) {
Employee build = Employee.builder()
.status(status)
.id(id)
.build();
employeeMapper.update(build);
}
这里借用了插件lomcat ,可以通过在类上面加入 @Build 注解来在构建类的时候可以使用 builder 方法
因为后面 update 方法会经常被使用,我们可以来在 sql 语句中加入判断数据是否为空来提高代码的复用性.
<update id="update">
update employee
<set>
<if test="name != null"> name = #{name}, </if>
<if test="username != null">username = #{username},</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="status != null"> status = #{status}, </if>
</set>
where id = #{id}
</update>
前端通过发送 id 和要修改的 status 来完成对登录状态的修改
3) 功能测试
通过前后端联调或者接口文档的方式均可以进行测试
4、编辑员工
这里需要完成两个操作,首先是完成通过 id 查询对象,来实现修改的复显功能然后通过修改功能来实现员工的编辑.
@GetMapping("/{id}")
@ApiOperation("通过 id 查询员工信息")
public Result<Employee> getById(@PathVariable Long id) {
Employee empById = employeeService.getEmpById(id);
empById.setPassword("******");
return Result.success(empById);
}
@PutMapping
public Result editEmp(@RequestBody EmployeeDTO employeeDTO) {
employeeService.editEmp(employeeDTO);
return Result.success();
}
5、导入分类模块的方法
将文件中的代码导入即可,注意要按照从底层到高层的顺序来导入