0
点赞
收藏
分享

微信扫一扫

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)



文章目录

  • 外卖-Day03
  • 课程内容
  • 1. 公共字段自动填充
  • 1.1 问题分析
  • 1.2 基本功能实现
  • 1.2.1 思路分析
  • 1.2.2 代码实现
  • 1.2.3 功能测试
  • 1.3 功能完善 (通过当前线程传递数据)
  • 1.3.1 思路分析
  • 1.3.2 ThreadLocal
  • 1.3.3 操作步骤
  • 1.3.4 代码实现
  • 1.3.5 功能测试
  • 2. 新增分类
  • 2.1 需求分析
  • 2.2 数据模型
  • 2.3 前端页面分析
  • 2.4 代码实现
  • 2.5 功能测试
  • 3. 分类信息分页查询
  • 3.1 需求分析
  • 3.2 前端页面分析
  • 3.3 代码实现
  • 3.4 功能测试
  • 4. 删除分类
  • 4.1 需求分析
  • 4.2 前端页面分析
  • 4.3 代码实现
  • 4.4 功能测试
  • 4.5 功能完善 (后台实现数据库外键约束)
  • 4.5.1 思路分析
  • 4.5.2 准备工作
  • 4.5.3 代码实现 (以重写或者调用方式增强模板service类的方法,以在service层加上(写)自己的业务逻辑)
  • 4.5.4 功能测试
  • 5. 修改分类
  • 5.1 需求分析
  • 5.2 前端页面分析
  • 5.3 代码实现
  • 5.4 功能测试


外卖-Day03

课程内容

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

1. 公共字段自动填充

好多表公共的字段,赋值逻辑也相同,不用每次为其赋值,‘拦截器’统一赋值

1.1 问题分析

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_mybatis


(查下数据库,确实很多表都有这些字段)

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

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

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

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_字段_02


SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_自动填充_03

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

答案是可以的,我们使用Mybatis Plus提供的公共字段自动填充功能。

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.在资料中提供的实体类,已经添加了该注解,并指定了填充策略)

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

FieldFill.INSERT: 插入时填充该属性值

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

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

很多表,很多模块的insert,update都要用,放在common包下合适

cn.whu.reggie.common.MyMetaObjecthandler

/**
 * 自定义元数据对象处理器
 * 用于insert或update sql语句时 自动填充公共字段值
 */
@Component //IOC容器中有 就能起作用
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 插入操作自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]...");
        log.info(metaObject.toString());

        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser",1l);
        metaObject.setValue("updateUser",1l);//此处获取不到request,session 先放着
    }

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

        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser",1l);//此处获取不到request,session 先放着
    }
}

1.2.3 功能测试

编写完了元数据对象处理器之后,我们就可以将之前在新增和修改方法中手动赋值的代码删除或注释掉。

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_mybatis_04


SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_mybatis_05

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

测试可以

1.3 功能完善 (通过当前线程传递数据)

1.3.1 思路分析

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

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_自动填充_06

注意,我们在MyMetaObjectHandler类中是不能直接获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_java_07

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

1). LoginCheckFilter的doFilter方法

2). EmployeeController的update方法

3). MyMetaObjectHandler的updateFill方法

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

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

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_java_08

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

1.3.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直接获取。

1.3.3 操作步骤

实现步骤:

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

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

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

1.3.4 代码实现

1). BaseContext工具类

工具类也放在common包下

cn.whu.reggie.common.BaseContext

/**
 * 基于ThreadLocal封装工具类,用于保存和获取当前登录用户id
 * 就是自己写的BaseContext上下文对象
 */
public class BaseContext {
    // new出来就是当前线程的局部变量区  一个线程内只会有一个ThreadLocal对象(内存区域)   泛型传入变量类型
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();


    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();//每个线程只有一个对象  以线程为作用域
    }
}

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

写的位置也是有讲究的

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

Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_前端_09

3). MyMetaObjectHandler中从ThreadLocal中获取

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_java_10


SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_字段_11

修改后cn.whu.reggie.common.MyMetaObjectHandler完整代码如下:

/**
 * 自定义元数据对象处理器
 * 用于insert或update sql语句时 自动填充公共字段值
 */
