Day3 苍穹外卖项目 公共字段自动填充(AOP)、文件上传、新增菜品、菜品分页查询、删除菜品、修改菜品

目录

1.公共字段自动填充

[1.1 问题分析](#1.1 问题分析)

[1.2 实现思路](#1.2 实现思路)

[1.3 代码开发](#1.3 代码开发)

[1.3.1 自定义注解 AutoFill](#1.3.1 自定义注解 AutoFill)

[1.3.2 自定义切面类 AutoFillAspect.java](#1.3.2 自定义切面类 AutoFillAspect.java)

[1.3.3 在Mapper接口的方法上加入 AutoFill 注解](#1.3.3 在Mapper接口的方法上加入 AutoFill 注解)

[1.4 功能测试](#1.4 功能测试)

[1.5 代码提交](#1.5 代码提交)

2.新增菜品

[2.1 需求分析与设计](#2.1 需求分析与设计)

[2.1.1 产品原型](#2.1.1 产品原型)

[2.1.2 接口设计](#2.1.2 接口设计)

[2.1.3 表设计](#2.1.3 表设计)

[2.2 代码开发](#2.2 代码开发)

[2.2.1 文件上传实现](#2.2.1 文件上传实现)

[2.2.1.1 定义OSS相关配置](#2.2.1.1 定义OSS相关配置)

[2.2.1.2 读取OSS配置](#2.2.1.2 读取OSS配置)

[2.2.1.3 生成OSS工具类对象](#2.2.1.3 生成OSS工具类对象)

[2.2.1.4 定义文件上传接口](#2.2.1.4 定义文件上传接口)

[2.2.2 新增菜品实现](#2.2.2 新增菜品实现)

[2.2.2.1 设计DTO类](#2.2.2.1 设计DTO类)

[2.2.2.2 Controller层](#2.2.2.2 Controller层)

[2.2.2.3 Service层接口](#2.2.2.3 Service层接口)

[2.2.2.4 Service层实现类](#2.2.2.4 Service层实现类)

[2.2.2.5 Mapper层](#2.2.2.5 Mapper层)

[2.3 功能测试](#2.3 功能测试)

[2.4 代码提交](#2.4 代码提交)

3.菜品分页查询

[3.1 需求分析和设计](#3.1 需求分析和设计)

[3.1.1 产品原型](#3.1.1 产品原型)

[3.1.2 接口设计](#3.1.2 接口设计)

[3.2 代码开发](#3.2 代码开发)

[3.2.1 设计DTO类](#3.2.1 设计DTO类)

[3.2.2 设计VO类](#3.2.2 设计VO类)

[3.2.3 Controller层](#3.2.3 Controller层)

[3.2.4 Service层接口](#3.2.4 Service层接口)

[3.2.5 Service层实现类](#3.2.5 Service层实现类)

[3.2.6 Mapper层](#3.2.6 Mapper层)

[3.3 功能测试](#3.3 功能测试)

[3.3.1 接口文档测试](#3.3.1 接口文档测试)

4.删除菜品

[4.1 需求分析和设计](#4.1 需求分析和设计)

[4.1.1 产品原型](#4.1.1 产品原型)

[4.1.2 接口设计](#4.1.2 接口设计)

[4.1.3 表设计](#4.1.3 表设计)

[4.2 代码开发](#4.2 代码开发)

[4.1.2 Controller层](#4.1.2 Controller层)

[4.2.2 Service层接口](#4.2.2 Service层接口)

[4.2.3 Service层实现类](#4.2.3 Service层实现类)

[4.2.4 Mapper层](#4.2.4 Mapper层)

[4.3 功能测试](#4.3 功能测试)

[4.4 代码提交](#4.4 代码提交)

5.修改菜品

[5.1 需求分析和设计](#5.1 需求分析和设计)

[5.1.1 产品原型](#5.1.1 产品原型)

[5.1.2 接口设计](#5.1.2 接口设计)

[5.2 代码开发](#5.2 代码开发)

[5.2.1 根据id查询菜品实现](#5.2.1 根据id查询菜品实现)

[5.2.1.1 Controller层](#5.2.1.1 Controller层)

[5.2.1.2 Service层接口](#5.2.1.2 Service层接口)

[5.2.1.3 Service层实现类](#5.2.1.3 Service层实现类)

[5.2.1.4 Mapper层](#5.2.1.4 Mapper层)

[5.2.2 修改菜品实现](#5.2.2 修改菜品实现)

[5.2.2.1 Controller层](#5.2.2.1 Controller层)

[5.2.2.2 Service层接口](#5.2.2.2 Service层接口)

[5.2.2.3 Service层实现类](#5.2.2.3 Service层实现类)

[5.2.2.4 Mapper层](#5.2.2.4 Mapper层)

[5.3 功能测试](#5.3 功能测试)

[5.4 代码提交](#5.4 代码提交)


1.公共字段自动填充

1.1 问题分析

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

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

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

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

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

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

1.2 实现思路

在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。

实现步骤:

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

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

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

**技术点:**枚举、注解、AOP、反射。

**原理:**切入点指定拦截的路径,拦截的是Mapper层的所有方法,并且方法上还需要有@AutoFill注解才会被拦截,切面类的通知设置为前置通知@Before,在执行Mapper层对应的方法会先执行前置通知里面的代码。因此公共字段可以在这里实现统一封装。

1.3 代码开发

1.3.1 自定义注解 AutoFill

进入到sky-server模块,创建com.sky.annotation包。

java 复制代码
package com.sky.annotation;

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
// ElementType.METHOD该注解只能在方法上使用
@Target(ElementType.METHOD)
// 生命周期
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //数据库操作类型:UPDATE INSERT
    OperationType value();
}

其中OperationType已在sky-common模块中定义枚举类型:

java 复制代码
package com.sky.enumeration;

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

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

    /**
     * 插入操作
     */
    INSERT
}

1.3.2 自定义切面类 AutoFillAspect.java

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

java 复制代码
package com.sky.aspect;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
// 定义该类为切面类
@Aspect
// 该类交给Spring管理
@Component
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点(要切入的某个类的某个方法)
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 前置通知,在通知中进行公共字段的赋值(操作数据库之前执行)
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        /重要
        //可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解
        log.info("开始进行公共字段自动填充...");

    }
}

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

java 复制代码
package com.sky.aspect;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    /**
     * 前置通知,在通知中进行公共字段的赋值
     */
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填充...");

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

        //获取到当前被拦截的方法的参数--实体对象
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0){
            return;
        }

        Object entity = args[0];

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

        //根据当前不同的操作类型,为对应的属性通过反射来赋值
        if(operationType == OperationType.INSERT){
            //为4个公共字段赋值
            try {
                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);

                //通过反射为对象属性赋值
                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做相同操作:

java 复制代码
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 功能测试

修改员工信息:

添加员工信息:

分类管理的测试也是类似的。

1.5 代码提交

2.新增菜品

2.1 需求分析与设计

2.1.1 产品原型

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

新增菜品原型:

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

业务规则:

  • 菜品名称必须是唯一的

  • 菜品必须属于某个分类下,不能单独存在

  • 新增菜品时可以根据情况选择菜品的口味

  • 每个菜品必须对应一张图片

2.1.2 接口设计

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

接口设计:

  • 根据类型查询分类(已完成)(显示下拉框)

  • 文件上传

  • 新增菜品(有可能涉及到两个表 dish表和dish_flavor表)

    • 注意事项:涉及到多张表要开启事务。而且还要注意要拿到刚添加菜品的主键,设置给dish_flavor表的dish_id属性。

      复制代码
      @Transactional //dishServiceImpl的新增方法加上
      复制代码
      @EnableTransactionManagement //开启注解方式的事务管理

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

1. 根据类型查询分类(已完成)

2. 文件上传

3. 新增菜品

2.1.3 表设计

通过原型图进行分析:

dish表:

dish_flavor表:口味表

2.2 代码开发

2.2.1 文件上传实现

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

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

  1. 直接将图片保存到服务的硬盘(springmvc中的文件上传)

    1. 优点:开发便捷,成本低

    2. 缺点:扩容困难

  2. 使用分布式文件系统进行存储

    1. 优点:容易实现扩容

    2. 缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO)

  3. 使用第三方的存储服务(例如OSS)

    1. 优点:开发简单,拥有强大功能,免维护

    2. 缺点:付费

在本项目选用阿里云的OSS服务进行文件存储。

访问aliyun官网:https://www.aliyun.com/

创建Bucket并且配置相应的信息就可以上传文件。

2.2.1.1 定义OSS相关配置

引入依赖:

XML 复制代码
<!-- https://mvnrepository.com/artifact/com.aliyun.oss/aliyun-sdk-oss -->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>

在sky-server模块

@ConfigurationProperties可以读取指定的配置文件内容:

application.yml

${ }这种配置方式更加灵活,可以通过设置不同的环境,读取不同的内容:

XML 复制代码
spring:
  profiles:
    active: dev    #设置环境
sky:
  alioss:
    endpoint: ${sky.alioss.endpoint}
    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

配置在开发环境下被读取的内容:

XML 复制代码
sky:
  alioss:
    endpoint: oss-cn-hangzhou.aliyuncs.com
    access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
    access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7
    bucket-name: sky-take-out
2.2.1.2 读取OSS配置

在sky-common模块中,已定义

java 复制代码
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;

}
2.2.1.3 生成OSS工具类对象

其中,AliOssUtil.java已在sky-common模块中定义

java 复制代码
package com.sky.utils;

@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();
    }
}

在sky-server模块

这里使用了导入第三方bean的思想,**扫描不到这个@Component注解,**所以需要通过配置类的方式引入。

在 Spring 框架中,@Bean 方法的参数是通过自动装配(Autowiring)机制来注入的。因此,在 @Bean 方法中声明的参数会自动由 Spring 容器解析并注入,而不需要显式使用 @Autowired 注解。

java 复制代码
package com.sky.config;

/**
 * 配置类,用于创建AliOssUtil对象
 */
@Configuration
@Slf4j
public class OssConfiguration {

    //引入第三方Bean的方式
    @Bean
    @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
        log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}
2.2.1.4 定义文件上传接口

在sky-server模块中定义接口

java 复制代码
package com.sky.controller.admin;

/**
 * 通用接口
 */
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

    @Autowired
    private AliOssUtil aliOssUtil;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    // 注意这里的file
    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;

            //文件的请求路径
            String filePath = aliOssUtil.upload(file.getBytes(), objectName);
            return Result.success(filePath);
        } catch (IOException e) {
            log.error("文件上传失败:{}", e);
        }

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

2.2.2 新增菜品实现

2.2.2.1 设计DTO类

在sky-pojo模块中

java 复制代码
package com.sky.dto;

@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<>();
}
2.2.2.2 Controller层

进入到sky-server模块

java 复制代码
package com.sky.controller.admin;

/**
 * 菜品管理
 */
@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();
    }
}
2.2.2.3 Service层接口
java 复制代码
package com.sky.service;

public interface DishService {

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

}
2.2.2.4 Service层实现类
java 复制代码
package com.sky.service.impl;


@Service
@Slf4j
public class DishServiceImpl implements DishService {

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

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

        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);

        //向菜品表插入1条数据
        dishMapper.insert(dish);//后绪步骤实现

        //获取insert语句生成的主键值。这里需要配置xml文件才能拿到主键值
        Long dishId = dish.getId();

        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            //向口味表插入n条数据
            dishFlavorMapper.insertBatch(flavors);//后绪步骤实现
        }
    }

}
2.2.2.5 Mapper层

DishMapper.java中添加

java 复制代码
	/**
     * 插入菜品数据
     *
     * @param dish
     */
    @AutoFill(value = OperationType.INSERT)
    void insert(Dish dish);

在/resources/mapper中创建DishMapper.xml

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>

注意事项:下面两个属性配合使用,可以实现类可以拿到刚插入数据库自增的菜品的主键。

1. useGeneratedKeys="true"

  • 作用 :MyBatis 会自动将数据库生成的主键值(通常是自增ID)返回给调用方法。

2. keyProperty="id"

  • 含义:指定Java对象中哪个属性接收生成的主键值。

DishFlavorMapper.java

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 复制代码
<?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">
    <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 功能测试

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

插入成功:

2.4 代码提交

3.菜品分页查询

3.1 需求分析和设计

3.1.1 产品原型

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

菜品分页原型:

注意事项:

在菜品列表展示时,有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类 ,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示(涉及多表查询)。

业务规则:

  • 根据页码展示菜品信息

  • 每页展示10条数据

  • 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询

3.1.2 接口设计

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

3.2 代码开发

3.2.1 设计DTO类

根据菜品分页查询接口定义设计对应的DTO:

在sky-pojo模块中,已定义

java 复制代码
package com.sky.dto;

@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:

在sky-pojo模块中,已定义

封装前端所需要的数据。

java 复制代码
package com.sky.vo;

@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分页查询方法:

java 复制代码
	/**
     * 菜品分页查询
     *
     * @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 中扩展分页查询方法:

java 复制代码
	/**
     * 菜品分页查询
     *
     * @param dishPageQueryDTO
     * @return
     */
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);

3.2.5 Service层实现类

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

java 复制代码
	/**
     * 菜品分页查询
     *
     * @param dishPageQueryDTO
     * @return
     */
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);//后绪步骤实现
        return new PageResult(page.getTotal(), page.getResult());
    }

3.2.6 Mapper层

在 DishMapper 接口中声明 pageQuery 方法:

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

在 DishMapper.xml 中编写SQL:

XML 复制代码
<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,进入菜品分页查询接口

前后端联调:

4.删除菜品

4.1 需求分析和设计

4.1.1 产品原型

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

删除菜品原型:

业务规则:

  • 可以一次删除一个菜品,也可以批量删除菜品

  • 起售中的菜品不能删除

  • 被套餐关联的菜品不能删除

  • 删除菜品后,关联的口味数据也需要删除掉

4.1.2 接口设计

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

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

4.1.3 表设计

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

注意事项:

  • 在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。

  • 若菜品为起售状态就禁止删除。(刚添加的菜品必须跟分类关联,并且为停售状态)

  • 若删除的菜品数据关联着某个套餐,此时,删除失败。

  • 若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。

4.2 代码开发

4.1.2 Controller层

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

java 复制代码
	/**
     * 菜品批量删除
     *
     * @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方法:

java 复制代码
	/**
     * 菜品批量删除
     *
     * @param ids
     */
    void deleteBatch(List<Long> ids);

4.2.3 Service层实现类

在DishServiceImpl中实现deleteBatch方法:

java 复制代码
    @Autowired
    private SetmealDishMapper setmealDishMapper;
	/**
     * 菜品批量删除
     *
     * @param ids
     */
    @Transactional//事务
    public void deleteBatch(List<Long> ids) {
        //判断当前菜品是否能够删除---是否存在起售中的菜品??
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);//后绪步骤实现
            if (dish.getStatus() == StatusConstant.ENABLE) {
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        //判断当前菜品是否能够删除---是否被套餐关联了??
        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if (setmealIds != null && setmealIds.size() > 0) {
            //当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }

        //删除菜品表中的菜品数据
        for (Long id : ids) {
            dishMapper.deleteById(id);//后绪步骤实现
            //删除菜品关联的口味数据
            dishFlavorMapper.deleteByDishId(id);//后绪步骤实现
        }
    }

4.2.4 Mapper层

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

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

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

java 复制代码
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
     *
     * @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 复制代码
<?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:

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

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

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

4.3 功能测试

前后端联调测试

删除停售菜品:

删除起售菜品:

4.4 代码提交

5.修改菜品

5.1 需求分析和设计

5.1.1 产品原型

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

修改菜品原型:

5.1.2 接口设计

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

接口:

  • 根据id查询菜品(回显)

  • 根据类型查询分类(已实现)

  • 文件上传(已实现)

  • 修改菜品

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

1). 根据id查询菜品

2). 修改菜品

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

5.2 代码开发

5.2.1 根据id查询菜品实现

5.2.1.1 Controller层

根据id查询菜品的接口定义在DishController中创建方法:

返回类型DishVO

java 复制代码
    /**
     * 根据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);
    }
5.2.1.2 Service层接口

在DishService接口中声明getByIdWithFlavor方法:

java 复制代码
	/**
     * 根据id查询菜品和对应的口味数据
     *
     * @param id
     * @return
     */
    DishVO getByIdWithFlavor(Long id);
5.2.1.3 Service层实现类

在DishServiceImpl中实现getByIdWithFlavor方法:

java 复制代码
	/**
     * 根据id查询菜品和对应的口味数据
     *
     * @param id
     * @return
     */
    public DishVO getByIdWithFlavor(Long id) {
        //根据id查询菜品数据
        Dish dish = dishMapper.getById(id);

        //根据菜品id查询口味数据
        List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);//后绪步骤实现

        //将查询到的数据封装到VO
        DishVO dishVO = new DishVO();
        BeanUtils.copyProperties(dish, dishVO);
        dishVO.setFlavors(dishFlavors);

        return dishVO;
    }
5.2.1.4 Mapper层

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

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

5.2.2 修改菜品实现

5.2.2.1 Controller层

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

java 复制代码
	/**
     * 修改菜品
     *
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}", dishDTO);
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }
5.2.2.2 Service层接口

在DishService接口中声明updateWithFlavor方法:

java 复制代码
	/**
     * 根据id修改菜品基本信息和对应的口味信息
     *
     * @param dishDTO
     */
    void updateWithFlavor(DishDTO dishDTO);
5.2.2.3 Service层实现类

在DishServiceImpl中实现updateWithFlavor方法:

注意事项:修改口味信息,是先删除原有的口味信息,再重新添加的操作。在插入数据库之前

要设置好 dish_id 的值。

java 复制代码
	/**
     * 根据id修改菜品基本信息和对应的口味信息
     *
     * @param dishDTO
     */
    public void updateWithFlavor(DishDTO dishDTO) {
        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) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
            //向口味表插入n条数据
            dishFlavorMapper.insertBatch(flavors);
        }
    }
5.2.2.4 Mapper层

在DishMapper中,声明update方法:

java 复制代码
	/**
     * 根据id动态修改菜品数据
     *
     * @param dish
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Dish dish);

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

java 复制代码
<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>

5.3 功能测试

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

5.4 代码提交

相关推荐
爪哇学长1 小时前
如何构建一个高效安全的图书管理系统
java·spring boot·安全
材料苦逼不会梦到计算机白富美1 小时前
DIY-Tomcat项目 part 1 实现和测试Request以及Response
java·tomcat
晚渔声1 小时前
【线程】Java多线程代码案例(2)
java·开发语言·多线程
5-StarrySky2 小时前
Java 线程中的分时模型和抢占模型
java·开发语言
君败红颜2 小时前
设计模式之结构型模式
java·算法·设计模式
阿维的博客日记2 小时前
java八股-分布式服务的接口幂等性如何设计?
java·接口幂等
爱吃烤鸡翅的酸菜鱼2 小时前
Java算法OJ(11)双指针练习
java·数据结构·算法·leetcode
Gary董2 小时前
ArraList和LinkedList区别
java
一到破伤风2 小时前
Java对象与XML互相转换(xstream)
xml·java·服务器