JAVA项目实战瑞吉外卖—day3

公共字段自动填充功能

Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候位指定字段赋予指定的值,使用它的好处就是对这些字段进行处理,避免重复代码

这种代码设置为自动填充即可

1
2
3
4
5
6
7
8
9
10
//设置创建时间为当前
employee.setCreateTime(LocalDateTime.now());
//设置更新时间为当前时间
employee.setUpdateTime(LocalDateTime.now());
//通过当前登录的Session中获得用户id
Long empId = (Long) request.getSession().getAttribute("employee");
//设置创建人
employee.setCreateUser(empId);
//设置更新人
employee.setUpdateUser(empId);

实现步骤:

  • 在实体类的属性上加入@TableField注解,指定自动填充的策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//插入时,进行自动填充
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

//插入或更新时,进行自动填充
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

//插入时,进行自动填充
@TableField(fill = FieldFill.INSERT)
private Long createUser;

//插入或更新时,进行自动填充
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
  • 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,且此类需要实现MetaObjectHandler接口
    • 但是实现该接口无法获得HttpSession,从而无法得到用户的id属性,则填充字段的createUser/updateUser暂时设置为死属性
1
2
metaObject.setValue("createUser", 1L);
metaObject.setValue("updateUser", 1L);

功能完善之ThreadLocal线程知识补充

使用ThreadLocal来根据线程获得之前存储在Session中的用户id,在执行公共字段填充的时候将用户id值进行填充

在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新 的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:

  • 1、LoginCheckFilter的doFilter方法
  • 2、EmployeeController的update方法
  • 3、MyMetaObjectHandler的updateFill方法

可以在上面的三个方法中分别加入下面代码(获取当前线程id):

1
2
long id = Thread.currentThread().getId();
log.info("线程id:{}",id);

执行编辑员工功能进行验证,通过观察控制台输出可以发现, 一次请求对应的线程id是相同的

image-20220809141133245

什么是ThreadLocal?

  • ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该 变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不 能访问。
  • ThreadLocal常用方法:
    • public void set(T value) 设置当前线程的线程局部变量的值
    • public T get( 返回当前线程所对应的线程局部变量的值
  • 以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线 变量的值(用户id),然后在MyMetaobjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前 对应的线程局部变量的值(用户id)

实现步骤:获得同线程存储的数据

  • 1、编写BaseContext工具类,基于ThreadLocal封装的工具类
  • 2、在Login CheckFilter的doFilter方法中调用BaseContex来设置当前登录用户的id
  • 3、在MyMetaobjectHandler的方法中调用BaseContex获取登灵用户的id

在拦截器中,同线程下记录用户已登录的id属性值

1
2
3
Long empId = (Long) request.getSession().getAttribute("employee");
//将登录用户的id值记录到线程块中
BaseContext.setCurrentId(empId);

在设置公共字段填充的时候,调用登录用户的id属性值

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
/**
* 自定义元数据处理器
* @ClassName: MyMetaObjectHandler
* @author: XD
* @date: 2022/8/9 13:34
* @Blog: null
*/

@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

/**
* 插入操作自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {

log.info("公共字段填充[[save]。。。。");
log.info(metaObject.toString());

long id = Thread.currentThread().getId();
log.info("线程id:{}",id);

metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());

}

/**
* 更新操作自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段填充[[update]。。。。");
log.info(metaObject.toString());

long id = Thread.currentThread().getId();
log.info("线程id:{}",id);

metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}

分类管理业务

新增分类

查看前端请求路径,配置对应请求接口即可

1
2
3
4
5
6
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("category:{}",category);
categoryService.save(category);
return R.success("新增分类成功!");
}

分类信息分页查询

注意

需要将实体的isDeleted字段注释或者在category表中添加一个isDeleted字段不然会错误sql

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

  • 页面发送ajax请求,将分页查询繁数(page. pagesize)提交到服务端
  • 服务端Controller接收页面提交的数据并调用Service查询数据
  • Service调用Mapper操作数据库,查询分页数据
  • controller将查询到的分页数据响应给页面
  • 页面接收到分页数据井通过ElementUI的Table組件展示到页面上

这根员信息分类一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/page")
public R<Page> page(int page,int pageSize){

//分特构造器
Page<Category> pageInfo = new Page<>(page,pageSize);

//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<Category>();

//添加排序条件,根据sort排序
queryWrapper.orderByAsc(Category::getSort);

//进行分页查询
categoryService.page(pageInfo,queryWrapper);

return R.success(pageInfo);
}

删除分类

这里老师没有使用restful风格传参,是使用参数传递的方式进行删除

restful风格删除

将前端接口的参数注释,拼接一个restful Url进行发送请求

1
2
3
4
5
6
7
8
// 删除当前列的接口
const deleCategory = (id) => {
return $axios({
url: '/category/'+id,
method: 'delete',
// params: { id }
})
}

后端在路径参数上获得值需要添加注解@PathVariables

1
2
3
4
5
6
@DeleteMapping("{id}")
public R<String> delete(@PathVariable Long id){
log.info("删除分类id为{}",id);
categoryService.removeById(id);
return R.success("删除成功");
}

传统方式

直接为传参形式即可

1
2
3
4
5
6
@DeleteMapping
public R<String> delete(Long id){
log.info("删除分类id为{}",id);
categoryService.removeById(id);
return R.success("删除成功");
}

功能完善

删除分类是判断当前分类是否关联了菜品或者套装,如果有即不能删除

读取菜品和套餐

判断菜品和套餐的字段categoryId有没有分类的字段

配置自定义异常处理类

将数据库表关联的删除信息都抛出这个异常

1
2
3
4
5
6
public class CustomException extends RuntimeException{

public CustomException(String message){
super(message);
}
}

配置全局异常处理类

集中处理自定义异常

返回异常信息给前端页面显示

1
2
3
4
5
6
7
8
9
/**
* 自定义异常出来方法
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(@NotNull CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}

接口实现

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
/**
* 根据id删除分类,删除之前需要进行判断,当前分类有没有关联菜品或者套装
* 如果有则不能删除
* @param id
*/
@Override
public void remove(Long id) {
//菜品匹配
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);

//套餐匹配
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);

//查询出来分类关联的菜品数
long count = dishService.count(dishLambdaQueryWrapper);

//查询出来分类关联的套餐数
long count1 = setmealService.count(setmealLambdaQueryWrapper);
//查询当前分类有没有关联菜品
if (count>0){
//已关联菜品,无法删除,抛出业务异常
throw new CustomException("当前分类关联了菜品,不能删除");
}

//查询当前分类有没有关联套餐
if (count1>0){
//已关联套餐,无法删除,抛出业务异常
throw new CustomException("当前分类关联了套餐,不能删除");
}

//正常删除
super.removeById(id);
}

修改分类

根据前端请求返回对应接口为PutMapping

在点击修改按钮的时候,实现的数据回显没有查询接口即可实现是因为前端在读取列表数据的时候,进行数据保存,点击修改的时候实际是给模型数据赋值,才能完成数据回显

1
2
3
4
5
6
7
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);

categoryService.updateById(category);
return R.success("修改分类信息成功!");
}