@Component //IOC容器中有 就能起作用
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 插入操作自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]...");
        log.info(metaObject.toString());

        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser",BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
        //此处获取不到request,session  只能自己基于当前线程写一个 自定义的上下文对象了
        //在当前线程可以获得到request域的地方将当前用户id放到线程局部变量区域内
        //这里再直接拿出来
    }

    /**
     * 更新操作自动填充
     * @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.3.5 功能测试

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

测试完毕,没有问题

切换到张三登陆后再添加用户: (添加用户无非是注册账号,谁都能做的)

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_前端_12

2. 新增分类

2.1 需求分析

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_自动填充_13

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_mybatis_14

2.2 数据模型

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_mybatis_15

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_前端_16

2.3 前端页面分析

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

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

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

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

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_字段_17

具体请求信息整理如下:

请求

说明

请求方式

POST

请求路径

/category

请求参数

json格式 - {“name”:“川菜”,“type”:“1”,“sort”:2}

2.4 代码实现

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

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

上次是插件自动生成的,这次就自己写一次吧

1). 实体类Category

所属包: cn.whu.reggie.entity

别忘了实现Serializable接口

/**
 *  分类
 */
@Data
public class Category implements Serializable{

    // 序列化必备
    public static final Long serialVersionUID = 1L;

    // 分类id
    private Long id;

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

    // 分类名称: 川菜  xx套餐
    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;

}

注意时间类型: LocalDateTime

2). Mapper接口CategoryMapper

所属包: cn.whu.reggie.mapper

@Mapper
public interface CategoryMapper extends BaseMapper<Category> {

}

3). 业务层接口CategoryService

所属包: com.itheima.reggie.service

public interface CategoryService extends IService<Category> {

}

4). 业务层实现类CategoryServiceImpl

所属包: com.itheima.reggie.service.impl

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
    // 基层自ServiceImpl(BaseService)  给他Service必要的 Mapper和POJO 他就能帮你做好所有基本的crud
}

5). 控制层CategoryController

@RestController
@RequestMapping("category")
@Slf4j
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 新增分类
     * @param category
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody Category category){
        log.info("新增分类:{}",category.toString());
        // 新增分类:Category(id=null, type=2, name=川菜, sort=4, createTime=null, updateTime=null, createUser=null, updateUser=null)
        // 确定了没有需要再指定的字段值 直接保存即可

        // 也不用怕抛异常 会转全局异常处理
        categoryService.save(category);

        return R.success("新增分类成功~");
    }

}

2.5 功能测试

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

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

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

3). 新增菜品分类

4). 新增套餐分类

全局异常处理多好啊

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_mybatis_18

3. 分类信息分页查询

3.1 需求分析

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_前端_19

3.2 前端页面分析

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

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

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

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

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

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

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_java_20

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_mybatis_21

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_自动填充_22

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

请求

说明

请求方式

GET

请求路径

/category/page

请求参数

?page=1&pageSize=10

3.3 代码实现

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

自己写得时候排序条件忘记了。排序条件最好有,不然用户体验会比较差
.
具体逻辑如下:

  1. 分页构造器
  2. 条件构造器
  3. 添加排序条件,根据sort
  4. 分页条件查询

/**
 * 分页查询
 * @param page
 * @param pageSize
 * @return
 */
@GetMapping("/page")
public R<Page> page(Integer page,Integer pageSize){
    // 1. 分页构造器
    Page<Category> pageBean = new Page<>(page, pageSize);
    // 2. 条件构造器
    LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
    // 3. 添加排序条件,根据sort升序排序
    lqw.orderByAsc(Category::getSort);
    // 4. 分页条件查询
    categoryService.page(pageBean,lqw);
    return R.success(pageBean);
}

3.4 功能测试

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

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_mybatis_23

4. 删除分类

4.1 需求分析

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_字段_24

查表发现也就菜品和分类表里面关联了category_id

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_java_25

4.2 前端页面分析

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_mybatis_26

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

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

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

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_java_27

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

请求

说明

请求方式

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 功能完善 (后台实现数据库外键约束)

其实就是数据库没有加外键,就得自己用后端java代码实现外键的逻辑

4.5.1 思路分析

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

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

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_前端_28

4.5.2 准备工作

直接用CodeGenerator生成了
使用前先将CodeGenerator剪切到test包下吧,复制一份,删除注释,然后执行这两个表的模块生成

cn.whu.reggie.utils.CodeGenerator2

import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;

