0


瑞吉外卖项目1 + 源码

效果展示:

软件开发整体介绍:

一、瑞吉外卖项目介绍

1.1、项目介绍

1.2、技术点

1.3、功能架构(项目中所用到的全部功能)

1.4、角色(不同角色所对应的不同权限)

二、开发环境搭建

2.1、创建项目对应的数据库(两种方式)

2.1.1、图形界面创建库形式

2.1.2、命令行形式创建数据库

2.2、向瑞吉数据库当中导入表结构

注1:这里是把项目中所用到的所有的表都放入到db_reggie.sql文件当中了,我们只需要向reggie库中导入该.sql文件即可把该项目所用到的所有的表导入到该reggie库当中了。

注2:注意该.sql文件不要放在中文的目录下,因为放在中文目录下的话会导入表失败。

通过上面的操作点击开始后,就能把.sql文件中的所有表结构和数据创建导入成功了(刷新一下):

对上面各个表解释:

2.3、maven项目搭建

创建的是maven形式的项目格式:

** 项目创建好之后,一定要注意检查项目的编码、项目maven仓库是否配置的是自己本地的仓库(如果不是本地的仓库,就设定成本地仓库)、jdk配置等。**

上面的maven项目创建好并且检查完之后,我们就可以导入该项目所用到的所有maven坐标和yml配置属性了(上面创建的项目形式是maven形式而不是springboot形式的,但是在坐标中导入了springboot的坐标,因此还是springboot的项目)

2.4、导入maven坐标和yml属性配置

当项目中的坐标和yml属性配置好之后,就说明项目已经连接配置好数据库了,因此我们就可以就行测试项目是否能启动成功了:

** 测试结果如下所示:(项目启动成功)**

2.5、项目能够启动成功后,导入前端资源:

注意:前端资源的数据一定要放在static静态资源的文件夹当中,如果不放在static静态资源的文件夹当中的话,springmvc就会把客户访问前端的路径拦截下来,因此就访问不到(就需要前面说过的springmvc拦截器了,就需要设定放行才能访问到该前端的路径)

前端资源导入好之后,开启服务器进行访问前端backend文件夹当中的index.html页面:

访问结果如下所示:

三、后台系统登录功能开发

3.1、需求分析

首先我们如果后台想做一些登录的逻辑肯定要先有前端登录的页面,登录页面如下所示:

没有登录页面我们后台怎么拿到一些数据呢(比如一些前端登录时向后端请求的访问资源路径和一些登录时输入的用户名和密码,我们后端只有拿到了这些数据,才能向数据库进行校验判断前端用户是否能够登录成功)

访问该登录页面login.html,所展示的登录页面样式如下所示:

** 我们知道,我们后台现在还没有开始写登录的逻辑,我们后台肯定想知道前端用户点击登录后,向后台请求的资源路径是哪里呢:我们点击登录后可以按F12进行查看前端向后台请求的登录资源路径,因此我们后端也好在该路径下做一些逻辑判断是否让前端用户登录了:**

** 注意1:为什么前端在登录页面点击登录时会是以上面求访问资源路径的形式向后端发送访问请求呢:**

** 注意2:之所以这个后台登录功能没有写注册功能,是因为这个登录功能只能后台管理人员才能登录进入,不能随便让其他人就能够注册。**

注意3:身为后端开发人员,一定要看得懂前端页面的三个地方:

因为我们只有看懂了前端这三个地方的逻辑,那么我们后台就可以把逻辑处理的结果按照这三个地方的形式响应给前端,供前端使用(就相当于我们前面写的把后台处理的逻辑数据封装到Result对象的属性当中响应给前端,一些标识符、是否成功状态码什么的都是开发前,前端人员和后端人员商量好的)

3.2、代码开发

首先第一步:创建实体类Employee,和数据库表employee进行映射

也就是说,通过把前端用户输入的账号和密码封装到实体类的属性当中然后和数据库表进行校验,判断是否有该用户的存在,有的话就登录成功,没有的话就登录失败。

第二步:创建mapper、contriller、service层 (数据层:mapper、service层基于mybatis-plus)

mapper层、service层如下所示:

EmployeeMapper:

重:思考:mybatis-plus是怎么知道我们操作的是哪张数据库表的呢:

** 是因为我们使用mybatis-plus时,数据层会有一个实体类泛型,该实体类的属性对应着哪张表的字段,就说明这个数据层操作的是哪张数据库表。**

EmployeeService:

EmployeeServiceImpl:

表现层逻辑代码如下所示:

注意1:我们在上面的笔记中知道,客户端在登录页面上点击登录按钮后,向后端请求资源的访问路径是:http://localhost:8080/employee/login ,因此我们表现层的登录功能的路径就按照客户端的访问路径写。

package com.Bivin.reggie.controller;

import com.Bivin.reggie.pojo.Employee;
import com.Bivin.reggie.service.impl.EmployeeServiceImpl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
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;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;

/**
 *  表现层
 */

@Slf4j  // 开启日志 出bug好调试
@RestController // @Controller和@ResponseBody注解 (表现层bean注解和响应前端数据注解)
@RequestMapping(value = "/employee")  // 请求访问路径
//写成/employee的目的就是:刚才前端用户在登陆页面点击登录的时候(3.1需求分析),访问后台的资源路径为post请求..8080/employee/login
public class EmployController {

    /**
     *   自动装配业务层对象 (也可以说拿到业务层对象引用,然后调用业务层对象当中的增删改查等一些功能方法)
     */
    @Autowired
    private EmployeeServiceImpl employeeService;

    /**
     *  1、登录功能
     *
     *      注意1: 一定要保证前端以json数据格式请求/login路径下的资源的时候,要加上@RequestBody注解,
     *              并且一定要保证json请求数据格式体的属性名一定要和employee类当中的属性名一致,要不然实体类
     *              的属性当中就封装不到前端请求访问的资源数据。
     *              我们知道前端用户在登录页面点击登录按钮后,前端是以json格式的请求数据:(F12查看)
     *                                      password:“123456”
     *                                      username: “admin”
     *              向后端/login路径下请求资源的,因此我们一定要保证employee实体类当中的属性名也为password和username,
     *              那么前端请求的这些登录数据才能封装到后台的实体类当中,我们才能拿到前端的请求数据后做一些逻辑校验,判断
     *              用户是否可以登录成功。
     *
     *
     *      注意2: 使用 HttpServletRequest request的目的:将登录成功的员工id存入到Session域当中
     *
     */
    @PostMapping(value = "/login")
    public R login(HttpServletRequest request, @RequestBody Employee employee){ // json格式注解

        // 1、将客户端访问提交的密码password进行md5加密处理
        String password = employee.getPassword();   // 拿到了封装到Employee实体类属性中封装到客户端请求的数据(登录密码数据)
        password = DigestUtils.md5DigestAsHex(password.getBytes()); // 将客户端登录的密码进行md5加密,并且把加密后的密码赋给password变量

        // 2、根据页面提交的用户名username查询数据库中对应的该用户名的所有字段数据 (  用的方法是:springboot整合SSMP笔记当中的Lambda对象方式的条件查询  )
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(Employee::getUsername, employee.getUsername());
        // 调用业务层的条件查询功能
        Employee one = employeeService.getOne(queryWrapper); // 这里和我们springboot整合SSMP笔记中有所区别,但是意思还是一样的,因为这里调用的是业务层
                                                            // 我们业务层因为也是用mybatis-plus写的,所以要遵守别人封装好的条件查询功能方法名getOne
                                                            // 其实意思都是通过客户提交的用户名,向数据库查询出该用户名对应的整条数据

        // System.out.println(one);    // 查询获取到了前端提供的用户名所对应的数据库中的整条数据,输出的结果如下所示

       /* Employee(id=1, username=admin, name=管理员, password=e10adc3949ba59abbe56e057f20f883e, phone=13812312312,
                sex=1, idNumber=110101199001010047, status=1, createTime=2021-05-06T17:20:07, updateTime=2021-05-10T02:24:09,
                createUser=1, updateUser=1)*/

        // 3、如果条件查询没有查询到结果,说明用户名不存在
        if (one == null){
            return new R("用户名不存在,登录失败");
        }

        // 4、到这里说明用户名存在,那么就用客户端输入的密码和该用户名在数据库中对应的md5处理后的密码进行校验
        // 注意:上面调用业务层的条件查询功能,查询出来用户名对应的数据库中的整条数据后,是封装到实体类Employee实体类属性当中了,
        // 因此我们直接在实体类的密码属性当中就能获取到客户端提供的用户名在数据库中对应的密码了,然后就能和客户端输入的密码进行比较了。
        if (! password.equals(one.getPassword())){
            return new R("密码错误,登录失败");
        }

        // 5、到这里说明用户名存在并且登录的密码也正确,那么我们就判断一下员工状态,如果为已禁用状态,则响应给客户端信息(0: 禁用  1: 启用)
        if (one.getStatus() == 0){
            return new R("账号已被禁用");
        }

        // 6、如果到了这里,就说明用户登录密码也正确,账号也没有被禁用,那么就将员工的id存入到Session域当中,并响应给客户端登录成功信息
        request.getSession().setAttribute("employee",one.getId());

        return new R(one,1);
        // 这里响应给前端的信息是上面条件查询出来的用户名整条数据,和成功状态码1
        // 之所以这样写的目的是:看login.html前端页面逻辑,拿到状态码为1的时候表示登录成功,并且跳转到index.html页面上,
        // 然后又用one中所有的数据进行逻辑的处理了
    }
}

测试登录功能结果如下所示:

第一种结果当客户端输入的用户名和密码都正确并且账号没有封禁时

我们后端响应给前端输入的该用户名所对应的整条数据信息,并且响应给前端成功状态码1。

在登录页面客户端点击登录按钮后(账户密码没有问题并且没有封禁的前提下),登录成功后跳转到了index.html页面,成功进入到了系统管理内部了。(这就是上面前端的逻辑了,所以这就是说我们必须要看懂前端的几个地方逻辑的原因):

第二种:客户端输入的密码不正确的时候

第三种客户端的账号被封禁的时候

3.3、补充:完善登录功能

重点:在springboot项目当中设定Filter过滤器的话,需要在启动类当中加上 @ServletComponentScan 注解

3.3.1、问题分析

问题:

    我们前端客户直接访问index.html路径下的资源不用登录一样能直接进入到该外卖项目的内部当中,那么这肯定是不安全的啊,我们肯定想让客户先登录后才能进入到外卖的项目内部,如果连登录都没有登录就能进入到里面了,那这个项目也太垃圾了,谁都能直接访问index.html路径到项目内部然后进行一些操作了。

问题解决思路:

    因此我们后端就需要设定一个过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经登录过了,如果没有登录过就跳转到登录的页面上,让用户去登录去。(TM的登录都不登录还想进项目内部,你想啥呢)

我们这里就用Filter过滤器进行解决该问题(忘记怎么使用的话,看Filter过滤器笔记)

补充知识: 我们知道使用Filter过滤器的时候,我们拦截的路径如果是 "/*"的话,那么客户端请求访问的所有路径都会被拦截下来(也就是说客户端通过某个路径访问后端资源的时候先走Filter过滤器的代码),而且Filter过滤器也会把css、js、html这些资源都给拦截下来,所有页面上的画面什么的也都没有了,因此我们需要在Filter过滤器代码中把一些用到的css、js、html这些资源放行(也就是说不拦截) 【忘记的话看Filter笔记】

因此:当我们Filter拦截器配置的路径为 : /* 的时候,客户端访问后端时所展示的效果如下所示(也就是上面说的什么都拦截掉了,甚至连css、html、js都拦截了,所以客户端以任何访问请求路径访问后端资源时,获取的是空白而已)

因此,就需要我们放行一些css,html页面,放行一些客户端请求的资源路径和登录有关的路径,那么客户端才能访问到和登录功能有关的路径资源,那么就能够先登录了(不放行的话,客户端请求访问什么路径都是得到的一片空白了,那还怎么登录啊,因此需要把登录有关的路径放行掉,那么客户端才能够访问到登录有关的资源然后登录。):

过滤器代码如下所示:

package com.Bivin.reggie.filter;

import com.Bivin.reggie.controller.R;
import com.alibaba.fastjson.JSON;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 *  Filter过滤器
 *
 *      注1: 只要Filter的拦截路径是/* 那么客户端向后端请求访问的任何url路径都会先被拦截下来。
 *      注2: 在springboot项目中如果使用Filter 一定要记得在springboot类当中加上@ServletComponentScan注解
 */

@WebFilter("/*")
public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        // 注意:Session中的调用getSession()方法的request是HttpServletRequest包下的request所以需要把
        // Filter包下的request转换成HttpServletRequest包下的request

        /**
         *  接下来的任务就是放行一些与登录功能有关的资源(如:登录页面html、css等)
         *  如果我们不放行的话,那么客户端当访问这些资源的时候,得到的响应是一片空白,那肯定是不行的。
         */

        // 第一步: 把和登录有关的资源路径都写在urls数组当中 (注意:这里最好路径写全,别用通配符 ** )
        String[] urls ={
                "/employee/login",
                "/employee/logout",
                "/backend/page/login/login.html",
                "/front/**"
        };

        // 第二步: 判断客户端请求的访问路径是否在urls数组里

        // 获取当前客户端请求访问的资源路径
        String url = req.getRequestURL().toString();
        System.out.println(url); // 如:http://localhost:8080/backend/index.html

        for (String u: urls){   // 遍历urls数组里面的资源路径

            if (url.contains(u)){   // 如果客户端请求的访问资源路径包含了遍历urls数组里面的资源路径

                // 包含的话,放行即可(让客户端访问资源去吧,毕竟人家访问的资源路径和登录有关咱们也不拦截这个资源路径了。)
                chain.doFilter(request,response);
                return;
            }
        }

        // 注意:放行后的return结束的是for循环语句,结束后代码还会继续for同级往下执行

        /**
         * 如果上面的urls数组都遍历完了,并且判断客户端请求的访问资源路径都没在urls数组(我们设立的放行资源数组)当中,
         * 那么就需要进行一些判断了:
         *
         *      我们知道在登录功能中,登录成功后我们把员工id存放到Session域当中了,那么我们这里就可以判断Session域当中
         *      是否能取到对应的储存的员工id数据:
         *          能取到的话说明客户端确实登录成功了,那么既然客户端登录过了,那么我们就放行客户端请求的这次访问资源路径就可以了(毕竟人家都登陆成功了,任何资源都让他访问就行了)。
         *          不能取到的话,说明客户端还没有登录过,那么我们就让客户端先登录去。
         */

        // 1、判断Session域当中是否有数据 (数据:登录功能登录成功时存储在Session域当中的员工id)
        Object o = req.getSession().getAttribute("employee");   // 通过key获取Session域当中的数据

        if (o !=null){
            // 不为null,说明客户端之前登录过(要不然Session域当中不会储存数据),那么我们就放行客户端这次向后端请求的路径访问资源就行了,
            // 人家登录过了,你还拦着人家不让人家访问干啥
            chain.doFilter(request,response);
            return;
        }

        else {
            // Session域当中数据为null,说明客户端就没有登录过,所以让他回去登录去,登录都没有登录你还想访问其他的资源想毛呢。
            // 通过输出流方式向客户端页面响应数据

            response.getWriter().write(JSON.toJSONString(new R("NOTLOGIN",0)));
        }

    }
}

