瑞吉外卖学习笔记

243 阅读53分钟

业务介绍及登录功能开发

哎呀,终于开始做项目了兄弟们,这次我们做的这个项目是一个十分简单的小的外卖项目,做完了之后有收获就行了,这个笔记是用于记录做项目时遇上的难点重点以及我们解决的方法的笔记,以后面试的时候指不定用得上

软件开发整体介绍

首先我们来介绍下我们的软件开发流程,这个自己看图就行了,不多谈了

瑞吉外卖项目整体介绍

接着我们来学习对瑞吉外卖的项目整体的介绍,这个直接看图就行了,如果看不明白,那么就去看黑马视频里的P3,这里我们就不一一赘述了。

然后我们来看看我们的这个项目用得到的技术

然后我们来看看我们的项目所拥有的功能

最后来看一下我们的项目所拥有的可能的角色及其他们应该具有的功能

  • 数据库环境搭建

本章节我们要进行一个数据库环境的搭建,搭建有两种方式,第一种是利用图形方式

我们这里的各种表后续我们用到了再提,我们这里现在自己看一下就行了

开发环境搭建

接着我们来做开发环境的搭建,首先我们要在我们的pom文件中导入对应的坐标,当然这里的前提是我们已经创建好了一个maven工程了,我们往其中的pom文件写入其代码如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
​
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
​
    <groupId>com.itheima</groupId>
    <artifactId>reggie_take_out</artifactId>
    <version>1.0-SNAPSHOT</version>
​
    <properties>
        <java.version>1.8</java.version>
    </properties>
​
    <dependencies>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>
​
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
​
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>
​
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
​
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
​
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
​
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>
​
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build></project>

然后我们导入我们的资源里的yml的配置设置文件,当然我们这里的连接的url是需要更改的,我们这里先提一下,真正需要修改的时候我们再来修改

server:
  port: 8080
spring:
  application:
    name: reggie_take_out
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

然后我们在resources文件夹中加入我们的静态资源和动态资源,一般来说,我们的静态资源是要放到我们的static文件夹中的,但是我们这里没有这么做,那么为了解决这个问题,我们可以在我们的启动类中创建一个配置类文件夹并创建一个配置类,然后写入其代码如下

package com.itheima.reggie.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 设置静态资源映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射...");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }
}

通过上面的代码,我们就可以将网址中的映射正确导向到我们的文件路径中了

最后我们编写一个启动类,然后启动我们的SpringBoot即可

package com.itheima.reggie;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@Slf4j
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class,args);
        log.info("项目启动成功");
    }
}

最后我们要提一下什么是Slf4j,其实就是类似于log4j的日志文件,我们可以这么简单的理解,如果要看更加详细的说明请参照本文档www.jianshu.com/p/6f7f70cc7…

后台系统登录功能实现

首先我们来进行需求分析,我们这里进入到我们的登录页面,按F12进入到控制器,然后我们发送请求看控制器中的记录情况,这里会显示我们发送了一个post请求,并且发送的也是json数据的格式,这些都是老生常谈就不多提了

然后我们来看看我们要用到的数据模型

最后我们来分析下我们的前端页面,前端页面里最重要的是分析这一段代码,我们可以看到我们返回的请求里,有用于判断是否成功的code,有数据内容data以及提示信息msg,也就是说,我们后端构造我们的返回的类的时候,一定要有这三个内容,那么我们就可以得到我们的后端开发目标

接着我们正式到我们的代码开发阶段,我们这里开发就很简单了,因为我们的mybatisplus,简化了我们的许多开发内容

首先我们要创建一个对应的员工实体类并写入其代码

package com.itheima.reggie.entity;

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

/**
 * 员工实体
 */
@Data
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

    private String idNumber;//身份证号码

    private Integer status;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

}

后面的TableField注解我们后面用到的时候再来解释,同时我们的对应属性可以和数据表中对应字段对应上是因为我们开启了我们的驼峰命名法,其会自动去除数据表中字段的下划线并将下划线后的第一个字母改为大写

然后我们创建对应的三个文件夹,对应我们的三层调用,分别是controller、serivce、mapper,其中我们的实体类放置在entity包下

mapper包下的实体类直接令其继承BaseMapper即可

package com.itheima.reggie.mapper;

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

@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}

然后我们来写service包下的内容,首先我们创建对应的service包下的接口,令其继承Iservice

package com.itheima.reggie.service;

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

public interface EmployeeService extends IService<Employee> {
}

然后写入其实现类,令其继承ServiceImpl并且令其实现我们之前设置的接口

package com.itheima.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.mapper.EmployeeMapper;
import com.itheima.reggie.service.EmployeeService;
import org.springframework.stereotype.Service;

@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService{
}

然后我们还需要创建一个通用包,里面存放我们返回的通用结果类,我们可以写入其代码如下,其中的动态数据等到我们用到的时候我们再来讲其作用,这里我们先按下不表

package com.itheima.reggie.common;

import lombok.Data;
import java.util.HashMap;
import java.util.Map;

/**
 * 通用返回结果,服务端响应的数据最终都会封装成此对象
 * @param <T>
 */
@Data
public class R<T> {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }

    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }

}

然后我们来确定下我们的密码的比对逻辑,具体如下图

最后我们进入到controller层的代码如下,写入其对应的注解,然后配置其映射为employee,其下用Autowired注解将对应的对象注入到我们的属性中,我们这里的登录方法里提供了request的请求对象,因为我们将数据注入到我们的对应的共享域中需要用(虽然不也不知道注入进去干嘛,不过总之先记着吧)

package com.itheima.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.service.EmployeeService;
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;

@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    /**
     * 员工登录
     * @param request
     * @param employee
     * @return
     */
    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){

        //1.将提交的密码password进行md5加密处理
        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());

        //2.根据用户名查询数据库
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Employee::getUsername,employee.getUsername());
        //之所以可以选择查询一个是因为在数据表中已经对用户名进行了唯一约束
        Employee emp = employeeService.getOne(queryWrapper);

        //3.如果没有查询到结果则返回登录失败的结果
        if(emp == null){
            return R.error("登录失败,用户名不存在");
        }

        //4.用户名比对成功,进行密码比对
        if(!emp.getPassword().equals(password)){
            return R.error("登录失败,密码错误");
        }

        //5.查看员工状态,若禁用则拦截该登录
        if(emp.getStatus()==0){
            return R.error("该账号已禁用");
        }

        //6.登录成功,将员工id存出Session并返回结果
        request.getSession().setAttribute("employee",emp.getId());
        return R.success(emp);
    }
}

退出功能功能实现

接着我们来实现退出功能,分析前端代码我们容易知道我们的退出功能会向服务器发送一个请求,因此我们可以在控制层中写入我们的退出方法的代码如下

    /**
     * 员工退出
     * @param request
     * @return
     */
    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request) {
        //清理Session中保存的当前登录员工的id
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    }
}

可以看到我们这里做的事情很简单,就是清除共享域中的保存内容并返回退出成功的结果,至于切换页面的事情前端的内容已经帮我们做好了。

最后我们提一下我们这里的PostMapping一类的注解的作用,其作用是指定一个对应的映射路径,和类上的RequestMapping是结合在一起的,结合起来的映射路径最终可以通过一些特定的地址来令其执行我们写好的方法,这个我们一般是通过前端的触发事件来达到这个效果的,点击对应的选项,前端就会自动往后端发送对应地址的请求来执行我们事先设置好的方法

完善登录功能

接着我们要完善我们的登录功能,我们现在的登录功能存在的一个很大的问题是,用户可以不登录,直接通过网页跳转的方式跳转到我们的主页面,这当然是不被允许的,我们要避免这种情况,此时我们应该要使用我们的拦截器对象来帮助我们完成这件事

首先我们创建对应的过滤器类,给其加上对应的WebFilter注解,给其取个名字然后拦截所有的请求,接着我们在主方法中加入ServletComponentScan注解,加上该注解之后我们的拦截器类才可以被正确扫描到,然后我们来看看我们的拦截器的逻辑

根据上面的逻辑,我们可以写入我们的过滤器的代码如下

package com.itheima.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 检查用户是否完成登录
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    //路径匹配器,支持通配符
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //1.获取本次请求的URI
        String requestURI = request.getRequestURI();

        log.info("拦截到请求:{}",requestURI);

        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",//登录
                "/employee/logout",//登出
                "/backend/**",//请求静态资源
                "/front/**"//请求前端静态资源
        };

        //2.判断本次请求是否需要处理
        boolean check = check(urls,requestURI);

        //3.如果不需要处理,则直接放行
        if(check){
            log.info("本次请求{}不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }

        //4.判断登录状态,如果已登录则直接放行
        if(request.getSession().getAttribute("employee")!=null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
            filterChain.doFilter(request,response);
            return;
        }

        log.info("用户未登录");
        //5.如果未登录则返回未登录结果,通过输出流的方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }

    public boolean check(String[] urls,String requestURI){
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url,requestURI);
            if(match) return true;
        }
        return false;
    }
}

我们这里用AntPathMatcher类来辅助我们判断URI地址是否是我们要拦截的,然后我们这里确定了要拦截了地址之后,由于前端已经有拦截器了,因此我们这里不需要手动进行页面的跳转,直接利用前端的页面拦截器进行页面跳转就可以了,这里只需要返回一个JSON格式的错误信息并写着NOTLOGIN即可触发前端的拦截器令页面图跳转到登录页面

