springboot project: Detailed analysis of the front and back ends of St. Regis takeaway part2

The first part part1 link
Part2 Part 2 (this page)
the third part part3 link

3 Category Management Business Development

insert image description here
Contents:
3.1 Common fields auto-fill
3.2 New categories(The whole process includes ThreadLocal, interceptor, meta-object processor, etc. worth learning)
3.3 Pagination query of category information
3.4 Delete category(It is necessary to consider whether there are dishes under the deleted category, and it is worth learning to write the service yourself instead of using mybatis_plus completely, and there is also sql optimization to focus on)
3.5 Modify classification

3.1 Common field autofill

3.1.1 Problem Analysis

insert image description here

  • It's like cutting out some public operations and encapsulating them into methods. These methods are automatically executed every time an insert or update operation is performed, and the specifiedpublic field propertiesassignment
  • There is another problem: when setting createUser and UpdateUser in the method of the MetaObjectHandler class, you need to get the id from the session, but this class cannot get the request, how to do this function? -----Using TreadLocal

3.1.2 The public fields provided by mybatis_plus are automatically filled

insert image description here

  • Simply put, when executing insert() or update(), the method in the implementation class of MetaObjectHandler is automatically executed. This class is called metadata object handler. This class will automatically have the metadata metaObject submitted by the front end, including: id, username... and so on.
  • Annotate @TableField, the parameter INSERT in it means that the value of this field is automatically filled when inserting, and the specific value to be filled is processed in the above two methods.

3.1.2.1 ThreadLocal

  • same thread
    insert image description here
  • What is ThreadLocal and what is its function?
    insert image description here

