苍穹外卖-day03

课程内容

  • 公共字段自动填充
  • 新增菜品
  • 菜品分页查询
  • 删除菜品
  • 修改菜品

功能实现:菜品管理

菜品管理效果图:

在这里插入图片描述

1. 公共字段自动填充

1.1 问题分析

在上一章节我们已经完成了后台系统的员工管理功能菜品分类功能的开发,在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在我们的系统中很多表中都会有这些字段,如下:

序号字段名含义数据类型
1create_time创建时间datetime
2create_user创建人idbigint
3update_time修改时间datetime
4update_user修改人idbigint

而针对于这些字段,我们的赋值方式为:

1). 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。

2). 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。

目前,在我们的项目中处理这些字段都是在每一个业务方法中进行赋值操作,如下:

新增员工方法:

	/**
     * 新增员工
     *
     * @param employeeDTO
     */
    public void save(EmployeeDTO employeeDTO) {
        //.......................
		//
        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());

        //设置当前记录创建人id和修改人id
        employee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改
        employee.setUpdateUser(BaseContext.getCurrentId());
		///
        employeeMapper.insert(employee);
    }

编辑员工方法:

	/**
     * 编辑员工信息
     *
     * @param employeeDTO
     */
    public void update(EmployeeDTO employeeDTO) {
       //........................................
	   ///
        employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(BaseContext.getCurrentId());
       ///

        employeeMapper.update(employee);
    }

新增菜品分类方法:

	/**
     * 新增分类
     * @param categoryDTO
     */
    public void save(CategoryDTO categoryDTO) {
       //....................................
       //
        //设置创建时间、修改时间、创建人、修改人
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateUser(BaseContext.getCurrentId());
        category.setUpdateUser(BaseContext.getCurrentId());
        ///

        categoryMapper.insert(category);
    }

修改菜品分类方法:

	/**
     * 修改分类
     * @param categoryDTO
     */
    public void update(CategoryDTO categoryDTO) {
        //....................................
        
		//
        //设置修改时间、修改人
        category.setUpdateTime(LocalDateTime.now());
        category.setUpdateUser(BaseContext.getCurrentId());
        //

        categoryMapper.update(category);
    }

如果都按照上述的操作方式来处理这些公共字段, 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐,那能不能对于这些公共字段在某个地方统一处理,来简化开发呢?

答案是可以的,我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。

1.2 实现思路

在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:

  • 插入数据时,需要设置的字段值,如下表格
  • 更新数据时,需要设置的字段值,如下表格
序号字段名含义数据类型操作类型
1create_time创建时间datetimeinsert
2create_user创建人idbigintinsert
3update_time修改时间datetimeinsert、update
4update_user修改人idbigintinsert、update

实现步骤:

1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法

2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

3). 在 Mapper 的方法上加入 AutoFill 注解

若要实现上述步骤,需掌握以下知识(之前课程内容都学过)

技术点

  • 枚举:因为枚举可以标识我们当前这个操作的类型,比如当前操作是insert操作还是update操作。因为你不同的操作,最终操作的字段是不一样的。
  • 注解
  • AOP
  • 反射:为公共字段赋值

1.3 代码开发(通过AOP实现)

按照上一小节分析的实现步骤依次实现,共三步。

1.3.1 步骤一

自定义注解 AutoFill

进入到sky-server模块,创建com.sky.annotation包。
在这里插入图片描述

package com.sky.annotation;

import com.sky.enumeration.OperationType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@Target(ElementType.METHOD)  //注解使用位置
@Retention(RetentionPolicy.RUNTIME) //注解声明周期
public @interface AutoFill {
    //注解中指定一个属性:指定当前数据库操作的类型(UPDATE INSERT)
    //   注意这几个公共字段只用在UPDATE INSERT操作时才有必要去设置,
    //   查询和删除不需要设置这几个公共字段。
    OperationType value(); //通过枚举方式,OperationType为创建的枚举类
}

其中枚举类OperationType已在sky-common模块中定义
在这里插入图片描述

package com.sky.enumeration;

/**
 * 数据库操作类型
 */
public enum OperationType {

    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT
}
1.3.2 步骤二

自定义切面 AutoFillAspect

在sky-server模块,创建com.sky.aspect包。

在这里插入图片描述

package com.sky.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect //标识当前类是一个AOP类
@Component //把当前类交给Spring容器去管理
@Slf4j  //方便记录一些日志
public class AutoFillAspect {