最后我们前端的拦截器的代码也可以通过F12直接在网页端进行调试,非常方便

员工管理功能开发

完善了登录功能之后,接着我们来开发我们的员工管理功能,也是我们的第一个大模块的功能。首先我们先来进行员工登录功能的需求分析

员工登录功能分析

我们首先知道我们添加员工的方式就是直接添加一个员工,点击添加会往后端发送一个请求,该请求的地址是直接以employee结尾的POST请求,请求的内容里含有用户提交的数据,那么我们可以整理出我们的步骤如下

那么我们可以写入我们的新增员工的方法在控制层中如下,由于我们在类名中已经加入了统一的employee前缀,因此我们的自定义方法不需要在定义额外的映射了。我们方法里则设置了初始的密码,合并给员工设置了创建和更新的时间以及创建和更新人等数据,最后调用MybatisPlus生成的方法即可完成保存工作了

/**
 * 新增员工
 * @param request
 * @param employee
 * @return
 */
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee) {
    log.info("新增员工,员工信息:{}",employee.toString());

    //设置初始密码为123456,要进行md5加密处理
    employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

    employee.setCreateTime(LocalDateTime.now());
    employee.setUpdateTime(LocalDateTime.now());

    //获得当前登录用户的id
    Long empId = (Long) request.getSession().getAttribute("employee");

    employee.setCreateUser(empId);
    employee.setUpdateUser(empId);

    employeeService.save(employee);

    return R.success("新增员工成功");
}

统一异常类处理异常

但是我们这个代码还存在问题,那就是在我们的数据库里,我们的用户名是唯一的,如果我们保存的时候保存了一样的名字,那么就会从数据库中抛出异常,我们要解决这个问题,有两种方法,一种是通过trycatch来捕获处理异常,但这个方法太low了,我们要用统一异常处理类来解决这个问题

那么我们定义全局处理类,写入对应的注解,然后定义一个专门用于处理该类异常的方法,这里的if进行的判断是确定数据库抛出的异常是不是用户名重复异常,课程里用的方法是看异常信息是否包含某个单词来判断,但是我们这里的异常信息就只是一个意义不明的符号,所以我们这里就用符号代替了

最后我们要提的是,ResponseBody这个注解的作用是为了让我们返回的数据自动转换成JSON的格式

package com.itheima.reggie.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
        
        if(ex.getMessage().contains("#23000")){
            return R.error("用户名已存在");
        }

        return R.error("未知错误");
    }
}

最后我们来做一个总结

员工信息分页查询

接着我们来学习如进行员工信息的分页查询,我们首先已经知道员工的信息会在我们进入首页的时候就自动向服务器发送请求获得数据并展示,我们可以看到其地址是employee连接page,后面还有一些拼接的传送数据,分别代表当前页和最大页数,在我们的页面展示中,还有姓名查询的选项,使用该查询会再拼接一个name的数据在请求中,了解了这些事请之后,那么我们可以总结出我们的写代码的步骤

要实现分页,首先我们要配置一个MP的分页插件,那么我们可以写入其代码如下

package com.itheima.reggie.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置MP的分页插件
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

接着我们分析前端的代码可以知道,我们响应的数据里是要包括数据数量和数据内容本身的,也就是说,我们的R的data内容里就应该包括这些内容,我们当然可以自己创建一个这样的内容,但是在MP中,已经提供了这样一个Page对象供我们使用了,我们可以直接用

那么我们可以写入我们的代码如下,由于查询的是姓名,所以我们这里使用模糊查询

/**
 * 员工信息分页查询
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name) {
    log.info("page = {},pageSize = {},name = {}",page,pageSize,name);

    //构造分页构造器
    Page pageInfo = new Page(page,pageSize);

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

    //添加过滤条件
    queryWrapper.like(!StringUtils.isEmpty(name),Employee::getName,name);

    //添加排序条件
    queryWrapper.orderByDesc(Employee::getUpdateTime);

    //执行查询
    employeeService.page(pageInfo,queryWrapper);

    return R.success(pageInfo);
}

禁启用员工账号

首先我们先来看看页面中是如何做到只有管理员admin才能够看到启用禁用的按钮的,其实逻辑非常简单,就是直接判断当前的用户名是不是admin,是的话就展示按钮,否则就不展示

然后我们来正式开发我们的代码,我们首先来看看前端发送的请求,其发送的是一个PUT请求,映射为employee,其下有状态数据以及要修改列的id,我们可以根据该id来修改对应的数据的状态值

那么我们可以写入修改代码如下,我们这里利用员工id进行修改,为了防止修改时出现错误,所以我们还一并修改了修改人以及修改时间,这种记录也符合我们的日常模式,我们最好就将修改的信息也一并记录下来

/**
 * 根据id修改员工信息
 * @param request
 * @param employee
 * @return
 */
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
    log.info(employee.toString());

    Long empId = (Long) request.getSession().getAttribute("employee");
    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser(empId);
    employeeService.updateById(employee);

    return R.success("员工信息修改成功");
}

但是实际上我们这份代码是无法生效的,这是因为我们数据库中的id是long类型的,其有19位,而在网页中最多保存16位,会出现精度丢失的问题,我们解决这个问题的方式是将我们的long类型的值改为String类型的值,这样就不会出现丢失精度的问题了

我们的转换步骤要依赖于我们的转换器,转换器类课件中提供给我们了,我们只需要将其配置好就可以了

我们首先来看看我们的转换器类,其本质是利用了java里的反射机制

package com.itheima.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)
                .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);
    }
}

然后我们在配置类中创建这个类型转换器并手动将其追加到我们的MVC框架的转换器集合的第一位中,这样我们的转换器就能够被正确使用了

/**
 * 扩展mvc框架的消息转换器
 * @param converters
 */
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器...");

    //创建消息转换器对象
    MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();

    //设置对象转换器,底层使用Jackson将Java对象转为json
    messageConverter.setObjectMapper(new JacksonObjectMapper());

    //将上面的消息转换器对象追加到MVC框架的转换器集合中,要添加到第一位,否则其不会优先使用我们定义的转换器
    converters.add(0,messageConverter);
}

编辑员工信息

接着我们来做编辑员工信息的开发,我们先来看看步骤,这个步骤就比较多,我们先要查询到到对应的数据并展示在我们的修改页面上,接着再进行修改的操作。注意我们这里的add.html是公共页面,新增员工和编辑员工都跳到这个页面,只不过他们触发的命令不同,实际上在他们的代码里也有做对应的不同处理

这里值得一提的是,我们之前是做好了一个通用的更新的方法,我们编辑选择保存时会调用这个方法,那么我们就只需要做展示的部分就可以了,我们可以看到其是使用Get请求,并且会传用户id过来,那么我们后面的代码里就要将这个id接住然后查询到对应的用户信息并响应

最终我们可以写入我们的查询的代码如下,由于我们这里要接收到id信息,因此我们这里使用PathVariable注解帮助我们接收

/**
 * 根据id查询员工信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id) {
    log.info("根据id查询员工信息...");
    Employee employee = employeeService.getById(id);
    if(employee==null){
        return R.error("没有查询到员工信息");
    }
    return R.success(employee);
}

公共字段自动填充

接着我们来学习公共字段自动填充,之前我们无论是添加用户还是更新用户,都需要花费进行一个重复的给对应的用户更新创建时间和创建人的代码,这样重复的代码当然不好,而且还臃肿,因此我们这里要使用MP提供给我们的公共字段自动填充的功能,可以让我们需要执行这些代码的时候这些代码会在统一的一个地方自动执行,而不需要我们在对应的位置手动写入这些重复的代码

这非常类似于我们的SB里的AOP,那么问题在于,我们为什么不使用AOP来实现这个功能呢?这其实是因为AOP的本质是利用了反射机制,而反射的效率都比较低,如果我们啥都用AOP去完成,那我们的项目就直接烂完了,效率会慢的不行,所以我要使用MP提供的方式

我们首先要往实体类型提供加入对应的TableField注解并指定自动填充的策略,对应的不同字段就是采用不同的填充策略,有些是插入是填充,有些是插入和更新时都填充

按照上面的内容,我们可以将我们的员工实体类改变如下

package com.itheima.reggie.entity;

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

/**
 * 员工实体
 */
@Data
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

    private String idNumber;//身份证号码

    private Integer status;

    @TableField(fill = FieldFill.INSERT) //插入时填充字段
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT) //插入时填充字段
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)//插入和更新时填充字段
    private Long updateUser;

}

其实就是往对应的属性下指定了填充的字段而已

然后我们要创建一个类,令这个类实现MetaObjectHandler接口,实现该接口要求你实现两个方法,这两个方法分别就是更新时要执行的方法和插入时要执行的方法,我们就往内部放入我们要令其执行的代码即可,其参数内会有一个MetaObject对象,我们只要往里写入对应的set后面的方法的字符串,然后再指定要设置的数据,其就会自动帮我们调用该方法完成设置了

我们要写入的公共代码必然是往对应的对象设置我们的对应创建更新时间以及创建和更新的id,时间好说,可以直接从本地上调出来,但是id怎么办?我们之前获得id的方法都是通过request中的共享域来获取登陆者的id的,但是在这个类中,我们却无法获取到request中的共享域对象,那么我们要怎么办呢?

我们可以使用ThreadLocal来解决这个问题,客户端每次发送的请求,在服务器端都会分配一个新的线程来处理,而在同一个线程里执行的各种的方法,他们都是在一个线程中的