代码解释: 为什么响应给前端的数据是 new R("NOTLOGIN",0);

设定好Filter过滤器之后,客户端再次访问index.html页面时,结果如下所示:(会发现就自动跳转到登录页面先让登录了。这就是过滤器Filter的魅力所在)

四、后台退出功能开发

4.1、需求分析

我们知道点击此退出系统按钮后,客户端是会向后端发送一个资源访问请求路径的:

4.2、代码开发

因此我们后台只需要在表现层根据前端点击退出按钮后发送的访问请求路径下,做一些处理逻辑即可,处理的逻辑如下所示:

表现层代码如下所示:

    /**
     *  2、退出功能
     *
     *      注1:我们知道客户端点击退出按钮后向后端请求的访问路径为post形式:http://localhost:8080/employee/logout ,因此我们后台的资源路径就按照这个写即可
     *      注2:客户端点击退出按钮的时候,是没有向后端提供任何参数请求数据的,因此我们后端也没必要用参数接收客户端请求的数据,
     *      因此我们这里只需要用一个request参数即可,因为我们需要清空Session域当中保存的登录成功的用户名id
     *
     */

    @PostMapping(value = "/logout")
    public R logout(HttpServletRequest request){

        // 清理登录成功时存储在Session域当中的用户id
        request.getSession().removeAttribute("employee");   // 通过key键删除登录功能登录成功
                                       // 时存储的用户id值(value值)
        return new R("退出成功",1);
    }

}

之所以响应给前端1字符码的原因如下所示:(因此退出成功后,就会帮我们自动跳转到了登录的页面上)

并且我们也会发现,当用户登录功能登录成功时存储在Session域当中的用户名id也清理成功了:

五、员工添加功能管理系统开发

员工管理系统样式如下所示:

5.1、需求分析

点击添加员工按钮时的样式如下所示(跳转到下面样式的页面上):

5.2、数据模型

也就是说:上面5.1分析中,保存员工信息,保存到哪张数据表当中。

5.3、代码开发

注1: 我们前面已经知道,实体类Employee属性名和数据库的字段名是一致的。

注2: 一定要保证客户端请求保存的JSON数据格式属性名要和实体类的属性名一致,要不然客户端请求的资源数据封装不到实体类的属性当中。

总体代码如下所示:

初始化这些前端没有传递的字段数据的原因还有:因为数据库当中有这些字段,如果前端没有传递这些字段的数据,并且我们也没有初始化这些字段数据的话,那么就会报错(因为数据库中有这些字段啊,并且这些字段设定的不能为null了,如果不初始化一些数据的话就会出错了,所以必须为这些前端没有传递的字段数据初始化值)

 /**
     *  3、新增员工功能
     *
     *      // 我们知道客户端请求新增员工功能时,是以JSON数据格式请求的,因此我们需要加上 @RequestBody 注解
     *      // 一定要保证json请求数据格式体的属性名一定要和employee类当中的属性名一致,要不然实体类的属性当中就封装不到前端请求访问的资源数据。
     */
    @PostMapping
    public R save(HttpServletRequest request,@RequestBody Employee employee){ // 前端请求的JSON资源数据,封装到了Employee实体类的属性当中了。
        // System.out.println(employee);
       /**
        *  我们知道:前端用户添加员工信息的时候,只添加了:账号、员工姓名、手机号、性别、身份证号。
        *   而我们数据库的字段名当中还有createTime、updateTime、createUser、updateUser、password这些字段。 (status字段数据库默认为1了)
        *   因此我们总不能让这些数据库中还有的这些字段名为null吧,(因为数据库中设定好了不能为null的属性,如果为null的话就会报错了)。因此我们可以在这里为这些客户端没有添加的字段数据设定一下初始化数据。
        *
        * Employee(id=null, username=daaa, name=a, password=null, phone=13523180000, sex=1,
        *       idNumber=000000000000000000, status=null, createTime=null, updateTime=null, createUser=null, updateUser=null)
        */

        // 设置初始化密码123456(md5加密处理)
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
        // 设置创建时间
        employee.setCreateTime(LocalDateTime.now());    //  LocalDateTime.now() 系统当前时间
        // 设置修改时间
        employee.setUpdateTime(LocalDateTime.now());
        // 记录谁添加的员工信息 (谁登录的该项目系统,肯定就是谁添加的员工信息,那么只需要把登录时存放到session域当中的登录人员id取出来,设置到实体类的createTime属性中即可)
        Object o = request.getSession().getAttribute("employee");
        employee.setCreateUser((Long) o);
        employee.setUpdateUser((Long) o);

        /**
         *  然后后台employee实体类的属性当中封装到了前端请求的资源数据的时候,
         *  并且把一些字段设定成初始化值后(其实不设定也没什么问题,只是为了让数据库中的每个字段都有数据,好记录而已)
         *  不设定的话同样可以把客户端请求的资源数据保存到数据库当中,只不过没有封装的实体类属性数据所对应的数据库字段数据为null而已。
         *
         *  就可以调用业务层的添加功能让员工信息添加到数据库当中了。
         */

        // 调用业务层的添加功能,将员工信息添加到数据库当中。
        employeeService.save(employee);
        return new R("添加成功");
    }

5.3.1、添加功能优化(异常处理)

问题: 我们知道数据库当中username字段我们已经设置成unique唯一了,也就是说数据库当中的username字段的数据是唯一的了,当我们再次添加一个username字段数据和数据库当中已经添加过的username字段数据一致的时候,那么因为设置了唯一标识,因此就会报错了。

因此我们需要对该异常进行处理,两种方式:

我们使用第二种:异常处理器方式 (忘记的话看笔记)

思路: 就是当我们项目中出现异常的时候,我们的异常处理器就可以捕获到该异常信息,然后运行异常处理器当中的方法,响应给前端客户一些提示信息。

用异常处理器处理好异常之后,当客户端再次以相同的账户向数据库当中储存数据的时候(数据库账户字段设定的是唯一的unique,因此不能重复储存相同的数据):客户端就不会收到异常信息了,而是收到我们异常处理器当中捕获到该异常的方法中响应给客户端的信息

六、员工信息分页查询

6.1、需求分析

6.2、代码开发

在项目员工管理中,我们可以看到前端用户是向后端发送了请求分页查询的资源路径的,请求路径格式如下所示:

注意:springboot分页查询需要设定一个分页查询拦截器(要不设定的话,尽管调用的是分页查询的功能,但是却拿到的是数据库中的全部数据信息),这个分页查询拦截器的写法是固定的。忘记的话就看springboot整合ssmp的笔记中的分页查询。

因此第一步:设定分页查询拦截器

第二步:在表现层接收客户端发送的请求分页查询资源,进行一些逻辑处理:

我们知道前端页面发送的请求分页查询访问路径有两种形式:

第一种:

第二种:

因此总结:

    通过上面两种的可能,我们后端可以直接设定三个参数(page、pageSize、name)用来接收客户端发送的请求资源。 当客户端没有进行模糊条件查询时,我们后台设定的这三个参数只接收前端发送的page、pageSize资源然后进行分页查询即可(name为null没有影响),而当客户端进行模糊条件查询的时候,我们后台的这三个参数都接收然后进行分页查询即可,只不过这次的分页查询是按照名字模糊查询出来的结果后进行分页查询的(还是按照page、pageSize 进行分页的)。

再次总结: 其实分页构造器(limit)和条件构造器(where、like等)的目的都是拼接sql语句,如:假如我们在代码逻辑中使用到了分页构造器,那么我们的sql语句就相当于多了一个limit分支,没有这个分页构造器的话,也就意味着sql语句中没有这个limt分支而已,并不会报错,并且分页构造器和条件构造器是可以一起使用的,一起使用的话相当于sql语句中多了limt分支和where或者like等分支而已。

总体代码如下所示:(下面的两个构造器,也可以理解为分页查询和条件查询功能写在一起了。)

  /**
     *  4、员工分页查询
     *
     *      注:一定要保证参数名和客户端请求资源的url属性名一致,要不然客户端的资源数据封装不到参数名当中。
     *
     *      int page    : 第page页
     *      int pageSize    : 一页中查询pageSize条数据
     *      String name  : 模糊条件查询
     */
    @GetMapping(value = "/page")
    public R page(int page,int pageSize,String name){
       
        /**
         *  构造分页查询构造器
         */
        IPage<Employee> page1 = new Page<>(page,pageSize); // 接收到前端传递的参数为1,10
        // 调用业务层分页查询功能方法
        // employeeService.page(page1, null);  // 到这一步:就能够按照第page页并且该页pageSize条数据的形式对数据库中的数据进行分页查询了。
                                                // 但是因为有条件查询的存在,所以放到条件查询构造器后面了。

        /**
         *  构造条件查询构造器

         *      构造条件查询构造器的目的:我们知道前端客户也有可能会通过输入用户名来进行条件查询后进行分页,因此我们这里也要把条件查询构造出来
         *
         *      注意1: StringUtils.isNotEmpty(name) 是判断name不为null时。(和 name != null 其实一样)
         *      注意2: import org.apache.commons.lang.StringUtils; 时该包下的StringUtils
         */
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name); // 客户端传递的条件查询的name不为null时,对name进行条件分页查询,
                                                                                // 如果name为null,下面调用业务层分页查询功能的方法当中queryWrapper
                                                                                // 就为null了,也就是说下面调用业务层分页查询时,只传递了一个分页查询的page1参数
                                                                                 // 因此可以说为null时,就只进行了分页查询,而不是分页条件查询

        //  调用业务层分页查询功能方法
        employeeService.page(page1,queryWrapper);   // 这里传递的一个是分页查询构造器page1,一个是条件查询构造器queryWrapper,当客户端传递的条件查询的name不为null时,
                                                    // 这两个构造器就都生效了,也就是说进行条件分页查询了。如果客户端传递的name为null,那么就不进行条件查询,
                                                    // 也就是说这个queryWrapper参数不传递到业务层的分页查询功能当中了,也就是说只传递了一个分页查询的参数page1
                                                    // 所以客户端传递的name为null时就只进行了分页查询,而不是分页条件查询。

 /**
         *  总结: 总的来说就是分页查询和条件查询写在一起了。 (这两个功能忘记怎么写的话看前面整合ssmp的笔记)
         *
         *      调用业务层功能的时候employeeService.page(page1,queryWrapper);传递的两个参数一个是分页查询参数page1,一个是条件查询参数queryWrapper
         *      当条件查询参数queryWrapper为null时(也可以说客户端传递的name为null时,queryWrapper就为null了),此时就只有一个分页查询的参数page1,
         *      传递给了业务层功能,那么就进行的只是分页查询功能。
         *      如果queryWrapper不为null(客户端传递的name不为null时),那么page1,和 queryWrapper两个参数都传递给了业务层功能,因此就是条件分页查询功能。
         *      如果传递的page1为null了,那么也就意味着sql语句中没有关于分页查询limit了,而是只有模糊条件查询 like的语句了,(相当于 select * from a like = "dawda")         
*/

        return new R(1,page1);
    }

}

思考1:为什么后端响应给前端的结果是:new R(1,page1)呢。

结果如下所示:

当客户端没有进行模糊条件查询时,只进行了分页查询:

当客户端进行模糊条件查询时,进行了分页条件查询:

七、启用/禁用员工账号

7.1、需求分析

补充细节:

我们这里有一个细节,也就是当用户是admin登录该系统的时候,才可以进行启用/禁用的操作,而当其他用户登录该项目系统的时候,是不可以进行启用/禁用操作的,只有admin用户有这个启用/禁用操作的权限。

7.2、代码开发

问题:代码开发前有个问题(精度损失问题)需要先处理:

我们知道数据库当中的id是通过雪花算法进行自动分配的id数值,而不是通过自动增长帮我们生成的id数值,使用雪花算法的方式是有精度损失的,问题如下所示:

前端发送的君哥id如果和数据库中君哥对应的id数据不一致的话,就会导致后台在数据库当中无法通过id字段修改君哥的数据(也就是说,没办法修改君哥的账户状态status的数据了~)。

解决雪花算法id精度损失问题:

具体实现步骤(两个步骤):

代码实现过程如下所示:

对象转换器类:

package com.Bivin.reggie.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance) // long精度损失转换器
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

扩展消息转换器:

package com.Bivin.reggie.config;

import com.Bivin.reggie.common.JacksonObjectMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 *  扩展消息转换器
 */

@Configuration  // 定义SpringMvc容器注解
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     *  扩展消息转换器
     */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 创建消息转换器
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        // 设置具体的对象映射器
        messageConverter.setObjectMapper(new JacksonObjectMapper());

        // 通过设置索引,让自己的转换器放在最前面,否则默认的jackson转换器会在前面,用不上我们设置的转换器
        converters.add(0,messageConverter);
    }
}

注意:如果出现404的报错问题的话:

** 扩展消息转换器类用实现 WebMvcConfigurer,别用继承WebMvcConfigurationSupport,要不然会出现下面的bug问题:(访问其他的好好的业务功能前端页面直接显示404问题)**

通过设定的上面两个步骤后,雪花算法精度损失问题就解决了。

我们知道前端通过id修改员工账户状态的时候,传递过来的请求数据如下所示:

表现层代码如下所示:

总体思路:

就是通过Employee实体类把客户端请求的修改员工账户状态的id和处理过的账户状态(如:已经把原来员工的账户状态1修改成0了,也就是说点击启用的时候,前端已经把账户状态从0变成1了。 0表示:已禁用、1表示:正常)封装到实体类的属性当中,然后我们知道实体类当中有很多属性呢:如修改人属性、更改时间属性、因此虽然客户端没有为这些属性传值,但是我们可以手动记录一下这是谁修改的等,最后我们就可以调用业务层的通过id修改功能了。最终由mybatis-plus拿到我们传递的实体类后,它就可以对封装到实体类属性当中的数据拿出来进行sql处理了。

/**
     *  5、通过id修改员工信息
     *
     *      注: (此时的账号状态status前端人员帮我们处理过了,1:正常、0:禁用。 当我们在前端点击禁用某个员工账户的时候,
     *              前端已经帮我们把该员工账户状态从1改成0传递封装到后台的实体类属性当中了,因此我们后台就不用再修改状态status属性了)
     */
    @PutMapping
    public R update(HttpServletRequest request,@RequestBody Employee employee){  // employee实体类当中封装到了客户端传递的员工id和员工账号状态status

        /**
         *  下面的操作和新增员工功能套路一样。(看笔记)
         */
        // 初始化一下修改的时间
        employee.setUpdateTime(LocalDateTime.now());    // 实体类中的该updateTime属性对应数据库中的update_Time字段

        // 初始化一下修改人员的信息(也就是说谁修改的此操作,肯定是谁登录的谁修改的啊)
        // 我们知道项目登录成功的时候,是会把谁登录的该项目存放到session域当中记录
        Object o = request.getSession().getAttribute("employee");
        employee.setUpdateUser((Long) o);

        /**
         *  接收到客户端请求传递的禁用某员工的id和status账户状态后,调用业务层的修改功能即可(业务层和数据层的处理就交给mybatis-plus了)。
         *
         */
        employeeService.updateById(employee);

        return new R("员工信息修改成功",1);
    }

