瑞吉外卖-day03

YangeIT大约 22 分钟瑞吉外卖MetaObjectHandler公共字段自动填充分类模块文件上传统一异常处理

瑞吉外卖-day03

课程内容

  • 管理端

    • 公共字段自动填充
    • 新增分类
    • 分类信息分页查询
    • 删除分类
    • 修改分类

1. 公共字段自动填充

1.1 问题分析 🍐 ⚠️

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

image-20210801085103062
image-20210801085103062

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

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

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

目前,在我们的项目中处理这些字段都是在每一个业务方法中进行赋值操作,如下:

image-20210801085615162image-20210801085715419

思考

如果都按照上述的操作方式来处理这些公共字段, 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐,那能不能对于这些公共字段在某个地方统一处理,来简化开发呢?

1.2 基本功能实现

1.2.1 思路分析

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

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

实现步骤:

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

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

1.2.2 代码实现

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

在员工 Employee 实体类的公共字段属性上, 加上注解, 指定填充策略。(ps.在资料中提供的实体类,已经添加了该注解,并指定了填充策略)

image-20210801092157093

FieldFill.INSERT: 插入时填充该属性值 FieldFill.INSERT_UPDATE: 插入/更新时填充该属性值

1.2.3 功能测试

编写完了元数据对象处理器之后,我们就可以

image-20210801093623217image-20210801093747896

然后,我们启动项目,在员工管理模块中,测试增加/更新员工信息功能,然后通过 debug 或者 直接查询数据库数据变更的形式,看看我们在新增/修改数据时,这些公共字段数据是否能够完成自动填充。

1.3 功能完善 ✏️ ⚠️

1.3.1 思路分析 🍐

前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充 createUser 和 updateUser 时设置的用户 id 是固定值,现在我们需要完善,改造成动态获取当前登录用户的 id。

大家可能想到,用户登录成功后我们将用户 id 存入了 HttpSession 中,现在我从 HttpSession 中获取不就行了?

image-20210801131449863所以我们需要通过其他方式来获取登录用户 id。

那么我先搞清楚一点,当我们在修改员工信息时, 我们业务的执行流程是什么样子的,如下图:

image-20210801133531663

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

1). LoginCheckFilter 的 doFilter 方法

2). EmployeeController 的 update 方法

3). MyMetaObjectHandler 的 updateFill 方法

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

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

执行编辑员工功能进行验证,通过观察控制台输出可以发现,

image-20210801133827264

经过上述的分析之后,发现我们可以使用 JDK 提供的一个类, 来解决此问题,它是 JDK 中提供的 ThreadLocal。

1.3.2 ThreadLocal

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

ThreadLocal 为

ThreadLocal 常用方法:

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

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

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

  1. 在 LoginCheckFilter 的 doFilter 方法中获取当前登录用户 id,并调用 ThreadLocal 的 set 方法来设置当前线程的线程局部变量的值(用户 id),1. 然后在 MyMetaObjectHandler 的 updateFill 方法中调用 ThreadLocal 的 get 方法来获得当前线程所对应的线程局部变量的值(用户 id)。
  2. 如果在后续的操作中, 我们需要在 Controller / Service 中要使用当前登录用户的 ID, 可以直接从 ThreadLocal 直接获取。

1.3.3 代码实现

实现步骤:

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

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

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

1️⃣ BaseContext 工具类

所属包: com.itheima.reggie.common

/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 */
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();
    }
}

1.3.5 功能测试

完善了元数据对象处理器之后,我们就可以重新启动项目,完成登录操作后, 在员工管理模块中,测试增加/更新员工信息功能, 直接查询数据库数据变更,看看我们在新增/修改数据时,这些公共字段数据是否能够完成自动填充, 并且看看填充的 create_user 及 update_user 字段值是不是本地登录用户的 ID。

2. 新增分类

2.1 需求分析

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

image-20210801163745391

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

image-20210801165118745

2.2 数据模型

新增分类,其实就是将我们新增窗口录入的分类数据,插入到 category 表,具体表结构如下:

image-20210801165801665
image-20210801165801665

我们添加的套餐名称,是唯一的,不能够重复的,所以在设计表结构时,已经针对于 name 字段建立了唯一索引,如下:

image-20210801165921450
image-20210801165921450

2.3 前端页面分析

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

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

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

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

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

image-20210801171125255

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

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

2.4 代码实现 ✏️

代码实现的具体步骤如下:

  1. 实体类 Category(直接从课程资料中导入即可)
  2. Mapper 接口 CategoryMapper
  3. 业务层接口 CategoryService
  4. 业务层实现类 CategoryServiceImpl
  5. 控制层 CategoryController

1). 实体类 Category

所属包: com.itheima.reggie.entity

/**
 * 分类
 */
@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;
}

2.5 功能测试 ⚠️

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

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

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