而ThrealLocal是Thread的一个局部变量,简单来说,我们可以通过这个变量往对应的线程中存储信息,只要还在同一个线程中,就可以往这个线程中将信息取出来。我们就利用这种方法来获得我们的id

来看看我们的实现步骤

首先我们要创建BaseContext类,令其拥有ThreadLocal对象并提供设置和获取该线程对象的方法,当然这个方法应该要是静态的,因为我们这个类是工具类,我们希望其他使用者可以直接使用该方法而不用进行实例化

package com.itheima.reggie.common;

/**
 * 基于ThreadLocal封装的工具类,用于保存和获取当前登录用户id
 */
public class BaseContext {

    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    /**
     * 设置值
     * @param id
     */
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    /**
     * 获取值
     * @return
     */
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

最后我们可以在我们的自动填充字段的类中写入代码如下,在这里我们的设置的id是通过线程共享的信息得到的,当然,能获取当然有写入,我们在之前插入时就写入了对应的代码将id信息写入了,这里我们就不展示这一部分的代码了

package com.itheima.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {

    /**
     * 插入操作,自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充insert...");
        log.info(metaObject.toString());
        metaObject.setValue("createTime",LocalDateTime.now());
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("createUser",BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }

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

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

        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
}

分页管理业务功能开发

新增分类

这个也是要经典进行一个实体类的提供,然后利用MP提供给我们的功能来创建对应的三层调用关系,这里就不贴上代码了,直接看步骤吧

然后我们来分析下前端往服务器上发送的请求,来确定我们的映射地址

最终我们可以写入我们的代码如下,没什么特别值得说的,自己看吧

/**
 * 分类管理
 */
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    @PostMapping
    public R<String> save(@RequestBody Category category){
        log.info("category:{}",category);
        categoryService.save(category);
        return R.success("新增分类成功");
    }
}

最后我们值得一提的是,我们这里的添加功能,实际上我们可以添加两种菜品和套餐,但是他们都共用一个添加方法,我们在后端中通过一个数字类型的变量来确定他们分别表示的是菜品还是套餐

分类信息分页查询

分类信息的分页查询的过程的步骤和我们员工管理的分页查询差不多,我们这里就迅速讲完了,先来看看步骤

那么我们可以写入我们的执行分页查询的代码如下,我们这里还要构造条件构造器,让我们的数据按照其优先级进行降序的排序,显示在我们的客户端上

/**
 * 分页查询
 * @param page
 * @param pageSize
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page,int pageSize) {
    //分页构造器
    Page<Category> pageInfo = new Page<>(page,pageSize);
    //条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
    //添加排序条件,根据sort进行排序
    queryWrapper.orderByAsc(Category::getSort);

    //分页查询
    categoryService.page(pageInfo,queryWrapper);
    return R.success(pageInfo);
}

删除分类

先来看看步骤

我们执行删除操作的时候,应该是要判断我们的分类是否关联了某一个菜品或者套餐的,如果真的关联了,那么我们就不能删除,否则我们就可以删除,我们这里先实现一个简单粗暴的删除功能的后端代码,那么我们可以写入代码如下

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

    categoryService.removeById(id);

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

当然我们这个逻辑属实是太简单粗暴了,接着我们要对这个逻辑进行改进。我们的想法是让删除方法执行之前先在数据库里查询有没有对应的菜品或者套餐,若有,则抛出业务异常,反之则正常进行删除。这里有的同学可能会问了,我们不是说要尽量减少业务上对数据库的交互吗?这里我们可以用主键关联来实现这个功能吧?为什么不用?这是因为比起去数据库再次查找数据所造成的负担,主键的负担显然更加重,也更加占用资源,所以我们不使用主键,我们使用再次往数据库中查找的方法。

这里值得一提的是,我们抛出的异常应该要是我们自定义的异常,因此我们要自定义一个异常类并对其进行处理,我们上抛时也抛出我们自己的异常,所以我们可以写入我们的异常类如下

/**
 * 自定义业务异常
 */
public class CustomException extends RuntimeException{

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

}

然后我们在全局异常的处理类里定义一个处理该异常的方法,这样我们的全局异常处理类就可以自动处理该类的异常了

/**
 * 异常处理方法
 * @param ex
 * @return
 */
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
    log.error(ex.getMessage());
    return R.error(ex.getMessage());
}

然后我就可以正式来写我们的自定义的删除方法了,首先我们在对应生成的业务层的接口里先自定义自己的删除方法

public interface CategoryService extends IService<Category> {
    public void remove(Long ids);
}

然后在实现层里实现这个方法,我们这里执行的查询是查询对应的菜品的数量,如果数量不为零,我们就抛出业务异常,查询时使用的LambdaQueryWrapper类,若全部都可以通过,我们则调用父类的通过id删除的方法

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService{

    @Autowired
    private DishService dishService;

    @Autowired
    private SetmealService setmealService;

    /**
     * 根据id删除分类,删除之前需要进行判断
     * @param ids
     */
    @Override
    public void remove(Long ids) {
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();

        //添加查询条件,根据分类id进行查询
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,ids);
        int count = dishService.count(dishLambdaQueryWrapper);

        //查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
        if(count!=0){
            //已经关联菜品,抛出业务异常
            throw new CustomException("当前分类下关联了菜品,不能删除");
        }

        //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();

        //添加查询条件,根据分类id进行查询
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,ids);
        int count2 = setmealService.count(setmealLambdaQueryWrapper);

        if(count2!=0){
            //已经关联套餐,抛出业务异常
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }

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

当然,我们这里要注意的是,我们这里查询时,我们使用了菜品和套餐的对象,这些对象我们是事先写好并加入的,同样的,我们也通过MP实现了其三层逻辑,最后由于我们前端传入的是变量名是ids,因此这里我们自定义方法时使用的变量名也要是ids

修改分类

首先我们通过分析前端代码,我们可以得到两件事情,第一件事情是数据的回显前端已经帮我们做好了,我们后端人员不需要再费心了。其次是前端发送的请求路径就是一个单纯的category,后面拼接上对应的json格式的菜品修改的数据。

那么根据上面的信息,我们容易写入我们的修改代码ruxi

/**
 * 根据id修改分类信息
 * @param category
 * @return
 */
@PutMapping
public R<String> update(@RequestBody Category category) {
    log.info("修改分类信息:{}",category);

    categoryService.updateById(category);

    return R.success("修改分类信息成功");
}

菜品管理业务开发

文件的上传与下载

首先我们来实现文件的上传与下载的代码,这里我们完成我们的菜品管理业务开发的第一步,先来看看步骤

首先我们来看看文件上传时对前端的表单的格式的要求

然后是文件上传时前端组件库提供了相应的上传组件,但是其底层原理还是基于form表单的文件上传

在Spring框架中封装了文件上传,我们可以调用MultipartFile对象来进行文件上传,但是我们要记住其本质还是调用Apache的上传的两个组件的

最后我们来看看文件下载,我们这里的文件下载是将文件从服务器传输到本地计算机的过程,我们直接在浏览器中下载,这样好直接将数据在浏览器中展示,具体请看图

我们文件上传的页面就直接使用ElementUI提供的上传组件即可,对应的页面在我们的课程资料里有,直接复制到我们的项目中即可使用

这里我们要提一下的是,我们测试时会发现我们无法进入该上传页面,这是由于我们的页面少了这么一行内容,在head中加入即可使用

<link rel="shortcut icon" href="../../favicon.ico" />

然后是我们的页面每次测试都要重新登录一次,这太麻烦了,所以我们干脆在过滤类中不用进行过滤的页面路径中加入我们的上传页面路径

"/common/**"

我们分析前端的代码容易发现我们的上传其实也是往服务器里发送一个请求,这个请求的映射路径还是以common起始的,因此我们要创建一个新的类用于处理该请求类,同时我们要知道我们Spring中提供的MultipartFile对象是一个临时文件的保存对象,该对象如果不保存到本地的话,就会自然消失,因此我们要将其保存到本地,保存到本地自然需要一个地址,我们在yml中配置这个地址(千万不要忘记冒号后要加一个空格哦)

reggie:
  path: D:\pain\

然后我们写入我们的该类的上传文件的代码如下

package com.itheima.reggie.controller;

import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

/**
 * 文件上传与下载
 */
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

    @Value("${reggie.path}")
    private String basePath;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("upload")
    public R<String> upload(MultipartFile file) {
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后file会被删除
        log.info(file.toString());

        //原始文件名
        String originalFilename = file.getOriginalFilename();//abc.jpg
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

        //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
        String fileName = UUID.randomUUID().toString() + suffix;

        //创建一个目录对象
        File dir = new File(basePath);
        //判断当前目录是否存在
        if(!dir.exists()){
            //目录不存在,需要创建
            dir.mkdirs();
        }

        try {
            file.transferTo(new File(basePath+fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        return R.success(fileName);
    }
}

我们这里首先通过对应的注解获取我们yml中定义的数据,然后我们使用UUID生成文件名,同时获得原来的文件名获得其文件格式名,接着将其进行拼接形成新的文件名,最后我们要对我们要保持的文件目录进行判断,如果存在我们则执行存入,不存在我们就创建之后存入,最后我们返回一个成功信息结合文件名即可

接着我们来开发与文件下载相关的代码,由于我们只是将对应的数据写会到我们的浏览器即可,因此我们这里不需要返回值,同时get请求的方式即可完成我们的目标,我们首先获取到对应的文件,然后写回到浏览器中即可,同时中间我们设置了我们相应的文件的类型,这里指定为图片类型

@GetMapping("/download")
public void download(String name, HttpServletResponse response) {

    try {
        //输入流,通过输入流读取文件内容
        FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));

        //输出流,通过输出流将文件写回浏览器,在浏览器中展示图片
        ServletOutputStream outputStream = response.getOutputStream();

        response.setContentType("image/jpeg");

        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fileInputStream.read(bytes)) != -1){
            outputStream.write(bytes,0,len);
            outputStream.flush();
        }

        outputStream.close();
        fileInputStream.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

