一、公共字段自动填充

1.1 问题分析

前面已经完成了后台系统的员工管理功能的开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在系统中很多表中都会有这些字段,如下:

image-20220807092551094

而针对于这些字段,目前的赋值方式为:

A. 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。

B. 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。

如果都按照这种操作方式来处理这些公共字段, 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐,所以可以使用Mybatis Plus提供的公共字段自动填充功能来简化开发。

1.2 思路分析

Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:

字段名 赋值时机 说明
createTime 插入(INSERT) 当前时间
updateTime 插入(INSERT) , 更新(UPDATE) 当前时间
createUser 插入(INSERT) 当前登录用户ID
updateUser 插入(INSERT) , 更新(UPDATE) 当前登录用户ID

实现步骤:

1、在实体类的属性上加入@TableField注解,指定自动填充的策略。

2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口。

1.给实体类添加注解

实体类的属性上加入**@TableField注解**,指定自动填充的策略。

image-20220807092951242

2.编写元数据对象处理器

按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口。

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
/**
* 自定义元数据对象处理器
*
* @auther xiaochen
* @create 2022-08-07 9:05
*/

@Slf4j
@Component
public class MyMetaObjecthandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
//在对象处理器中没办法获取request对象,暂时写死,以后解决
metaObject.setValue("createUser", new Long(1));
metaObject.setValue("updateUser", new Long(1));

}

@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());


metaObject.setValue("updateTime", LocalDateTime.now());
//在对象处理器中没办法获取request对象,暂时写死,以后解决
metaObject.setValue("updateUser", new Long(1));
}
}

然后将原来的添加和更新操作中的手动赋值操作注释掉

添加操作

image-20220807093051314

更新操作

image-20220807093106155

3.功能测试

添加功能测试:

image-20220807092150153

添加操作公共字段添加没有问题

image-20220807092234701

更新功能测试:

将飞飞性别改成女

image-20220807092333062

公共字段自动也填充成功

image-20220807092432170

1.3 功能完善

前面完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是在自动填充createUser和updateUser时设置的用户id是固定值,因为在MyMetaObjectHandler类中是不能直接获得HttpSession对象的,所以需要通过其他方式来获取登录用户id。

1.业务执行流程

image-20220807104154156

客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:

1). LoginCheckFilter的doFilter方法

2). EmployeeController的update方法

3). MyMetaObjectHandler的updateFill方法

在上述类的方法中加入如下代码(获取当前线程ID,并输出):

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

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

image-20220807104441938

所以可以使用ThreadLocal来进行保存员工的id并填充

2.ThreadLocal介绍

ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问当前线程对应的值。

ThreadLocal常用方法:

A. public void set(T value) : 设置当前线程的线程局部变量的值

B. public T get() : 返回当前线程所对应的线程局部变量的值

C. public void remove() : 删除当前线程所对应的线程局部变量的值

所以我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。

如果在后续的操作中, 我们需要在Controller / Service中要使用当前登录用户的ID, 可以直接从ThreadLocal直接获取。

3. 操作步骤

实现步骤:

  1. 编写BaseContext工具类,基于ThreadLocal封装的工具类

  2. 在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id

  3. 在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id

4.代码实现:

1、在common包下创建BaseContext工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
* @auther xiaochen
* @create 2022-08-07 10:49
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}

2、LoginCheckFilter中存放当前登录用户到ThreadLocal

在doFilter方法中, 判定用户是否登录, 如果用户登录, 在放行之前, 获取HttpSession中的登录用户信息, 调用BaseContext的setCurrentId方法将当前登录用户ID存入ThreadLocal。

1
2
3
//将用户id从Session中取出来,存入线程的ThreadLocal变量中
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);

image-20220807105903199

3、MyMetaObjectHandler中从ThreadLocal中获取

将之前在代码中固定的当前登录用户1, 修改为动态调用BaseContext中的getCurrentId方法获取当前登录用户ID

image-20220807110716910

5. 功能测试

测试增加/更新员工信息功能, 直接查询数据库数据变更,看看在新增/修改数据时,这些公共字段数据是否能够完成自动填充, 并且看看填充的create_user 及 update_user字段值是不是本地登录用户的ID。

二、新增分类

2.1 需求分析

后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类套餐分类 。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。

image-20220807112659881

在分类管理中,我们新增分类时, 可以选择新增菜品分类(川菜、湘菜、粤菜…), 也可以选择新增套餐分类(营养早餐、超值午餐…)。 在添加套餐的时候, 输入的排序字段, 控制的是移动端套餐列表的展示顺序。