3.1.3 Backend code analysis

  • 1. Based on the ThreadLocal encapsulation tool class, the user saves and obtains the current login user id (in the 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. After entering the interceptor, bind the session to threadLocal, and when the public fields needed later are automatically filled, fill in the 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. Add the @Table annotation to the attributes required by the entity class, and specify the automatic filling strategy
/**
     * 创建时间,注意类型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. Custom Metadata Object Processor
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());
    }
}

  • After the configuration of the above four parts, the corresponding functions can be realized. The main thing is to pay attention to the use of @TableField and the MetaObjectHandler implementation class of the metadata object processor.

3.2 New categories

3.2.1 Overall Analysis

  • The overall process: in the clickAdd dishesAfter the button is pressed, an http request is initiated, and a new thread will be assigned to process the request, and the request will access the corresponding url address: then through the interceptorhttp://localhost:8080/backend/page/category/add.do , if the session is not empty, it will be released, and the value of ThreadLocal will be set LoginInterceptorThe value saved for the session, and then to the custom metadata object processor: MyMetaObjectHandler, set the corresponding time and other public fields metaObject.setValue("createUser",BaseContext.threadLocal.get());, and then complete the insert operation.
  • Similarly, if there is a problem, the global exception handling will take action: the GlobalExceptionHandler class monitors the exception of the controller class. If it is a repeated error, it can be directly prompted, and other errors (such as inserting null into a non-empty required field) are directly prompted: unknown mistake!
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'  

insert image description here

  • The front-end and back-end multiplex each other, and use a field type in the table to distinguish whether it is a dish classification or a set meal classification.
    insert image description here

insert image description here

  • A new category table is used, the fields are as follows (note that the processing strategy of public fields is the same as before), and after mybatis-plusX automatically generates everything, @Mapper annotations and so on need to be checked!
    insert image description here
  • Pay attention to the summary, since the front-end also sends data to the back-end in json format, the @RequestBody annotation needs to be added where the back-end parameters are received
    insert image description here
  • Similarly, the category name field of dishes is also uniquely constrained. If it is added repeatedly, it will automatically jump to the global exception handler, and then there will be a pop-up prompt. Before this review, there is a detailed analysis.

3.2.2 Front-end code analysis

  • Click the two buttons, click to jump to the corresponding new page, pay attention to the passed parameter meal or class, and vue will process it later
 <el-button type="primary" class="continue" @click="addClass('class')">+ 新增菜品分类</el-button>
 <el-button type="primary" @click="addClass('meal')">+ 新增套餐分类</el-button>
  • The addClass() method, if it is a class, corresponds to opening the small page of adding new dish categories
// 添加
 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
},
  • Send the data to the backend through ajax, judge the result, get the data and display it
//数据提交
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('请输入分类名称或排序')
    }
  • Ajax request corresponding to addCategory()
// 新增接口
const addCategory = (params) => {
    
    
  return $axios({
    
    
    url: '/backend/page/category/add.do',
    method: 'post',
    data: {
    
     ...params }
  })
}

3.2.3 Backend code analysis

  • The back-end code is very simple, similar to the previous part, such as automatic filling of public fields, using the previously defined metadata object processor, only need to add the attribute corresponding to @TableFile (xxxx) in the Category entity class to automatically generate
    @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 Pagination query of classification information

3.3.1 Overall Analysis

  • This part is very similar to the previous one. Including each of the following modules has paging queries, the main difference is the difference between tables.

insert image description here

  • In the controller of the paging query, the paging constructor is definitely required first, and then a conditional constructor is also required. Think about the reason why the conditional constructor is needed. There are additional conditions before, such as fuzzy query by name. Don't forget that there are sorting conditions here. The sort field was set when adding it before, and it will be used here.

3.3.2 Front-end analysis

  • It can be seen from the getCategoryPage() method that a total of two parameters are passed, page and pageSize, and then the backend will receive and process correspondingly.
  • For paging query, pay attention to what data the front-end needs, such as recode and other data in the Page object, then when writing the back-end code later, you RetObj<Page>must pay attention to the Page type data passed. Specifically: after Page pageInfo = new Page(page,pageSize);the backend executes the conditional query:empService.page(pageInfo,lambdaQueryWrapper)Directly encapsulate the query results into the pageInfo object. The backend parses the json converted from this object, and you can get the recods and other attribute values ​​inside! recodes is the information of each category queried, and the front-end code is assigned to tableData. counts is the following "a total of xxx data display"
   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
  })
}
  • As for how to display the page, the element-ui component in vue is mainly used, and the information including the default number of items per page is determined. There are also some details. For example, the json data returned by the backend originally only has 1 and 0 for the status, but it shows normal and disabled on the page, which is because the frontend has processed it.<span>{ { scope.row.type == '1' ? '菜品分类': '套餐分类' }}</span>
  • tableData: [], attributes focus on
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() {
    
    
   },
  • Specifically shown in the entire table, template slot (template slot-scope) is a way in Vue.js to dynamically display different content in components. It allows parent components to pass content to child components and render those content in child components. The "slot-scope" attribute is used to provide some contextual data for the slot. After getting the data (row) of each row, you can call out the field of the corresponding type
<!--表格开始,就是一开始进入页面自动查询那个地方的表格-->
<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>

  • paging navigation bar
     <!--共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 Back-end code analysis

  • Same as before, MP's paging plugin (configured before, just use it here)
/**
 * MP分页插件的配置
 */
@Configuration
public class MybatisPlusConfig {
    
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
    
    
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}
  • One of the Page class has a parameter structure, the parameter current page, and the size of the current page
 public Page(long current, long size) {
    
    
     this(current, size, 0L);
 }
  • controller: feel free to write, when an exception is encountered, it is directly caught by the global exception, and he monitors these controllers
@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 Delete category

3.4.1 Overall Idea

insert image description here

  • It is very simple to delete the category without considering other things. Just check the url, method, and parameter type according to F12, whether it is json or other types, and you can write the controller.
  • Perfect consideration: A certain dish or set meal is associated with a certain category, which is not allowed to be deleted!(When adding specific dishes later, it will be linked to the category here)For example, if there is boiled pork slices in the category of fine hot dishes, then you cannot directly delete the fine hot dishes, and a prompt will be given to the page: the current category has been associated with dishes and cannot be deleted!
    insert image description here
  • The specific idea is to use: the categoryId attribute in the corresponding entity class of the dish management and package management table. This idea should be thought of, and it is handled in this way. Define a method yourself in CategoryService (It was provided by mybatis_plus before, here is the point to practice!)。
  • Check how many dishes are under the current category. The idea is to introduce dishService and setmealService in CategoryServiceImpl, use dishService to query how many dishes are under the categoryId, and then determine whether to delete them.That is to say, construct a desired deletion method in CategoryService, which needs to introduce other services
    insert image description here
    insert image description here
    The above code can actually dosql optimization
    insert image description here

3.4.2 SQL optimization

  • According to a certain condition, query "yes" and "no" from the database table, there are only two states, so why do you need to SELECT count(*) when writing SQL?
    In the business code, it is necessary to query whether there are records based on one or more conditions, regardless of how many records there are. Common SQL and code writing are as follows
SELECT count(*) FROM table WHERE a = 1 AND b = 2
  • Corresponding java code
int nums = xxDao.countXxxxByXxx(params);
if ( nums > 0 ) {
    
    
  //当存在时,执行这里的代码
} else {
    
    
  //当不存在时,执行这里的代码
}
  • Analyze the reason why the count is slow:
    InnoDB is a clustered index that supports things at the same time, and it uses real-time statistics in the implementation of the count command. In the case of no secondary index available, executing count will cause MySQL to scan the entire table data. When there are large fields or many fields in the data, its efficiency is very low (each page can only contain a small number of data items, More physical pages need to be accessed).

  • Optimization idea: SQL no longer uses count, but uses LIMIT 1 instead, so that the database will return when it encounters one when querying, and there is no need to continue to search for how many more.

  • sql and java code

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

3.4.3 Front-end analysis

//删除
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)
    })
  })
},
  • corresponding method