菜品管理业务开发

我们首先来看看我们的业务要达到的效果

然后是我们的数据模型,涉及到两个表,想看具体的内容就自己去数据库中看吧

然后我们要做一些代码开发的准备工作,这里照旧是三次架构的构建,依葫芦画瓢即可

然后我们来梳理下我们的交互过程

然后我们先来实现我们的新增菜品中的菜品分类的下拉框的内容,分析前端代码,我们容易知道其向服务器发送的请求是在category路径中的,因此我们要往category中添加方法,同时菜品分类下拉框的原理是从数据库中读取所有的菜品,获取其所有分类保存到集合中再返回给浏览器,浏览器则用该集合进行回显

那么我们可以写入我们的代码如下

/**
 * 根据条件查询分类数据
 * @param category
 * @return
 */
@GetMapping("/list")
public R<List<Category>> list(Category category) {
    //条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
    //添加条件
    queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
    //添加排序条件
    queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);

    List<Category> list = categoryService.list(queryWrapper);

    return R.success(list);
}

我们这里通过菜品对象直接获得传入的属性数据,其会自动封装到我们的菜品对象中,然后我们创建对应的构造器,利用该菜品对象构建构造器最终获得一个集合,并将该集合返回。

最后值得一提的是,由于前端提交的路径里传送的数据并不是json类型的,因此我们这里的参数不需要使用RequestBody注解

接着我们来讲解我们如何开发我们的新增菜品功能,我们首先分析前端点击新建菜品功能之后传送过来的数据,我们会发现起其下不但有菜品本身的各种属性,甚至还有一些额外的菜品口味属性,此时我们如果只用菜品对象,那么是无法承接这一份数据的

那么我们应该要如何解决这个问题呢?其实非常简单,我们只要创建一个全新的类用于展示层和服务层的数据传输即可,将该类继承原类,然后在其下定义我们所需要的承接其他数据的属性即可,具体代码如下

package com.itheima.reggie.dto;

import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;

    private Integer copies;
}

上面的代码里还多了两个属性,这些是我们以后要用的属性,这里我们先按下不表

然后我们要注意的是,我们保存菜品时,其实是保存了两个内容的,一是我们菜品本身,二是和菜品关联的口味,因此我们的保存方法实际是要保存菜品和口味两个内容的,而MP提供给我们的方法就只有保存菜品和保存口味,因此我们要创建一个新方法让我们能够一起保存。首先我们在DishService的接口中创建该方法

public interface DishService extends IService<Dish> {

    //新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavor
    public void saveWithFlavor(DishDto dishDto);

}

然后我们去具体的实现类中重写该方法,我们这里首先给我们的类中注入对应的要调用方法的菜品口味对象

package com.itheima.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.dto.DishDto;
import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.DishFlavor;
import com.itheima.reggie.mapper.DishMapper;
import com.itheima.reggie.service.DishFlavorService;
import com.itheima.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

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

    @Autowired
    private DishFlavorService dishFlavorService;

    /**
     * 新增菜品,同时保存对应的口味数据
     * @param dishDto
     */
    @Override
    @Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到菜品表dish
        this.save(dishDto);

        Long dishId = dishDto.getId();//菜品id

        //菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());

        //保存菜品口味数据到菜品口味表dish_flavor
        dishFlavorService.saveBatch(flavors);
    }
}

然后我们先调用菜品自身的保存方法,将菜品保存,然后我们获得菜品的id,然后用输入流的方式将菜品id注入到我们的菜品口味对象中,我们的菜品口味是必须要先经过这个处理的,否则谁知道这个口味是对应哪个菜品的啊,最后我们再调用MP提供给我们的保存菜品口味数据的方法即可

而上面的方法整个都在我们的菜品服务层中,我们只要保存的时候调用该方法即可,那么我们可以构造如下代码

package com.itheima.reggie.controller;

import com.itheima.reggie.common.R;
import com.itheima.reggie.dto.DishDto;
import com.itheima.reggie.service.DishFlavorService;
import com.itheima.reggie.service.DishService;
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;

/**
 * 菜品管理
 */
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {

    @Autowired
    private DishService dishService;

    @Autowired
    private DishFlavorService dishFlavorService;

    @PostMapping
    public R<String> save(@RequestBody DishDto dishDto) {
        log.info(dishDto.toString());

        dishService.saveWithFlavor(dishDto);

        return R.success("新增菜品成功");
    }
}

菜品分页查询

接着我们来实现菜品的分页查询

这里我们要注意的是,我们的菜品的分页查询和我们之前员工的分页查询并不同,那就是我们菜品的分页查询中是有菜品分类这一栏的,而我们的实体类的菜品中是没有这个属性的,具体我们从前端代码中就可以看出,前端代码会从传回数据中寻找categoryName的属性,这个属性能够用于让前端判断展示菜品分类的内容,而由于我们原来的菜品中压根没有这玩意,那么就会让我们的菜品分页查询中的这一栏直接失效

那么我们要如何解决这个问题呢?我们的一个简单想法就是用另外一个类来代替我们原来的菜品类,没错,就是我们之前拷贝进来的Dishdto类。我们创建对应的方法,然后构造分页构造器的Dish和DishDto对象,前者我们要传入指定的页数和最大页数,而后者不用,然后用一系列的查询排序语句得到我们所需要的pageInfo的对象,但是这个对象不是我们所需要的,因此我们要将这个对象的内容拷贝到我们的dishDtoPage中,注意我们这里拷贝的是属性,而非其下的内容,这里第一次拷贝的属性是分页的各种数据,而保存分页查询的结果的数据我们则跳过拷贝,我们拷贝使用工具类拷贝

/**
 * 菜品管理分页查询
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name) {

    //构造分页构造器对象
    Page<Dish> pageInfo = new Page<>(page,pageSize);
    Page<DishDto> dishDtoPage = new Page<>();

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

    //添加过滤条件
    queryWrapper.like(name != null,Dish::getName,name);

    //添加排序条件
    queryWrapper.orderByDesc(Dish::getUpdateTime);

    //执行分页查询
    dishService.page(pageInfo,queryWrapper);

    //对象拷贝
    BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");

    List<Dish> records = pageInfo.getRecords();

    List<DishDto> list = records.stream().map((item) -> {
        DishDto dishDto = new DishDto();

        BeanUtils.copyProperties(item,dishDto);

        Long categoryId = item.getCategoryId();//分类id

        //根据id查询分类对象
        Category category = categoryService.getById(categoryId);

        if(category != null){
            String categoryName = category.getName();
            dishDto.setCategoryName(categoryName);
        }

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

    dishDtoPage.setRecords(list);

    return R.success(dishDtoPage);
}

然后获取到内容的集合,然后每次用流的形式将集合内的内容取出,拷贝其所有内容到我们的新对象中,然后取出其id在数据库里查询其分类,得到分类数据之后再设置到我们的新对象中,最后返回这个集合,然后将这个集合设置到我们的分页数据中,最后返回这个分页对象就行了

菜品修改功能开发

接着我们来开发菜品修改的功能,首先我们来梳理下我们的菜品开发的交互过程,我们首先要完成的当然是数据的回显

我们点击修改之后,页面会获取到对应的id然后向服务器发送这个id,服务器接收到这个id之后应该要响应对应的数据回来,我们注意到这里我们不但要响应菜品本身的数据,还要显示菜品的口味数据,因此我们最好就返回DishDto的对象,这个对象正好就有我们所需要的两个属性

我们先去对应的接口中创建对应的查询方法

//根据id查询菜品信息和对应的口味信息
public DishDto getByIdWithFlavor(Long id);

然后我们实现这个方法,我们这里实现方法的方式无非也就是拷贝和转换对象以及调用原来的查询方法等过程,这里就不赘述了

/**
 * 根据id查询菜品信息和对应的口味信息
 * @param id
 * @return
 */
@Override
public DishDto getByIdWithFlavor(Long id) {
    //查询菜品基本信息,从dish表查询
    Dish dish = this.getById(id);

    DishDto dishDto = new DishDto();
    BeanUtils.copyProperties(dish,dishDto);

    //查询当前菜品对应的口味信息,从dish_flavor表查询
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(DishFlavor::getDishId,dish.getId());
    List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
    dishDto.setFlavors(flavors);

    return dishDto;
}

然后我们在对应的查询方法中调用我们新创建的方法即可

