• 【SpringBoot项目】SpringBoot项目-瑞吉外卖【day03】分类管理


    🌕博客x主页:己不由心王道长🌕!
    🌎文章说明:SpringBoot项目-瑞吉外卖【day03】分类管理🌎
    ✅系列专栏:SpringBoot项目
    🌴本篇内容:对黑马的瑞吉外卖项目的day03进行笔记和项目实现🌴
    ☕️每日一语:生活不可能像你想象得那么好,但也不会像你想象得那么糟。☕️
    🚩 交流社区:己不由心王道长(优质编程社区)

    前言

    本次文章对应所属项目的第3天,我在想,我项目进度到底是快了还是慢了。这个问题有点深奥,如果对于官方给的进度,那我项目肯定是慢了;但是项目得消化,不能做完即可,图个完成任务的心态是不可取的,所以还是慢慢来吧。

    公共字段自动填充

    问题分析

    我们在day02已经对后台的员工管理功能进行了开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段信息,在编辑员工时需要设置修改时间和修改人等字段信息。这些字段都是属于公共字段,也就是很多表中都有的字段,如下所示:

    在这里插入图片描述
    在这里插入图片描述

    基本每个表都有以上字段,而且我们在每一个需要用到的修改、新增时都用到了这些公共字段。

    在这里插入图片描述
    在这里插入图片描述
    这些代码十分冗余,没有技术含量,每次都写一遍是不可接受的。那么我们能不能对于这些公共字段做一个统一的处理,以便简化开发,让代码更加美观呢?可以!
    MybatisPlus为我们提供了公共字段自动填充功能。
    Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
    实现步骤:

    1、在实体类的属性上加入@TableField注解,指定自动填充的策略
    在这里插入图片描述
    在这里插入图片描述
    可以看到,我们能在相应的公共字段上,添加@TableField注解,然后在括号里选择方式,最后选择填充策略。填充策略有默认、插入、插入和更新、更新四种。

    @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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    以上是公共字段填充,为什么填充策略不同呢?这里解释一下:

    因为createTime只有在新建的时候使用,而updateTime在插入的时候就已经算更新了,在后面的更新中当然也算。所以updateTime的策略是插入和更新时填充,同理可以理解其他几个。

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

    代码实现
    package com.example.commons;
    
    import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.reflection.MetaObject;
    import org.springframework.stereotype.Component;
    
    /**
     * 自定义公共字段自动填充
     * @author 不止于梦想
     * @date 2022/11/15 20:23
     */
    @Component
    @Slf4j
    public class MyMetaObjectHandler implements MetaObjectHandler {
        /**
         * insert策略填充
         * @param metaObject
         */
        @Override
        public  void insertFill(MetaObject metaObject) {
            log.info(metaObject.toString());
            log.info("insert填充策略......");
        }
    
        /**
         * update策略填充
         * @param metaObject
         */
    
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info(metaObject.toString());
            log.info("update填充策略......");
        }
    }
    
    
    • 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

    这里没有进行任何填充,先测试一下代码是否能够走通

    功能测试

    我们在update策略里输出日志并且打上断点,验证我们程序是否能够执行成功。
    在这里插入图片描述
    这是更新策略,所以我们修改员工信息:
    在这里插入图片描述
    可以看到,代码是可以走通的。并且是在UPDATE之前,这就是我们想看到的
    在这里插入图片描述

    功能完善

    这里其实把上面没写的代码一并在这里完成,这里原本是解决ThreadLocal问题的,一并解决了吧。
    先把update的里面这几句注释掉,现在要用公共字段填充,这些不写了,拜拜勒:
    在这里插入图片描述
    重启项目发送更新请求:
    在这里插入图片描述
    注意看参数,update时间跟我当前时间不符合,说明了现在没有填充时间。下面依次完成需要的填充:

    package com.example.commons;
    
    import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.reflection.MetaObject;
    import org.springframework.stereotype.Component;
    
    import java.time.LocalDateTime;
    
    /**
     * 自定义公共字段自动填充
     * @author 不止于梦想
     * @date 2022/11/15 20:23
     */
    @Component
    @Slf4j
    public class MyMetaObjectHandler implements MetaObjectHandler {
        /**
         * insert策略填充
         * @param metaObject
         */
        @Override
        public  void insertFill(MetaObject metaObject) {
            metaObject.setValue("createTime",LocalDateTime.now());
            log.info("insert填充策略......");
        }
    
        /**
         * update策略填充
         * @param metaObject
         */
    
        @Override
        public void updateFill(MetaObject metaObject) {
            metaObject.setValue("updateTime", LocalDateTime.now());
            log.info("update填充策略......");
        }
    }
    
    
    • 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

    上面这段代码是不完整的,没有设置本次插入或者更新的人的id,我们能不能用session对象设置呢?不行,因为在方法执行的时候,真正的方法压根没有明着调用我们这个公共填充,而一次request请求你也给不了它。

    解决办法,首先我们要知道的是一次request请求其实对应的是一次线程,而我们要用到的线程是JDK为我们提供的ThreadLocal.
    这里我们先要确认一件事情,就是每当前台发一次http请求,我们后台对应的服务器是不是分配了一个新的线程来处理:

    多余解释画蛇添足,下面是官方给的方法,我们可以试试:

    在处理过程中涉及到下面类中的方法都属于相同的一个线程:
    1、LoginCheckFilter的doFilter方
    2、EmployeeController的update方法
    3、MyMetaObjectHandler的updateFill方法
    可以在上面的三个方法中分别加入下面代码 (获取当前线程id):
    long id Thread. current Thread() getId() :
    Log. info(“线程id:1”,id) :
    执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    可以知道的是一次请求确实是对应一个线程,还得验证一件事情,就是不同请求不是一次线程。再发一次:
    在这里插入图片描述
    既然每次请求对应一个线程,我们不可以共有一个请求,一个线程我们是可以共享的,而且别的请求线程也影响不到你的线程。

    介绍ThreadLocal:

    还是看一下官方解释:
    什么是ThreadLocal?
    ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不
    能访问。
    ThreadLocal常用方法:

    public void set(T value):设置当前线程的线程局部变量的值
    public T get() :返回当前线程所对应的线程局部变量的值

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

    有了步骤咱就整它,打它啊,打它mad!:

    实现步骤:
    1、编写BaseContext工具类,基于ThreadLocal封装的工具类

    package com.example.commons;
    
    /**
     * @author 不止于梦想
     * @date 2022/11/15 21:45
     */
    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
    • 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

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

    if (httpServletRequest.getSession().getAttribute("employee")!=null) {
                log.info("用户已经登录"+httpServletRequest.getSession().getAttribute("employee"));
                //获取当前请求的用户id
                long empId = (long) httpServletRequest.getSession().getAttribute("employee");
               //设置当前线程的线程局部变量的值
                BaseContext.setCurrentId(empId);
                filterChain.doFilter(httpServletRequest,httpServletResponse);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    只有已经登录过的用户才能获取到对应的id。
    3、在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id

     metaObject.setValue("updateUser",BaseContext.getCurrentId());
    
    • 1

    测试:
    在这里插入图片描述

    最后把所有公共字段去掉

    新增分类

    需求分析

    在这里插入图片描述
    在我们的分类管理中,有两个新增分类,分别是新增菜品分类和新增套餐分类。
    在这里插入图片描述
    在这里插入图片描述
    新增菜品分类和新增套餐分类其实基本无差别,只是发给后台时的type属性不同。
    在这个功能中,我们需要连接前端,并且在后端区分,然后把操作数据存入数据库。
    在这里插入图片描述

    在这里插入图片描述
    调用了axios、方法是post方法。
    在这里插入图片描述
    只判断了code,所以新的controller应该是String类型。

    模型

    在这个功能中,我们的数据模型跟前面的不一样了,不再是employee,而是category
    在这里插入图片描述
    两个分类模式,数据其实存入了一张表之中。

    需要注意的是这里的name设置了唯一性约束,如果名字重复是会抛出异常的
    在这里插入图片描述

    代码开发

    这里直接导入类category:

    package com.example.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 lombok.Getter;
    import lombok.Setter;
    import java.io.Serializable;
    import java.time.LocalDateTime;
    
    /**
     * 分类
     */
    @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;
    
    }
    
    
    • 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

    接下来把service、mapper弄好。
    然后·写好controller:

    package com.example.controller;
    
    import com.example.commons.R;
    import com.example.entity.Category;
    import com.example.service.CategoryService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author 不止于梦想
     * @date 2022/11/17 18:21
     */
    @RestController
    @RequestMapping("/category")
    @Slf4j
    public class CategoryController {
    
        @Autowired
        CategoryService categoryService;
    
        /**
         * 新增分类
         * @param category
         * @return
         */
        @PostMapping
        public R<String> save(@RequestBody Category category){
            log.info("新增分类");
            boolean save = categoryService.save(category);
            if(save) {
                return R.success("新增分类成功");
            }
            return R.error("新增分类失败");
        }
    
    
    
    }
    
    
    • 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
    功能测试

    在这里插入图片描述
    在这里插入图片描述
    添加分类成功,接口异常是接下来要处理的信息分页查询

    这里还有一个问题就是,如果我们添加的菜品名字一样,会出异常的,因为我们设计表时就已经把name字段设置为非空了。我们测试一下
    在这里插入图片描述
    但是我们不会受到影响,因为我们在前面已经设置了一个全局处理异常“Duplicate entry”
    在这里插入图片描述
    所以会提示我们已经存在。

    分类信息分页查询

    需求分析

    在上面的新增分类,我们已经提到了系统接口404异常,那么这个异常其实就是当我们点击分类管理时,页面就会发送请求去后台查询数据并且返回展示了:
    在这里插入图片描述
    由上图,当我们点击新增分类时,vue就创建了钩子函数,并调用了getCategoryPage方法。并且传入了页码和页码所在页的大小。其实就是一个分页查询,我们在employee时已经做过,所以这里直接跟进getCategoryPage:
    在这里插入图片描述
    细节如图。

    代码开发
    @GetMapping("/page")
        public R<Page> page(int page,int pageSize){
            log.info("分页查询");
            //构造分页构造器
            Page pageInfo = new Page(page,pageSize);
            //构造条件构造器,输出时要用到
            LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.orderByAsc(Category::getSort);
            //进行分页查询
            categoryService.page(pageInfo, queryWrapper);
            return R.success(pageInfo);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    功能测试

    在这里插入图片描述

    删除分类

    需求分析

    在这里插入图片描述
    可以看到,我们的分类管理后面其实是可以操作的,而这里要介绍的操作就是删除分类。
    这里需要注意是当分类关联了菜品或者套餐时,此分类是不允许删除的。这里解释一下,我们这里只是套餐分类,真正的细节并不是存在这个表里的,而是分别存在相应的表中:
    在这里插入图片描述
    在这里插入图片描述
    如上图,分类表只能表示有没有当前种类和添加种类,删除不归它管理,如果不存在该种类,查询时自然不显示。

    代码开发

    我们还是先做简单的删除
    在这里插入图片描述
    在这里插入图片描述
    注意这里通过id删除,但参数传递时是ids

      @DeleteMapping
        public R<String> delete(Long ids){
            log.info("删除操作......");
            categoryService.removeById(ids);
            return  R.success("删除成功");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    功能完善

    这里细节就不多说了。
    上代码,不过那些需要导入和构建架构的代码就不上了,太水:
    CustomExcption:

    package com.example.commons;
    
    /**
     * @author 不止于梦想
     * @date 2022/11/17 21:00
     */
    
    /**
     * 自定义异常
     */
    public class CustomerExcption extends RuntimeException{
        /**
         * 传入异常信息,交给父类
         * @param msg :异常信息
         */
        public CustomerExcption(String msg){
            super(msg);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    CategoryServiceImpl:

    package com.example.service.impl;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.example.commons.CustomerExcption;
    import com.example.entity.Category;
    import com.example.entity.Dish;
    import com.example.entity.Setmeal;
    import com.example.mapper.CategoryMapper;
    import com.example.service.CategoryService;
    import com.example.service.DishService;
    import com.example.service.SetmealService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * @author 不止于梦想
     * @date 2022/11/17 18:19
     */
    @Service
    public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
        @Autowired
        DishService dishService;
        @Autowired
        SetmealService setmealService;
    
        /**
         * 通过id删除分类,删除之前检查有没有关联套餐(Setmeal)或者菜品(Dish),需要用到这两者的服务,所以在上边进行注入
         * @param id
         */
        @Override
        public void remove(Long id) {
            //判断是否关联Dish,设置查询条件
            LambdaQueryWrapper<Dish> dish = new LambdaQueryWrapper<>();
            //菜品分类id
            //    private Long categoryId;比较两者id是否相等
            //设置条件判断,条件为传入id与Dish表中的属性CategoryId相等
            dish.eq(Dish::getCategoryId,id);
            //调用dishService服务,查询相等的条数
            int count1 = dishService.count(dish);
            //如果存在,则说明关联,抛出异常,提示前台
            if(count1>0){
                throw new CustomerExcption("菜品已被关联,不能删除");
            }
            //判断是否关联Dish,设置查询条件
            LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
            //分类id
            //private Long categoryId;
            //设置查询条件
            setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
            //调用setmealService服务,查询相等的条数
            int count2 = setmealService.count(setmealLambdaQueryWrapper);
            //如果存在,则说明关联,抛出异常,提示前台
            if(count2>0){
                throw new CustomerExcption("套餐已被关联,不能删除");
            }
            //否则,则没有关联,正常关联分类,调用接口的ById方法
            super.removeById(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
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    别忘了再最开始的地方更改为你刚修改的方法:
    在这里插入图片描述
    如果还是留着上次的方法,小心数据丢失(悲伤)。
    测试:
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/c0e1ef401fd44bb48c7be78de336424a.png
    在这里插入图片描述
    接下来验证没有关联的能不能删除,隆江猪脚饭是我刚添加的没有关联:
    在这里插入图片描述

    修改分类

    需求分析

    在这里插入图片描述

    当我们点击修改时,前端根据id进行查询,并进行了一个回显操作,这里就不细究了,我们可以看到这里可以更新两个信息,名称和排序。
    在这里插入图片描述
    当点击确定时,会把以上信息作为参数进行查询。
    在这里插入图片描述
    参数时id,name,和sort,但是更新时间什么的都会设置,所以这里直接用对象作为参数。返回值是code、请求时put,路径明细如下:

    在这里插入图片描述

    代码实现
    @PutMapping
        public R<String> update(@RequestBody Category category){
            log.info("参数:{}",category.toString());
            categoryService.updateById(category);
            return R.success("修改成功");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    验证:
    在这里插入图片描述
    在这里插入图片描述

    结尾

    创作不易,喜欢的给个三连。

  • 相关阅读:
    LeetCode 0236. 二叉树的最近公共祖先
    原来引用还有这么多不为人知的秘密呢?
    用Python自动生成 图文并茂的数据分析 报告
    Pycharm远程调试、运行服务器代码
    华为云云耀云服务器L实例评测|Elasticsearch的Docker版本的安装和参数设置 & 端口开放和浏览器访问
    (附源码)ssm招聘网站 毕业设计 250858
    Docker实践经验:Docker 上部署 mysql8 主从复制
    STM32单片机学习3--STM32控制键盘
    逆向-还原代码之url_encode (Arm 64)
    Linux Shell 自动交互功能
  • 原文地址:https://blog.csdn.net/qq_63992577/article/details/127871395