public class CodeGenerator2 {
    public static void main(String[] args) {
        //1. 创建代码生成器对象,执行生成代码操作
        AutoGenerator autoGenerator = new AutoGenerator();

        // 中间三大配置 让他一步步生成我们想要的代码样式
        //设置全局配置
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setOutputDir(System.getProperty("user.dir") + "/src/main/java");    //设置代码生成位置 不是项目下的module 不需要写项目名了
        globalConfig.setOpen(false);    //设置生成完毕后是否打开生成代码所在的目录
        globalConfig.setAuthor("whu");    //设置作者
        globalConfig.setFileOverride(false);     //设置是否覆盖原始生成的文件
        autoGenerator.setGlobalConfig(globalConfig);

        //设置包名相关配置
        PackageConfig packageInfo = new PackageConfig();
        packageInfo.setParent("cn.whu.reggie");   //设置生成的包名,与代码所在位置不冲突,二者叠加组成完整路径
        autoGenerator.setPackageInfo(packageInfo);

        //策略设置
        StrategyConfig strategyConfig = new StrategyConfig();
        strategyConfig.setInclude("setmeal","dish");  //设置当前参与生成的表名,参数为可变参数 // 不写就是生成所有表
        strategyConfig.setRestControllerStyle(true);    //设置是否启用Rest风格

        strategyConfig.setEntityLombokModel(true);  //设置是否启用lombok 实体类就自动用lombok了
        autoGenerator.setStrategy(strategyConfig);


        //2. 数据源相关配置:读取数据库中的信息,根据数据库表结构生成代码
        DataSourceConfig dataSource = new DataSourceConfig();
        dataSource.setDriverName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        dataSource.setUsername("root");
        dataSource.setPassword("1234");
        autoGenerator.setDataSource(dataSource);

        //3. 执行生成操作
        autoGenerator.execute();
    }
}

1). 准备菜品(Dish)及套餐(Setmeal)实体类
(课程资料中直接拷贝 或者 代码生成器直接生成,这里选择代码生成器直接生成)

所属包: cn.whu.reggie.entity

两个entity修改一下驼峰命名即可

/**
 * <p>
 * 菜品管理
 * </p>
 */
@Data
@EqualsAndHashCode(callSuper = false)
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;
}

/**
 * <p>
 * 套餐
 * </p>
 */
@Data
@EqualsAndHashCode(callSuper = false)
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;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    /**
     * 创建人
     */
    private Long createUser;

    /**
     * 修改人
     */
    private Long updateUser;

    /**
     * 是否删除
     */
    private Integer isDeleted;

}

2). Mapper接口DishMapper和SetmealMapper

所属包: cn.whu.reggie.mapper

删除xml,加上@Mapper注解

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Dish;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Setmeal;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}

3). Service接口DishService和SetmealService

所属包: cn.whu.reggie.service

重命名,将接口名前的I去掉即可

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Dish;

public interface DishService extends IService<Dish> {
}

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Setmeal;

public interface SetmealService extends IService<Setmeal> {
}

4). Service实现类DishServiceImpl和SetmealServiceImpl

不需要修改

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.mapper.DishMapper;
import com.itheima.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper,Dish> implements DishService {
}

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.mapper.SetmealMapper;
import com.itheima.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {
}

5) Controller也一并帮你生成了 (虽然还用不到)

注入一下对应的Service即可

@RestController
@RequestMapping("/dish")
public class DishController {
    @Autowired
    private DishService dishService;

}

@RestController
@RequestMapping("/setmeal")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

}

4.5.3 代码实现 (以重写或者调用方式增强模板service类的方法,以在service层加上(写)自己的业务逻辑)

1). 创建自定义异常

之前全局异常处理仅仅只是处理了@ExceptionHandler(SQLIntegrityConstraintViolationException.class) 这一种异常
其实可以处理各种类型的异常,注解里配置成各种XXXException.class即可
这里我们将所有的 RuntimeException 包装成我们自定义异常,统一处理所有RuntimeException

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

所在包: cn.whu.reggie.common.exception

已经约定好了code,且Result里面封装好success和error方法,自定义异常里就不必有太多消息字段了

/**
 * 自定义业务异常类
 * GlobalExceptionHandler里统一匹配处理所有抛到Controller的RuntimeException异常
 */
public class CustomException extends RuntimeException{// 代码执行过程中主动抛出的异常 当然属于RuntimeException了
    public CustomException(String message) {
        super(message);
    }
}

custom:自定义 (的意思)

2). 在CategoryService中扩展remove方法

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

写一个自己的方法,写好逻辑后再调用现成的crud方法(service层方法)
直接重写也行 我就直接重写了(CategoryService接口里不需要加方法了)