/**
 * 根据id查询菜品信息以及对应的口味信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id) {

    DishDto dishDto = dishService.getByIdWithFlavor(id);

    return R.success(dishDto);
}

最后我们来讲解我们的保存的方法,我们的更新保存方法和我们的之前的新创建的方法非常像,我们也是依葫芦画瓢,我们首先在对应的接口中创建一个新方法

//保存修改的菜品信息
public void updateWithFlavor(DishDto dishDto);

然后我们具体实现其代码,我们这里怎么实现我们的更新呢?想当然的事情当然是往里面查找对应的id并修改,但是这样的话,我们要处理的情况可就多了去了,因为我们修改的内容可能是更多,或者更少,还有不变,这样我们分情况讨论一个个处理能把我们麻烦死。其实我们的有一个简单的想法就是一旦进行了提交修改,我们就我先将数据库的对应内容给删除掉,然后再直接执行原来的保存的内容,这样我们就不必去区分什么情况了

根据这想法,我们容易写入我们的代码如下,我们这里先更新dish表中的信息(这里可以直接更新是因为MP给我们预先提供好了这些东西),然后我们对口味的数据则是直接删除所有数据,接着再填入新的数据

/**
 * 修改菜品信息
 * @param dishDto
 */
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
    //更新dish表基本信息
    this.updateById(dishDto);

    //清理当前菜品对应口味数据---dish_flavor表的delete操作
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());

    dishFlavorService.remove(queryWrapper);

    //添加当前提交过来的口味数据---dish_flavor表的inset操作
    List<DishFlavor> flavors = dishDto.getFlavors();

    flavors = flavors.stream().map((item) -> {
        item.setDishId(dishDto.getId());
        return item;
    }).collect(Collectors.toList());

    dishFlavorService.saveBatch(flavors);
}

最后我们再修改菜品的方法上调用该方法即可

/**
 * 修改菜品
 * @param dishDto
 * @return
 */
@PutMapping
public R<String> update(@RequestBody DishDto dishDto) {
    log.info(dishDto.toString());

    dishService.updateWithFlavor(dishDto);

    return R.success("修改菜品成功");
}

菜品批量删除以及菜品批量停售启售功能的开发

下面的功能就属于是自己实战开发的功能了,可以说是极具挑战性,还就是突出一个起飞我只能说,首先我们先来实现菜品删除和批量删除的功能,首先我们通过分析我们很容易能够得知,其实删除和批量删除请求的都是一个方法,所以我们要将这个两个方法开发到一出去,而我们从前端的F12页面分析其请求,我们会发现前端发送批量与否的内容主要区别在于后面跟的id是一个还是多个,所以我们这里可以用RequestParam注解来接受ids属性后面的所有id,并令其自动封装到一个List集合中

接着我们要知道,我们的菜品删除不只是要删除菜品,还需要删除与菜品关联的菜品口味,因此我们这里需要新创建一个删除方法,利用该删除方法来实现我们的需求。首先在对应的业务层接口中创建该方法如下

//删除菜品信息
public void deleteWithFlavor(List<Long> ids);

然后我们具体实现该方法,我们这里取出该集合,用foreach循环一个个进行单独的菜品以及菜品风格的删除处理,这里要注意由于我们这里涉及到了多表操作,因此别忘了加上Transactional注解

/**
 * 删除或批量删除菜品
 * @param list
 */
@Override
@Transactional
public void deleteWithFlavor(List<Long> list) {
    for (Long ids:list) {
        this.removeById(ids);

        //清理当前菜品对应口味数据---dish_flavor表的delete操作
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,ids);

        dishFlavorService.remove(queryWrapper);
    }
}

最后我们在控制层中调用该方法即可

/**
 * 根据id删除或者批量删除数据
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> deleteByIds(@RequestParam("ids") List<Long> ids){
    log.info(ids.toString());

    dishService.deleteWithFlavor(ids);

    return R.success("删除菜品成功");
}

然后我们再来讲讲批量改变状态的方法,这个其实也依葫芦画瓢了,不过我们这里要注意的是,其传入的地址不仅有id,其会在id前先拼接上status的地址以及一个/要改变的状态,其中0代表启售状态,其他数字代表非启售状态,因此我们开发的方法里应该要承接这个前面的启售停售的数据之后我们再来承接后面的id内容

同样的我们先添加一个抽象方法

//改变菜品状态
public void modifyStatus(Integer stu,List<Long> ids);

然后实现该方法,我们这里实现该方法的方式是提取出对应id的对象,然后改变其状态之后再执行新对象对就对象的修改

/**
 * 改变或批量改变菜品状态
 * @param list
 */
@Override
public void modifyStatus(Integer stu,List<Long> list) {
    for (Long ids:list) {
        Dish dish = this.getById(ids);
        dish.setStatus(stu);
        this.updateById(dish);
    }
}

然后在控制层中调用我们造的方法即可

/**
 * 根据id改变或批量改变菜品状态
 * @param ids
 * @return
 */
@PostMapping("/status/{stu}")
public R<String> modifyStatusByIds(@PathVariable Integer stu,@RequestParam("ids") List<Long> ids){
    log.info(ids.toString());

    dishService.modifyStatus(stu,ids);

    return R.success("更改状态成功");
}

到此为止,我们的菜品管理的所有业务就开发完毕了

套餐管理业务开发

新增套餐

首先我们现在完成我们的新增套餐的功能的代码开发,还是先来看看我们的需求吧,我们首先需要导入我们的实体类,以及实现一些必须要实现的接口,这些事情我们就不演示了

然后我们来梳理下我们的交互过程,我们这里首先会发送一个请求来获取套餐分类的数据并展示到我们的下拉框中。有个好消息,那就是我们的这个方法在我们之前已经实现好了,我们这里重复会重复调用这个方法并展示,所以就不用再自己构造这个方法了

然后我们要完善的功能是向服务端获取请求,并在页面中回显各式菜品的方法,从前端发送的请求中我们可以知道该方法请求的地址是dish/list,因此我们要到我们的dishcontroller中去添加我们的方法,该地址发送的请求是一个long型的id,我们可以用long型参数来承接,也可以用一个对象来承接,用对象的时候不需要加任何注解,我们的SpringMVC会自动将该类封装为一个对象,我们这里选择后者,因为其扩展性更强(其运作逻辑是当我们选择任何一个菜类的时候,就发送一个对应的id,然后我们可以用这个id查出所有的对应的分类的菜品)

然后我们要查询并返回的结果就是我们们的菜品id,这里我们做一个过滤条件,所有停售的菜品,我们都不收集他们的结果,这也符合逻辑,都停售了我收集来干嘛

那么最终我们可以写入我们的代码如下

/**
 * 根据条件查询菜品数据
 * @param dish
 * @return
 */
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){

    //构造查询条件
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
    //添加条件,查询状态为1(启售状态)的菜品
    queryWrapper.eq(Dish::getStatus,1);
    //添加排序条件
    queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

    List<Dish> list = dishService.list(queryWrapper);

    return R.success(list);
}

接着我们来学习如何新增套餐,这个过程其实和新增菜品几乎一模一样,首先我们的上传图片这个功能我们之前已经实现过了,所以我们这里不需要实现这个功能了

然后我们进行我们的前端代码的分析,我们会发现,我们前端发送的数据里,包含一个json数据,json数据中含有我们所选择的菜品对象,那么我们原来的菜品对象中没有这个属性,那么我们就无法承接,此时我们要就需要使用我们的自造对象dto了,请看代码

可以看到我们这里继承原对象,并添加了新的承接的属性,然后我们还设置了一个字符串类型的参数,该参数的作用我们先按下不表

package com.itheima.reggie.dto;

import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.entity.SetmealDish;
import lombok.Data;
import java.util.List;

@Data
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;

    private String categoryName;
}

然后我们在对应的接口中创建一个新的保存方法

public interface SetmealService extends IService<Setmeal> {

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     */
    public void saveWithDish(SetmealDto setmealDto);

}

然后我们在对应的实现类中实现这个方法

package com.itheima.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.dto.SetmealDto;
import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.entity.SetmealDish;
import com.itheima.reggie.mapper.SetmealMapper;
import com.itheima.reggie.service.SetmealDishService;
import com.itheima.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

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

    @Autowired
    private SetmealDishService setmealDishService;

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     */
    @Override
    @Transactional
    public void saveWithDish(SetmealDto setmealDto) {

        //保存套餐的基本信息,操作setmeal,执行insert操作
        this.save(setmealDto);

        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();

        setmealDishes.stream().map((item) -> {
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());

        //保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
        setmealDishService.saveBatch(setmealDishes);

    }
}

这里我们要保存套餐的基本信息,还要保存其关联信息,这个讲过一遍了,这里就不赘述了

然后我们在对应的控制层中调用该方法即可

/**
 * 新增套餐
 * @param setmealDto
 * @return
 */
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto) {
    log.info("套餐信息:{}",setmealDto);

    setmealService.saveWithDish(setmealDto);

    return R.success("新增套餐成功");
}

套餐管理分页查询

接着我们来学习如何开发套餐管理分页查询,其实这个很简单,跟我们之前搞菜品的分页查询那一套不能说十分相像,只能说是一模一样,来做一下吧

我们直接来看代码吧,其实也没啥值得说的,这里做的事情无非就是复制新的分页对象,然后创建我们的新的对象的集合,利用复制工具类而且从数据库中执行二次查找,都是以前的老东西了,不多提了