结果如下所示:

禁用后,我们看后端会发现君哥的帐号状态status字段确实变成0了(总的来说就是通过前端传递的君哥的id修改的君哥的账户状态status):

八、编辑员工信息

8.1、需求分析

8.2、代码开发

8.2.1、数据回显

: 也就是说前端用户通过点击编辑按钮后,通过携带员工信息的id向后端发送get请求,然后后端通过拿到请求的id后,可以通过id对数据库中的该id对应的数据进行查询,把该id对应的整条员工数据先查询出来然后响应给前端,最后前端拿到响应的数据后进行数据回显。(数据回显的形式如上面8.1中的形式,数据显示出来了)

表现层代码如下所示:

   /**
     *  6、通过id查询员工信息 (把查询出来的员工信息响应给前端,前端把数据回显在页面上)
     *
     *      步骤:   第一步:获取客户端传递的id
     *              第二步:获取到客户端传递的id后,通过调用业务层通过id查询数据,把该id对应的员工数据全部查询出来
     *              第三步:数据查询出来后,响应给前端,前端接收到响应后进行数据回显操作。
     *
     *
     *  客户端点击编辑按钮后向后端发送的get请求如:http://localhost:8080/employee/1564536166675939329
     *  因此我们该如何获取到该客户端发送的请求中携带的员工id呢?
     *      通过@Pathvariable占位符获取该请求id。(因为前端是以REST风格进行发送的,所以url上的id需要通过占位符获取,忘记的话看Restful笔记)
     *      注意:这个客户端传递的id是long型的。(因为数据库中的id就是long类型的)
     */
    @GetMapping(value = "/{id}")
    public R getById(@PathVariable long id){

        // 接收到员工传递的id后,就可以调用业务层的通过id查询该id对应在数据库中的整条数据的方法了
        Employee employee = employeeService.getById(id);

        // 查询完之后,我们就可以把查询出来的该id在数据库中对应的整条数据响应给前端了,并且把成功状态码也响应给前端,前端拿到数据后就可以进行回显数据操作了
        if (employee !=null){
            return new R(employee,1);
        }
        return new R("该id对应的员工信息不存在");
    }

前端拿到后台查询的数据和成功标识符后,就可以进行回显操作了:

回显数据如下所示(假设点击的就是妲己的编辑按钮,会发现把妲己对应的数据库中的数据展示在了页面上了):

回显成功后,我们就可以对上面的一些数据进行修改了,修改完之后点击保存即可修改成功之所以能够修改成功,是因为点击保存后前端向后台发送的请求是后端第五个功能:通过id修改员工信息功能,通过把修改后的员工信息封装到Employee实体类当中然后调用业务层的修改功能,最终就修改成功了。)

===============分类管理=====================

九、公共字段自动填充

9.1、问题分析

总的来说、就是一句话:

    当客户端访问添加功能或者修改功能的时候,这些数据库中对应的创建时间、创建人、修改时间、修改人等字段数据,当前端没有为这些字段传递数据的时候,我们后端不再手动为这些字段初始化数据了,直接让mybatis-plus填充功能帮我们自动初始化这些数据。

9.2、代码实现

再次理解一下这个公共字段自动填充到底是干啥的:

首先我们看数据库中的字段(也就是说框起来的字段我们在设计表的时候字段类型设置了不能为null):

但是呢:前端用户向后端请求添加员工功能的时候,传递过来的数据没有上面框起来的四个字段的数据:

因此我们知道,后台我们的数据库中有九个字段,客户端有四个字段的数据没有传递,我们知道客户端没有传递的这四个字段类型是不能为null的,既然客户端没有为这四个字段传递数据,那么前端传递的五个字段数据是肯定插入不到数据库表当中的,肯定会报错(因为向数据库表中插入数据的时候必须要保证数据库的九个字段都有数据)。

因此为了解决客户端没有传递的这四个字段数据,我们后端就初始化手动为该四个字段赋值了:

赋上值后,我们知道实体类属性当中拿到前端传递的五个字段数据后再加上我们手动赋值的四个字段数据,刚好满足数据库表中的九个字段数据了,那么肯定就能把客户端请求的添加员工信息插入到数据库当中了(毕竟sql语句不会出错了因为九个数据都有了,肯定就能插入成功了)。

现在我们使用mybatis-plus的自动填充功能目的就是:解决上面手动初始化字段值的问题。

    也就是说,我们手动赋值的四个字段数据不再用代码写了,用mybatis-plus的自动填充功能帮我们写,其意义还是和我们上面手动为四个字段数据赋值是一样的。

mybatis-plus的自动填充代码实现:

第一步:在和数据库表对应的实体类属性上加上@TableFiled注解,并且指定上是在添加功能时还是修改功能时为其字段赋值或者修改值。

分析该类上加上注解的作用:

    就是只要哪个属性当中加上的有@TableField注解,那么这些加上该注解的属性就能自动填充数据,属性上加上的有fill = FieldFill.INSERT注解的属性 意味着当客户端向后端访问添加功能的时候,并且客户端没有对这个属性对应的数据库中的字段传数据的时候,那么mybatis-plus就会自动为这个属性(数据库字段)填充值(赋值)。

fill = FieldFill.INSERT_UPDATE 意味着当客户端访问添加功能或者修改功能的时候,并且客户端没有对这个属性对应的数据库中的字段传数据的时候,那么mybatis-plus就会自动为这个属性(数据库字段)填充值(赋值)、修改功能的话就会把上次那个值覆盖掉然后重新填充值。

package com.Bivin.reggie.pojo;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

@Data   // 简化实体类的 getter 、setter、toString方法注解
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

   

    private String phone;

    private String sex;

    private String idNumber;

    private Integer status;
    
    @TableField(fill = FieldFill.INSERT)
    private String password;

    @TableField(fill = FieldFill.INSERT)  // 配上FieldFill.INSERT 意味着 当客户端访问添加功能并且客户端没有为该属性传数据的时候,就用mybatis-plus自动填充该数据
    private LocalDateTime createTime; // 该属性字段对应的数据库中的create_time 创建时间字段(创建时间在添加的时候填充一次就可以了,修改的时候不用再填充了)

    @TableField(fill = FieldFill.INSERT_UPDATE) // 配成FieldFill.INSERT_UPDATE 意思是:当客户端访问添加功能或者修改功能的时候,都会为该对应的数据库中的字段填充值
                                                // (当是修改功能的时候,代表的就不是赋值了,代表的是修改值,也就是说把以前数据库中对应的该字段的数据覆盖掉重新赋上我们定义的值)。
    private LocalDateTime updateTime; // 对应的是数据库表中修改时间字段

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;    // 对应的是数据库表中的创建人字段

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;    // 对应的是数据库表中的修改用户字段

}

因为就上面四个字段前端基本上不传递数据,因此我们就在这四个字段上加注解进行配置。

第二步:指定好之后,设定为第一步该字段赋值的数据

分析这个类的作用:

** 该类的作用就是该类有两个方法,一个是添加功能时的方法,另一个是修改功能的方法,意义就是为第一步设定注解的属性赋值(也就是说为第一步实体类的属性设定填充的值),第一步实体类属性当中有fill=FileldFill.INSERT注解的,就在该类的添加功能当中为第一步的添加属性赋值**

package com.Bivin.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import java.time.LocalDateTime;

/**
 *  第二步:数据对象处理器
 */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     *   该方法的作用:就是为第一步实体类中有FieldFill.INSERT的属性赋值(也就是说设定填充的值)
     *
     */
    @Override
    public void insertFill(MetaObject metaObject) {

        // ! 为createTime属性赋填充值 (也对应着数据库中的create_time字段)
        metaObject.setValue("createTime", LocalDateTime.now()); // 为createTime属性设定填充的值为当前时间LocalDateTime.now()

        // ! 为updateTime属性赋填充值
        metaObject.setValue("updateTime",LocalDateTime.now());

        // ! 为createUser属性赋填充值
        /**
         *  这里注意: (我们知道createUser属性我们以前用set方法为其赋值的时候,设定的是session域当中的employee对应的数据,
         *        但是我们这里拿不到session域中的数据,因为没办法获取到request域,因此我们先随便设定一个值,等会再讲如何拿session域当中储存的登录成功的数据)
         */
        metaObject.setValue("createUser",new Long(1)); // 先赋一个new Long(1),等会再处理怎么赋session域当中的数据

        // ! 为updateUser属性赋填充值
        metaObject.setValue("updateUser",new Long(1)); // 同理先赋一个new Long(1),等会再处理怎么赋session域当中的数据

        // ! 为password属性赋填充值
        metaObject.setValue("password", DigestUtils.md5DigestAsHex("123456".getBytes()));
    }

    /**
     *  该方法的作用:就是为第一步实体类中有FieldFill._UPDATE的属性赋值(也就是说设定填充的值)
     *
     */
    @Override
    public void updateFill(MetaObject metaObject) {

        // ! 为updateTime属性赋填充值
        metaObject.setValue("updateTime",LocalDateTime.now());

        // ! 为updateUser属性赋填充值
        metaObject.setValue("updateUser",new Long(1));  // 同理先赋一个new Long(1),等会再处理怎么赋session域当中的数据
    }
}

因此通过上面的两个步骤后,我们就可以把表现层中手动为属性初始化值的操作注释掉了:

也就是说上面两个步骤代替了下面我们手动为属性初始化赋值的操作(更高级了)

修改功能中初始化值的方法也可以注释掉了:

9.3、解决9.2第二步中无法获取session域当中数据问题

问题分析:

我们以前用老方法set设定创建人/修改人属性的时候,我们设定的是session域当中的employee对应的数据:

而9.2第二步当中为其属性赋填充值的时候,无法获取到request域(因为在第二步的MyMetaObjectHandler类当中是不能获取HttpSession对象的),因此没有办法为其赋session域当中的数据:

因此我们有什么办法解决上面的问题呢(如何能够在9.2第二步骤中为其属性赋上session域中的数据呢)?

我们可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。