    /**
     * 切入点:哪些方法需要被拦截
     *  mapper包下所有的类所有的方法,同时还要满足这个方法上加入了AutoFill注解
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 通知:代码增强的业务逻辑部分
     * 应该用的是前置通知,在执行insert和update方法之前,需要为这几个公共字段赋上值。
     * 如果sql执行完毕后在赋值就没有意义了。
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        /重要
        //可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解
        log.info("开始进行公共字段自动填充...");

    }
}

完善自定义切面 AutoFillAspect 的 autoFill 方法

package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect //标识当前类是一个AOP类
@Component //把当前类交给Spring容器去管理
@Slf4j  //方便记录一些日志
public class AutoFillAspect {

    /**
     * 切入点:哪些方法需要被拦截
     *  mapper包下所有的类所有的方法,同时还要满足这个方法上加入了AutoFill注解
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 通知:代码增强的业务逻辑部分
     * 应该用的是前置通知,在执行insert和update方法之前,需要为这几个公共字段赋上值。
     * 如果sql执行完毕后在赋值就没有意义了。
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填充...");

        //1.获取到当前被拦截的方法上的数据库操作类型  需要转化为它的子接口MethodSignature
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
        OperationType operationType = autoFill.value();//获得数据库操作类型

        /*
        * 2.获取到当前被拦截的方法的参数--实体对象
        * 约定:如果你想要实现自动填充的话,一定要保证这个实体对象放在第一个参数位置,
        *      因为我们接下来要获取的话就获取第一个就可以了。
        * */
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0){ //当前方法没有参数,后续代码没必要执行了,防止出现空指针
            return;
        }

        /*
        * 取出第一个参数,之前已经做了约定 实体放在第一个参数位置
        * 注意:不要使用Employee来接收,因为这个实体类型是不确定的,现在员工管理是
        *      Employee实体类接收参数,后期在分类功能中使用的是Category实体类接收参数,
        *      菜品mapper中传入的是菜品的实体。所以这个地方使用Object来接收。
        *
        * */
        Object entity = args[0];

        //3.准备赋值的数据
        LocalDateTime now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //4.根据当前不同的操作类型,为对应的属性通过反射来赋值   OperationType:之前创建的枚举
        if(operationType == OperationType.INSERT){
            //为4个公共字段赋值
            try {
                //通过实体类的set方法为公共的属性赋值,所以需要先获取对应的set方法对象
                //  参数:方法名,方法的形参类型列表    此方法需要处理异常
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值   方法对象.invoke:调用对应的方法
                // 参数:为哪个对象,具体赋的值
                setCreateTime.invoke(entity,now);
                setCreateUser.invoke(entity,currentId);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else if(operationType == OperationType.UPDATE){
            //为2个公共字段赋值
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                //通过反射为对象属性赋值
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }
}

1.3.3 步骤三

在Mapper接口的方法上加入 AutoFill 注解

CategoryMapper为例,分别在新增和修改方法添加@AutoFill()注解,也需要EmployeeMapper做相同操作

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

package com.sky.mapper;

@Mapper
public interface CategoryMapper {
    /**
     * 插入数据
     * @param category
     */
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    @AutoFill(value = OperationType.INSERT)
    void insert(Category category);
    /**
     * 根据id修改分类
     * @param category
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Category category);

}

同时,将业务层为公共字段赋值的代码注释掉。

1). 将员工管理的新增和编辑方法中的公共字段赋值的代码注释。
在这里插入图片描述
在这里插入图片描述

2). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。

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

1.4 功能测试

新增菜品分类为例,进行测试

启动项目和Nginx

在这里插入图片描述

查看控制台

通过观察控制台输出的SQL来确定公共字段填充是否完成

在这里插入图片描述

查看表

category表中数据

在这里插入图片描述

其中create_time,update_time,create_user,update_user字段都已完成自动填充。

由于使用admin(id=1)用户登录进行菜品添加操作,故create_user,update_user都为1.

1.5 代码提交

点击提交:

在这里插入图片描述

提交过程中,出现提示:

在这里插入图片描述

继续push:

在这里插入图片描述

推送成功:

在这里插入图片描述

2. 新增菜品

2.1 需求分析与设计

2.1.1 产品原型

后台系统中可以管理菜品信息,通过 新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片。

新增菜品原型:

在这里插入图片描述

当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。

业务规则:

  • 菜品名称必须是唯一的
  • 菜品必须属于某个分类下,不能单独存在
  • 新增菜品时可以根据情况选择菜品的口味
  • 每个菜品必须对应一张图片
2.1.2 接口设计

根据上述原型图先粗粒度设计接口,共包含3个接口。

接口设计:

  • 根据类型查询分类(已完成)
  • 文件上传
  • 新增菜品

接下来细粒度分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。

1. 根据类型查询分类

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

2. 文件上传

在这里插入图片描述

在这里插入图片描述

3. 新增菜品

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

2.1.3 表设计

通过原型图进行分析:

在这里插入图片描述

新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:

菜品表、菜品口味表:一对多

表名说明
dish菜品表
dish_flavor菜品口味表

1). 菜品表:dish

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)菜品名称唯一
category_idbigint分类id逻辑外键
pricedecimal(10,2)菜品价格
imagevarchar(255)图片路径
descriptionvarchar(255)菜品描述
statusint售卖状态1起售 0停售
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

2). 菜品口味表:dish_flavor

字段名数据类型说明备注
idbigint主键自增
dish_idbigint菜品id逻辑外键
namevarchar(32)口味名称
valuevarchar(255)口味值

2.2 代码开发

2.2.1 文件上传实现(配置文件引用配置文件,文件上传)

因为在新增菜品时,需要上传菜品对应的图片(文件),包括后续其它功能也会使用到文件上传,故要实现通用的文件上传接口。

文件上传,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发抖音、发朋友圈都用到了文件上传功能。

实现文件上传服务,需要有存储的支持,那么我们的解决方案将以下几种:

  1. 直接将图片保存到服务的硬盘(springmvc中的文件上传)
    1. 优点:开发便捷,成本低
    2. 缺点:扩容困难
  2. 使用分布式文件系统进行存储
    1. 优点:容易实现扩容
    2. 缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO)
  3. 使用第三方的存储服务(例如OSS)
    1. 优点:开发简单,拥有强大功能,免维护
    2. 缺点:付费

在本项目选用阿里云的OSS服务进行文件存储。(前面课程已学习过阿里云OSS,不再赘述)

在这里插入图片描述

实现步骤:

1). 定义OSS相关配置

在sky-server模块

application.yml

问题:这个地方具体的值不希望直接配置在这个地方,因为当前配置文件是Spingboot项目的主配置文件,最终项目上线有可能这个地方需要修改,在开发环境下和生产环境下可能用的是不同的账号,如果上线的时候忘了修改这个地方,是不是就出现了问题???

解决:通过引用的方式引用开发环境使用的配置文件或者生产环境下使用的配置文件,想要修改时只需要在这个地方通过active属性切换环境即可。

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

spring:
  profiles:
    active: dev    #设置环境
sky:
  alioss:
    endpoint: ${sky.alioss.endpoint}
    #实体类的属性为accessKeyId,配置文件习惯使用-分割,实体类习惯使用驼峰规则命名,这个地方
    #    Springboot框架会自动进行转化,当然都保持和实体类的属性一样也可以。
    access-key-id: ${sky.alioss.access-key-id}
    access-key-secret: ${sky.alioss.access-key-secret}
    bucket-name: ${sky.alioss.bucket-name}

application-dev.yml
在这里插入图片描述

sky:
  alioss:
    endpoint: oss-cn-hangzhou.aliyuncs.com
    access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
    access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7
    bucket-name: sky-take-out

2). 读取OSS配置

在sky-common模块中,已定义
在这里插入图片描述

package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

}

3). 生成OSS工具类对象

在sky-server模块
在这里插入图片描述

package com.sky.config;

import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置类,用于创建AliOssUtil对象,调用对应的构造方法,
 *   通过构造方法为AliOssUtil的4个属性赋值。
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
    //判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器。可以保证整个spring容器里面只有
    //  一个AliOssUtil对象,因为对于这种工具类对象没有必要创建这么多,假设在其他地方已将创建好了
    //  这个对象,那么我们就不需要在去创建了。
    @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){ //因为AliOssProperties已经交给Spring容器管理了,所以直接注入
        log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}

其中,AliOssUtil.java已在sky-common模块中定义
在这里插入图片描述

package com.sky.utils;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;

@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);

        log.info("文件上传到:{}", stringBuilder.toString());

        return stringBuilder.toString();
    }
}

4). 定义文件上传接口

在sky-server模块中定义接口

package com.sky.controller.admin;

import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

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

/**
 * 通用接口:其它模块也可能用到文件上传
 */
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

    @Autowired
    private AliOssUtil aliOssUtil;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    /**
     * 为什么泛型指定为String类型???
     * 由接口文档可知,返回的Result中data属性值是 文件上传到阿里云后的url地址  类型为String类型
     *
     * 通过MultipartFile 类型来接收上传的文件,要求方法的形参名和表单项的name属性的值/前端提交的参数名(
     * 由接口文档可知参数名为file)一致,如果不一致使用@requesrparam注解进行参数绑定
     */
    public Result<String> upload(MultipartFile file){
        log.info("文件上传:{}",file);

        try {
            //原始文件名
            String originalFilename = file.getOriginalFilename();
            //截取原始文件名的后缀   dfdfdf.png
            String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
            //构造新文件名称:防止上传到阿里云的文件,因为名字重复导致覆盖的问题
            String objectName = UUID.randomUUID().toString() + extension;

            //文件的请求路径
            //参数:  byte数组,文件对象转成的数组     传上去的图片在阿里云存储空间里面的名字
            String filePath = aliOssUtil.upload(file.getBytes(), objectName);
            return Result.success(filePath);
        } catch (IOException e) {
            log.error("文件上传失败:{}", e);
        }

        return Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

功能测试:

  • 说明:测试文件上传的接口,Swagger对文件上传支持的并不是很好,没有办法上传图片提交测试
    在这里插入图片描述

  • 可以使用postman或者前后端联调
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

2.2.2 新增菜品实现(多张表插入数据)

1). 设计DTO类

在sky-pojo模块中
在这里插入图片描述

package com.sky.dto;

import com.sky.entity.DishFlavor;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDTO implements Serializable {

    private Long id;
    //菜品名称
    private String name;
    //菜品分类id
    private Long categoryId;
    //菜品价格
    private BigDecimal price;
    //图片
    private String image;
    //描述信息
    private String description;
    //0 停售 1 起售
    private Integer status;
    //口味 :通过对象类型的集合来接收
    private List<DishFlavor> flavors = new ArrayList<>();
}

在这里插入图片描述

package com.sky.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 菜品口味
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishFlavor implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    //菜品id
    private Long dishId;

    //口味名称
    private String name;

    //口味数据list
    private String value;

}

2). Controller层