image-20220807112709875

2.2 前端页面分析

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

1). 在页面(backend/page/category/list.html)的新增分类表单中填写数据,点击 “确定” 发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端

2). 服务端Controller接收页面提交的数据并调用Service将数据进行保存

3). Service调用Mapper操作数据库,保存数据

可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,只是type不同,所以服务端只需要提供一个方法统一处理即可:

image-20220807112759265

具体请求信息整理如下:

请求 说明
请求方式 POST
请求路径 /category
请求参数 json格式 - {“name”:”川菜”,”type”:”1”,”sort”:2}

2.3 代码实现

代码实现的具体步骤:

  • 实体类Category

  • Mapper接口CategoryMapper

  • 业务层接口CategoryService

  • 业务层实现类CategoryServiceImpl

  • 控制层CategoryController

1.实体类Category

在entity包下新建实体类Category

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
@Data
public class Category implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;


//类型 1 菜品分类 2 套餐分类
private Integer type;


//分类名称
private String name;


//顺序
private Integer sort;


//创建时间
@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;


//是否删除
private Integer isDeleted;

}

2.Mapper接口

在mapper包下新建Mapper接口CategoryMapper

1
2
3
4
5
6
7
/**
* @auther xiaochen
* @create 2022-08-07 11:13
*/
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

3.业务层接口CategoryService

在service包下新建业务层接口CategoryService

1
2
3
4
5
6
/**
* @auther xiaochen
* @create 2022-08-07 11:15
*/
public interface CategoryService extends IService<Category> {
}

4.业务层实现类CategoryServiceImpl

在service包下的impl包下新建业务层接口CategoryService

1
2
3
4
5
6
7
/**
* @auther xiaochen
* @create 2022-08-07 11:15
*/
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}

5. 控制层CategoryController

在controller包下新建CategoryController类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @auther xiaochen
* @create 2022-08-07 11:16
*/
@Slf4j
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;


@PostMapping
public R<String> save(@RequestBody Category category) {
log.info("当前category:{}", category);
categoryService.save(category);
return R.success("新增分类成功");
}
}

2.4 功能测试

新增分类的代码编写完毕之后, 我们需要重新启动项目,进入管理系统访问分类管理, 然后进行新增分类测试,需要将所有情况都覆盖全,例如:

1). 输入的分类名称不存在

鲁菜添加成功

image-20220807113920249

image-20220807113953935

2). 输入已存在的分类名称

image-20220807113854767

三、分类信息分页查询

3.1 需求分析

系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

image-20220807141212238

3.2 前端页面分析

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

1). 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端

2). 服务端Controller接收页面提交的数据并调用Service查询数据

3). Service调用Mapper操作数据库,查询分页数据

4). Controller将查询到的分页数据响应给页面

5). 页面接收到分页数据并通过ElementUI的Table组件展示到页面上

页面加载时,就会触发Vue声明周期的钩子方法,然后执行分页查询,发送异步请求到服务端,前端代码如下:

image-20220807141220749

页面中使用的是ElementUI提供的分页组件进行分页条的展示:

image-20220807141227018

我们通过浏览器,也可以抓取到分页查询的请求信息, 如下:

image-20220807141240320

具体的请求信息整理如下:

请求 说明
请求方式 GET
请求路径 /category/page
请求参数 ?page=1&pageSize=10

3.3 代码实现

在CategoryController中增加分页查询的方法,在方法中传递分页条件进行查询,并且需要对查询到的结果,安排设置的套餐顺序字段sort进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
//分页构造器
Page<Category> pageInfo = new Page<>(page,pageSize);
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);

//分页查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}

3.4 功能测试

可以正常查询到数据并分页

image-20220807141323634

四、删除分类

4.1 需求分析

在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。

image-20220807144955349

4.2 前端页面分析

在前端页面中,点击 “删除” 按钮,就会触发定义的方法,然后往服务端发送异步请求,并传递参数id,执行删除分类操作。

image-20220807145005124

删除操作的具体执行流程如下:

1). 点击删除,页面发送ajax请求,将参数(id)提交到服务端

2). 服务端Controller接收页面提交的数据并调用Service删除数据

3). Service调用Mapper操作数据库

image-20220807145012793

从上述的分析中,可以得到请求的信息如下:

请求 说明
请求方式 DELETE
请求路径 /category
请求参数 ?id=1395291114922618881