/**
 * 套餐管理分页查询并展示
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name) {
    //分页构造器对象
    Page<Setmeal> pageInfo = new Page<>(page,pageSize);
    Page<SetmealDto> dtoPage = new Page<>();

    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    //添加查询条件,根据name进行like模糊查询
    queryWrapper.like(name !=null,Setmeal::getName,name);
    //添加排序条件,根据更新时间降序排序
    queryWrapper.orderByDesc(Setmeal::getUpdateTime);

    setmealService.page(pageInfo,queryWrapper);

    //对象拷贝
    BeanUtils.copyProperties(pageInfo,dtoPage,"records");
    List<Setmeal> records = pageInfo.getRecords();

    List<SetmealDto> list = records.stream().map((item) -> {
        SetmealDto setmealDto = new SetmealDto();
        //对象拷贝
        BeanUtils.copyProperties(item,setmealDto);
        //分类id
        Long categoryId = item.getCategoryId();
        //根据分类id查询分类对象
        Category category = categoryService.getById(categoryId);
        if(category != null){
            //分类名称
            String categoryName = category.getName();
            setmealDto.setCategoryName(categoryName);
        }
        return setmealDto;
    }).collect(Collectors.toList());

    dtoPage.setRecords(list);
    return R.success(dtoPage);
}

套餐删除功能代码开发

首先我们还是来看看我们的套餐删除的前端的并发过程吧

然后我们容易知道该删除过程是要操作多表的,不但如此,而且我们的批量删除和删除都是在一个方法下的,因此我们要在一个方法下完成这两个功能,首先我们要在对应的服务层中创建对应的方法

/**
 * 批量删除套餐
 * @param ids
 */
public void deleteWithDish(List<Long> ids);

接着我们来实现我们的方法,写入其具体代码如下,我们这里值得一提的是,要注意我们的泛型规定的实体类和我们调用的方法所需要的对应的实体类,如果不同的话,会报错的,具体的错误说实话我现在也不好说,总之是会有的。

我们是真的有必要做完这个项目之后自己一个人盲做一次,把错误总结总结,否则我们的基础是不牢固的。

/**
 * 删除或批量删除套餐
 * @param list
 */
@Override
@Transactional
public void deleteWithDish(List<Long> list) {
    //查询套餐状态,确定是否可用删除
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.in(Setmeal::getId,list);
    queryWrapper.eq(Setmeal::getStatus,1);

    int count = this.count(queryWrapper);
    if(count > 0){
        //如果不能删除,抛出异常业务异常
        throw new CustomException("套餐正在售卖中,不能删除");
    }

    //如果可以删除,先删除套餐表中的数据---setmeal
    this.removeByIds(list);

    LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.in(SetmealDish::getSetmealId,list);

    //删除关系表中的数据----setmeal_dish
    setmealDishService.remove(lambdaQueryWrapper);
}

然后我们的控制层中我们调用该方法即可

/**
 * 根据id删除或者批量删除套餐
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> deleteByIds(@RequestParam("ids") List<Long> ids){
    log.info("ids:{}",ids);

    setmealService.deleteWithDish(ids);

    return R.success("套餐数据删除成功");
}

接下来的内容就是我们自主开发的了,首先我们来讲解我们的修改功能,首先我们要做的事情是读取到数据库中的数据并回显到我们的页面上,因此我们首先需要开发的方法是根据id的查询方法,因为点击修改前端就会发出这个含有id数据附在地址上的请求

我们首先在对应的接口上创建方法

/**
 * 根据id查询套餐信息
 * @param ids
 * @return
 */
public SetmealDto getByIdWithDish(Long ids);

然后我们实现该方法,我们这里由于我们的对象还含有分类的数据,因此我们这里要做的事情是先查出普通的菜品对象,然后复制到dto对象中,接着查出对应套餐的具体菜品,封装到我们的dto对象中并返回该dto对象

/**
 * 根据id查询套餐内容并回显
 * @param ids
 * @return
 */
@Override
public SetmealDto getByIdWithDish(Long ids) {
    //查询套餐的基本信息,从数据表中查询
    Setmeal setmeal = this.getById(ids);
    SetmealDto setmealDto = new SetmealDto();

    BeanUtils.copyProperties(setmeal,setmealDto);

    //查询当前套餐下的具体菜品
    LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(SetmealDish::getSetmealId,ids);
    List<SetmealDish> list = setmealDishService.list(queryWrapper);
    setmealDto.setSetmealDishes(list);

    return setmealDto;
}

最后要做的事情就是去控制层中调用该方法即可,注意我们的返回值里就要返回该对象,这样我们的前端才可以取出这些数据并使用,然后我们取出连接中的代码的方法是通过PathVariable注解获取

/**
 * 根据id查询对应的套餐信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<SetmealDto> get(@PathVariable Long id) {

    SetmealDto setmealDto = setmealService.getByIdWithDish(id);

    return R.success(setmealDto);
}

接着我们来实现修改功能的保存,其实也是依葫芦画瓢,先删除再添加即可,这里的有意思的一点在于,我发现直接调用修改和增添的方法也可以实现我们的目的,而不需要我们再去做其他更多的工作,但是老师这里总是再写一份代码,我们也是配合一下老师的写法吧,所以我们这里来个梅开二度

首先创建新的修改方法

/**
 * 修改套餐信息
 * @param setmealDto
 */
public void updateWithDish(SetmealDto setmealDto);

然后我们实现该方法

/**
 * 修改套餐信息
 * @param setmealDto
 */
@Override
public void updateWithDish(SetmealDto setmealDto) {
    //查询套餐状态,确定是否可以修改
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();

    List<Long> list = new ArrayList<>();
    list.add(setmealDto.getId());
    queryWrapper.in(Setmeal::getId,list);
    queryWrapper.eq(Setmeal::getStatus,1);

    int count = this.count(queryWrapper);

    if(count > 0){
        //如果不能修改,抛出业务运行异常
        throw new CustomException("套餐正在售卖中,不能修改");
    }

    //如果可以修改,则先删除表中的对应数据
    this.removeByIds(list);

    LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.in(SetmealDish::getSetmealId,list);

    //删除记录套餐中具体餐品的表中的指定数据
    setmealDishService.remove(lambdaQueryWrapper);


    //saveWithDish(setmealDto);

    //保存套餐的基本信息,操作setmeal,执行insert操作
    this.save(setmealDto);

    List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();

    setmealDishes.stream().map((item) -> {
        item.setSetmealId(setmealDto.getId());
        return item;
    }).collect(Collectors.toList());

    //保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
    setmealDishService.saveBatch(setmealDishes);
}

接着在控制层中调用我们的方法即可

/**
 * 修改套餐内容的方法
 * @param setmealDto
 * @return
 */
@PutMapping
public R<String> update(@RequestBody SetmealDto setmealDto){
    log.info(setmealDto.toString());

    setmealService.updateWithDish(setmealDto);

    return R.success("套餐修改成功");
}

停售启售功能

然后我们来实现我们的最后一个功能,就是停售和启售功能的代码的开发这个说实话真的是非常简单了,获取对应的值然后直接修改套餐的对应数据即可,请看代码

/**
 * 修改或批量修改套餐的停售启售状态
 * @param stu
 * @param list
 * @return
 */
@PostMapping("status/{stu}")
public R<String> modify(@PathVariable Integer stu,@RequestParam("ids") List<Long> list){

    for (Long ids:list) {
        Setmeal setmeal = setmealService.getById(ids);
        setmeal.setStatus(stu);
        setmealService.updateById(setmeal);
    }

    return R.success("套餐状态修改成功");
}

手机验证码登录业务开发

接下来我们就要学习来开发移动端的内容了,当然我们这里客户端上还有订单明细这一份代码我们还没有开发,但是这个不着急,我们做到最后再来开发也不着急,因为现在我们的下单方法都还没开发

然后我们一般在移动端登录都是使用验证码,我们这里一般验证码可以使用一些大公司提供给我们的短信服务来完成,请看完成步骤

但是呢,我们现在还只是学习阶段,属实是没有必要真的整一个短信服务来,我们这里了解下上面的内容即可,具体的登录过程我们直接模拟即可

我们这里进入到我们阿里的账户中直接进行一个对应id的创建,注意选择使用API调用,获得对应的账号密码之后放到阿里提供给我们的工具类中使用即可(这里就不多提了,想要了解的可以自己重新去看P80之后的内容)

手机验证码登录

然后我们来正式实现手机验证码登录的功能,首先我们来进行需求分析

然后我们来看看我们数据表中的用户user表,这是我们的移动端登录会涉及到的表

接着我们来梳理下登录与后端服务器的交互过程

然后来看看我们的代码开发的准备工作,很多类在课程中都已经写好了,我们直接用就可以了

为了让工具类不报红,我们需要在maven中导入这两个坐标

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
    <version>4.5.16</version>
</dependency>

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
    <version>2.1.0</version>
</dependency>

我们的工具类在课程中都有所提供了,我们这里直接导入即可

然后我们来学习移动端登录,移动端登录我们首先要对我们的过滤类进行一个修改,首先我们要加入两个不被拦截的请求,这两个请求分别是请求验证码和登录的请求,这两个如果不拦截,那么我们发送这个请求就立刻跳回登录页面的话就没意义了

那么首先我们需要在定义不需要处理的请求路径中加入这两个请求的路径

//定义不需要处理的请求路径
String[] urls = new String[]{
        "/employee/login",//登录
        "/employee/logout",//登出
        "/backend/**",//请求静态资源
        "/front/**",//请求前端静态资源
        "/common/**",
        "/user/sendMsg",
        "/user/login"
};

然后我们还需要在写一个方法来处理移动端用户的登录,这里的逻辑和之前电脑端登录的代码的运行逻辑是一样的