进入到sky-server模块
在这里插入图片描述

package com.sky.controller.admin;

import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;

/**
 * 菜品管理
 */
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {

    @Autowired
    private DishService dishService;

    /**
     * 新增菜品
     *
     * @param dishDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavor(dishDTO);//后绪步骤开发
        return Result.success();
    }
}

3). Service层接口

package com.sky.service;

import com.sky.dto.DishDTO;
import com.sky.entity.Dish;

public interface DishService {

    /**
     * 新增菜品和对应的口味
     *
     * @param dishDTO
     */
    public void saveWithFlavor(DishDTO dishDTO);

}

4). Service层实现类

package com.sky.service.impl;

import com.sky.dto.DishDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Slf4j
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;

    /**
     * 新增菜品和对应的口味
     *
     * @param dishDTO
     */
    @Transactional //涉及到多个表的数据操作,所以需要保证数据的一致性
    public void saveWithFlavor(DishDTO dishDTO) {

        //1.向菜品表插入1条数据,由页面原型可知一次只能插入一条
        //  不需要把整个DishDTO传进去,因为DishDTO包含菜品和菜品口味数据,
        //   现在只需要插入菜品数据,所以这个地方只需要传递dish菜品实体对象即可
        //   通过对象属性拷贝,前提是2者属性保持一致
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);
        dishMapper.insert(dish);//后绪步骤实现

        //2.向口味表插入n条数据,一条、多条、没有
        //获取insert语句生成的主键值
        //注意:此时前端不能传递dishId属性,因为当前是新增菜品,此时这个菜品还没有添加完,
        //     这个dishId根本没有值。它是菜品插入玩自动生产的id,也就是口味表所关联的外键dishId。
        //解决:上面已经向菜品表插入了一条数据,这个dishId已经分配好了,所以可以在sql上
        //     通过useGeneratedKeys开启获取插入数据时生成的主键值,赋值给keyProperty
        //     指定的属性值id
        Long dishId = dish.getId();

        //口味数据通过实体类的对象集合属性封装的,所以需要先把集合中的数据取出来
        List<DishFlavor> flavors = dishDTO.getFlavors();
        //口味不是必须的有可能用户没有提交口味数据,所以需要判断一下
        if (flavors != null && flavors.size() > 0) {
            //用户确实提交的有口味数据,此时插入口味数据才有意义
            //有了菜单表这个主键值id,就需要为dishFlavor里面的每一个dishId(关联外键)属性赋值,
            //   所以在批量插入数据之前需要遍历这个对象集合,为里面的每个对象DishFlavor的dishId赋上值
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });

            /*
            * 口味数据flavors是一个对象类型的list集合来接收的,
            * 不需要遍历这个集合一条一条的插入数据,因为sql支持批量插入
            * 直接把这个集合对象传进去,通过动态sql标签foreach进行遍历获取。
            * */
            dishFlavorMapper.insertBatch(flavors);//后绪步骤实现
        }
    }

}