// 删除当前列的接口
const deleCategory = (ids) => {
    
    
  return $axios({
    
    
    url: 'backend/page/category/delete.do',
    method: 'delete',
    params: {
    
     ids }
  })
}
  • After the backend finds that the current category is bound to something, how to return the prompt information to the frontend, so that the frontend can prompt the corresponding information?
  1. Regardless of whether there is an exception for information capture, the backend will pass it to the frontend such an object: (analyze the following code)
    @DeleteMapping("backend/page/category/delete.do")
    public RetObj<String> deleteController(Long ids){
    
    
        categoryService.removeIfNotBind(ids);
        //如果出异常了下面的代码执行不到,在异常处理中:return RetObj.error("分类包含套餐/菜品,无法删除!");
        return RetObj.success("分类信息删除成功");
    }
  1. Now the key is how to deal with this object at the front end, let the object RetObj.success(Data);read the data attribute under the object, if it is an error, then read the msg attribute of the returned object! !
    insert image description hereinsert image description here
  2. Hit the log to see the browser's response
    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)
              })
            })
          },

insert image description here

Analyzing the above information, res is the RetObj object returned to the browser by the backend (passed to the frontend in the form of json), and res.msg is the msg attribute value of the RetObj object! ! Use this.$message.error(res.msg || '操作失败')to display, if there is a message passed, it will display the message, if not, it will display 'operation failed'.
Why is res in the form of json? ----Because the annotation is added to the GlobalExceptionHandler class

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

So at RetObj.error(customException.getMessage());the time, what is returned is json! Perfect

3.4.4 Backend code writing

3.4.4.1 Abnormal processing analysis

  1. First remember how to customize exceptions (construction methodology)
/**
 *  Custom adj.定做(制)的;  Custom Exception 自定义异常
 */
public class CustomException extends RuntimeException{
    
    
    public CustomException(String msg){
    
    
        super(msg);
    }
}
  1. Then write code to manually throw an exceptionthrow new CustomException("该分类已经绑定了套餐,无法删除!");
  2. In the global exception handling, the exception is caught and the corresponding information is returned. Pay attention to remember the form of the annotation @ExceptionHandler and the formal parameters.
@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()How is it called?
First: CustomException extends RuntimeException
Second: public class RuntimeException extends Exception {…}
Third: public class Exception extends Throwable {…}
Fourth: 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 Improve the Service to meet the needs

@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 Simply complete the 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 Modify classification

3.5.1 Overall Idea

  • The implementation of this part of the function is relatively simple. The main points to note are: echo (it is similar to modifying employee information before review), and then delete.

3.5.2 Front-end code analysis

  • When the database is not queried and the back-end processing is performed, it is found that there is actually an echoed value, but this is only an effect of the front-end.
<el-button
  type="text"
  size="small"
  class="blueBug"
  @click="editHandle(scope.row)"    并且把当前这条数据传过去
>
  修改
</el-button>
  • Clicking the button corresponds to the @click="editHandle(scope.row)" function
//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 Dishes management (see the third part part3)

Guess you like

Origin blog.csdn.net/YiGeiGiaoGiao/article/details/129777132