springboot项目:瑞吉外卖 前后端详细分析 part2

第一部分part1 链接
Part2 第2部分(本页)
第三部分part3 链接

3 分类管理业务开发

在这里插入图片描述
内容:
3.1 公共字段自动填充
3.2 新增分类 (整个过程包括ThreadLocal、拦截器、元对象处理器等值得学习)
3.3 分类信息分页查询
3.4 删除分类 (需要考虑删除分类下有没有菜品,自己写service而不再是完全使用mybatis_plus值得学习,其中还有sql优化重点关注)
3.5 修改分类

3.1 公共字段自动填充

3.1.1 问题分析

在这里插入图片描述

  • 就像是把一些公共的操作切出来,封装到方法里去。每次insert或者update操作的时候,都自动执行这些方法,给指定的公共字段属性赋值
  • 还有一个问题:MetaObjectHandler类中的方法中设置createUser和UpdateUser的时候,需要从session中获取id,但是该类无法获取request,如何做到这个功能呢?-----使用TreadLocal

3.1.2 mybatis_plus 提供的公共字段自动填充

在这里插入图片描述

  • 简单的说,就是在执行insert(),或者 update()的时候,自动执行MetaObjectHandler的实现类里面的方法,这个类叫元数据对象处理器。这个类中自动会有前端提交过来的元数据metaObject,其中:id、username…等等都有。
  • 加注解@TableField,里面参数INSERT就代表在插入的时候,自动填充这个字段的值,具体填充什么值,就是上面那两个方法中处理的了。
3.1.2.1 ThreadLocal
  • 同一线程
    在这里插入图片描述
  • 什么是ThreadLocal、作用是什么?
    在这里插入图片描述

3.1.3 后端代码分析

  • 1、基于ThreadLocal封装工具类,用户保存和获取当前登录用户id(session中的)
public class BaseContext {
    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    public static void setThreadLocal(Long id){
        threadLocal.set(id);
    }

    public static Long getThreadLocal(){
        return threadLocal.get();
    }
}
  • 2、进入拦截器后,给threadLocal绑定session,让后面需要的公共字段自动填充的时候,填充这个updateUser
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        log.info("当前路径:{}", uri);

        /**
         * HandlerMethod=>Controller中标注@RequestMapping的方法
         *  需要配置静态资源不拦截时,添加这块逻辑  => 前后端分离项目
         */
        // 是我们的conrtoller中的方法就拦截,如果不是的话,放行,给加载静态资源
        if (!(handler instanceof HandlerMethod)) {
            log.info("是静态资源或非controller中的方法,放行");
            return true;
        }
        //通过session判断是否登入
        if (request.getSession().getAttribute(Contants.SESSION_USERID) == null) {
            //这里应该跳转到登入页面,,如何做?
            //5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
            log.info("用户未登入,通过输出流方式向客户端页面响应数据,打回登入页面");
            response.getWriter().write(JSON.toJSONString(RetObj.error("NOTLOGIN")));//与前端request.js中的代码呼应
            return false;
        } else {
            log.info("用户已经登入,id={}", request.getSession().getAttribute(Contants.SESSION_USERID));

            //进入拦截器后,给threadLocal绑定session,让后面需要的公共字段自动填充的时候,填充这个updateUser
            //每次http请求,会分配一个新的线程来处理
            BaseContext.setThreadLocal((Long) request.getSession().getAttribute(Contants.SESSION_USERID));
            log.info("拦截器这里设置了ThreadLocal,值为:{}",(Long) request.getSession().getAttribute(Contants.SESSION_USERID));
            log.info("当前线程id={}",Thread.currentThread().getId());
            return true;
        }
    }

  • 3、为实体类需要的属性添加@Table注解,指定自动填充的策略