5). Mapper层

DishMapper.java中添加

	/**
     * 插入菜品数据
     *
     * @param dish
     */
    @AutoFill(value = OperationType.INSERT)//AOP实现公共字段填充
    void insert(Dish dish);

在/resources/mapper中创建DishMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user, status)
        values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})
    </insert>
</mapper>

DishFlavorMapper.java

package com.sky.mapper;

import com.sky.entity.DishFlavor;
import java.util.List;

@Mapper
public interface DishFlavorMapper {
    /**
     * 批量插入口味数据
     * @param flavors
     */
    void insertBatch(List<DishFlavor> flavors);

}

在/resources/mapper中创建DishFlavorMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper">
    <!--注意这个dishId-->
    <insert id="insertBatch">
        insert into dish_flavor (dish_id, name, value) VALUES
        <foreach collection="flavors" item="df" separator=",">
            (#{df.dishId},#{df.name},#{df.value})
        </foreach>
    </insert>
</mapper>

2.3 功能测试

进入到菜品管理—>新建菜品

因为录入的数据比较多所以推荐使用前后端联调测试。

在这里插入图片描述

由于没有实现菜品查询功能,所以保存后,暂且在表中查看添加的数据。

dish表:

在这里插入图片描述

dish_flavor表:

在这里插入图片描述

测试成功。

2.4代码提交

在这里插入图片描述

后续步骤和上述功能代码提交一致,不再赘述。

3. 菜品分页查询(多表,表中字段和VO中属性不一致)

3.1 需求分析和设计

3.1.1 产品原型

系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

菜品分页原型:

在这里插入图片描述

在菜品列表展示时,除了菜品的基本信息(名称、售价、售卖状态、最后操作时间)外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。

业务规则:

  • 根据页码展示菜品信息
  • 每页展示10条数据
  • 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
3.1.2 接口设计

根据上述原型图,设计出相应的接口。

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

3.2 代码开发

3.2.1 设计DTO类

根据菜品分页查询接口定义设计对应的DTO:
在这里插入图片描述

在sky-pojo模块中,已定义

package com.sky.dto;

import lombok.Data;
import java.io.Serializable;

@Data
public class DishPageQueryDTO implements Serializable {

    private int page;
    private int pageSize;
    private String name;
    private Integer categoryId; //分类id
    private Integer status; //状态 0表示禁用 1表示启用

}
3.2.2 设计VO类
  • 为什么还要设计一个VO呢???
  • 由接口文档可知,返回的数据是categoryName分类名称,但是菜品表实际上存的是分类的id,现在页面想要展示分类的名称所以这个地方专门设计了一个VO,VO存的是分类名称属性。

根据菜品分页查询接口定义设计对应的VO:
在这里插入图片描述

在sky-pojo模块中,已定义

package com.sky.vo;

import com.sky.entity.DishFlavor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {

    private Long id;
    //菜品名称
    private String name;
    //菜品分类id
    private Long categoryId;
    //菜品价格
    private BigDecimal price;
    //图片
    private String image;
    //描述信息
    private String description;
    //0 停售 1 起售
    private Integer status;
    //更新时间
    private LocalDateTime updateTime;
    //分类名称
    private String categoryName;
    //菜品关联的口味
    private List<DishFlavor> flavors = new ArrayList<>();
}
3.2.3 Controller层

根据接口定义创建DishController的page分页查询方法:
在这里插入图片描述

	/**
     * 菜品分页查询
     *
     * @param dishPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("菜品分页查询")
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
        log.info("菜品分页查询:{}", dishPageQueryDTO);
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);//后绪步骤定义
        return Result.success(pageResult);
    }
3.2.4 Service层接口

在 DishService 中扩展分页查询方法:

	/**
     * 菜品分页查询
     *
     * @param dishPageQueryDTO
     * @return
     */
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
3.2.5 Service层实现类

在 DishServiceImpl 中实现分页查询方法:

    @Override
    /**
     * 菜品分页查询
     *
     * @param dishPageQueryDTO
     * @return
     */
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
        //需要在查询功能之前开启分页功能:当前页的页码   每页显示的条数
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
        //这个方法有返回值为Page对象,里面保存的是分页之后的相关数据
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);//后绪步骤实现
        //封装到PageResult中:总记录数  当前页数据集合
        return new PageResult(page.getTotal(), page.getResult());
    }