//4.2 判断登录状态,如果已登录则直接放行
if(request.getSession().getAttribute("user")!=null){
    log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));

    Long userId = (Long) request.getSession().getAttribute("user");
    BaseContext.setCurrentId(userId);

    filterChain.doFilter(request,response);
    return;
}

接着我们来实现获取验证码的代码(注意在我们的控制类中应该要拥有服务类作为其私有属性)

/**
 * 移动端登录方法
 * @param user
 * @param session
 * @return
 */
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){
    //获取手机号
    String phone = user.getPhone();

    if(!StringUtils.isEmpty(phone)){
        //生成随机的4位验证码
        String code = ValidateCodeUtils.generateValidateCode(4).toString();
        log.info("code={}",code);

        //调用阿里云提供的短信服务API完成发送短信
        //SMSUtils.sendMessage("瑞吉外卖","",phone,code);

        //将需要生成的验证码保存到Session
        session.setAttribute(phone,code);

        return R.success("手机验证短信发送成功");
    }

    return R.error("短信发送失败");
}

我们的代码逻辑非常简单,先获取手机号,然后调用工具类生成随机的四位验证码,接着原本的情况是我应该要调用阿里云的短信服务向我们的手机发送短信,但是要花钱,我反正懒得,我们这里使用模拟的方式,直接在控制台上打印验证码,到时候发送了验证码我们直接去服务器上看就完了,然后我们将生成的验证码保存到Session中,返回一个成功的对象信息,如果没进去则返回出错信息

接着我们来实现我们的登录方法本身,我们这里要注意我们的登录请求会传送一个带有手机号和验证码的数据,我们直接用User对象是无法承接的,我们这里可以用dto对象来承接,但是这样有些麻烦,偷懒的方法是直接用map对象承接,我们这里就使用这种方法

@PostMapping("/login")
public R<User> login(@RequestBody Map map,HttpSession session){
    log.info(map.toString());

    //获取手机号
    String phone = map.get("phone").toString();

    //获取验证码
    String code = map.get("code").toString();

    //从Session中获取保存的验证码
    Object codeInSession = session.getAttribute(phone);

    //进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
    if(codeInSession != null && codeInSession.equals(code)){
        //比对成功则说明登录成功

        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getPhone,phone);

        User user = userService.getOne(queryWrapper);
        if(user == null){
            //判断当前手机号对应的用户是否为新用户,如果是新用户则自动完成注册
            user = new User();
            user.setPhone(phone);
            user.setStatus(1);
            userService.save(user);
        }
        session.setAttribute("user",user.getId());
        return R.success(user);
    }
    return R.error("登录失败");
}

我们取出手机号和验证码,然后取出保存在Session中的验证码,接着进行验证码比对,若比对成功则说明登录成功,此时我们从数据库中查询对应的手机号,若无则直接创建保存,相当于注册,登录成功之后将对应的用户id保存到session中表示其已经登录成功了

菜品展示及购买相关业务开发

接下来我们来学习菜品展示以及相关购买业务的开发,首先我们来开发我们的菜品展示的相关功能

地址相关业务开发

在正式做我们的菜品展示的相关功能之前,我们要先将地址簿功能给开发了,首先我们来做地址簿功能的需求分析,首先我们来分析下其拥有的功能

然后我们来看看我们的用户拥有的信息表,这里拥有的字段很多,但是其实需要我们填充的只有前面的打√的几个,后面的字段都可以不填充

然后我们来看看我们的构造地址簿的步骤

我们首先做经典的MP三层调用配置,这一段我们就省略了,然后我们首先来完成我们的展示我们的所有收货地址的方法,首先我们去我们的地址管理可以看到其请求地址是addressBook且连接list,是GET方式请求

本来这玩意课程要求直接导入就完了,但我还是自己去试着开发了下,结果就是处处出错,对了下答案发现不是我的逻辑有问题,而是前端需要的东西就没有传送对,我可懒得去分析前端的玩意,我直接进行一个复制就完了

我们这里首先来分析下我们的查询所有的方法,这里前端需要的是地址的集合对象,我们这里的逻辑就是查出所有地址并封装到集合对象中,这里调用的是MP提供的list方法实现将地址封装到集合中的操作

/**
 * 查询指定用户的全部地址
 */
@GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook) {
    addressBook.setUserId(BaseContext.getCurrentId());
    log.info("addressBook:{}", addressBook);

    //条件构造器
    LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
    queryWrapper.orderByDesc(AddressBook::getUpdateTime);

    //SQL:select * from address_book where user_id = ? order by update_time desc
    return R.success(addressBookService.list(queryWrapper));
}

然后我们来说下新增的方法,我们这里新增前要先设置执行新增操作的用户id在地址中,我们这里利用之前我们学习过的公共字段自动填充的知识,利用我们之前添加的工具类来获得当前用户的id并设置,然后执行保存操作,最后返回这个新增的对象到前端中

/**
 * 新增
 */
@PostMapping
public R<AddressBook> save(@RequestBody AddressBook addressBook) {
    addressBook.setUserId(BaseContext.getCurrentId());
    log.info("addressBook:{}", addressBook);
    addressBookService.save(addressBook);
    return R.success(addressBook);
}

然后是根据id查询地址的方法,逻辑非常简单,不多提了

/**
 * 根据id查询地址
 */
@GetMapping("/{id}")
public R get(@PathVariable Long id) {
    AddressBook addressBook = addressBookService.getById(id);
    if (addressBook != null) {
        return R.success(addressBook);
    } else {
        return R.error("没有找到该对象");
    }
}

接着是设置默认地址的方法,这个方法就有点重量级了,逻辑上比较绕,我们这里重点说一下,我们这里首先取出当前用户的所有的地址数据,将其所有的默认地址的数值都改为0,即是否,接着再将其对应要修改的地址改为1,最终返回这个修改的对象

/**
 * 设置默认地址
 */
@PutMapping("default")
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
    log.info("addressBook:{}", addressBook);
    LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
    wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
    wrapper.set(AddressBook::getIsDefault, 0);
    //SQL:update address_book set is_default = 0 where user_id = ?
    addressBookService.update(wrapper);

    addressBook.setIsDefault(1);
    //SQL:update address_book set is_default = 1 where id = ?
    addressBookService.updateById(addressBook);
    return R.success(addressBook);
}

然后是查询默认地址的方法,这也没啥好说的

/**
 * 查询默认地址
 */
@GetMapping("default")
public R<AddressBook> getDefault() {
    LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
    queryWrapper.eq(AddressBook::getIsDefault, 1);

    //SQL:select * from address_book where user_id = ? and is_default = 1
    AddressBook addressBook = addressBookService.getOne(queryWrapper);

    if (null == addressBook) {
        return R.error("没有找到该对象");
    } else {
        return R.success(addressBook);
    }
}

菜品展示

接着我们来开发菜品展示功能,首先我们来做一个基本的需求分析,来看看我们的菜品都需要些啥玩意

首先我们可以看到我们的菜品展示中是有分类数据的,这个分类方法其实我们之前就已经做好了,实际上这里也发送了对应的请求然后获得了对应的数据,但是为什么我们进入到首页得到的页面是一片空白呢?这其实是因为我们的前端代码里设置了我们进入页面就会向服务器发送两个请求,前者请求分类数据,后者请求购物车数据,只有两者都成功执行才能正确展示界面,但我们这里连购物车的代码都没实现呢,那肯定没法展示了,我们这里为了先让我们的代码展示起来,所以我们这里先将请求购物车数据的代码改为请求一个我们自己编写的实体数据,这样可以简单骗过前端,当然后期肯定是要修改的

别忘了要刷新我们浏览器的缓存,否则我们的手机端页面的请求是不会改变的

然后我们进入页面,就可以看到我们的分类的菜品的展示数据了,这里我们进入页面就会请求到我们的分类数据,然后点击具体的分类又会请求对应分类的菜品数据

但是我们这里还有一个小问题,那就是我们这里并不会展示我们的口味选择,我们最开始的需求是,如果我们的菜品有各种口味的选择,但我们这里是没有展示的,究其原因是因为我们最开始返回的数据里就没有口味的属性,所以我们这里要对我们的获取菜品数据的代码进行一个修改

容易看到我们的Dto对象是有对应的口味属性的,所以我们希望其返回这个对象

package com.itheima.reggie.dto;

import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDto extends Dish {

    //菜品对应的口味数据
    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;

    private Integer copies;
}

那么我们可以修改我们的代码如下,我们这里突出的就是一个复制,然后查询数据库之后再次赋值到新对象中,能成但是效率不咋地

/**
 * 根据条件查询菜品数据
 * @param dish
 * @return
 */
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){

    //构造查询条件
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
    //添加条件,查询状态为1(启售状态)的菜品
    queryWrapper.eq(Dish::getStatus,1);
    //添加排序条件
    queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

    List<Dish> list = dishService.list(queryWrapper);

    List<DishDto> dishDtoList = list.stream().map((item) ->{
        DishDto dishDto = new DishDto();

        BeanUtils.copyProperties(item,dishDto);

        Long categoryId = item.getCategoryId();//分类id
        //根据id查询分类对象
        Category category = categoryService.getById(categoryId);

        if(category != null){
            String categoryName = category.getName();
            dishDto.setCategoryName(categoryName);
        }

        //当前菜品的id
        Long dishId = item.getId();
        LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
        //SQL:select * from dish_flavor where dish_id = ?
        List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
        dishDto.setFlavors(dishFlavorList);
        return dishDto;
    }).collect(Collectors.toList());

    return R.success(dishDtoList);
}