4.3 代码实现

在CategoryController中增加根据ID删除的方法,在方法中接收页面传递参数id,然后执行删除操作。

1
2
3
4
5
6
7
8
9
10
11
/**
* 根据id删除分类
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long ids){
log.info("删除分类,ids为:{}",ids);
categoryService.removeById(ids);
return R.success("分类信息删除成功");
}

这里前端传来的是ids,可以选择把前端的ids改为id,也可以在后端将id改为ids

3.5 功能完善

在上述的测试中,可以看到分类数据是可以正常删除的。但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。

完善后的逻辑为:

  • 根据当前分类的ID,查询该分类下是否存在菜品,如果存在,则提示错误信息
  • 根据当前分类的ID,查询该分类下是否存在套餐,如果存在,则提示错误信息
  • 执行正常的删除分类操作

1. 准备工作

1. 准备菜品(Dish)及套餐(Setmeal)实体类(课程资料中直接拷贝)放在entity包下

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.itheima.reggie.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
菜品
*/
@Data
public class Dish implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;


//菜品名称
private String name;


//菜品分类id
private Long categoryId;


//菜品价格
private BigDecimal price;


//商品码
private String code;


//图片
private String image;


//描述信息
private String description;


//0 停售 1 起售
private Integer status;


//顺序
private Integer sort;


@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;


//是否删除
private Integer isDeleted;

}
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.itheima.reggie.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
* 套餐
*/
@Data
public class Setmeal implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;


//分类id
private Long categoryId;


//套餐名称
private String name;


//套餐价格
private BigDecimal price;


//状态 0:停用 1:启用
private Integer status;


//编码
private String code;


//描述信息
private String description;


//图片
private String image;


@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;


//是否删除
private Integer isDeleted;
}

2.Mapper接口DishMapper和SetmealMapper

1
2
3
@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}
1
2
3
4
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}

3.Service接口DishService和SetmealService

1
2
3
public interface DishService extends IService<Dish> {
}

1
2
3
public interface SetmealService extends IService<Setmeal> {
}

4.Service实现类DishServicelmpl和SetmealServicelmpl

1
2
3
4
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
}

1
2
3
4
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService{
}

2.代码实现

1.在CategoryService中扩展remove方法

1
2
3
4
public interface CategoryService extends IService<Category> {
//根据ID删除分类
public void remove(Long id);
}

2.在CategoryServiceImpl中实现remove方法

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
@Service
public class CategoryServicelmpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

@Autowired
private DishService dishService;

@Autowired
private SetmealService setmealService;

@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper=new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dishLambdaQueryWrapper);

//查询当前分类是否关联菜品,如果已经关联,抛出业务异常
if(count1>0){
//已经关联菜品,抛出业务异常
throw new CustomException("已经关联菜品,不能删除");
}

//查询当前分类是否关联了套餐,如果已经关联,抛出业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper=new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);

if(count2>0){
//已经关联套餐,抛出业务异常
throw new CustomException("已经关联套餐,不能删除");
}
//正常删除分类
super.removeById(id);
}
}

在上述的业务逻辑中,当分类下关联的有菜品或者套餐时,我们在业务代码中抛出了自定义异常,并且在异常中封装了错误提示信息,那这个错误提示信息如何提示给页面呢?

异常抛出之后,会被异常处理器捕获,我们只需要在异常处理器中捕获这一类的异常,然后给页面返回对应的提示信息即可。

3.定义异常类CustomException

1
2
3
4
5
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}

4.在全局异常处理器GlobalExceptionHandler添加异常处理

1
2
3
4
5
6
7
//进行异常处理方法
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());

return R.error(ex.getMessage());
}

5.改造CategoryController的delete方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    /**
* 根据id删除分类
*
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(Long ids) {
log.info("删除分类,id为{}",ids);
// categoryService.removeById(ids);
categoryService.remove(ids);
//代码完善之后categoryService.remove(ids);
return R.success("分类信息删除成功");
}

五、修改分类

5.1 需求分析

在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作

5.2 代码实现

1
2
3
4
5
6
//修改分类
@PutMapping
public R<String> update(@RequestBody Category category){
categoryService.updateById(category);
return R.success("分类修改成功");
}

5.3 功能测试

修改湘菜信息image-20220807150908759

页面展示正常

image-20220807150923924

SQL语句成功执行

image-20220807150943748

数据库中数据修改成功

image-20220807151005586