3.2.6 Mapper层

在 DishMapper 接口中声明 pageQuery 方法:

	/**
     * 菜品分页查询
     *
     * @param dishPageQueryDTO
     * @return
     */
    Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

在 DishMapper.xml 中编写SQL:

  • 技巧:先在SqlYog中写好sql语句,如果测试没有问题,再把sql考到程序里面来,这样可以保证这个程序是可以正常运行的。

  • 菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。

  • sql编写:select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id,暂时不写where条件。
    在这里插入图片描述

  • 通过2张表的左外连接查询,查出菜品表dish中所有的字段以及对应分类表category 中的分类名称字段。

  • 注意:此时查询出菜品表中的字段 菜品名称为name,分类表中的字段分类名称也是name,那这样的话我们在封装数据的时候就会出现问题,通过mybatis框架去封装数据的时候由于这2个字段名相同,封装VO这个数据的时候就会对应错,分类表中的字段分类名称是name字段,DishVO是categoryName属性,字段名和属性名不一致所以封装不了数据。------------通过起字段别名方式解决c.name as categoryName

  • 名称习惯使用模糊查询而不是等号。

<select id="pageQuery" resultType="com.sky.vo.DishVO">
        select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id
        <where>
            <if test="name != null">
                and d.name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and d.category_id = #{categoryId}
            </if>
            <if test="status != null">
                and d.status = #{status}
            </if>
        </where>
        order by d.create_time desc
</select>

3.3 功能测试

3.3.1 接口文档测试

启动服务:访问http://localhost:8080/doc.html,进入菜品分页查询接口

注意:使用admin用户登录重新获取token,防止token失效。

在这里插入图片描述

点击发送:

在这里插入图片描述

3.3.2 前后端联调测试

启动nginx,访问 http://localhost

点击菜品管理

在这里插入图片描述

数据成功查出。

4. 删除菜品(批量删除,删除的业务规则)

4.1 需求分析和设计

4.1.1 产品原型

在菜品列表页面,每个菜品后面对应的操作分别为修改删除停售,可通过删除功能完成对菜品及相关的数据进行删除。

删除菜品原型:

在这里插入图片描述

业务规则:

  • 可以一次删除一个菜品,也可以批量删除菜品
    • 没有必要设计2个删除的接口,因为单个删除可以包含在批量删除里面,所以只需要设计一个批量删除的接口即可。
  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
    • 因为菜品删掉之后会影响我们这个套餐
  • 删除菜品后,关联的口味数据也需要删除掉
    • 删除菜品后,这些菜品所关联的口味数据就没有用了。
4.1.2 接口设计

根据上述原型图,设计出相应的接口。

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

注意:删除一个菜品和批量删除菜品共用一个接口,故ids可包含多个菜品id,之间用逗号分隔。

4.1.3 表设计

在进行删除菜品操作时,会涉及到以下三张表。

菜品表、菜品口味表、套餐和菜品的中间关系表(菜品表 套餐表是多对多关系)
在这里插入图片描述

注意事项:

  • 在dish菜品表中删除菜品基本数据时,同时,也要把关联在dish_flavor口味表中的数据一块删除。
  • setmeal_dish表为菜品和套餐关联的中间表。
  • 若删除的菜品数据关联着某个套餐,此时,删除失败。
  • 若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。

4.2 代码开发

4.1.2 Controller层

根据删除菜品的接口定义在DishController中创建方法:
在这里插入图片描述

	/**
     * 菜品批量删除
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("菜品批量删除")
    public Result delete(@RequestParam List<Long> ids) {
        log.info("菜品批量删除:{}", ids);
        dishService.deleteBatch(ids);//后绪步骤实现
        return Result.success();
    }
4.2.2 Service层接口

在DishService接口中声明deleteBatch方法:

	/**
     * 菜品批量删除
     *
     * @param ids
     */
    void deleteBatch(List<Long> ids);
4.2.3 Service层实现类