3). 新增菜品分类 ❓ 如果添加相同的分类名字,会报错,点击查看解决方案:全局异常处理 ⚠️

4). 新增套餐分类

3. 分类信息分页查询

3.1 需求分析 🍐

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

image-20210801172259439

3.2 前端页面分析

执行过程:

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

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

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

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

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

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

image-20210801172847501

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

image-20210801173229949

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

image-20210801172951915

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

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

3.3 代码实现 ✏️

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

/**
 * 分页查询
 * @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 功能测试

分页查询的代码编写完毕之后, 我们需要重新启动项目,然后登陆系统后台,点击分类管理,查询分类列表是否可以正常展示。测试过程中可以使用浏览器的监控工具查看页面和服务端的数据交互细节。

测试完毕后,大家会发现,我们查询数据库返回的类型为 1 或者 2, 但是实际展示到页面上的却是 "菜品分类" 或 "套餐分类",这一块是在前端页面中进行处理的,处理代码如下:

image-20210801173758580image-20210801173850606

4. 删除分类

4.1 需求分析

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

image-20210801220637396

4.2 前端页面分析

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

image-20210801221049176

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

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

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

  3. Service 调用 Mapper 操作数据库

image-20210801221343539

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

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

4.3 代码实现 ✏️

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

/**
 * 根据id删除分类
 * @param id
 * @return
 */
@DeleteMapping
public R<String> delete(Long id){
    log.info("删除分类,id为:{}",id);
    categoryService.removeById(id);
    return R.success("分类信息删除成功");
}

4.4 功能测试

基本的删除操作代码实现完毕后,重启项目,进行测试。可以通过 debug 断点调试进行测试,同时结合浏览器监控工具查看请求和响应的具体数据。

4.5 功能完善 🍐 ✏️ ⚠️

4.5.1 思路分析

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

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

那么在这里又涉及到我们后面要用到的两张表结构 dish(菜品表) 和 setmeal(套餐表)。具体的表结构,我们目前先了解一下:

image-20210802001302912image-20210802001348928

4.5.2 准备工作

提示

  1. 准备菜品(Dish)及套餐(Setmeal)实体类(课程资料中直接拷贝)
  2. Mapper 接口 DishMapper 和 SetmealMapper
  3. Service 接口 DishService 和 SetmealService
  4. Service 实现类 DishServiceImpl 和 SetmealServiceImpl

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

所属包: com.itheima.reggie.entity

/**
 菜品
 */
@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;
}

/**
 * 套餐
 */
@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;
}

4.5.3 代码实现 ✏️

提示

  1. 创建自定义异常
  2. 在 CategoryService 中扩展 remove 方法
  3. 在 CategoryServiceImpl 中实现 remove 方法
  4. 在 GlobalExceptionHandler 中处理自定义异常
  5. 改造 CategoryController 的 delete 方法

1). 创建自定义异常

在业务逻辑操作过程中,如果遇到一些业务参数、操作异常的情况下,我们直接抛出此异常。

所在包: com.itheima.reggie.common

/**
 * 自定义业务异常类
 */
public class CustomException extends RuntimeException {
    public CustomException(String message){
        super(message);
    }
}

4.5.4 功能测试

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

1). 新增一个分类,然后再直接删除,检查是否可以正常删除成功。(新增的分类时没有关联菜品和套餐的)

2). 在数据库表(dish/setmeal)中,找到一个与菜品或套餐关联的分类,然后在页面中执行删除操作,检查是否可以正常的提示出对应的错误信息。

image-20210801235124007

5. 修改分类

5.1 需求分析 🍐

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

image-20210801235311435

5.2 前端页面分析

这里面大家会发现,修改功能我们还没有实现,但是当点击 "修改" 按钮的时候,我们并没有开发根据 ID 查询数据,进行页面回显的功能,但是页面的分类数据确实回显回来了。这是怎么做到的呢,我们来解析一下前端的代码实现(前端代码已经实现):

image-20210802000227359

那么回显这一步的操作前端已经实现,我们就只需要开发一个方法,修改操作的方法即可。我们可以通过浏览器来抓取一下修改操作的请求信息,如图:

image-20210802000605946

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

请求说明
请求方式PUT
请求路径/category
请求参数{id: "1399923597874081794", name: "超值午餐", sort: 0}

5.3 代码实现 ✏️

html 页面中相关的代码都已经提供好了,我们已经分析了请求的信息,接下来就可以来创建服务端的 CategoryController 方法 update 方法。

/**
 * 根据id修改分类信息
 * @param category
 * @return
 */
@PutMapping
public R<String> update(@RequestBody Category category){
    log.info("修改分类信息:{}",category);
    categoryService.updateById(category);
    return R.success("修改分类信息成功");
}

5.4 功能测试

按照前面分析的操作流程进行测试,查看数据是否正常修改即可。