3). 在CategoryServiceImpl中实现remove方法

复杂业务本来就该在service层写
删除之前要判断,就重写方法,还是调用super.removeById(id); 但是调用前加一些逻辑就是了
重写IService方法

@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;

/**
 * 根据id删除分类,删除之前需要进行判断
 * @param id
 */
@Override
public void remove(Long id) {
    //添加查询条件,根据分类id进行查询菜品数据
    LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
    dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
    int count1 = dishService.count(dishLambdaQueryWrapper);
    //如果已经关联,抛出一个业务异常
    if(count1 > 0){
        throw new CustomException("当前分类下关联了菜品,不能删除");//已经关联菜品,抛出一个业务异常
    }

    //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
    LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
    setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
    int count2 = setmealService.count(setmealLambdaQueryWrapper);
    if(count2 > 0){
        throw new CustomException("当前分类下关联了套餐,不能删除");//已经关联套餐,抛出一个业务异常
    }

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

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

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

我的做法,直接重写removeById方法,CategoryService接口不需要动

业务层里注入其他模块的Service,是合理的

2)3)5)三步 我的做法就这一步

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
    // 基层自ServiceImpl(BaseService)  给他Service必要的 Mapper和POJO 他就能帮你做好所有基本的crud

    // 业务层里注入其他模块的Service,是合理的
    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;


    @Override
    public boolean removeById(Serializable id) {

        // 1. 查询该分类有无关联菜品
        LambdaQueryWrapper<Dish> dishLqw = new LambdaQueryWrapper<>();
        // 1.1 该分类下有菜品 不可以删除 抛出异常统一处理
        dishLqw.eq(Dish::getCategoryId,id);
        int count = dishService.count(dishLqw);
        if(count>0){
            throw new CustomException("该分类下有关联菜品,不可以删除");
            // 不需要return 编译都不过 没有被捕获的异常一旦触发,其下面的代码不会被执行
        }


        // 2. 查询该分类有无关联套餐
        LambdaQueryWrapper<Setmeal> setmealLqw = new LambdaQueryWrapper<>();
        // 2.1 该分类有关联套餐 不可以删除 抛出异常统一处理
        setmealLqw.eq(Setmeal::getCategoryId,id);
        count = setmealService.count(setmealLqw);
        if(count>0){
            throw new CustomException("该分类有关联套餐,不可以删除");
        }

        return super.removeById(id);
    }
}

4). 在GlobalExceptionHandler中处理自定义异常

在全局异常处理器中增加方法,用于捕获我们自定义的异常 CustomException

/**
 * 异常处理方法
 * AOP:统一处理抛到Controller层的所有CustomException
 */
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {//形参类型不同,可以构成方法重载 (仅仅返回参数不同,不能构成重载)
    log.error("捕获到了自定义的异常 CustomException: {}",ex.getMessage());
    return R.error(ex.getMessage());// controller的AOP 此处return和Controller里return,一个效果
}

5). 改造CategoryController的delete方法

(直接重写方法的话,此处就不需要改)

注释掉原有的代码,在delete方法中直接调用categoryService中我们自定义的remove方法。

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

    //categoryService.removeById(id);
    categoryService.remove(id);

    return R.success("分类信息删除成功");
}

4.5.4 功能测试

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

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

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_前端_29

5. 修改分类

5.1 需求分析

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_java_30

5.2 前端页面分析

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

早就想这么实现了,之前以为是防止用户按F12修改静态页面,刚刚发现,即使修改静态页面,vue的model-data也不会被修改,所以回显时不会受到影响,说明这么做完全是可行的。

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_自动填充_31

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

SSM实战-外卖项目-03-公共字段自动填充(实现MP的接口方法,再把类给IOC)、新增分类、分类信息分页查询、删除分类(后台实现外键约束,抛自定义异常+统一异常处理 重写)、修改分类(前端实现回显)_前端_32

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

请求

说明

请求方式

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); // 也会自动调用toString()
    categoryService.updateById(category);//千万注意是updateById 而不是 update
    //XX categoryService.update(category,null);//否则直接调用update方法,会将所有的记录都修改(成一样的)了
    return R.success("分类信息修改成功!");
}

5.4 功能测试

按照前面分析的操作流程进行测试,查看数据是否正常修改即可。
每次测试也看看控制台打印的sql语句,毕竟不是自己写的,想想人家怎么做到的!

测试ok~


举报
0 条评论