在DishServiceImpl中实现deleteBatch方法:

    @Autowired
    private SetmealDishMapper setmealDishMapper;
    
    /**
     * 菜品批量删除
     *
     * @param ids
     */
    @Transactional//事务
    @Override
    public void deleteBatch(List<Long> ids) {
        //判断当前菜品是否能够删除---是否存在起售中的菜品??
        //思路:遍历获取传入的id,根据id查询菜品dish中的status字段,0 停售 1 起售,
        //    如果是1代表是起售状态不能删除
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);//后绪步骤实现
            if (dish.getStatus().equals(StatusConstant.ENABLE)) { //常量类方式
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        //判断当前菜品是否能够删除---是否被套餐关联了??
        //思路:菜品表 套餐表是多对多关系,它们的关系表为菜品套餐关系表setmeal_dish(菜品id 对应 套餐id)
        //     当前要删除菜品此时是知道菜品的Id的,所以可以根据这个菜品id去查询套餐id,如果能查出来说明
        //     菜品被套餐关联了,不能删除。
        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if (setmealIds != null && setmealIds.size() > 0) {
            //当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }

        //删除菜品表中的菜品数据
        //这个地方是在业务层循环遍历一个个删除的。
        //缺点:这个地方循环遍历删除,写了2次sql性能比较差
        //解决:动态sql-foreach,只需要一条sql就可以实现批量删除。详情查看4.5
        for (Long id : ids) {
            dishMapper.deleteById(id);//后绪步骤实现
            //删除菜品关联的口味数据
            //思路:这个地方不需要先去查一下有没有这个口味在去删除,因为不管你有还是没有
            //     我都尝试进行删除,所以这个地方不需要在去查了。
            // 根据菜品的id去删除口味表:菜品表--》口味表 一对多,菜品的id 保存在口味表当中充当外键dish_id,
            //     删除口味表的sql条件为dish_id也就是这个传入的菜品id
            dishFlavorMapper.deleteByDishId(id);//后绪步骤实现
        }


    }
4.2.4 Mapper层

在DishMapper中声明getById方法,并配置SQL:

	/**
     * 根据主键查询菜品
     *
     * @param id
     * @return
     */
    @Select("select * from dish where id = #{id}")
    Dish getById(Long id);

创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:

package com.sky.mapper;

import com.sky.entity.SetmealDish;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface SetmealDishMapper {
    /**
     * 根据菜品id查询对应的套餐id
     * 多对多关系,有可能查询出多个套餐,所以使用list集合接收
     * @param dishIds
     * @return
     */
    //select setmeal_id from setmeal_dish where dish_id in (1,2,3,4)
    List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
}

SetmealDishMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
        
<mapper namespace="com.sky.mapper.SetmealDishMapper">

    <select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
        select setmeal_id from setmeal_dish where dish_id in
        <foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
            #{dishId}
        </foreach>
    </select>
    
</mapper>

在DishMapper.java中声明deleteById方法并配置SQL:

	/**
     * 根据主键删除菜品数据
     *
     * @param id
     */
    @Delete("delete from dish where id = #{id}")
    void deleteById(Long id);

在DishFlavorMapper中声明deleteByDishId方法并配置SQL:

    /**
     * 根据菜品id删除对应的口味数据
     * @param dishId
     */
    @Delete("delete from dish_flavor where dish_id = #{dishId}")
    void deleteByDishId(Long dishId);

4.3 功能测试

既可以通过Swagger接口文档进行测试,也可以通过前后端联调测试,接下来,我们直接使用前后端联调测试

进入到菜品列表查询页面

在这里插入图片描述

对测试菜品进行删除操作

在这里插入图片描述

同时,进到dish表和dish_flavor两个表查看测试菜品的相关数据都已被成功删除。

再次,删除状态为启售的菜品

在这里插入图片描述

点击批量删除

在这里插入图片描述

删除失败,因为起售中的菜品不能删除。

4.4 代码提交

在这里插入图片描述

后续步骤和上述功能代码提交一致,不再赘述。

4.5 批量删除优化

修改DishServiceImpl代码:
在这里插入图片描述

    /**
     * 菜品批量删除
     *
     * @param ids
     */
    @Transactional//事务
    @Override
    public void deleteBatch(List<Long> ids) {
        //判断当前菜品是否能够删除---是否存在起售中的菜品??
        //思路:遍历获取传入的id,根据id查询菜品dish中的status字段,0 停售 1 起售,
        //    如果是1代表是起售状态不能删除
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);//后绪步骤实现
            if (dish.getStatus().equals(StatusConstant.ENABLE)) { //常量类方式
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        //判断当前菜品是否能够删除---是否被套餐关联了??
        //思路:菜品表 套餐表是多对多关系,它们的关系表为菜品套餐关系表setmeal_dish(菜品id 对应 套餐id)
        //     当前要删除菜品此时是知道菜品的Id的,所以可以根据这个菜品id去查询套餐id,如果能查出来说明
        //     菜品被套餐关联了,不能删除。
        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if (setmealIds != null && setmealIds.size() > 0) {
            //当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }

/*        //删除菜品表中的菜品数据---有缺点
        //这个地方是在业务层循环遍历一个个删除的。
        //缺点:这个地方循环遍历删除,写了2次sql性能比较差
        //解决:动态sql-foreach,只需要一条sql就可以实现批量删除。详情查看4.5
        for (Long id : ids) {
            dishMapper.deleteById(id);//后绪步骤实现
            //删除菜品关联的口味数据
            //思路:这个地方不需要先去查一下有没有这个口味在去删除,因为不管你有还是没有
            //     我都尝试进行删除,所以这个地方不需要在去查了。
            // 根据菜品的id去删除口味表:菜品表--》口味表 一对多,菜品的id 保存在口味表当中充当外键dish_id,
            //     删除口味表的sql条件为dish_id也就是这个传入的菜品id
            dishFlavorMapper.deleteByDishId(id);//后绪步骤实现

        }*/

        //删除菜品表中的菜品数据---优化:
        //   上述代码每一次for循环遍历都会发生2条sql(删除菜品、删除口味),如果遍历的
        //   次数比较多,那么发出的sql数量也很多,可能会引发性能一些方面的问题。
        // 解决:减少sql语句的数量,使用动态sql批量删除 只需要1条sql就可以把需要删除的菜品删除掉
        //      同样1条sql删除口味表的数据

        //根据菜品id集合批量删除菜品数据
        dishMapper.deleteByIds(ids);

        //根据菜品id集合批量删除关联的口味数据
        dishFlavorMapper.deleteByDishIds(ids);


    }

DishMapper:

    /**
     * 根据主键删除菜品数据
     *
     * @param id
     */
/*    @Delete("delete from dish where id = #{id}")
    void deleteById(Long id);*/

    //根据菜品id集合批量删除菜品数据
    void deleteByIds(List<Long> ids);

DishFlavorMapper:

    /**
     * 根据菜品id删除对应的口味数据
     * @param dishId
     */
/*    @Delete("delete from dish_flavor where dish_id = #{dishId}")
    void deleteByDishId(Long dishId);*/

    //根据菜品id集合批量删除关联的口味数据
    void deleteByDishIds(List<Long> dishIds);

DishMapper.xml

    <delete id="deleteByIds">
        delete from dish where id in
        <foreach collection="ids" open="(" close=")" separator="," item="id">
            #{id}
        </foreach>
    </delete>

DishFlavorMapper .xml

    <delete id="deleteByDishIds">
        delete from dish_flavor where dish_id in
        <foreach collection="dishIds" open="(" close=")" separator="," item="dishId">
            #{dishId}
        </foreach>
    </delete>

5. 修改菜品

5.1 需求分析和设计

5.1.1 产品原型

在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击保存按钮完成修改操作。

修改菜品原型:

在这里插入图片描述

5.1.2 接口设计

通过对上述原型图进行分析,该页面共涉及4个接口。

接口:

  • 根据id查询菜品
    • 查询菜品的同时,还要把关联的口味一起查出来,因为这个地方也要回显。
  • 根据类型查询分类(已实现)
  • 文件上传(已实现)
  • 修改菜品

我们只需要实现根据id查询菜品修改菜品两个接口,接下来,我们来重点分析这两个接口。

1). 根据id查询菜品
在这里插入图片描述

在这里插入图片描述

2). 修改菜品

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