9.3.1、解决问题:ThreadLocal

    首先我们要知道:![](https://img-blog.csdnimg.cn/2e3173d97b72435cacbcd013e4938372.png)

因为上面三个地方的方法中是同一个线程了,所以我们可以通过ThreadLocal线程的常用方法,set、get方法设置一些局部变量的值(一个地方set设定封装到值,另一个地方get取数据):

(也就是说:因为客户端请求的资源路径,是先进入到拦截器的,然后进入到表现层的功能当中,我们知道了ThreadLocal线程在上面的上个地方的方法当中是同一个线程了,因此我们可以在表现层的update方法中,通过ThreadLocal线程的set方法把储存在session域当中的登录员工数据封装到ThreadLocal的储存空间中,然后再在mybatis-plus自动填充的第二步类当中的updatefill方法中(注意有updatefill方法是和那两个玩意同一个线程)通过ThreadLocal线程的get方法把刚才封装到ThreadLocal储存空间中的数据取出来就可以了,最后就可以填充session域当中的数据了。)

** 代码实现步骤如下所示:**

** 再次注意:只有在拦截器的doFilter方法、表现层的update方法、填充类的updateFill方法中的线程是同一个,才能够在这三个方法中get、set,其他方法中就不是同一个线程了,就get/set不到数据了。**

第一步:编写BaseContext工具类:

package com.Bivin.reggie.common;

/**
 *  基于ThreadLocal封装工具类,用来封装数据。
 */
public class BaseContext {

    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    // 用来封装session域当中的数据。(因为session域当中储存的数据:登录员工id是Long型,因此这里泛型是Long)

    /**
     *  该方法的作用就是:ThreadLocal线程通过set方法把一些数据封装到ThreadLocal提供的储存空间中(上面那个new ThreadLocal<>();就可以认为是ThreadLocal线程的储存空间)
     *
     */
    public static void setThreadLocal(Long id){
        threadLocal.set(id);
    }

    /**
     *  该方法的作用就是:ThreadLocal线程通过get方法把封装到ThreadLocal储存空间中的数据获取出来
     */
    public static Long getCurrentId(){
        return threadLocal.get();
    }

}

第二步:在表现层的update方法中把session域当中的数据封装到ThreadLocal线程的储存空间中:

第三步:在mybatis-plus自动填充类当中获取ThreadLocal线程储存空间中封装的session数据:

** 注意:**

十、分类管理----新增分类

10.1、需求分析

10.2、数据模型

10.3、代码开发

    因为从新换数据库表了(换成category数据库表了),也就是说重新做一些逻辑了,因此在开发之前首先要先把架子搭起来,先把映射数据库字段名的实体类、表现层、业务层、数据层骨架搭出来(和分类管理上面的员工管理的增上改查的过程其实还是一样的,只不过这个处理的是category表,上面那个是employee员工表。)。

注意:一定要保证实体类Category的属性和分类管理category数据库表的字段名保持一致:

只有一致了才能将数据封装到实体类当中,然后映射到数据库的字段上,如果不一致那就映射不到数据库对应的字段上,那么数据就进入不到数据库当中了。

然后再把基于mybatis-plus的数据层和业务层和表现层骨架先搭出来(也就是说一切重新开始了,而且开始的过程和做员工管理的项目过程是一摸一样的。):搭建的过程和3.2笔记中的员工管理时的搭建过程是一样的。

骨架做好之后,我们表现层就可以接收前端用户传递的请求数据,然后进行一些逻辑操作了:

前端向后端发送的请求如下所示:

代码如下所示:

package com.Bivin.reggie.controller;

import com.Bivin.reggie.pojo.Category;
import com.Bivin.reggie.service.impl.CategoryServiceImpl;
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;

/**
 *  分页管理的表现层
 */

@Slf4j
@RestController // @Controller和@ResponseBody注解 (表现层bean注解和响应前端数据注解)
@RequestMapping(value = "/category")
public class CategoryController {

    /**
     *   自动装配业务层对象 (也可以说拿到业务层对象引用,然后调用业务层对象当中的增删改查等一些功能方法)
     */
    @Autowired
    private CategoryServiceImpl categoryService;

    /**
     *  1、新增菜品分类
     *
     *      分析:
     *          前端发送的POST请求:http://localhost:8080/category,并且请求资源是以JSON数据格式发送到后端的。
     *
     *      后台逻辑:
     *          后台使用@RequestBody注解接收前端发送的JSON请求数据,把前端请求的数据封装到实体类Category属性当中,
     *          然后调用业务层的保存功能,将客户端请求的新增菜品添加到数据库当中。
     */
    @PostMapping
    public R save(@RequestBody Category category){ // 将前端请求数据封装到了实体类的属性当中

        // 调用业务层的保存功能
        categoryService.save(category);

        return new R("新增菜品分类成功",1);

    }
    
}

当前端向后台请求保存一个数据库中name字段没有的数据的时候就能添加成功了:

并且数据库中确实保存了前端用户请求保存的数据了:(注意我们知道前端仅仅向后端发送的请求资源是name字段数据和type字段数据还有sort字段数据,而我们数据库当中还有四个字段呢并且数据库设定的这四个字段不能为null,客户端仅仅传递了三个字段的数据之所以能够添加成功到数据库当中,是因为那四个字段我们设定了mybatis-plus自动填充

十一、分类管理----分页查询

11.1、需求分析

11.2、代码开发

代码如下所示:

 /**
     *  2、分类管理分页查询功能
     *
     *      思路:
     *          通过接收客户端请求的page,pageSize资源数据,我们后端调用业务层的分类查询功能把这两个参数传递到
     *          数据层进行分页查询sql语句即可。
     *
     *      注:一定要保证参数名和客户端请求资源的url属性名一致,要不然客户端的资源数据封装不到参数名当中。
     *      *
     *      *      int page    : 第page页
     *      *      int pageSize    : 一页中查询pageSize条数据
     *
     *      注意:别忘记配分页查询拦截器 (项目中如果配过了就不用配了)
     *
     *
     */
    @GetMapping(value = "/page")
    public R page(int page,int pageSize){

        /**
         *  构造分页查询构造器(忘记的话看springboot整合SSMP笔记)
         */
        IPage<Category> page1 = new Page<>(page,pageSize);

        /**
         *  构造条件查询构造器
         *      注意:这个构造条件查询构造器的目的和员工管理构造的意义不一样。
         *      这里构造的目的:我们知道数据库中有个排序sort字段,我们这里使用条件构造器的目的就是
         *      让分页查询出来的数据在页面上按照排序字段sort的大小顺序展列在前端页面上。
         */
        LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
        // 添加排序条件,根据sort进行排序
        lqw.orderByAsc(Category::getSort);

        // 调用业务层的分页查询功能
        categoryService.page(page1,lqw);     // 其实没有这个条件构造器的话,也会是说sql语句中没有了order by语句而已,分页的limit语句还是有的。

        return new R(page1,1);    // 把调用业务层分页查询查询出来的数据和成功标识符响应给前端
    }

客户端拿到后端响应的分页查询出来的数据后,把数据展示在页面上的格式如下所示:

十二、分类管理----删除分类

12.1、需求分析

12.2、代码开发

代码写之前,需要处理一下id精度损失问题。

** 我们知道数据库当中的id是通过雪花算法进行自动分配的id数值,而不是通过自动增长帮我们生成的id数值,使用雪花算法的方式是有精度损失的。(看7.2的笔记)**

因此我们需要用到扩展消息转换器来解决雪花算法生成的id精度损失问题(如果项目中已经配的有扩展消息转换器了,那么就不用再配置了。)

前端点击删除按钮时,前端向后端发送的路径资源请求如下图所示:

** 因此后端代码如下所示:**

  /**
     *   3、通过id删除功能
     *
     *      客户端向后台发送的DELETE请求: http://localhost:8080/category?ids=1397844263642378242 (通过ids删除)
     *
     *      因此我们后台要保证参数名和前端用户传递的url参数名保持一致,那样客户端url请求的ids资源数据才能封装到我们后台的参数当中。
     *
     */
    @DeleteMapping
    public R delete(Long ids){
        log.info("ids:"+ids);   // 输出日志判断是否拿到了客户端发送的ids请求数据资源

        // 调用业务层的通过id删除功能
        categoryService.removeById(ids);

        return new R("删除成功",1);
    }

**上面的过程仅仅是简单的删除逻辑,下面我们也可以进行优化。 **

12.2.1、删除分类----功能完善

我们在12.1的需求分析当中知道,当分类管理的分类名称,关联了菜品(菜品数据库)或者套餐(套餐数据库)的时候(也就是说客户端想要删除的一个菜的id,对应着菜品或者套餐的category_id,也就是说相同,那么就不让他删除了。),就不允许它删除了。

菜品数据库:

** 套餐数据库:**

功能完善的思路:

当分类管理的分类名称,关联了菜品或者套餐的时候,就不允许它删除了。

简单点说就是:通过上面的演示知道我们通过在分类管理中点击删除按钮删除某个菜的时候,是会向后端发送一个请求路径的,并且携带着该菜的id,后台通过该前端发送过来的菜的id把该菜的数据删除掉了。

而我们这里要完善的功能就是:当前端向后端发送的删除某个菜的id的时候,我们后台拿到前端发送的请求删除某个菜的id的时候,而不是直接把该id对应的数据库中的菜的信息删除掉,而是拿着前端客户传递的菜的id值先进行一下判断,判断该客户传递的该菜的id是否和上面两个数据库(菜品数据库、套餐数据库)中的某个category_id字段的值一致,一致的话就说明客户端想要删除的该菜关联了菜品或者套餐那么我们后端就不允许客户端删除这个菜。(你这个菜都关联菜品或者套餐了,你还想删除你想啥呢,滚犊子把。不关联这两个数据库的话我才能让你删除懂了吗。)

因此完善的逻辑我们搞懂后,就可以用代码进行处理了:

   ** 重**:因为关联到了上面的菜品数据库和套餐数据库,因此我们首先需要把菜品和套餐数据库对应的实体类、业务层、数据层、表现层(表现层可以先不写)写出来,也就是说只要关联某个数据库了,首先先把该数据库对应的实体类、数据层、业务层、表现层写出来,方便以后做各种逻辑。(**写法和员工管理/分类管理刚开始写骨架(实体类、业务层、表现层、数据层)的格式是一样的,都是基于mybatis-plus**)

注:表现层可以先不写,等前端真正想向该数据库发送资源请求的时候再写。

因此这些都弄完之后,就可以写判断客户端传递的菜的id是否关联该两个数据库(菜品、套餐数据库)的代码逻辑了:

注:因为要写很多的判断逻辑代码了,因此我们就不用mybatis-plus的增删改查的方法了,我们自己手写一个通过id删除的功能,在业务层做一些逻辑,就不再表现层直接调用mybatis-plus的通过id删除的功能方法了。

我们如何判断客户端想要删除的菜是否和菜品(菜品数据库)、套餐(套餐数据库)关联呢:

** ** 因此我们可以通过拿到客户端想要删除的菜的id,然后通过条件sql分别在菜品数据库、套餐数据库中进行select查询,判断菜品数据库、套餐数据库中的category_id字段是否有和客户端想要删除的菜的id相同,如果查询出来的数量不为0,那么就说明客户端想要删除的菜确实和菜品或者套餐关联了。(毕竟菜品或者套餐中category_id字段有该菜的id)

具体判断是否有关联的sql语句如下所示:

补充知识:

我们知道sql中有where条件形式,因此我们首先要在写代码的时候想到用条件构造器,如果没有用条件构造器的话,就相当于sql语句中没有写where后面的东西,就相当于select

count(*) frim dish;了,只有用了条件构造器才能有where条件部分。

代码如下所示:

表现层逻辑代码:

  /**
     *   3、通过id删除功能
     *
     *      客户端向后台发送的DELETE请求: http://localhost:8080/category?ids=1397844263642378242 (通过ids删除)
     *
     *      因此我们后台要保证参数名和前端用户传递的url参数名保持一致,那样客户端url请求的ids资源数据才能封装到我们后台的参数当中。
     *
     */
    @DeleteMapping
    public R delete(Long ids){
        log.info("ids:"+ids);   // 输出日志判断是否拿到了客户端发送的ids请求数据资源

        // 调用业务层的通过id删除功能
        categoryService.remove(ids);

        return new R("删除成功",1); // 如果到这里就说明在业务层确实把客户端想要删除的菜删除掉了,然后这里提示信息即可。
    }

业务层逻辑代码:

package com.Bivin.reggie.service.impl;

import com.Bivin.reggie.common.BusinessException;
import com.Bivin.reggie.controller.R;
import com.Bivin.reggie.mapper.CategoryMapper;
import com.Bivin.reggie.pojo.Category;
import com.Bivin.reggie.pojo.Dish;
import com.Bivin.reggie.pojo.Employee;
import com.Bivin.reggie.pojo.Setmeal;
import com.Bivin.reggie.service.CategoryService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 *  业务层实现类      ---- 基于 mybatis-plus
 *
 *  用法: 继承mybatis-plus提供的ServiceImpl,并且泛型第一个是数据层接口,第二个是实体类。
 *      并且也要实现业务层接口
 */
@Slf4j
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

    /**
     *  自动装配菜品(菜品数据库)和套餐(套餐数据库)的业务层对象
     */
    @Autowired
    private DishServiceImpl dishService;

    @Autowired
    private SetmealServiceImpl setmealServicel;

    /**
     *  通过id删除功能
     *
     */

    @Override
    public void remove(Long ids) {

        /**
         * 判断该客户端传递过来的想要删除的菜id是否关联菜品(也就是说该菜id是否和菜品数据库中的category_id字段数据一致,一致说明关联了,那么就不允许删除即可)
         *
         *  sql: SELECT COUNT(*) FROM dish WHERE category_id = ids;
         */

        // 看到sql语句有用到where,首先要先想到条件构造器,根据客户端请求删除菜的ids,判断ids是否和category_id字段数据一致
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();   // 因为是对dish数据库进行查询的,因此泛型用的是Dish实体类
        dishLambdaQueryWrapper.eq(Dish::getCategoryId, ids);    // 条件构造器的eq方法:用来拼接dish数据库表中SELECT COUNT(*) FROM dish WHERE category_id = ids;的sql语句中的的category_id = ids语句;
        // 通过上面的条件构造器把:WHERE category_id = ids;sql条件语句写好之后,对应的sql语句就差一个SELECT COUNT(*) FROM dish 了

        // 因此我们可以调用dish对应的业务层中的查询数量的功能方法(这也是我们为什么要把dish对应的数据层、业务层写出来的原因):
        int i = dishService.count(dishLambdaQueryWrapper);  // 再把上面的条件构造器放进到count中,那么就代表执行完查询条件后查询的数量。
        // 如果上面查询出来的数量不为0,说明客户端传递的想要删除的菜的ids,在菜品的数据库当中确实存在,那么就说明关联着呢,那我们肯定不让他删除了。
        if (i > 0) {
            // 大于0,说明客户端传递的想要删除的菜关联着菜品呢,那么就不让他删除。
            // 抛出一个业务异常就可以了。

            throw new BusinessException("当前分类下关联了菜品,不能删除~",1); // 把提示给前端的响应信息封装到BusinessException的属性当中
        }

        /**
         * 判断该客户端传递过来的想要删除的菜id是否关联套餐(也就是说该菜id是否和套餐数据库中的category_id字段数据一致,一致说明关联了,那么就不允许删除即可)
         * 和上面的使用方法是一致的。
         *
         *  sql: SELECT COUNT(*) FROM setmeal WHERE category_id = ids;
         */

        // 构造条件构造器
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        // // 因为是对dish数据库进行查询的,因此泛型用的是Setmeal实体类
        queryWrapper.eq(Setmeal::getCategoryId, ids); // 该条件构造器的eq方法作用:category_id = ids;
        // 通过上面的条件构造器把:WHERE category_id = ids;sql条件语句写好之后,对应的sql语句就差一个SELECT COUNT(*) FROM setmeal 了

        // 调用setmeal对应的业务层中查询数量的功能方法(这也是为什么把setmeal的业务层、数据层写出来的原因)
        int i1 = setmealServicel.count(queryWrapper);
        if (i1 > 0) {
            // 大于0,说明客户端传递的想要删除的菜关联着套餐呢,那么就不让他删除。
            // 抛出一个业务异常就可以了。(忘记怎么写的话,看异常笔记即可)

            throw new BusinessException("当前分类下关联了套餐,不能删除~"); // 把提示给前端的响应信息封装到BusinessException的属性当中

        }

        /**
         * 如果到了这一步:就说明客户端传递的想要删除的菜,没有关联菜品或者套餐。
         *  那么我们就让客户端删除他想要删除的菜即可了。
         */
        // 调用删除功能(注意:这里删除的是category数据库中的菜,因此需要用到category对应的业务层、数据层)
        super.removeById(ids);  // 因为现在是在category对应的业务层当中,super相当于上一级:也就是category的表现层(我们知道在category的表现层
        // 自动装配的有category业务层的对象,因此直接用业务层对象调用数据层的removeById删除方法即可。 )

        /**
         *  总结: 什么时候使用条件构造器,什么时候使用分页构造器 ?
         *
         *      只要我们sql语句中涉及到where后面的条件的时候,如:order by 条件,就需要想到条件构造器,
         *      当我们使用条件构造器的时候,这个order by 条件才能用得到,如果没有条件构造器的话,这个order by 条件就相当于没有。
         *
         *      而分页查询什么时候用呢: 就是sql语句中需要用到limit分页的时候,就需要想到分页构造器了,同理如果没有用分页构造器,就相当于sql语句中没有limit。
         *
         *
         */

    }
}

业务异常类:

package com.Bivin.reggie.common;

/**
 *  业务异常类
 */
public class BusinessException extends RuntimeException{

    private Integer code;   // // 加个code编号属性,来用状态码标注以后出现的是哪一种异常

    /**
     *  把构造方法最好都写出来
     */

    public BusinessException(Integer code) {
        this.code = code;
    }

    public BusinessException(String message, Integer code) {
        super(message);
        this.code = code;
    }

    public BusinessException(String message) {
        super(message);
    }

    public BusinessException(String message, Throwable cause, Integer code) {
        super(message, cause);
        this.code = code;
    }

    public BusinessException(Throwable cause, Integer code) {
        super(cause);
        this.code = code;
    }

    public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Integer code) {
        super(message, cause, enableSuppression, writableStackTrace);
        this.code = code;
    }

    /**
     *  getter and setter方法
     */
    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}

异常处理器:

package com.Bivin.reggie.common;

import com.Bivin.reggie.controller.R;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 *  异常处理器
 */

@RestControllerAdvice   // REST风格的异常处理的注解
public class GlobalExceptionHandler {

    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)// 我们知道账户重复添加时报错的异常就是这个异常SQLIntegrityConstraintViolationException,
                                                // 因此我们可以把这个异常信息写在上面的注解当中就可以捕获到该异常,
                                                // 当出现该异常并且捕获到该异常的时候,就执行下面的方法中的信息
    public R doException(){

        return new R("兄弟,账户已存在,请重新输入");
    }

    /**
     *  业务异常处理器
     */
    @ExceptionHandler(BusinessException.class)
    public R doBusinessException(BusinessException bs){
        return new R(bs.getMessage()); // 把封装在BusinessException业务异常类属性中的数据响应给客户端
    }
}

代码逻辑完成之后,客户端删除关联着菜品或者套餐的菜时页面效果如下所示:

十三、分类管理----修改分类功能

12.1、需求分析

** 注意:前端向后端请求携带的数据是JSON格式的数据。**