接着我们要实现的套餐的查询,我们的套餐数据是发送的另外一个请求,是在套餐控制层里发送的请求,但我们这里压根没有这个映射,因此我们这里要自己构造这份代码,那么我们容易写入其代码如下,我们这里就查询对应的分类和在启售状态中的套餐并展示

@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal){
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
    queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
    queryWrapper.orderByDesc(Setmeal::getUpdateTime);

    List<Setmeal> list = setmealService.list(queryWrapper);

    return R.success(list);
}

那么到此为止,我们的菜品展示的代码就开发完了

购物车功能开发

接着我们来开发我们的购物车功能,首先我们来看看我们的需求分析

然后我们来看看我们购物车与服务器间的交互过程

然后我们来看看其准备工作

整完了准备工作之后,我们容易写出我们的添加套餐或者是菜品到购物车中的代码如下,这里的逻辑是先将用户ID设置到购物车对象中,然后我们查询当前的菜品是否在购物车中,我们这里查询的方式是直接获取菜品的id,如果为空则说明是套餐,不为空则说明是菜品,两种不同的情况我们都设置不同的比较条件,然后我们统一往数据库中查出一个对象,若该对象为空,则说明数据库中没有该数据,此时我们执行插入操作,若有,我们执行更新操作,令其数量+1即可

/**
 * 购物车
 */
@Slf4j
@RestController
@RequestMapping("/shoppingCart")
public class ShoppingCartController {

    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 往购物车里添加菜品或套餐
     * @param shoppingCart
     * @return
     */
    @PostMapping("/add")
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
        log.info("购物车数据:{}",shoppingCart);

        //设置用户id,指定当前是哪个用户的购物车数据
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);

        //查询当前菜品或者套餐是否在购物车中
        Long dishId = shoppingCart.getDishId();

        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,currentId);

        if(dishId != null){
            //添加到购物车的是菜品
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
        }else {
            //添加到购物车的是套餐
            queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        }

        //SQL:select * from shopping_cart where user_id ? and dish_id/setmeal_id = ?
        ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);

        if(cartServiceOne != null){
            //如果已经存在,就在原来数量的基础上加一
            Integer number = cartServiceOne.getNumber();
            cartServiceOne.setNumber(number + 1);
            shoppingCartService.updateById(cartServiceOne);
        }else {
            //如果不存在则添加到购物车中,其数量默认为一
            shoppingCart.setNumber(1);
            shoppingCartService.save(shoppingCart);
            cartServiceOne = shoppingCart;
        }

        return R.success(cartServiceOne);
    }
}

然后我们来开发减少购物车中菜品数量的代码,这种代码其实也非常简单,跟我们的新增的代码差不多,我们这里的逻辑同样是先判断是菜品还是套餐,然后查询处对应的数据,接着我们判断其数量是否为1,若为1则直接删除,反之则更新其数量,令其数量-1,最后返回对应对象的结果

/**
 * 减少购物车中菜品数量
 * @param shoppingCart
 * @return
 */
@PostMapping("/sub")
public R<ShoppingCart> subtract(@RequestBody ShoppingCart shoppingCart){
    log.info("购物车数据:{}",shoppingCart);

    //设置用户id,指定当前是哪个用户的购物车数据
    Long currentId = BaseContext.getCurrentId();
    shoppingCart.setUserId(currentId);

    //查询当前菜品或者套餐是否在购物车中
    Long dishId = shoppingCart.getDishId();

    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(ShoppingCart::getUserId,currentId);

    if(dishId != null){
        //添加到购物车的是菜品
        queryWrapper.eq(ShoppingCart::getDishId,dishId);
    }else {
        //添加到购物车的是套餐
        queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
    }

    //SQL:select * from shopping_cart where user_id ? and dish_id/setmeal_id = ?
    ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
    int num = cartServiceOne.getNumber();

    if(num != 1){
        //如果数量大于1,则在原先的数量上执行-1的更新操作
        Integer number = cartServiceOne.getNumber();
        cartServiceOne.setNumber(number - 1);
        shoppingCartService.updateById(cartServiceOne);
    }else {
        //若数量为1,则直接删除
        shoppingCartService.remove(queryWrapper);
        cartServiceOne = shoppingCart;
    }

    return R.success(cartServiceOne);
}

然后是我们的查看购物车的方法,我们这里无非是根据id查出所有的结果,然后封装到一个集合中返回给前端就行了

/**
 * 查看购物车
 * @return
 */
@GetMapping("/list")
public R<List<ShoppingCart>> list() {
    log.info("查看购物车...");

    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
    queryWrapper.orderByDesc(ShoppingCart::getCreateTime);

    List<ShoppingCart> list = shoppingCartService.list(queryWrapper);

    return R.success(list);
}

最后是清空购物车的方法,这个非常简单,直接根据id移除所有菜品即可

/**
 * 清空购物车
 * @return
 */
@DeleteMapping("/clean")
public R<String> clean(){
    //SQL:delete from shopping_cart where user_id = ?

    LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());

    shoppingCartService.remove(queryWrapper);

    return R.success("清空购物车成功");
}

用户下单相关业务开发

最后我们来实现用户下单的相关业务开发,首先我们要实现的下单这个功能的开发,先来看看我们的需求分析

然后我们来看看我们的数据模型

订单表里保存着订单的大概信息

订单明细表则保存订单的详细信息包括点的菜品,套餐分别都是啥之类的

接着我们来看看我们用户下单的交互过程,这里的交互过程其实挺多的,但是我们之前已经完成了大多数了,这里调用的都是之前的方法,所以除了最后一个请求之外我们其他的都不用去理会了

接着我们来做代码开发的准备工作,这一部分的内容我们就省略了

由于下单的方法比较繁琐,因此我们新创建一个方法来完成这个请求

/**
 * 用户下单
 * @param orders
 */
public void submit(Orders orders);

然后我们在下面实现这个方法,前端传过来的内容里的数据可以用一个订单对象承接到,因此我们这里使用该对象承接,接着我们查询对应的购物车数据,用户数据,用户地址数据用于下面的下单功能的实现,由于我们的订单需要一个id,所以我们这里调用MP提供的工具类随机生成一个id,然后我们要统计下单金额,而正好我们的下单内容里还需要将具体数据保存到我们的订单明细表中,因此我们用stream流的形式构建这么一个对应的集合,我们这里使用我们的AtomicInteger对象来统计,这个对象在多线程高并发的情况下也有良好的表现,所以我们用这个对象

接着中间的一大堆的有的没的设置内容我们就不提了,都是些没什么含金量的内容,我们这里直接复制即可,最后我们往订单表和订单明细表中插入数据即可,插入之后我们则是下单完成,别忘了此时要清空购物车的数据

@Service
@Slf4j
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {

    @Autowired
    private ShoppingCartService shoppingCartService;

    @Autowired
    private UserService userService;

    @Autowired
    private AddressBookService addressBookService;

    @Autowired
    private OrderDetailService orderDetailService;

    /**
     * 用户下单
     * @param orders
     */
    @Override
    @Transactional
    public void submit(Orders orders) {
        //获得当前用户id
        Long currentId = BaseContext.getCurrentId();

        //查询当前用户的购物车数据
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,currentId);
        List<ShoppingCart> shoppingCarts = shoppingCartService.list(queryWrapper);

        if(shoppingCarts == null || shoppingCarts.size()==0){
            throw new CustomException("购物车为空,不能下单");
        }

        //查询用户数据
        User user = userService.getById(currentId);

        //查询地址数据
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);

        if(addressBook == null){
            throw new CustomException("用户地址信息有误,不能下单");
        }

        long orderId = IdWorker.getId();

        AtomicInteger amount = new AtomicInteger(0);

        List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) ->{
            OrderDetail orderDetail = new OrderDetail();
            orderDetail.setOrderId(orderId);
            orderDetail.setNumber(item.getNumber());
            orderDetail.setDishFlavor(item.getDishFlavor());
            orderDetail.setDishId(item.getDishId());
            orderDetail.setSetmealId(item.getSetmealId());
            orderDetail.setName(item.getName());
            orderDetail.setImage(item.getImage());
            orderDetail.setAmount(item.getAmount());
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
            return orderDetail;
        }).collect(Collectors.toList());

        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        orders.setStatus(2);
        orders.setAmount(new BigDecimal(amount.get()));//总金额
        orders.setUserId(currentId);
        orders.setNumber(String.valueOf(orderId));
        orders.setUserName(user.getName());
        orders.setConsignee(addressBook.getConsignee());
        orders.setPhone(addressBook.getPhone());
        orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
                + (addressBook.getCityName() == null ? "" : addressBook.getCityName())
                + (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
                + (addressBook.getDetail() == null ? "" : addressBook.getDetail()));

        //向订单表插入一条数据
        this.save(orders);

        //向订单明细表中插入一条或多条数据
        orderDetailService.saveBatch(orderDetails);

        //清空购物车数据
        shoppingCartService.remove(queryWrapper);
    }
}

最后我们在对应的控制层中调用我们创建的方法即可

/**
 * 用户下单
 * @param orders
 * @return
 */
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
    log.info("订单数据:{}",orders);
    ordersService.submit(orders);
    return R.success("下单成功");
}