注:因为是修改功能,请求方式可设置为PUT。

5.2 代码开发

5.2.1 根据id查询菜品实现(分步查询)

用到的DishVO类:

package com.sky.vo;

import com.sky.entity.DishFlavor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {

    private Long id;
    //菜品名称
    private String name;
    //菜品分类id
    private Long categoryId;
    //菜品价格
    private BigDecimal price;
    //图片
    private String image;
    //描述信息
    private String description;
    //0 停售 1 起售
    private Integer status;
    //更新时间
    private LocalDateTime updateTime;
    //分类名称
    private String categoryName;
    //菜品关联的口味
    private List<DishFlavor> flavors = new ArrayList<>();

    //private Integer copies;
}

1). Controller层

根据id查询菜品的接口定义在DishController中创建方法:
在这里插入图片描述

    /**
     * 根据id查询菜品
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询菜品")
    public Result<DishVO> getById(@PathVariable Long id) {
        log.info("根据id查询菜品:{}", id);
        DishVO dishVO = dishService.getByIdWithFlavor(id);//后绪步骤实现
        return Result.success(dishVO);
    }

2). Service层接口

在DishService接口中声明getByIdWithFlavor方法:

	/**
     * 根据id查询菜品和对应的口味数据
     *
     * @param id
     * @return
     */
    DishVO getByIdWithFlavor(Long id);

3). Service层实现类

在DishServiceImpl中实现getByIdWithFlavor方法:

    /**
     * 根据id查询菜品和对应的口味数据
     *
     * @param id
     * @return
     */
    @Override
    public DishVO getByIdWithFlavor(Long id) {
        //根据id查询菜品数据
        Dish dish = dishMapper.getById(id); //删除的时候已经写过了,所以这里直接调用方法即可

        //根据菜品id查询口味数据
        //菜品表 口味表是一对多关系,菜品表的id保存在口味表当中充当外键为dish_id
        List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);//后绪步骤实现

        //将查询到的数据封装到VO
        DishVO dishVO = new DishVO();
        //通过对象拷贝的方式,把查询到的菜品数据封装到dishVO类中,
        //  注意:这个Dish类中没有categoryName分类名称属性,它不是必须的,所以这个地方拷不过来也没有关系
        //       但是点击修改页面确实回显分类名称了,它是通过这个分类的id回显得,查询菜品数据回显给前端的是
        //       分类的id,根据分类的id获取分类名称(接口已实现)进而来回显分类名称。
        BeanUtils.copyProperties(dish, dishVO);
        //通过set方法把口味数据封装到dishVO类中
        dishVO.setFlavors(dishFlavors);

        return dishVO;
    }

4). Mapper层

在DishFlavorMapper中声明getByDishId方法,并配置SQL:

    /**
     * 根据菜品id查询对应的口味数据
     * @param dishId
     * @return
     */
    @Select("select * from dish_flavor where dish_id = #{dishId}")
    List<DishFlavor> getByDishId(Long dishId);

DishMapper:

    /**
     * 根据主键查询菜品
     *
     * @param id
     * @return
     */
    @Select("select * from dish where id = #{id}")
    Dish getById(Long id);
5.2.1 修改菜品实现(修改的业务逻辑)

1). Controller层

根据修改菜品的接口定义在DishController中创建方法:

在这里插入图片描述

	/**
     * 修改菜品
     *
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}", dishDTO);
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }

2). Service层接口

在DishService接口中声明updateWithFlavor方法:

	/**
     * 根据id修改菜品基本信息和对应的口味信息
     *
     * @param dishDTO
     */
    void updateWithFlavor(DishDTO dishDTO);

3). Service层实现类