12.2、代码开发

    /**
     *  4、修改功能
     *
     *      客户端向后台发送的PUT请求:http://localhost:8080/category
     *      并且携带着JSON数据:{id: "1397844303408574465", name: "川菜", sort: 2}
     *
     *      后台逻辑思路:
     *          因此我们可以用Category实体类,把前端请求时携带的JSON数据封装到实体类的属性当中,然后调用业务层的修改功能进行修改即可。(mybatis-plus封装的修改功能)
     */
    @PutMapping
    public R update(HttpServletRequest request, @RequestBody Category category){    // @RequestBody : 接收json格式数据注解

        // ThreadLocal 把session域当中的数据存入到ThreadLocal线程储存空间中(不设置的话要不然mybatis-plus帮我们自动填充update_user字段数据的时候填不上去)
        Object o = request.getSession().getAttribute("employee");
        BaseContext.setCurrentId((Long) o); // 把session域当中的数据存入到ThreadLocal储存空间中。

        // 调用业务层的修改功能
        categoryService.updateById(category);

        return new R("分类信息修改成功~",1);
    }

=============== 菜品管理 =============

十四、菜品管理---- 效果展示

移动端(手机)展示效果:

十五、菜品管理----新增菜品【多表查询重点】

15.1、需求分析

15.2、数据模型

菜品dish表结构:

补充:status字段。 0:代表停售 1:代表起售

菜品口味表dish_flavor:

15.3、代码开发

代码开发:

因此我们首先要先在categroy表现层接收客户端发送的资源请求(因为这个菜品管理想要拿到的就是分类管理对应的数据库中的name字段数据,毕竟这个是菜品管理,想要拿分类管理中的菜品分类type=1正常操作啊。):

代码演示如下所示:

 /**
     *  5、查询category数据库中的type为1的字段数据。
     *
     *      前端get请求:http://localhost:8080/category/list?type=1
     *
     */
    @GetMapping(value = "/list")
    public R list(Integer type){

        // 创建条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        // 添加sql语句中“=”条件
        queryWrapper.eq(type!=null,Category::getType,type);// 前端请求传递过来的type不为null的时候,
                                            // 就对Category实体类当中的Type属性对应的数据库中的type字段与前端请求传递过来的type字段做相等条件比较 (相当于前面说过的拼接条件sql语句)

        // 调用业务层的查询功能
        List<Category> list = categoryService.list(queryWrapper);// 这一步就会把category数据库中的type字段为1的数据全部查询出来存放到list集合中了。(因为会查询出来很多条数据,因此用list查询方法)

        // 把数据响应给前端
        return new R(1,list);   // 把封装到list集合中查询的数据响应给前端
    }

前端用户拿到我们响应给他的type为1(菜品分类)的所有数据的时候,前端就可以做一些逻辑把这些type为1的数据的name字段的数据拿出来展示在下拉框中了:

然后我们就可以填写添加的菜品进行保存了:

补充:前端拿到菜品分类后,当客户端选择一个菜品的时候,就把这个菜品对应的id携带过去了,就是下面的categoryId。

但是通过上面的客户端请求dish路径的请求所携带的数据来看,我们后端的Dish实体类是没有flavors属性的,因此我们的Dish实体类根本无法接收客户端请求时所携带的数据,也就是说客户端请求时所携带的请求数据无法封装到实体类的属性当中。(实体类Dish没有flavors属性,但是其他的对应接收前端请求数据的属性还是都有的,仅仅是没有flavors属性)

因此我们需要怎么解决实体类中没有flavors属性来封装前端请求所携带的flavors的json数据呢?

15.3.1、解决方法:DTO思想

DishDto代码如下所示:

package com.Bivin.reggie.dto;

import com.Bivin.reggie.pojo.Dish;
import com.Bivin.reggie.pojo.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

/**
 *  注意: 我们知道客户端向后端发送请求的时候携带的请求数据,实体类Dish属性当中flavors属性是没有的,
 *        因此就无法接收封装到客户端请求携带的flavors数据,因此我们就需要用到DTO思想。
 *
 *        DTO思想:
 *          就是用一个有flavors属性的实体类如DishDto实体类再继承一下Dish实体类,那么这个DishDto实体类就
 *          不仅有了flavors属性,也有了Dish实体类中的所有属性,因此就可以用DishDto实体类的属性来封装前端页面
 *          请求时携带的资源数据了。
 *
 *          但是会有一个问题:
 *              Dish实体类中的属性都是private私有性的,我们知道不在同一个类中的话私有的属性和方法是无法被继承的,
 *              但是为什么DishDto实体类就能继承到Dish实体类中的私有属性呢?
 *
 *                  这是因为我们使用了lombok,lombok的作用就是帮我们简写了实体类中的getter and setter方法,
 *                  这里我们并不是因为DishDto实体类继承了Dish实体类所以才有了Dish实体类中的所有属性的
 *                  (这是不可能的,因为Dish实体类属性是私有的,DishDto实体类根本继承不到),
 *                  为什么能继承到Dish实体类中的属性的呢?是因为通过getter,setter方法来为Dish实体类属性赋值的,
 *                  也可以说是继承吧(反正这里模模糊糊的)
 *
 *
 */

@Data   // lombok
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();    // flavors属性
    /**
     *  问题1:
     *      这里的flavors属性写成List集合的原因是?
     *
     *           因为前端向后端发送请求的时候,携带的flavors请求数据是集合的形式,因此我们实体类当中的flavors属性要写成List集合的形式用来封装前端请求的集合数据。
     *
     *      前端携带的flavors请求数据格式如下所示(确定是集合的形式):
     *      flavors:
     *              0: {name: "忌口", value: "["不要葱","不要蒜","不要香菜","不要辣"]", showOption: false}
     *              1: {name: "辣度", value: "["不辣","微辣","中辣","重辣"]", showOption: false}
     *
     *  问题2:
     *      这里的List集合泛型为什么要用DishFlavor实体类呢?
     *
     *          因为我们知道前端请求携带的flavors资源是集合的形式,并且集合里面的数据如:name: "忌口", value: "["不要葱","不要蒜","不要香菜","不要辣"]"
     *          key的name、value对应的和实体类DishFlavor属性name、value一样,因此List集合的泛型使用DishFlavor实体类的目的就是:
     *              可以说把前端请求携带的flavors资源数据封装到实体类DishFlavor的属性当中,然后以DishFlavor对象的形式封装在List集合中。
     *              (当前端请求携带多条flavors数据的时候:如上面的
     *
     *                      0: {name: "忌口", value: "["不要葱","不要蒜","不要香菜","不要辣"]", showOption: false}
     *      *              1: {name: "辣度", value: "["不辣","微辣","中辣","重辣"]", showOption: false}
     *
     *                  那么实体类DishFlavor的name属性就封装了“忌口”数据,value属性就封装了“["不要葱","不要蒜","不要香菜","不要辣"]",
     *                  然后把这个DishFlavor对象封装到List集合当中,然后再把实体类DishFlavor的name属性就封装了“辣度”数据,
     *                  value属性就封装了“["不辣","微辣","中辣","重辣"]",再把这个实体类封装到List集合当中,多条的话就一直这样封装DishFlavor对象
     *              )
     *
     *
     *
     */

    /**
     *  这两个属性用到再说,暂时不需要
     */
    private String categoryName;

    private Integer copies;

}

当我们表现层用DTO创建的实体类来接收客户端传递请求的数据的时候,会发现确实能够把前端请求的数据参数封装到DTO实体类的属性当中了,并且前端携带的flavors集合数据确实是封装到集合的泛型DishFlavor实体类的属性当中,然后把该对象封装到List集合中的:

DishController表现层测试代码如下所示(还真的接收到了数据):

  /**
     * * 1、添加菜品功能
     *
     *      客户端发送的post请求: http://localhost:8080/dish
     *
     *      思路: 用DTO思想创建的实体类接收客户端请求时携带的json数据,
     *
     */
    @PostMapping
    public R dish(@RequestBody DishDto dishDto){
        log.info("是否接收到了携带的数据:"+dishDto.toString());
        return null;
    }

}

总体的逻辑代码如下所示:

DishController表现层逻辑代码:

package com.Bivin.reggie.controller;

import com.Bivin.reggie.dto.DishDto;
import com.Bivin.reggie.service.impl.DishFlavorServiceImpl;
import com.Bivin.reggie.service.impl.DishServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

/**
 *  菜品管理表现层
 */

@RestController // @Controller和@ResponseBody注解 (表现层bean注解和响应前端数据注解)
@RequestMapping(value = "/dish")
@Slf4j
public class DishController {

    /**
     *  把菜品表和菜品口味表的业务层对象注入进来
     */
    @Autowired
    private DishServiceImpl dishService;
    @Autowired
    private DishFlavorServiceImpl dishFlavorService;

    /**
     * * 1、添加菜品功能
     *
     *      客户端发送的post请求: http://localhost:8080/dish
     *
     *      思路: 用DTO思想创建的实体类接收客户端请求时携带的json数据,
     *
     */
    @PostMapping
    public R dish(@RequestBody DishDto dishDto){
        log.info("是否接收到了携带的数据:"+dishDto.toString());

        /**
         *  调用业务层做一些处理逻辑问题
         */
        dishService.DishWith(dishDto);  // 把客户端请求数据时携带的数据传递到业务层的DishWith方法中。

        return new R(1,"添加成功");
    }
}

业务层逻辑代码如下所示:

package com.Bivin.reggie.service.impl;

import com.Bivin.reggie.dto.DishDto;
import com.Bivin.reggie.mapper.DishMapper;
import com.Bivin.reggie.pojo.Dish;
import com.Bivin.reggie.pojo.DishFlavor;
import com.Bivin.reggie.service.DishService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 *  业务层实现类      ---- 基于 mybatis-plus
 *
 *  用法: 继承mybatis-plus提供的ServiceImpl,并且泛型第一个是数据层接口,第二个是实体类。
 *      并且也要实现业务层接口
 */

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

    /**
     *  自动装配口味表和菜品表业务层对象
     */
    @Autowired
    private DishServiceImpl dishService;
    @Autowired
    private DishFlavorServiceImpl dishFlavorService;

    /**
     *      我们知道,参数dishDto中封装的是客户端请求时携带的数据,而这些数据我们前面已经知道了,
     *      前端请求时flavors集合中携带的数据是封装到了实体类DishFlavor的属性当中的,然而这个实体类DishFlavor的属性对应的是dish_flavor这张数据库表,
     *      因此我们这里把客户端请求时携带的flavors集合数据保存到dish_flavors口味表当中。
     *
     *      然后又因为客户端请求时携带的数据除了flavors数据其他的所有数据对应的是Dish实体类的属性,而Dish实体类属性对应的是dish这张数据库表
     *      因此我们这里把客户端请求时携带的除flavors集合数据外的所有数据保存到dish菜品表当中。
     *
     *      总结: 因此我们这里就是相当于操作两张表,一张是口味表dish_flavor、一张是dish菜品表,
     *             而这两种表中所要保存的数据现在都在实体类DishDto属性当中了(一个flavor集合属性,一个继承的Dish属性)

      * 再次总结: 也就是说DishDto实体类中的属性中flavors集合属性中封装的是菜品的口味数据,我们现在要把菜品的口味单独放入一个口味数据库当中进行保存,DishDto实体类中的除了flavors集合的其他属性中封装的数据是菜品所对应的菜品名字、价格....,因此我们把这些实体类中的属性单独再放入一个菜品数据库当中。最终为了知道口味对应的是什么菜,就需要让口味数据库中关联着菜品的id
     *
     */
    public void DishWith(DishDto dishDto){

        /**
         *  因此这里就可以先调用菜品表对应的业务层,将DishDto实体类的属性中封装的数据保存到菜品表中 (就是flavors除外的所有数据都是菜品表中的数据)
         *
         *      思路: 直接用菜品表对应的业务层调用数据层的保存功能即可。
         */
        dishService.save(dishDto);  // 这里直接把dishDto对象传递给保存功能即可,因为dishDto对象属性中封装的有菜品表对应的数据和口味表对应的数据(flavors),
                                    // 这里直接把对象传入给保存功能,mybatisPlus会自动识别获取菜品表在dishDto对象属性中想要的数据

        /**
         *  这里上面菜品表保存数据成功后,那么那条保存的菜品数据在数据库中就会自动生成一个id
         *  我们这里先获取到那个id,等会和口味表做关联。
         *
         *      因为菜品表保存成功后,我们口味表肯定要用菜品表某条菜品数据的id和我们的口味相关联一下啊,要不然谁他妈知道这个口味表中的口味是哪个菜的口味啊。
         *
         *      做法: 直接用实体类.getId的方式就能获取dishService.save(dishDto);保存成功后,该保存的菜在数据库中对应的id了。
         */
        Long id = dishDto.getId();  // 获取到了菜品的id

        /**
         *  调用口味表的数据层的保存功能,把实体类DishDto对象flavors属性当中封装的口味集合数据保存到口味数据库当中。
         *
         *      但是要注意的是:将口味数据保存到口味数据库之前,要把该口味数据关联上菜品的id,不关联的话谁他妈知道这个口味是哪个菜啊。
         *
         */
         // 首先我们知道,口味数据是封装到DishDto实体类的flavors属性当中的,该属性是List集合,因为一个菜可能客户会选择好几种口味,
        // 因此我们这里需要先把该集合属性中封装的口味数据遍历循环出来,然后为遍历出来的口味数据添加到口味数据库之前关联一下菜品的id,
        // 假如在flavors属性口味集合中遍历出来了6条口味数据,那么我们就要让这6条数据都关联一下菜品id,要不关联的话这6条口味数据保存
        // 到口味数据库中,我们查看口味数据库的时候谁他妈知道这6条口味是哪个菜的啊卧槽。

        // 1、因此先把DishDto实体类当中的flavors属性获取出来
        List<DishFlavor> flavors = dishDto.getFlavors();    // 我们知道前端请求携带的数据都封装在了这个List集合当中了
        // 2、遍历List集合,把里面的每条口味的数据都遍历出来关联一下菜品的id然后再调用数据层进行往口味数据库中保存处理。
        for (DishFlavor dishFlavor:flavors){    // 我们知道List集合中的数据是封装到DishFlavor属性当中后以DishFlavor对象的形式封装到List集合当中的,所以元素类型就是DishFlavor
            // System.out.println(dishFlavor); //  DishFlavor(id=null, dishId=null, name=辣度, value=["不辣","微辣","中辣","重辣"], createTime=null, updateTime=null, createUser=null, updateUser=null, isDeleted=null)
                                            // 循环遍历打印就可以把封装在List集合中的DishFlavor对象一条一条的循环打印出来了(上面我们只列出了一条循环打印的数据,格式就是这样的)

            dishFlavor.setDishId(id);   // 因此我们就可以循环遍历出每一个口味属性的对象,然后为其dishId关联菜品的id字段赋值,关联上菜品的id

            /**
             *  然后为其菜品口味数据关联了菜品的id后,就可以调用口味数据层的保存功能了,将其对象中封装的口味数据保存到菜品口味数据库中。
             */
            dishFlavorService.save(dishFlavor); // 这里也就相当于传入的一个dishFlavor对象,
                // 我们知道传入到一个数据库中对象的话,如果该对象的属性映射的到数据库的字段名        
               // 的话,就能把属性中的数据封装到数据库当中了。 之所以这里相当于传入的一个 
             // dishFlavor对象,就是因为我们把List集合中封装数据的dishFlavor对象遍历出来后一    
  // 个一个的调用数据层保存功能,把该对象一个一个传入进去了,因此就又回到以前的写法了。(只要对象 
   // 中的属性能对应到数据库中的字段名,那么调用数据层的增删改查功能的时候,mybatisplus都可以 
    // 帮我们自动把属性中封装的数据映射到数据库对应的字段上)

        }

    }
}