/**
     * 创建时间,注意类型LocalDateTime
     */
    @TableField(fill = FieldFill.INSERT) //执行insert操作的时候,自动填充该字段
    private LocalDateTime createTime;

    /**
     * 更新时间,注意类型LocalDateTime
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
     * 创建人
     */
    @TableField(fill= FieldFill.INSERT) //执行insert操作的时候,自动填充该字段
    private Long createUser;

    /**
     * 修改人
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)//执行insert和update操作的时候,自动填充该字段
    private Long updateUser;
  • 4、自定义元数据对象处理器
import java.time.LocalDateTime;
/**
 *  自定义元数据对象处理器
 */
@Slf4j
@Component ///少了这个导致出错,去复习该注解作用,为什么要交给spring去管理
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());
        metaObject.setValue("createUser",BaseContext.threadLocal.get());
        metaObject.setValue("updateUser",BaseContext.threadLocal.get());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段【update】自动填充");
        log.info(metaObject.toString());
        log.info("当前线程id为:{}",BaseContext.threadLocal.get());
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("updateUser",BaseContext.threadLocal.get());
    }
}

  • 经过上面四部的配置,就能够实现对应的功能了,主要就是注意@TableField和元数据对象处理器MetaObjectHandler实现类的配合使用。

3.2 新增分类

3.2.1 整体分析

  • 整体流程:在点击添加菜品按钮后, 发起了一个http请求,发起这个请求就会分配一个新的线程来处理,这个请求就会访问对应的url地址:http://localhost:8080/backend/page/category/add.do之后通过 LoginInterceptor 拦截器,如果session不为空就放行,并且设置ThreadLocal的值为session存的值,之后就到了自定义元数据对象处理器:MyMetaObjectHandler,设置了对应的时间等公共字段metaObject.setValue("createUser",BaseContext.threadLocal.get());,之后完成了插入操作。
  • 类似的,如果出现问题了,全局异常处理会出手:GlobalExceptionHandler类对controller类进行了异常监听,如果是重复的错误能直接提示出来,其他错误(例如非空要求的字段插入了null)直接提示:未知错误!
023-03-29 21:35:59.481 ERROR 14664 --- [nio-8080-exec-1] c.e.u.r.c.e.GlobalExceptionHandler       
: Duplicate entry '二逼菜' for key 'category.idx_category_name'  

在这里插入图片描述

  • 前后端对来个进行了复用,通过表中的一个字段type,来区分是菜品分类还是套餐分类。
    在这里插入图片描述

在这里插入图片描述

  • 新使用了一张category表,字段如下(注意公共字段的处理策略和前面一样),注意mybatis-plusX自动生成所有东西之后,@Mapper注解等等要去检查!
    在这里插入图片描述
  • 注意总结,既然前端也是以json的格式发送数据给后端,那后端参数接收那里就需要加@RequestBody注解
    在这里插入图片描述
  • 类似的,菜品 这个分类名称 字段也加了唯一性约束,如果重复添加,会自动跳转到全局异常处理器,然后会有弹窗提示,这个复习之前,有详细分析。

3.2.2 前端代码分析

  • 点击两个按钮,点击跳转到对应的新增页面,注意传递的参数meal还是class,后面vue会进行处理
 <el-button type="primary" class="continue" @click="addClass('class')">+ 新增菜品分类</el-button>
 <el-button type="primary" @click="addClass('meal')">+ 新增套餐分类</el-button>
  • addClass()方法,如果是class,对应打开新增菜品分类这个小页面
// 添加
 addClass(st) {
   if (st == 'class') {
     this.classData.title = '新增菜品分类'
     this.type = '1'
   } else {
     this.classData.title = '新增套餐分类'
     this.type = '2'
   }
   this.action = 'add'
   this.classData.name = ''
   this.classData.sort = ''
   this.classData.dialogVisible = true
 },

// 关闭弹窗方法, dialogVisible是自定义属性classData中的一个变量
// el-dialog(对话框),visible.sync控制弹框的显示,就设置这个dialogVisible为true或者false
handleClose(st) {
  this.classData.dialogVisible = false
},
  • 将数据通过ajax送到后端,判断结果,得到数据再进行展示