在DishServiceImpl中实现updateWithFlavor方法:

    /**
     * 根据id修改菜品基本信息和对应的口味信息
     *
     * 思路分析:菜品表修改直接使用update语句即可,对于这个关联的口味表,
     *         口味的修改比较复杂,因为它的情况有很多 有可能口味没写修改 有可能
     *         口味是追加的 也有可能口味是删除了,那么这个地方我们有没有一种比较
     *         简单的处理方式呢???
     *         可以先把你当前这个菜品原先关联的口味数据全都统一删掉,然后在按照你当前
     *         传过来的这个口味,重新再来插入一遍这个数据就可以了。
     *
     * @param dishDTO
     */
    @Override
    @Transactional
    public void updateWithFlavor(DishDTO dishDTO) {
        //说明:DishDTO含有口味数据,当前只是修改菜品的基本信息,所以直接传递DishDTO不合适,
        //     可以把DishDTO的数据拷贝到菜品的基本信息类Dish中更合适。
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);

        //修改菜品表基本信息
        dishMapper.update(dish);

        //删除原有的口味数据
        dishFlavorMapper.deleteByDishId(dishDTO.getId());

        //重新插入口味数据
        //口味数据通过实体类的对象集合属性封装的,所以需要先把集合中的数据取出来
        List<DishFlavor> flavors = dishDTO.getFlavors();
        //口味不是必须的有可能用户没有提交口味数据,所以需要判断一下
        if (flavors != null && flavors.size() > 0) {
            //用户确实提交的有口味数据,此时插入口味数据才有意义
            //注意:口味数据的dishId前端并不能传递,它是菜单表插入数据后自动生成的主键值,也就是
            //     口味表的关联外键dishId,有了菜单表这个主键值id,就需要为dishFlavor里面的每一个dishId(关联外键)
            //     属性赋值,所以在批量插入数据之前需要遍历这个对象集合,为里面的每个对象DishFlavor的dishId赋上值
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
            //向口味表插入n条数据 (新增菜品的时候已经写过了)
            /*
             * 口味数据flavors是一个对象类型的list集合来接收的,
             * 不需要遍历这个集合一条一条的插入数据,因为sql支持批量插入
             * 直接把这个集合对象传进去,通过动态sql标签foreach进行遍历获取。
             * */
            dishFlavorMapper.insertBatch(flavors);
        }
    }

4). Mapper层

在DishMapper中,声明update方法:修改菜品表基本信息

	/**
     * 根据id动态修改菜品数据
     *
     * @param dish
     */
    @AutoFill(value = OperationType.UPDATE) //公共字段自动填充
    void update(Dish dish);

并在DishMapper.xml文件中编写SQL:

<update id="update">
        update dish
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="categoryId != null">category_id = #{categoryId},</if>
            <if test="price != null">price = #{price},</if>
            <if test="image != null">image = #{image},</if>
            <if test="description != null">description = #{description},</if>
            <if test="status != null">status = #{status},</if>
            <if test="updateTime != null">update_time = #{updateTime},</if>
            <if test="updateUser != null">update_user = #{updateUser},</if>
        </set>
        where id = #{id}
</update>

在DishFlavorMapper中,声明delete方法:删除原有的口味数据

    //根据菜品id删除对应的口味数据
    @Delete("delete from dish_flavor where dish_id = #{dishId}")
    void deleteByDishId(Long id);

在DishFlavorMapper中,声明insert方法:重新插入口味数据

    /**
     * 批量插入口味数据
     *
     * @param flavors
     */
    void insertBatch(List<DishFlavor> flavors);

并在DishMapper.xml文件中编写SQL:

    <!--注意这个dishId-->
    <insert id="insertBatch">
        insert into dish_flavor (dish_id, name, value) VALUES
        <foreach collection="flavors" item="df" separator=",">
            (#{df.dishId},#{df.name},#{df.value})
        </foreach>
    </insert>

5.3 功能测试

本次测试直接通过前后端联调测试 ,可使用Debug方式启动项目,观察运行中步骤。

进入菜品列表查询页面,对第一个菜品的价格进行修改

在这里插入图片描述

点击修改,回显成功

在这里插入图片描述

菜品价格修改后,点击保存

在这里插入图片描述

修改成功

5.4 代码提交

在这里插入图片描述

后续步骤和上述功能代码提交一致,不再赘述。

6. 菜品起售停售

1. 根据产品原型进行需求分析,分析出业务规则

菜品起售表示该菜品可以对外售卖,在用户端可以点餐,菜品停售表示此菜品下架,用户端无法点餐。

业务规则为:如果执行停售操作,则包含此菜品的套餐也需要停售

2. 设计 菜品起售停售 功能的接口

在这里插入图片描述

3. 根据接口设计进行代码实现

3.1 DishController

在这里插入图片描述

/**
     * 菜品起售停售
     * @param status
     * @param id
     * @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品起售停售")
public Result<String> startOrStop(@PathVariable Integer status, Long id){
    dishService.startOrStop(status,id);
    return Result.success();
}
3.2 DishService
/**
     * 菜品起售停售
     * @param status
     * @param id
*/
void startOrStop(Integer status, Long id);
3.3 DishServiceImpl
    @Autowired
    private SetmealMapper setmealMapper;

  /**
     * 菜品起售停售
     *
     * @param status
     * @param id
 */
@Transactional
public void startOrStop(Integer status, Long id) {
    Dish dish = Dish.builder()
        .id(id)
        .status(status)
        .build();
    dishMapper.update(dish);

    if (status == StatusConstant.DISABLE) {
        // 如果是停售操作,还需要将包含当前菜品的套餐也停售
        List<Long> dishIds = new ArrayList<>();
        dishIds.add(id);
        // select setmeal_id from setmeal_dish where dish_id in (?,?,?)
        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
        if (setmealIds != null && setmealIds.size() > 0) {
            for (Long setmealId : setmealIds) {
                Setmeal setmeal = Setmeal.builder()
                    .id(setmealId)
                    .status(StatusConstant.DISABLE)
                    .build();
                setmealMapper.update(setmeal);
            }
        }
    }
}
3.4 SetmealMapper
/**
     * 根据id修改套餐
     *
     * @param setmeal
 */
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);
3.5 SetmealMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealMapper">

    <update id="update" parameterType="Setmeal">
        update setmeal
        <set>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="categoryId != null">
                category_id = #{categoryId},
            </if>
            <if test="price != null">
                price = #{price},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="description != null">
                description = #{description},
            </if>
            <if test="image != null">
                image = #{image},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>

</mapper>

4. 分别通过swagger接口文档和前后端联调进行功能测试

在这里插入图片描述

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