当客户端向后端发送添加菜品请求时:

通过我们后端的逻辑我们会发现我们的菜品数据库当中确实添加成功了菜品数据:

并且口味数据库也添加成功了数据,并且客户对上面的那个菜所选择的几种口味也成功关联了菜的id(这样就知道这个口味数据库中的口味对应的是哪个菜了,如果不关联,那他妈谁知道这个口味是哪个菜啊,太牛逼了把):

十六、菜品管理----菜品信息分页查询 【多表查询重点】

16.1、需求分析

16.2、代码开发

首先我们要知道(和我们写员工管理时的模糊条件查询一样的):

注意:springboot分页查询需要设定一个分页查询拦截器(要不设定的话,尽管调用的是分页查询的功能,但是却拿到的是数据库中的全部数据信息),这个分页查询拦截器的写法是固定的。忘记的话就看springboot整合ssmp的笔记中的分页查询。

代码如下所示:

    /**
     *  2、分页查询功能
     */
    /**
     *  4、员工分页查询
     *
     *      注:一定要保证参数名和客户端请求资源的url属性名一致,要不然客户端的资源数据封装不到参数名当中。
     *
     *      int page    : 第page页
     *      int pageSize    : 一页中查询pageSize条数据
     *      String name  : 模糊条件查询
     */
    @GetMapping(value = "/page")
    public R page(int page,int pageSize,String name){

        /**
         *  构造分页查询构造器
         *      泛型是Dish的原因: 是因为客户端想要分页查询的数据就是Dish实体类对应的dish数据库中的数据,因此泛型使用Dish就可以说整体的sql语句就是对dish数据库进行操作的。
         */
        IPage<Dish> page1 = new Page<>(page,pageSize); // 接收到前端传递的参数为1,10

        /**
         *  构造条件构造器
         */
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();

        // 添加过滤条件 (相当于为sql拼接like语句)
        queryWrapper.like(StringUtils.isNotEmpty(name),Dish::getName,name); // 客户端传递的条件查询的name不为null时,对name进行条件分页查询,

        // 添加排序条件 (相当于为sql拼接order by语句)
        queryWrapper.orderByAsc(Dish::getUpdateTime);   // 根据修改时间进行降序排列(也就是说等会前端拿到数据之后,这个修改时间的地方是以降序的形式排序的)

        //  调用业务层分页查询功能方法
        dishService.page(page1,queryWrapper);

        return new R(1,page1);  // 把查询出来的数据响应给前端,前端拿到数据后就可以展示在页面上了。
    }

通过上面的代码逻辑,把dish数据库中的数据查询出来后,后端就可以响应给前端查询出来的这些数据了,然后前端拿到这些数据之后,就可以把这些数据展示在前端的页面上了:

但是需要思考的是:为什么菜品分类没有数据呢?

    这是因为我们后端对dish数据库查询出来的数据中,是没有菜品分类的数据的,我们dish数据库表中只有菜品分类数据对应的id(我们把这个id响应给前端了,前端也不知道是啥玩意,这他妈一串什么玩意),因此后端对数据库dish查询出来的数据响应给前端的时候,前端自然也就拿不到菜品分类的数据:

而前端想要的并不是dish数据库中响应的一串菜品分类的id,前端想要的就是下面菜品分类的名称,也就是下面的name字段的数据,但是呢这个name字段名字又TM不在dish的数据库当中,是category数据库中的。但是两张表category_id字段数据和id字段数据是有关联的,因此就可以想办法,把下面这张不是dish表的name字段对应的数据响应给前端让前端展示菜品分类名称。

我们还要知道前端是通过拿到后端响应的数据进行在页面上展示的,前端的代码逻辑是这样的:

因此我们也可以看出,前端对菜品分类展示的数据对应的后端名称是categoryName字段,因此需要我们后端实体类当中有这个categoryName属性,那么前端才能从这个属性当中获取数据,而这个属性呢在我们dish实体类当中又他妈没有(其他的售卖状态、最后操作时间等实体类当中都是有的,所以前端能够获取到这几个数据就是因为没有categoryName属性,因此菜品分类的数据获取不到)

因此就一句话,前端如果想获取到分类名称的数据,那么就需要我们后端实体类当中有这个对应着前端分类名称的属性categroyName,要是没有这个属性的话,怎么封装数据库中的数据响应给前端进行页面展示呢?(注意除了categroyName属性没有,其他的前端页面对应的属性Dish实体类当中都是有的)

因此解决思路就是还用DTO思想:

package com.Bivin.reggie.controller;

import com.Bivin.reggie.common.BusinessException;
import com.Bivin.reggie.dto.DishDto;
import com.Bivin.reggie.pojo.Category;
import com.Bivin.reggie.pojo.Dish;
import com.Bivin.reggie.service.impl.CategoryServiceImpl;
import com.Bivin.reggie.service.impl.DishFlavorServiceImpl;
import com.Bivin.reggie.service.impl.DishServiceImpl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

/**
 *  菜品管理表现层
 */

@RestController // @Controller和@ResponseBody注解 (表现层bean注解和响应前端数据注解)
@RequestMapping(value = "/dish")
@Slf4j
public class DishController {

    /**
     *  把菜品表和菜品口味表的业务层对象注入进来
     */
    @Autowired
    private DishServiceImpl dishService;
    @Autowired
    private DishFlavorServiceImpl dishFlavorService;

    @Autowired
    private CategoryServiceImpl categoryService;

    /**
     * * 1、添加菜品功能
     *
     *      客户端发送的post请求: http://localhost:8080/dish
     *
     *      思路: 用DTO思想创建的实体类接收客户端请求时携带的json数据,
     *
     */
    @PostMapping
    public R dish(@RequestBody DishDto dishDto){
        log.info("是否接收到了携带的数据:"+dishDto.toString());

        /**
         *  调用业务层做一些处理逻辑问题
         */
        dishService.DishWith(dishDto);  // 把客户端请求数据时携带的数据传递到业务层的DishWith方法中。

        return new R(1,"添加成功");
    }

    /**
     *  2、分页查询功能
     */
    /**
     *  4、员工分页查询
     *
     *      注:一定要保证参数名和客户端请求资源的url属性名一致,要不然客户端的资源数据封装不到参数名当中。
     *
     *      int page    : 第page页
     *      int pageSize    : 一页中查询pageSize条数据
     *      String name  : 模糊条件查询
     */
    @GetMapping(value = "/page")
    public R page(int page,int pageSize,String name){

        /**
         *   构造分页构造器
         */
        IPage<Dish> page1 = new Page<>(page,pageSize);  // 我们知道这个分页构造器把分页查询出来的数据都是封装到Dish属性当中了。

        // 因为上面那个泛型Dish分页查询出来封装的数据,实体类当中没有categoryName属性,因此分页查询出来的数据page1响应给前端的时候,前端是拿不到该属性对应的数据的,因此就无法展示出菜品分类。

        /**
         *  构造条件构造器
         */
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();

        // 添加过滤条件 (相当于为sql拼接like语句)
        queryWrapper.like(StringUtils.isNotEmpty(name),Dish::getName,name); // 客户端传递的条件查询的name不为null时,对name进行条件分页查询,

        // 添加排序条件 (相当于为sql拼接order by语句)
        queryWrapper.orderByAsc(Dish::getUpdateTime);   // 根据修改时间进行降序排列(也就是说等会前端拿到数据之后,这个修改时间的地方是以降序的形式排序的)

        //  调用业务层分页查询功能方法
        dishService.page(page1,queryWrapper);

        /**
         *  从上面dishService.page(page1,queryWrapper);这个调用功能,就说明page1分页查询功能查询出来的数据已经封装到了
         *  Page对象的源码records集合当中了,这也就是下面为什么IPage<Dish> page1 = new Page<>(page,pageSize);
         *  此时上面 IPage<Dish> page1 = new Page<>(page,pageSize); 这个分页构造器已经拿到了除了前端想要的categoryName除外的所有数据了并且数据就是以Dish对象的形式封装在了源码records集合中。
         */

        // 1、构建一个DTO分页构造器
        Page<DishDto> dishDtoPage = new Page<>();   // 我们这里再构造一个分页查询构造器,注意:这个分页查询构造器是查询不出来数据的,因为new Page<>()
                                                    // 后面并没有传入页码和页码对应的展示数量数,也就是说这个分页构造器里面什么都没有,相当于一个空壳

        /**
         *   首先我们要知道,DishDto实体类是Dish实体类的他哥,
         *   (因为DishDto继承了Dish实体类,所以DishDto实体类当中有了Dish实体类的属性包括DishDto实体类中自己的categoryName属性)
         *
         *  我们如果想让前端拿到categoryName对应的数据,就需要把数据封装到DishDto实体类的属性当中然后响应给前端相对应的数据,那么前端就可以拿到所有的数据进行展示了。
         *
         *      思路:
         *          我们知道IPage<Dish> page1 = new Page<>(page,pageSize); 这个分页构造器已经拿到了除了前端想要的categoryName除外的所有数据了,
         *          因此我们就可以用拷贝源的方式,我们知道page1分页构造器已经拿到了基本的数据了,那么就肯定拿到了dish数据库中的对应着菜品分类数据的id了,
         *          因此我们就可以把page1分页查询出来的数据和菜品分类对应的id查询出来的菜品分类数据拷贝到dishDtoPage的分页查询构造器当中(因为这个构造器相当于一个空壳)
         *          现在我们把数据都拷贝过去了,那么前端就能拿到原始的除categoryName的所有数据和categoryName对应的分类管理数据了。
         */

        // 2、把page1分页查询构造器的属性copy到dishDtoPage分页查询构造器中(这时的dishDtoPage还是一个空壳呢)
        BeanUtils.copyProperties(page1,dishDtoPage,"records");   // 这里就是把page1的属性先拷贝到dishDtoPage构造器
                // 这里我们知道page1构造器中已经有分页查询出来的数据了,并且数据protected List<T> records;;已经封装在records集合当中了,(点击page1的Page查看源码)
                // 因此我们这里拷贝的时候后面加上了“records”的目的就是不拷贝page1构造器中分页查询出来的数据,其他的属性都先拷贝上,就相当于先克隆个page1的壳子

        // 3、既然page1构造器已经拿到了除了前端想要的categoryName除外的所有数据了,那么我们就把page1分页查询出来封装在List<OrderItem> orders()集合中的数据先获取出来
              // 也就是说先把page1分页查询出来的数据拿出来,我们需要用category_id字段获取那个有菜品分类的name数据
        List<Dish> records = page1.getRecords();    // 把page1分页查询出来的数据获取出来了,
                    // (我们知道该List集合当中储存的就是分页查询出来的那些数据一个一个封装在Dish实体类属性当中以Dish对象的形式封装在List集合当中【就是页面展示出来的那些数据,其实底层是分页查询出来的数据是以一条一条封装在实体类属性当中然后封装在集合当中的】)

        // 4、因此我们就可以遍历该List集合,把page1分页查询出来的Dish实体类中的categoryId属性数据获取出来(该实体类的categoryId对应dish数据库中的category_id字段)
              // 我们又知道该dish数据库中的category_id字段对应着category数据库中的id字段,该category数据库中就有前端想要的菜品分类数据(也就是category数据库中的name字段数据)
             // 因此知道这个关系后,我们就可以通过获取Dish实体类当中的categoryId属性数据传给category数据库对应的数据层,然后通过id的形式把category数据库中的菜品分类数据查询出来了。
        // 注意:这里的遍历用的是stream流的方式遍历的(增强for也是可以的,我是写不出来了操)

        // 因此我们这里就相当于遍历records, 遍历List集合中封装了分页查询出来的封装在Dish属性中数据的Dish对象 (把封装了分页查询数据的Dish对象一个一个遍历出来)
        List<DishDto> list = records.stream().map((item) ->{    // 这相当于增强for循环

            Long categoryId = item.getCategoryId();// 然后一个一个获取其records集合中封装在Dish实体类的categoryId属性中的数据了
            log.info("dwadwa:"+categoryId);

            // 5、调用有菜品分类数据的category数据库的数据层,然后把上面获取到的categoryId数据与category数据库的id相比较,然后拿到category数据库中的name字段数据(也就是菜品分类数据)
            // Category category = categoryService.getById(categoryId);    // 因此我们就可以让category数据库对应的数据层调用通过id查询数据的方法功能
            Category category = categoryService.getById(categoryId);    // 因此我们就可以让category数据库对应的数据层调用通过id查询数据的方法功能

            /**
             *  我们这里需要进行判断一下,因为有可能category数据库中通过我们传递的categoryId进行id查询的时候,可能会查询不到数据,
             *  如果不判断的话,就会出现BUG,判断查询出来的数据不为null的时候,再进行获取该Category实体类对应的数据库中的name字段数据
             */
            // 7、获取了category数据库中的name字段:代表的就是菜品分类数据后,就直接封装到DishDto实体类的categoryName属性当中即可,
            DishDto dishDto = new DishDto();    // 把DTO实体类对象new出来,供下面封装数据

            if (category != null){
                // 6、上面拿到了category数据库中的数据之后(我们知道category数据库通过categoryId查询出来的数据是封装到在Category对象属性中的),
                //  因此我们这里就可以获取Category实体类中的name属性(相当于category数据库中的name字段:代表的就是菜品分类)来获取菜品分类的数据了。
                String name1 = category.getName();

                dishDto.setCategoryName(name1); // 获取的name菜品分类数据封装到DTO实体类的属性当中
            }
            
            // 如果对应的id查询的category数据为null的话,就还是返回page1分页查询出来的数据而已

            // 8、到了这里我们就知道,DTO实体类的菜品分类数据已经成功封装到了categoryName属性当中了。
            // 此时DTO对应的分页查询构造器中我们知道只有categoryName属性的数据,因此我们也要通过拷贝的方式,把page1分页查询出来的数据封装到DTO实体类当中
                // (因为DTO实体类继承了Dish,因此现在DTO实体类属性中只有categoryName属性有数据,因此我们把page1分页查询出来的封装在records集合中的数据一个一个封装到DTO实体类当中)
            // 做法: 就是我们知道现在我们遍历的就是page1分页查询封装在records集合中的数据,因此我们可以在遍历的过程中,把records集合中的数据一个一个拷贝封装到DTO的属性当中,然后存入到集合中
            BeanUtils.copyProperties(item,dishDto); // item就是遍历出来的一个一个的page1分页查询封装在records集合中的数据对象(Dish对象,分页查询的每条数据都以封装在Dish对象属性当中封装到集合中),然后直接把这一个一个的数据拷贝封装到DTO实体类属性中

            return dishDto; // 到了这一步后,我们就知道dishDto实体类categoryName属性当中已经封装到了菜品分类数据了( dishDto.setCategoryName(name1);),并且也把page1分页查询的数据依次封装到了dishDto实体类
                            // 属性当中了(也就是说继承的Dish实体类的属性,BeanUtils.copyProperties(item,dishDto)),然后依次循环返回给list集合当中。

            // return dishDto; 也可以说把DishDto dishDto = new DishDto(); 当这个dishDto对象通过下面的dishDto.setCategoryName(name1);和
            // 拷贝: BeanUtils.copyProperties(item,dishDto); 把category_name 分类菜品数据和其他的page1分页查询出来的数据封装到该对象的属性当中后,
            // 就返回这个dishDto对象到list集合当中(List<DishDto> list ),最终通过依次循环,把该对象属性中封装的数据全部封装到了list集合当中了,最后再把
            // 这些数据dishDtoPage.setRecords(list); 给dishDtoPage分页查询的records集合当中响应给前端即可了,前端就都能获取到了。

                }).collect(Collectors.toList());

        dishDtoPage.setRecords(list);   // 最终再把list集合中封装的DishDto实体类(该实体类属性中封装完成了前端所要的全部数据),设置到dishDtoPage分页查询
                                        // 所对应的查询出来封装在records储存的records集合当中,那么就相当于dishDtoPage分页查询出来了所有前端想要的数据了。

        return new R(1,dishDtoPage);  // 因此这里再把分页查询储存在records集合当中的前端想要的数据响应给前端即可。
    }

}