//数据提交
submitForm(st) {
    const classData = this.classData
    const valid = (classData.name === 0 ||classData.name)  && (classData.sort === 0 || classData.sort)
    if (this.action === 'add') {
      if (valid) {
        const reg = /^\d+$/
        if (reg.test(classData.sort)) { //正则表达式匹配一下输入是不是数字
          //这里就对应转到ajax处理
          addCategory({'name': classData.name,'type':this.type, sort: classData.sort}).then(res => {
            console.log(res)
            if (res.code === 1) {
              this.$message.success('分类添加成功!')
              if (!st) {
                this.classData.dialogVisible = false
              } else {
                this.classData.name = ''
                this.classData.sort = ''
              }
              this.handleQuery() //回调之前的init函数进行数据显示
            } else {
              this.$message.error(res.msg || '操作失败')
            }
          }).catch(err => {
            this.$message.error('请求出错了:' + err)
          })
        } else { //正则表达式判断输入的如果不是数字
          this.$message.error('排序只能输入数字类型')
        }
        
    } else {
        this.$message.error('请输入分类名称或排序')
    }
  • addCategory()对应的ajax请求
// 新增接口
const addCategory = (params) => {
  return $axios({
    url: '/backend/page/category/add.do',
    method: 'post',
    data: { ...params }
  })
}

3.2.3 后端代码分析

  • 后端代码很简单,和之前的部分也很类似,像公共字段自动填充,使用之前定义的元数据对象处理器,仅仅需要在Category实体类中加上@TableFile(xxxx)对应的属性就可以自动生成
    @PostMapping("/backend/page/category/add.do")
    public RetObj<String> addController(@RequestBody Category category){
        log.info("category = {}",category);
        boolean save = categoryService.save(category);
        if (save){
            return RetObj.success("成功新增分类");
        }else {
            return RetObj.error("新增分类失败!");
        }
    }

3.3 分类信息分页查询

3.3.1 整体分析

  • 该部分和前面很像。包括后面每个模块都有分页查询,主要区别就是表的区别。

在这里插入图片描述

  • 在分页查询的controller中,首先分页构造器是肯定要的,之后也需要一个条件构造器。思考需要条件构造器的原因,之前有附加条件,比如通过名字模糊查询。这里别忘了有排序的条件,之前添加的时候都设置了排序sort字段,这里就要使用起来。

3.3.2 前端分析

  • getCategoryPage()这个方法就可以看出来,一共就传两个参数过去,page和pageSize,到时候后端对应进行接收处理。
  • 分页查询,注意思考前端需要哪些数据,比如Page对象中的recode等数据,那后面写后端代码的时候返回值RetObj<Page> 就要注意传递的Page类型数据了。具体的说:Page pageInfo = new Page(page,pageSize); 后端在执行完条件查询:empService.page(pageInfo,lambdaQueryWrapper) 之后,直接将查询到的结果封装到pageInfo对象中了。后端解析这个对象转成的json,就能够拿到里面的recods等属性值!recodes就是查询到的每个分类的信息,前端代码赋值给了tableData。counts是下面“共xxx条数据显示的”
   methods: {
     async init () {
       //分页查询,告诉后端需要的page、pageSize
       await getCategoryPage({'page': this.page, 'pageSize': this.pageSize}).then(res => {
         if (String(res.code) === '1') {
           //注意看,既然前端需要拿这种数据,那后端响应回来的对象,也要符合这种要求!
           //mybatisPlus中的Page类中就有就有record属性,所以返回的时候:RetObj<Page>
           this.tableData = res.data.records
           this.counts = Number(res.data.total)
         } else {
           this.$message.error(res.msg || '操作失败')
         }
       }).catch(err => {
         this.$message.error('请求出错了:' + err)
       })
     },
// 查询列表接口
const getCategoryPage = (params) => {
  return $axios({
    url: 'backend/page/category/page.do',
    method: 'get',
    params
  })
}
  • 至于如何展示页面的,主要使用到了vue中的element-ui组件,定了包括每页默认条数等信息。还有一些细节比如后端返回的json数据本来对于如果状态只有1和0,但是在页面上显示了正常、禁用,这就是因为前端进行了处理。<span>{{ scope.row.type == '1' ? '菜品分类': '套餐分类' }}</span>
  • tableData: [],属性重点关注
new Vue({
   el: '#category-app',
   data() {
     return {
       //下面应该是默认值
       action: '',
       counts: 0,
       page: 1,
       pageSize: 10,
       //页面展示数据tableData,其他地方取这个数据进行了展示
       tableData: [],
       type :'',
       classData: {
         'title': '添加菜品分类',
         'dialogVisible': false,
         'categoryId': '',
         'name': '',
         sort: ''
       }
     }
   },
   computed: {},
   // created -----------vue实例创建之后(钩子函数)
   //mounted -----------DOM挂载之后
   created() {
     this.init() //init方法下面写了
   },
   mounted() {
   },
  • 具体展示在整个表格中了,模板插槽(template slot-scope)是Vue.js中一种用于在组件中动态展示不同内容的方式。它允许父组件将内容传递给子组件,并在子组件中渲染这些内容。 "slot-scope"属性用于给插槽提供一些上下文数据。 拿到每一行的数据(row),就可以调出对应类型这个字段
<!--表格开始,就是一开始进入页面自动查询那个地方的表格-->
<el-table :data="tableData" stripe class="tableBox">
  <!--下面这个是表格中的字段-->
  <el-table-column prop="name" label="分类名称"/></el-table-column>

  <el-table-column prop="type" label="分类类型">
    <!--
    模板插槽(template slot-scope)是Vue.js中一种用于在组件中动态展示不同内容的方式。
    它允许父组件将内容传递给子组件,并在子组件中渲染这些内容。
    "slot-scope"属性用于给插槽提供一些上下文数据。

    拿到每一行的数据(row),就可以调出对应类型这个字段
    -->
    <template slot-scope="scope">
      <span>{{ scope.row.type == '1' ? '菜品分类': '套餐分类' }}</span>
    </template>
  </el-table-column>

  <el-table-column prop="updateTime" label="操作时间">
  <template slot-scope="scope">
   {{scope.row.updateTime}}
  </template>
  </el-table-column>

  <el-table-column
    prop="sort"
    label="排序"
  /></el-table-column>

  <el-table-column label="操作" width="160" align="center">
    <template slot-scope="scope">
      <!--操作那一栏的按钮,对应的函数editHandle等等-->
      <el-button
        type="text"
        size="small"
        class="blueBug"
        @click="editHandle(scope.row)"
      >
        修改
      </el-button>
      <el-button
        type="text"
        size="small"
        class="delBut non"
        @click="deleteHandle(scope.row.id)"
      >
        删除
      </el-button>
    </template>
  </el-table-column>
</el-table>

  • 分页导航栏
     <!--共xx条,每页xx条,前往第几页的那一块-->
      <el-pagination
        class="pageList"
        :page-sizes="[10, 20, 30, 40]"
        :page-size="pageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="counts"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      ></el-pagination>

3.3.3 后端代码分析

  • 和之前一样,MP的分页插件(之前配置好了,这里直接用就好)
/**
 * MP分页插件的配置
 */
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}
  • Page类的其中一个有参构造,参数当前页,和当前页的大小
 public Page(long current, long size) {
     this(current, size, 0L);
 }
  • controller:放心写,遇到异常直接被全局异常捕获,他监听着这些controller
@GetMapping("backend/page/category/page.do")
public RetObj<Page> pageController(int page, int pageSize){
    log.info("前端数据:page={},pageSize={}",page,pageSize);
    log.info("当前线程id={}",Thread.currentThread().getId());

    Page<Category> pageInfo = new Page<>(page,pageSize);
    LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.orderByAsc(Category::getSort);

    //Page<Category> page1 = categoryService.page(pageInfo, lambdaQueryWrapper);
    categoryService.page(pageInfo, lambdaQueryWrapper);//直接将查询到的结果封装到pageInfo对象中了
    return RetObj.success(pageInfo);
}

3.4 删除分类

3.4.1 整体思路

在这里插入图片描述

  • 删除分类不考虑其他就很简单,只要根据F12看url、method、参数类型是json还是其他类型就可以写controller.
  • 完善地考虑:某个分类下关联了某个菜品或者套餐,这是不允许删除的!(后续添加具体菜品的时候,会挂到这里的分类中去) 例如:精品热菜这个分类中有水煮肉片这个菜品,那就不可以直接将精品热菜删除,给页面一个提示:当前分类已经关联了菜品,无法删除!
    在这里插入图片描述
  • 具体思路就是利用:菜品管理、套餐管理对应表对应实体类中有categoryId属性。这个思路应该得想到了,就是这样处理的。在CategoryService中自己定义一个方法(之前都是使用mybatis_plus提供的,这里就是要练习的点了!)。
  • 查看当前分类下有多少菜品,想法就是CategoryServiceImpl中 引入dishService、setmealService,使用dishService去查询该categoryId下还有多少个菜品,进而判断要不要删除。就是说在CategoryService中自己构造一个想要的删除方法,这个方法需要引入其他Service
    在这里插入图片描述
    在这里插入图片描述
    上述代码其实可以做sql优化
    在这里插入图片描述

3.4.2 sql优化

  • 根据某一条件从数据库表中查询 『有』与『没有』,只有两种状态,那为什么在写SQL的时候,还要SELECT count(*)?
    业务代码中,需要根据一个或多个条件,查询是否存在记录,不关心有多少条记录。普遍的SQL及代码写法如下
SELECT count(*) FROM table WHERE a = 1 AND b = 2
  • 对应java代码
int nums = xxDao.countXxxxByXxx(params);
if ( nums > 0 ) {
  //当存在时,执行这里的代码
} else {
  //当不存在时,执行这里的代码
}
  • 分析一下count慢的原因:
    innodb为聚簇索引同时支持事物,其在count指令实现上采用实时统计方式。在无可用的二级索引情况下,执行count会使MySQL扫描全表数据,当数据中存在大字段或字段较多时候,其效率非常低下(每个页只能包含较少的数据条数,需要访问的物理页较多)。

  • 优化思路:SQL不再使用count,而是改用LIMIT 1,让数据库查询时遇到一条就返回,不用再继续查找还有多少条了。

  • sql和java代码

SELECT 1 FROM table WHERE a = 1 AND b = 2 LIMIT 1
*************************************************
Integer exist = xxDao.existXxxxByXxx(params);
if ( exist != NULL ) {
  //当存在时,执行这里的代码
} else {
  //当不存在时,执行这里的代码

3.4.3 前端分析

//删除
deleteHandle(id) {
  this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
    'confirmButtonText': '确定',
    'cancelButtonText': '取消',
    'type': 'warning'
  }).then(() => {
    deleCategory(id).then(res => {
      if (res.code === 1) {
        this.$message.success('删除成功!')
        this.handleQuery()
      } else {
        this.$message.error(res.msg || '操作失败')
      }
    }).catch(err => {
      this.$message.error('请求出错了:' + err)
    })
  })
},
  • 对应方法
// 删除当前列的接口
const deleCategory = (ids) => {
  return $axios({
    url: 'backend/page/category/delete.do',
    method: 'delete',
    params: { ids }
  })
}
  • 后端发现当前分类绑定了东西后,如何将提示信息返回给前端,让前端提示对应的信息?
  1. 不管是否出现了异常进行信息捕获,后端都会传给前端这样一个对象:(分析下面代码)
    @DeleteMapping("backend/page/category/delete.do")
    public RetObj<String> deleteController(Long ids){
        categoryService.removeIfNotBind(ids);
        //如果出异常了下面的代码执行不到,在异常处理中:return RetObj.error("分类包含套餐/菜品,无法删除!");
        return RetObj.success("分类信息删除成功");
    }
  1. 现在关键就是在前端如何处理这个对象,让对象RetObj.success(Data);读取该对象底下的data属性,如果是error,那就是读返回对象的msg属性!!
    在这里插入图片描述在这里插入图片描述
  2. 打了log看浏览器的响应
    console.log(res)
    console.log(res.msg)
          //删除
          deleteHandle(id) {
            this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
              'confirmButtonText': '确定',
              'cancelButtonText': '取消',
              'type': 'warning'
            }).then(() => {
              deleCategory(id).then(res => {
                if (res.code === 1) {
                  this.$message.success('删除成功!')
                  this.handleQuery()
                } else {
                  console.log(res)
                  console.log(res.msg)
                  this.$message.error(res.msg || '操作失败')
                }
              }).catch(err => {
                this.$message.error('请求出错了:' + err)
              })
            })
          },

在这里插入图片描述

分析上面信息,res就是后端返回给浏览器的RetObj对象(传给前端以json的形式),而res.msg就是RetObj对象的msg属性值!!使用this.$message.error(res.msg || '操作失败')来显示吗,如果有传递信息过去,就显示该消息,没有那就显示 ‘操作失败’。
为什么res是json的形式?----因为GlobalExceptionHandler类中加了注解

@ResponseBody //因为一会儿的写的方法,需要返回一个json数据,所以需要写这个注解(否则返回的就是那个对象!!),RestController直接复合了这个注解

所以在RetObj.error(customException.getMessage()); 的时候,返回的是json! 完美

3.4.4 后端代码编写

3.4.4.1异常的处理分析
  1. 首先记住如何自定义异常(构造方法论)
/**
 *  Custom adj.定做(制)的;  Custom Exception 自定义异常
 */
public class CustomException extends RuntimeException{
    public CustomException(String msg){
        super(msg);
    }
}
  1. 接着写代码手动抛出异常 throw new CustomException("该分类已经绑定了套餐,无法删除!");
  2. 全局异常处理中对异常进行捕获,返回对应的信息,注意记住注解@ExceptionHandler以及形参的形式。
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody //因为一会儿的写的方法,需要返回一个json数据,所以需要写这个注解(否则返回的就是那个对象!!),RestController直接复合了这个注解
@Slf4j
/**
 * 全局异常处理器,结合前端了解一下是如何将错误信息传送给前端的(对象返回之后,如何取出信息)
 */
public class GlobalExceptionHandler {

    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public RetObj<String> exceptionHandler(SQLIntegrityConstraintViolationException exception){
        log.error(exception.getMessage());
        if (exception.getMessage().contains("Duplicate entry")){
            String[] split = exception.getMessage().split(" ");
            return RetObj.error("用户名:" + split[2] + "已存在!");
        }else {
            return RetObj.error("未知错误!");
        }
    }
    /**
     * 构成方法重载
     * @param customException 注意要传过来的参数
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public RetObj<String> exceptionHandler(CustomException customException){
        return RetObj.error(customException.getMessage());
    }

4.customException.getMessage()是如何调用的?
第一:CustomException extends RuntimeException
第二:public class RuntimeException extends Exception {…}
第三:public class Exception extends Throwable {…}
第四:Throwable中:

    public String getMessage() {
        return detailMessage;
    }
**************************************************************
注:  
  /**
     * Specific details about the Throwable.  For example, for
     * {@code FileNotFoundException}, this contains the name of
     * the file that could not be found.
     *
     * @serial
     */
    private String detailMessage;
3.4.4.2 自己完善Service满足需求
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category>
        implements CategoryService {

    @Resource
    DishService dishService;
    @Resource
    SetmealService setmealService;

    /**
     * 分类如果绑定了菜品,不能直接删除套餐,应该给出对应的提示
     * (具体做法就是抛出自定义异常,全局捕获,捕获后将返回对象RetObj中包含了对应的提示信息)
     * private Long categoryId; 这个字段的使用很重要!
     * 想法就是:select count(*) dish where categoryId = xxx 如果有数据,说明该categoryId下关联了count条数据
     *
     * @param id 这个id对应分类下有没有关联菜品
     * @return
     */
    @Override
    public boolean removeIfNotBind(Long id) {
        // 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
        //dishService.getOne()
        LambdaQueryWrapper<Dish> lambdaQueryWrapperDish = new LambdaQueryWrapper<>();
        LambdaQueryWrapper<Setmeal> lambdaQueryWrapperSetmeal = new LambdaQueryWrapper<>();

        lambdaQueryWrapperDish.eq(Dish::getCategoryId, id);
        long countDish = dishService.count(lambdaQueryWrapperDish);

        lambdaQueryWrapperSetmeal.eq(Setmeal::getCategoryId, id);
        long countMeal = setmealService.count(lambdaQueryWrapperSetmeal);

        if (countDish != 0){
            //已经关联了套餐或者分类,抛出异常
            //throw new RuntimeException("分类绑定了菜品或套餐,无法删除!");
            //自己定义异常类
            throw new CustomException("该分类已经绑定了菜品,无法删除!");

        }else if(countMeal != 0){
            throw new CustomException("该分类已经绑定了套餐,无法删除!");
        }else{
            this.removeById(id);
            return true;
        }
    }
}
3.4.4.2 简单地完成controller
@DeleteMapping("backend/page/category/delete.do")
public RetObj<String> deleteController(Long ids){
    /*
        前端:
        Request URL: http://localhost:8080/backend/page/category/delete.do?ids=1641066227887951873
        后端上面的形参要和ids这个名字保持一致。则无法删除,对应的id是null
     */
    log.info("要删除的id:{}",ids);
    //categoryService.removeById(ids);
    categoryService.removeIfNotBind(ids);
    //如果出异常了下面的代码执行不到,在异常处理中:return RetObj.error("分类包含套餐/菜品,无法删除!");
    return RetObj.success("分类信息删除成功");
}

3.5 修改分类

3.5.1 整体思路

  • 这部分功能实现比较简单,主要要注意的有:回显(复习之前修改员工信息也是类似的)、然后删除。

3.5.2 前端代码分析

  • 在没有查询数据库,进行后端处理的时候就发现其实已经有回显的值了,但是这个只是前端的一个效果。
<el-button
  type="text"
  size="small"
  class="blueBug"
  @click="editHandle(scope.row)"    并且把当前这条数据传过去
>
  修改
</el-button>
  • 点击按钮对应@click="editHandle(scope.row)"函数
//editHandle(dat)方法,有参,以为要edit,需要name、sort、id信息
editHandle(dat) {
  this.classData.title = '修改分类' //展示标题
  this.action = 'edit' //使用的是同一个窗口,动态的修改标题,这里标识一下
  this.classData.name = dat.name //classDate,模型数据
  this.classData.sort = dat.sort //为一些模型数据赋值,赋值完毕就会回显了(输入框和模型数据进行了绑定)
  this.classData.id = dat.id
  this.classData.dialogVisible = true
},
// 关闭弹窗方法, dialogVisible是自定义属性classData中的一个变量
// el-dialog(对话框),visible.sync控制弹框的显示,就设置这个dialogVisible为true或者false
handleClose(st) {
  this.classData.dialogVisible = false
},

4 菜品管理(见第三部分 part3)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值