最终前端效果展示:

十七、菜品管理----修改菜品

17.1、需求分析

最终回显的结果如下所示:

结论: 就是需要拿着客户端想要删除的id值,对菜品表和口味表进行查询操作。

步骤如下:

我们知道我们点击修改菜品后,前端页面给我们就展示了如上的形式,会发现页面上没有回显数据,因此我们需要先把想要修改的菜品数据回显在上面的格式里面,然后再进行修改操作:

那么我们点击修改按钮后:就会跳转到第一张没有回显的图上了:

因此前端就先通过拿想要删除的辣子鸡的id,进行向后端发送资源请求,先把辣子鸡在dish数据库中对应的整条数据通过后端查询出来并且把该辣子鸡对应的口味表中的口味查询出来后响应给前端,然后前端先把该辣子鸡的数据回显在页面上后,再供客户进行修改:

17.2、代码开发1 ---- (页面数据回显)

表现层:

 /**
     *  通过前端传递的菜品id,查询所对应的菜品数据和口味数据,将数据回显在页面上
     *
     *      客户端传递的get请求: http://localhost:8080/dish/1397849739276890114
     */
    @GetMapping(value = "/{id}")
    public R dish(@PathVariable Long id){

        // 拿到前端传递请求删除的菜品id后,调用业务层,在业务层做一些逻辑处理。
        DishDto dishDto = dishService.dishWith(id);
        return new R(1,dishDto);
    }

业务层:

/**
     *  通过前端传递的菜品id,查询所对应的菜品数据和口味数据,然后把查询出来的数据响应给前端页面
     *
     *  思路: 我们知道客户端页面上回显的数据有两个分块,一个是菜品的数据回显,一个是菜品的口味数据回显。
     *          又我们知道菜品数据库中的id字段是和菜品口味表中的dish_id有关联的,因此我们就可以做一些逻辑,
     *          把客户端传递删除的菜品id,对菜品数据库和菜品口味数据库都做一下查询操作,然后将数据封装到DTO
     *          实体类的属性当中响应给前端
     *
     *       思考:这里为什么不把查询到的数据封装到口味实体类Dish属性中响应给前端页面呢?
     *              因为前端要的不仅有菜品数据库查询出来的数据,还有口味数据库查询出来的数据,Dish实体类属性不够啊,
     *              只有DTO对象能封装这两块页面想要的数据,因此只能响应DTO对象
     *              (因为DTO对象当中有一个专门封装口味的属性:private List<DishFlavor> flavors = new ArrayList<>();
     *                  又因为DTO继承了Dish菜品实体类,因此也有Dish的属性,因此可以把两块数据都封装到DTO属性当中。
     *              )
     * @param id
     * @return
     */
    public DishDto dishWith(Long id){

        DishDto dishDto = new DishDto();

        // 1、通过该id查询菜品数据库中所对应的该客户端传递的菜的数据
        Dish dish = dishService.getById(id);    // 将菜品数据库中的数据获取后,拷贝到DishDto实体类的属性当中(因为DTO对象继承了Dish实体类,因此Dish实体类中的一些属性也继承了,所以可以把dish查询出来的数据拷贝封装到DTO对象属性中,上面new好DTO实体类对象了,此时DTO对象属性数据都为null,因为直接把通过id查询出来的一条dish数据拷贝进去就可以了)
        BeanUtils.copyProperties(dish,dishDto); // 将客户端传递的id查询出来菜品数据库对应的数据后,将该查询出来的菜品数据封装到DTO实体类的属性当中

        // 通过上面的第一步:我们就知道此时的DTO实体类中的属性中已经封装好Dish数据库中查询出来的数据了。
        // 接下来的任务就是把该菜品id对应的口味数据库表中的口味数据获取出来封装到DTO的专门封装口味数据的list集合属性当中,
        // 那么我们的DTO实体类属性当中就封装到了客户端想要展示的那两块数据了(口味数据和菜品数据)

        // 2、通过该菜品id查询口味数据库中所对应的该客户端传递的菜品的口味
        LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>(); // 对菜品口味数据库对应的实体类DishFlavor,进行条件查询
        lambdaQueryWrapper.eq(DishFlavor::getDishId,dish.getId());
        // 做的是等于比较判断(因为我们通过Dish dish = dishService.getById(id);查询出来的这条菜品的数据,此菜品id(直接通过查询出来的数据后dish.getId获取即可)所对应在口味表中的口味有可能
        // 会有多种口味,也就是多条口味数据,因此这里做eq判断,也就是说和查询出来的菜品id一样的口味dish_id,就把一样的那些口味数据都拿出来)
        //  然后就可以调用口味数据层查询所能满足条件构造器条件的数据了。
        List<DishFlavor> list = dishFlavorService.list(lambdaQueryWrapper);

        // 3、 最终我们就可以将查询出来的该菜品id在口味数据库中对应的多条口味数据封装到DTO实体类的flavors集合数据当中,
        // 最终DTO实体类属性当中即封装到了该id对应的菜品,又封装到了该id菜品对应的口味数据库中的口味数据了,因此直接响应给前端,让前端拿到数据展示在页面上就行了。

        dishDto.setFlavors(list);   // 简单点说:其实也就是把查询出来的List<DishFlavor> list 口味集合数据,封装到DTO实体类的一个list集合当中而已。然后一块把数据响应给前端而已。

        return dishDto; // 该DTO对象属性当中已经封装好了菜品数据和该菜品对应的口味数据,返回响应给前端即可
    }

最终前端拿到数据后页面展示如下所示:(确实展示出来了。)

补:

17.3、代码开发2---- 修改菜品

假设我们现在想要修改的菜品就是辣子鸡这个菜品,我们点击辣子鸡对应的修改按钮后,会知道首先会先通过辣子鸡的id对数据库进行查询辣子鸡的数据然后展示在前面页面上:

此时假设我们想修改这个辣子鸡的菜品数据,想修改的样式如下所示:

此时我们修改了辣子鸡的数据之后,点击提交按钮后,前端页面就会带着我们刚修改的菜品数据向后端发送请求(注意请求携带的id值,还是以前没修改之前的id值):

代码如下所示:

表现层:

 /**
     *  功能:修改菜品。
     *
     *      客户端发送的PUT请求:http://localhost:8080/dish
     *
     *      思路: 首先我们知道,客户端在页面上再怎么修改菜品数据,无非就是修改菜品的名字,价格,图片和描述,还会有可能再修改一些菜品的口味。
     *          因为又涉及到了菜品和口味,因此我们这里还是相当于操作两种数据库表。
     *          首先我们需要先把客户端修改的菜品和该菜品对应的口味数据先在后端拿到,因此我们用菜品实体类Dish或者口味实体类都是不行了,因为这两个实体类接收不完
     *          客户端请求的数据,
     *
     *          因此我们还用DTO实体类进行接收客户端发送的请求资源 (因为我们知道DTO实体类当中有专门封装口味数据的flavors集合属性,又因为这个DTO实体类
     *              继承了Dish菜品实体类,因此就继承了菜品实体类Dish的所有属性,因此这个DTO实体类就可以封装前端请求的菜品数据和口味数据了)
     *
     */
    @PutMapping
    public R update(@RequestBody DishDto dishDto){  // RequestBody注解: 接收前端传递的json数据注解

        log.info("dwadwa:"+dishDto);
        // 调用业务层,在业务层做一些逻辑处理。
        dishService.UpdateDish(dishDto);

        return new R(1);
    }

业务层:

 /**
     *  功能:修改菜品。
     */
    public void UpdateDish(DishDto dishDto){    // 这里拿到客户端请求修改的菜品和该菜品对应的口味数据的(dishDto属性当中)

        //  我们从客户端发送请求的时候知道,客户端无论怎么修改菜品,其传递到后端的id还是以前没有修改前(辣子鸡)在数据库中对应的id字段的。
        // 1、因此我们就可以通过客户端传递过来的那个修改前的菜品id,在数据库当中通过该id,把该id对应的辣子鸡数据更新成新修改的数据。
        dishService.updateById(dishDto);// DTO对象当中封装的有那些新修改的数据,因此我们这里直接调用通过id修改数据库中id对应的辣子鸡字段数据即可。

        // 我们知道客户端新修改的菜品其实就是占据了没修改前的辣子鸡的数据位置而已,修改的新的菜品数据的id其实还是辣子鸡以前所在的id的,
        // 这里我们知道以前辣子鸡菜品的时候对应的菜品口味在我们这次修改菜品的时候,有可能口味也会修改了。

        // 2、因此我们这里可以先通过客户端请求时携带的该修改的菜品数据对应的id字段,把对应的以前口味数据库中的口味数据清空掉。
        LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(DishFlavor::getDishId,dishDto.getId()); // 我们知道DishFlavor口味数据库中的dish_id口味数据是关联着菜品dish的id的,
                                                        // 要不然谁他妈知道这个口味是哪个菜啊,这里的条件构造器就是把客户端请求修改的菜品id对应在口味数据库中的
                                                        // 以前的老口味给查询出来然后清空掉,(一个菜有可能对应几种口味,因此用eq条件形式,把和菜品id相关联的
                                                        // 口味数据都拿出来   最后清空掉这些老口味   )
        // 4、把查询出来的老口味数据直接在数据库中清空掉
        dishFlavorService.remove(lambdaQueryWrapper);// 清空DishFlavor数据库的remove方法,加上上面的where 菜品id 和 口味相同的id条件进行清除

        // 5、老口味清空之后,再把客户端新修改的口味添加到口味数据库当中,并且把该菜品id也存入到口味数据库的dish_id字段上,如果不添加菜品的id,那他妈谁能知道这口味对应的是哪个菜
        // 5.1、因为一个菜品口味有可能有几好几种,因此我们这里先把客户端请求过来的口味数据拿到手
        List<DishFlavor> flavors = dishDto.getFlavors();    // 我们知道口味数据是以封装在DishFlavor实体类属性当中然后封装在DTO的flavors集合属性当中的

        // 5.2、遍历集合,获取到DishFlavor实体类(因为我们知道客户端请求的口味数据都是封装在该DishFlavor实体类的属性当中的)
        for (DishFlavor dishFlavor:flavors){
            // 5.3、获取到一个个DishFlavor实体类对象,然后我们就可以为其一个个封装了口味数据的DishFlavor对象的dish_id属性上再set个菜品的id
            // 因此每个封装口味数据的DishFlavor实体类对象当中都有了相关联的菜品id了,因此再调用口味数据层调用保存功能的时候,在数据库中也知道这个口味数据对应的是哪个菜品id了
            dishFlavor.setDishId(dishDto.getId());  // 把客户端新传递过来的(也就是修改好的)菜品id,封装到封装了口味数据的DishFlavor对象属性中

            // 6、调用口味数据层,将口味数据保存到数据库当中
            dishFlavorService.save(dishFlavor); // 将对象一个一个再保存到数据库当中即可。
        }

    }

假设就是修改的辣子鸡的数据:把辣子鸡变成辣子鸡12345:

前端页面确实展示成功了:

后端数据库也都修改成功:

==================套餐管理==================

十八、套餐管理----新增套餐

18.1、需求分析

18.2、数据模型

套餐表结构:

注:setmeal是套餐表,setmeal_dish: 套餐菜品关系表

18.3、代码开发----前期准备

前期准备:

     **因为涉及到setmeal是套餐表,setmeal_dish: 套餐菜品关系表,两张表,因此我们先把该两张表对应的实体类、表现层(表现层可以先不写)、业务层、数据层骨架搭出来。**

【也就是说以后项目开发中,只要有表,就先把表对应的实体类、业务层、数据层、(表现层)骨架先搭建出来,然后想获取表中数据的话,就直接在骨架中写代码就能获取了。】

** 并且通过下面套餐表和套餐菜品关系表可以看出,套餐表和套餐菜品关系表是通过id键和setmeal_id做关联的。**

18.4、新增套餐代码开发

首先我们要知道:当客户端点击

新建套餐按钮时,套餐分类那个地方的代码逻辑,前端已经向后端发送过请求了,并且我们后台已经做过了该请求逻辑的代码了,因此套餐分类那里已经查询到了数据了:

现在我们要做的就是一点一点的向后端发送请求获取数据进行添加套餐:

我们点击添加菜品按钮时:我们会知道的效果如下所示:

我们要知道周口菜套餐表中的id字段是对应菜品表中的category_id字段的,也就是说当我们选择周口菜这个套餐的时候,我们就可以通过周口菜在数据库表中对应的id字段,通过在菜品表的category_id字段进行比对,把菜品表中的category_id字段和周口菜套餐对应的id一致的菜品数据都查询出来响应给前端页面,那么就能获取到所有是周口菜套餐里面的菜了。(同理其他的菜品套餐对应的其他的菜品数据也是可以获取出来的,如获取川菜套餐里面的所有的菜品,也是通过前端传递的川菜套餐id和菜品里面的category_id字段做比较,然后把查询到的所有川菜套餐里面的菜品都查询出来响应给前端页面):

因此前端页面通过传递请求携带着套餐的id(如周口菜套餐的id),我们就可以拿着这个套餐id和菜品数据库中的category_id作比较了,把菜品数据库中category_id字段和套餐id对应的菜品数据都响应给前端,那么也就可以说如果前端选择传递的是周口菜套餐id,那么我们后台就把周口菜套餐中所有的菜品都响应给前端页面了:

代码如下所示:

 /**
     *     假设客户端就拿着周口菜套餐的id进行GET请求:
     *          http://localhost:8080/dish/list?categoryId=1397844263642378242
     *
     *     思路: 我们的最终目的就是:获取周口菜套餐里面所包含的所有菜品数据并响应给前端页面。
     */
    @GetMapping(value = "/list")
    public R list(Long categoryId){ // 该参数接收到了客户端传递过来的周口菜套餐在数据库中对应的id值

        /**
         * // 我们知道我们获取到的前端请求传递的categoryId字段数据,就是对应在菜品套餐里的id(周口菜在数据库中对应的id了)了,
         *         // 因此我们这里就可以进行菜品数据库中的category_id与客户端传递的菜品套餐id作比较了,
         *         // 就可以把周口菜套餐所包含的所有菜品数据获取出来了。
         */

        // 1、构造条件构造器
        // (我们这个构造器的逻辑是对菜品dish数据库中的category_id字段和套餐数据库中的周口菜id相同的数据都获取出来)
        LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();   // 泛型是Dish的原因:是因为我们就对dish表进行条件查询的
        lambdaQueryWrapper.eq(Dish::getCategoryId,categoryId);  // 加上菜品数据库中category_Id和套餐数据库中对应的周口菜套餐id 相同的条件
        //  上面的条件构造器就相当于拼接这么一个sql语句: WHERE (category_id = categoryId)

        // 2、 条件构造好之后,调用菜品查询功能即可。
        List<Dish> list = dishService.list(lambdaQueryWrapper); // 就可以把菜品数据库中的对应着周口套餐的所有菜品都查询出来了。

        return new R(1,list);   // 把在菜品dish数据库中查询出来的周口套餐下的所有菜品响应给前端
    }

因此优化后的代码如下所示:

 /**
     *     假设客户端就拿着周口菜套餐的id进行GET请求:
     *          http://localhost:8080/dish/list?categoryId=1397844263642378242
     *
     *     思路: 我们的最终目的就是:获取周口菜套餐里面所包含的所有菜品数据并响应给前端页面。
     */
    @GetMapping(value = "/list")
    public R list(Long categoryId){ // 该参数接收到了客户端传递过来的周口菜套餐在数据库中对应的id值

        /**
         * // 我们知道我们获取到的前端请求传递的categoryId字段数据,就是对应在菜品套餐里的id(周口菜在数据库中对应的id了)了,
         *         // 因此我们这里就可以进行菜品数据库中的category_id与客户端传递的菜品套餐id作比较了,
         *         // 就可以把周口菜套餐所包含的所有菜品数据获取出来了。
         */

        // 1、构造条件构造器
        // (我们这个构造器的逻辑是对菜品dish数据库中的category_id字段和套餐数据库中的周口菜id相同的数据都获取出来)
        LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();   // 泛型是Dish的原因:是因为我们就对dish表进行条件查询的
        lambdaQueryWrapper.eq(Dish::getCategoryId,categoryId);  // 加上菜品数据库中category_Id和套餐数据库中对应的周口菜套餐id 相同的条件
        //  上面的条件构造器就相当于拼接这么一个sql语句: WHERE (category_id = categoryId)

        /**
         *  优化:再添加一个判断菜品status状态的条件构造器  (0:代表停售 、 1:代表在售)
         *
         */
        lambdaQueryWrapper.eq(Dish::getStatus,1);   // 也就是说在sql语句上再拼接一个status为1的sql语句

        // 2、 条件构造好之后,调用菜品查询功能即可。
        List<Dish> list = dishService.list(lambdaQueryWrapper); // 就可以把菜品数据库中的对应着周口套餐的所有菜品都查询出来了。

        return new R(1,list);   // 把在菜品dish数据库中查询出来的周口套餐下的所有菜品响应给前端
    }

选择好之后点击确定就可变成下面的页面形式了。

这里的逻辑处理好之后,接下来我们就可以进行新增套餐了:

注意:接下来的操作就和菜品管理新增菜品的过程是一样的了。

分析前端页面添加的数据是怎么分配到后端数据库的:

携带的数据如下所示:(注意:这些数据是json格式的)

重点之处:(套餐表和套餐菜品表之间的关系【和菜品管理添加菜品时一样,一个菜品对应多种口味,但是要在口味表中关联着菜品,要不然谁知道他妈这些添加的口味是哪个菜的,这里也一样套餐菜品表中的数据也要关联着套餐表中的数据,要不然谁知道这些菜是他妈哪个套餐里面的。】)

因此我们到底该如何接收客户端请求携带的上面的那些数据呢?用套餐表对应setmeal实体类接收?那肯定是不行的,因为用套餐表setmeal实体类接收的话,那些紫色的套餐菜品的数据就无法接收到了(也就是说,套餐表setmeal实体类当中没有那些接收套餐菜品的数据属性),同理,用套餐菜品表对应的setmealDish实体类接收上面的客户端请求的数据也是不行的,因为又接收不到套餐数据了,也就是红色圈起来的数据。

所以我们到底该怎么解决这种接收不在同一条数据库中的前端请求携带的数据呢?

使用DTO思想。

    DTO思想简单点说:(就是创建一个能封装前端请求携带的全部数据的实体类)

DTO实体类格式如下所示:

package com.Bivin.reggie.dto;

import com.Bivin.reggie.pojo.Setmeal;
import com.Bivin.reggie.pojo.SetmealDish;
import lombok.Data;
import java.util.List;

@Data
/**
 *  还是和菜品管理时新增菜品的操作是一样的。
 *
 *      1、  这里因为DTO实体类继承了Setmeal实体类,我们知道Setmeal实体类中的属性是专门用来封装套餐表数据的
 *          (也就是我们前面客户端传递的数据红色框起来的那部分数据,是可以封装到Setmeal实体类属性当中,然后存入到setmeal套餐表数据库中的)
 *          因此该DTO实体类继承了Setmeal实体类,也就是说该DTO实体类就有了Setmeal实体类的属性,可以说该DTO实体类可以接收前端请求传递的套餐数据了(红色框起来的)
 *
 *      2、  又因为这个DTO实体类有一个setmealDishes集合属性,又因为集合泛型是SetmealDish实体类,我们知道SetmealDish实体类中的属性就是专门封装客户端请求携带过来
 *           的那套餐菜品数据的(也就是紫色部分的数据),因此接收过来后,就可以保存到setmeal_dish数据库当中了(因为setmeal_dish数据库对应的实体类就是SetmealDish)
 *           又因为套餐菜品数据有可能是多条数据,因此我们就把该接收套餐菜品数据的属性用了集合的形式,也就是说把客户端请求的数据封装到SetmealDish实体类属性当中,然后以
 *           对象的形式封装在List集合中。最终这个DTO实体类就能接收封装到前端传递过来的套餐菜品的所有数据了。
 *
 *              3、这就是客户端请求携带的四条套餐菜品数据:
 *                  这些数据的 copies、dishId、name、price....对应的就是套餐菜品实体类SetmealDish的属性,因此可以把这客户端请求的数据封装在SetmealDish的属性中
 *                  然后以对象的形式存入到List集合中 (因为数据多,所以以集合的时候储存)
     *
     *              0: {copies: 1, dishId: "1397854865672679425", name: "鱼香炒鸡蛋", price: 2000}
     *              1: {copies: 1, dishId: "1397854652581064706", name: "麻辣水煮鱼", price: 14800}
     *              2: {copies: 1, dishId: "1568582209071792129", name: "毛蛋", price: 2000}
     *              3: {copies: 1, dishId: "1413385247889891330", name: "米饭", price: 200}
 *
 *
 *      总结: 因此最终我们这个DTO实体类就可以接收到前端传递请求时携带的所有数据了。
 */
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;

    private String categoryName;
}

业务代码如下所示:

表现层:

 /**
     *  新增套餐
     *      客户端传递的POST请求:http://localhost:8080/setmeal
     *
     */
    @PostMapping
    public R save(@RequestBody SetmealDto setmealDto){    // 使用DTO实体类接收客户端请求携带的所有数据
        
        // 调用业务层,在业务层做一些逻辑的处理。
        setmealService.saveWithDish(setmealDto);

        
        return new R(1);
    }

业务层:

 /**
     *  新增套餐
     */
    public void saveWithDish(SetmealDto setmealDto){      // 拿到客户端请求保存携带的数据后,做下面几个步骤的逻辑

        log.info("dwad:"+setmealDto);
        // 1、把封装在DTO实体类当中的套餐表中的数据先添加到套餐表setmeal当中
       setmealService.save(setmealDto);

        Long id = setmealDto.getId(); // setmealDto.getId()  通过DTO实体类就能拿套餐id,因为DTO实体类继承了套餐实体类,因此也继承了套餐实体类的id属性

        // 2、把客户端传递过来封装在DTO实体类setmealDishes属性当中的套餐菜品数据遍历出来,为其添加上套餐表中套餐对应的id
        // (要不然直接把客户端请求的套餐菜品数据保存到套餐菜品表当中了,那他妈谁知道这几个菜是哪个套餐的,因此遍历出
        //  DTO :List<SetmealDish> setmealDishes属性当中封装的套餐的几个菜品数据对象,然后为其setmeal_id加上套餐的id
        //  id字段,然后再保存到套餐菜品表当中,这样就知道这些菜品是哪个套餐里面的菜了)
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();    // 获取到了客户端请求携带封装在DTO实体类setmealDishes属性集合当中的套餐菜品数据

        // 遍历其封装在DTO实体类属性集合当中的套餐菜品数据,(在集合当中是以套餐菜品实体类setmealDish实体类对象形式封装在List集合当中的)
        for (SetmealDish setmealDish:setmealDishes){    // setmealDish :这个就是集合里面封装的套餐菜品数据的对象,我们可以为其对象的setmeal_id属性设置上套餐id,
                                                        // 最后再保存到套餐菜品数据库当中那么套餐菜品数据库当中的setmeal_id字段就和套餐的id字段一样了,
                                                        // 我们就知道这几个菜是哪个套餐里面的菜了哈哈

            // 3、获取到一个个封装的套餐菜品数据后,为其套餐菜品表dish_id属性设置上套餐id,然后再一起保存到套餐菜品数据库当中
            setmealDish.setSetmealId(id);  // 将套餐id设置到套餐菜品的setmealId属性当中(就相当于设置到套餐菜品表的setmeal_Id)
            // 设置好后,我们就知道套餐菜品表中的这几个菜是他妈哪个套餐里面的菜了。

            // 4、调用套餐菜品数据层进行保存数据功能
            setmealDishService.save(setmealDish);

        }

假设客户端想要添加的套餐,和套餐里面对应的几个菜如下所示:

会发现:套餐数据和套餐菜品数据确实添加到指定的数据库当中来 :

并且套餐菜品数据库中的数据setmeal_id确实关联到了套餐数据库中的id,因此就知道这几个菜是哪个套餐里的菜了哈哈。

十九、套餐管理----分页查询

逻辑其实和菜品管理的分页查询思路和代码是大差不差的(直接看菜品管理的分页查询笔记即可)。

二十、套餐管理----删除套餐功能

20.1、需求分析

注意要求:对于售卖状态套餐为起售状态时,需要先转换成停售然后才能删除该套餐。

20.2、代码开发准备

首先我们要先熟悉sql语句in的用法:

业务层代码:

/**
     *  删除功能
     *      因为我们知道客户端传递过来的删除请求,有可能是删除一个套餐数据,也有可能批量删除数据,因此我们可以用数组或者集合接收客户端传递删除的套餐id
     *
     *          注意: 不过需要注意的是: 如果使用集合的形式来接收客户端发送的请求数据的时候,要加上@RequestParam注解,要不然就报错了
     * @return
     */
    @Transactional  // spring事务注解
    public void deleteDish(List<Long> ids){    // 因为ids是套餐的id,是Long型的,因此泛型用Long,表示该List集合中只能出存储Long型数据
 
        /**
         *  首先我们通过分页查询的时候就知道,这个页面上展示的数据是两个数据库中的数据,(一个是套餐表、一个是套餐菜品表)
         *  因此我们在删除套餐数据的时候,不仅要删除掉在套餐数据库中的套餐数据,还要把对应的套餐里的菜品也要删除掉。
         */
        // 第一步:我们知道不是停售状态的套餐数据,我们是不让客户端删除该套餐数据的。
        // 因此我们需要先把客户端传递的想要删除的套餐id,对套餐数据库进行查询功能
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(ids!=null,Setmeal::getId,ids);  // 相当于拼接一个这样的sql语句条件:where id in (ids);
        // 也就是说把客户端传递想要删除的套餐id对应在数据库中的套餐数据先获取出来。
 
        // 我们知道上面in条件查询出来的数据就是客户端想要删除的数据了,又因为我们知道如果客户端想要删除的套餐数据出售状态是在售的状态(status=1)的
        // 时候我们是不让客户端删除该套餐数据的,停售状态status=0的时候才让客户端进行删除操作。
 
        // 因此我们这里可以再添加一个状态条件,也就是说添加一个状态status为0的条件
        queryWrapper.eq(Setmeal::getStatus,0);  // 也就是说再为sql语句拼接一个 status = 0;
        /**
         *  通过上面的两个条件的过滤后,我们的sql基本上就成这个类型了: 也就是说刚好可以把客户端想要批量删除的套餐数据并且是停售状态的数据给查询出来了。
         *   SELECT * FROM setmeal WHERE (id IN (?,?,?,?) AND status = ?)   // 满足这个sql条件的话,我们就让客户端进行删除操作,不满足就滚蛋
         */
 
        // 第二步: 因此我们这里就可以先把满足删除套餐条件的数据从数据库中查询出来
        List<Setmeal> list = setmealService.list(queryWrapper); 
        /**     这里调用了setmealService.list(queryWrapper) 就是把满足上面的删除条件的数据库中对应的数据,以封装在Setmeal实体类属性中以对象的形式一个一个封装在List集合当中了
                                                                        */
        
 
        if (list.size()>0){    //   判断集合是否为null,不能 !=null,用list.size()判断集合里面数据是否为0,如果大于0的话,就说明客户端想要删除的数据满足了sql条件了
 
            // 把List集合中满足删除套餐条件封装在Setmeal实体类对象的数据遍历出来,获取其数据在数据库中对应的id字段值
            for (Setmeal item:list){    // 把List集合中封装的Setmeal查询出来的数据对象遍历出来,
                Long id = item.getId(); // 遍历出来后,获得到此想删除的套餐数据的id
 
                // 获取后,就可以通过id字段一个一个删除其满足删除条件的套餐数据了。
                setmealService.removeById(id);
 
                // 然后还可以通过套餐id就可以把对应的在套餐菜品数据库中的菜品删除掉了。(菜品数据有可能多个,因此需要使用条件构造器)
                LambdaQueryWrapper<SetmealDish> queryWrapper1 = new LambdaQueryWrapper<>();
                queryWrapper1.eq(SetmealDish::getSetmealId,id); // 把和上面套餐数据的id一致的套餐菜品数据查询出来,然后调用删除功能删除掉就可以了。
                setmealDishService.remove(queryWrapper1);
 
            }
        } else throw new BusinessException("商品在售中,删除失败~",1);
 
 
    }

本文转载自: https://blog.csdn.net/lwj_07/article/details/126519250
版权归原作者 lwj_07 所有, 如有侵权,请联系我们删除。

“瑞吉外卖项目1 + 源码”的评论:

还没有评论