1.公共字段的内容填充

1.1问题分析

苍穹外卖——菜品管理_java

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

苍穹外卖——菜品管理_阿里云_02


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

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

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

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

1.2实现思路

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

苍穹外卖——菜品管理_阿里云_03


苍穹外卖——菜品管理_java_04


苍穹外卖——菜品管理_java_05

1.3代码实现

1.3.1定义一个枚举类

sky-common模块中定义一个枚举类

package com.sky.enumeration;

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

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

    /**
     * 插入操作
     */
    INSERT

}

1.3.2自定义注解** AutoFill**

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

package com.sky.annotation;

/**
 * ClassName: AutoFill
 * Package: com.sky.annotation
 * Description:
 *
 * @Author pkq
 * @Create 2024/4/3 15:34
 * @Version 1.0
 */

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
    OperationType value() default OperationType.INSERT;
}

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

CategoryMapper

package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.enumeration.OperationType;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface CategoryMapper {

    /**
     * 插入数据
     * @param category
     */
    @AutoFill
    @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})")
    void insert(Category category);

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    @Delete("delete from category where id = #{id}")
    void deleteById(Long id);

    /**
     * 根据id修改分类
     * @param category
     */
    @AutoFill(OperationType.UPDATE)
    void update(Category category);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}

EmployeeMapper

package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.dto.EmployeePageQueryDTO;
import com.sky.entity.Employee;
import com.sky.enumeration.OperationType;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface EmployeeMapper {

    /**
     * 根据用户名查询员工
     * @param username
     * @return
     */
    @Select("select * from employee where username = #{username}")
    Employee getByUsername(String username);

    /**
     * 插入员工数据
     * @param employee
     */

    /**
     * 插入员工数据
     * @param employee
     */
    @AutoFill
    @Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
            "values " +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
    void insert(Employee employee);

    Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
    @AutoFill(OperationType.UPDATE)
    void update(Employee employee);

    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @Select("select * from employee where id = #{id}")
    Employee getById(Long id);
}

1.3.4 自定义切面 AutoFillAspect

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

package com.sky.aspect;


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

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

/**
 * ClassName: AutoFillAspect
 * Package: com.sky.aspect
 * Description:
 *
 * @Author pkq
 * @Create 2024/4/3 15:43
 * @Version 1.0
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    @Before("@annotation(com.sky.annotation.AutoFill)")
    public void autoFill(JoinPoint joinPoint) throws NoSuchFieldException, IllegalAccessException {
        //0.获取方法参数
        Object arg = joinPoint.getArgs()[0];
        //1.获取方法签名
        MethodSignature signature=(MethodSignature)joinPoint.getSignature();
        //2.根据方法签名获取方法Method对象
        Method method = signature.getMethod();
        //3.根据方法Method对象,获取方法身上的注解@AutoFill
        AutoFill annotation = method.getAnnotation(AutoFill.class);
        //4.根据注解的对象,获取它里面的value属性值
        OperationType value = annotation.value();
        //5.根据属性值来判断是添加还是更新,以此区分是填充4个值还是2个值
        if (value==OperationType.INSERT){
            //添加操作 4个属性
            //5.1获取参数的字节码对象
            Class clazz = arg.getClass();
            //5.2获取属性
            Field createTime = clazz.getDeclaredField("createTime");
            Field updateTime = clazz.getDeclaredField("updateTime");
            Field createUser = clazz.getDeclaredField("createUser");
            Field updateUser = clazz.getDeclaredField("updateUser");
            //暴力反射
            createTime.setAccessible(true);
            updateTime.setAccessible(true);
            createUser.setAccessible(true);
            updateUser.setAccessible(true);
            //给属性赋值
            createTime.set(arg, LocalDateTime.now());
            updateTime.set(arg,LocalDateTime.now());
            createUser.set(arg, BaseContext.getCurrentId());
            updateUser.set(arg,BaseContext.getCurrentId());


        }else{
            //修改操作,2个属性值
            //5.1获取参数的字节码对象
            Class clazz = arg.getClass();
            //5.2获取属性
            Field updateTime = clazz.getDeclaredField("updateTime");
            Field updateUser = clazz.getDeclaredField("updateUser");
            //暴力反射
            updateTime.setAccessible(true);
            updateUser.setAccessible(true);
            //给属性赋值
            updateTime.set(arg,LocalDateTime.now());
            updateUser.set(arg,BaseContext.getCurrentId());
        }
        //6.通过反射来获取对应的4个属性或者2个属性

        //7.通过反射直接给他们赋值




    }
}

同时,将业务层为公共字段赋值的代码注释掉。
1). 将员工管理的新增和编辑方法中的公共字段赋值的代码注释。
2). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。

1.4测试

新增菜品分类

苍穹外卖——菜品管理_阿里云_06


苍穹外卖——菜品管理_大数据_07

1.5代码提交

苍穹外卖——菜品管理_微服务_08

2.新增菜品

2.1需求分析

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

新增菜品原型:

苍穹外卖——菜品管理_阿里云_09


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

业务规则:

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

2.2接口设计

接口设计:

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

苍穹外卖——菜品管理_java_10


苍穹外卖——菜品管理_java_11

  • 文件上传
  • 新增菜品

2.3表的设计

苍穹外卖——菜品管理_数据库_12


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

表名

说明

dish

菜品表

dish_flavor

菜品口味表

1). 菜品表:dish

字段名

数据类型

说明

备注

id

bigint

主键

自增

name

varchar(32)

菜品名称

唯一

category_id

bigint

分类id

逻辑外键

price

decimal(10,2)

菜品价格

image

varchar(255)

图片路径

description

varchar(255)

菜品描述

status

int

售卖状态

1起售 0停售

create_time

datetime

创建时间

update_time

datetime

最后修改时间

create_user

bigint

创建人id

update_user

bigint

最后修改人id

2). 菜品口味表:dish_flavor

字段名

数据类型

说明

备注

id

bigint

主键

自增

dish_id

bigint

菜品id

逻辑外键

name

varchar(32)

口味名称

value

varchar(255)

口味值

新增菜品之前我们首先要完成上传菜品图片

2.4代码实现

2.4.1文件上传

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

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

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

快速使用OSS SDK - 对象存储 OSS - 阿里云

苍穹外卖——菜品管理_阿里云_13


苍穹外卖——菜品管理_微服务_14

2.4.1.1引入依赖

sky-take-out

<dependency>
                <groupId>com.aliyun.oss</groupId>
                <artifactId>aliyun-sdk-oss</artifactId>
                <version>${aliyun.sdk.oss}</version>
            </dependency>

sky-common

<dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
        </dependency>

sky-server

<dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
        </dependency>
2.4.1.2定义oss相关配置

在sky-server模块
application-dev.yml

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

application.yml

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}
2.4.1.3读取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;

}
2.4.1.4生成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对象
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
        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();
    }
}
2.4.1.5定义文件上传接口

在sky-server模块中定义接口

文件上传接口就是要去调取我们编写好的文件上传AliOssUtil的upload方法完成上传,返回给前端图片地址

上传文件到oss中文件名不能取图片的文件名,假如有两个菜品图片都叫 鱼.png 就会出现错误,所以我们使用UUID接上原来图片的后缀作为上传的文件名

//        1.获取原始文件名
//        2.截取文件名的后缀
//        3.通过UUID拼接后缀构建新的文件名称
//        4.通过aliOssUtil 传入文件字节数组和新的文件名称,返回图片路径
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("文件上传")
    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.4.2新增菜品

苍穹外卖——菜品管理_阿里云_15


苍穹外卖——菜品管理_阿里云_16


苍穹外卖——菜品管理_java_17

2.4.2.1 DishDTO
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<>();

}
2.4.2.2 controller
package com.sky.controller.admin;

import com.sky.dto.DishDTO;
import com.sky.mapper.DishMapper;
import com.sky.result.Result;
import com.sky.service.DishService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * ClassName: DishController
 * Package: com.sky.controller.admin
 * Description: 新增菜品
 *
 * @Author pkq
 * @Create 2024-04-04 13:54
 * @Version 1.0
 */
@RestController
@Api(tags = "菜品相关接口")
@Slf4j
@CrossOrigin
@RequestMapping("/admin/dish")
public class DishController {
    @Autowired
    private DishService dishService;
    @PostMapping
    @ApiOperation("新增菜品")
    public Result addDish(@RequestBody DishDTO dishDTO){
        log.info("新增菜品:{}", dishDTO);
        dishService.addDish(dishDTO);
        return Result.success();
    }
}
2.4.2.3 service
package com.sky.service;

import com.sky.dto.DishDTO;

/**
 * ClassName: DishService
 * Package: com.sky.service
 * Description:
 *
 * @Author pkq
 * @Create 2024-04-04 13:58
 * @Version 1.0
 */
public interface DishService  {
    void addDish(DishDTO dishDTO);

}
2.4.2.4 service实现类

1.新建菜品对象
2.用DTO给对应的菜品对象赋值
3.插入菜品表中的数据
4.获取插入菜品表中的菜品id
5.对插入的菜品口味对象的菜品id赋值
6.插入菜品口味
:::info
菜品数据包括菜品基本数据和口味数据
菜品数据保存在菜品表dish,口味数据保存在口味表dish_flavor
所以新增一道菜品数据,要往这两个表添加数据
:::

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 org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * ClassName: DishServiceImpl
 * Package: com.sky.service.impl
 * Description:
 *
 * @Author pkq
 * @Create 2024-04-04 13:59
 * @Version 1.0
 */
@Service
public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;

    @Override
    public void addDish(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);
        dishMapper.addDish(dish);
        //口味不是必须的
        List<DishFlavor> flavorList = dishDTO.getFlavors();
        if (flavorList!=null && flavorList.size()>0){
            for (DishFlavor dishFlavor:flavorList){
                dishFlavor.setDishId(dish.getId());
                dishFlavorMapper.add(dishFlavor);
            }
        }
    }
}
2.4.2.5 mapepr

DishMapper.java中添加

package com.sky.mapper;

import com.sky.dto.DishDTO;
import com.sky.entity.Dish;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface DishMapper {

    /**
     * 根据分类id查询菜品数量
     * @param categoryId
     * @return
     */
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);
    @Insert("insert into dish values (null, #{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{create_time}, #{update_time}, #{create_user}, #{update_user})")
    void addDish(Dish dish);


}

DishFlavorMapper.java

package com.sky.mapper;

import com.sky.entity.DishFlavor;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

/**
 * ClassName: DishFlavorMapper
 * Package: com.sky.mapper
 * Description:
 *
 * @Author pkq
 * @Create 2024-04-04 14:04
 * @Version 1.0
 */
@Mapper
public interface DishFlavorMapper {
    @Insert("insert into dish_flavor(id, dish_id, name, value) values (default,#{dishId},#{name},#{value})")
    void add(DishFlavor dishFlavor);
}
2.4.2.6测试

苍穹外卖——菜品管理_阿里云_18


苍穹外卖——菜品管理_数据库_19


苍穹外卖——菜品管理_大数据_20

3.菜品的分页查询

3.1需求分析与设计

苍穹外卖——菜品管理_阿里云_21


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

业务规则:

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

苍穹外卖——菜品管理_微服务_22


苍穹外卖——菜品管理_java_23


苍穹外卖——菜品管理_微服务_24

3.2代码实现

3.2.1DTO

sky-pojo的dto已经定义好了

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;

    //分类id
    private Integer categoryId;

    //状态 0表示禁用 1表示启用
    private Integer status;

}

3.2.2VO

根据菜品分页查询接口定义设计对应的VO:
在sky-pojo模块中,已定义

PageResult

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<>();
}
package com.sky.result;

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

import java.io.Serializable;
import java.util.List;

/**
 * 封装分页查询结果
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private long total; //总记录数

    private List records; //当前页数据集合

}

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 中实现分页查询方法:

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

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

在 DishMapper.xml 中编写SQL:

<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接口文档测试

苍穹外卖——菜品管理_大数据_25

3.3.2前后端联调

苍穹外卖——菜品管理_阿里云_26

4.删除菜品

4.1需求分析

苍穹外卖——菜品管理_阿里云_27


业务规则:

  • 可以一次删除一个菜品,也可以批量删除菜品
  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除掉

4.2接口设计

苍穹外卖——菜品管理_微服务_28


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

4.3表设计

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

苍穹外卖——菜品管理_大数据_29


注意事项:

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

4.4代码

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//事务
    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:

/**
     * 根据主键查询菜品
     *
     * @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
     *
     * @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.5 功能测试

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

进入到菜品列表查询页面

苍穹外卖——菜品管理_微服务_30



对测试菜品进行删除操作

苍穹外卖——菜品管理_大数据_31



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

苍穹外卖——菜品管理_java_32


点击批量删除

苍穹外卖——菜品管理_阿里云_33


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

4.6代码提交

苍穹外卖——菜品管理_数据库_34

5. 修改菜品

5.1 需求分析和设计

5.1.1 产品原型

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

修改菜品原型:

苍穹外卖——菜品管理_大数据_35


5.1.2 接口设计

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

接口:

  • 根据id查询菜品
  • 根据类型查询分类(已实现)
  • 文件上传(已实现)
  • 修改菜品

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

1). 根据id查询菜品

苍穹外卖——菜品管理_微服务_36


苍穹外卖——菜品管理_数据库_37


2). 修改菜品

苍穹外卖——菜品管理_大数据_38


苍穹外卖——菜品管理_java_39



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

5.2 代码开发

5.2.1 根据id查询菜品实现

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
     */
    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;
    }

4). Mapper层

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

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

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修改菜品基本信息和对应的口味信息
     *
     * @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);
        }
    }

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>

5.3 功能测试

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

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

苍穹外卖——菜品管理_微服务_40


点击修改,回显成功

苍穹外卖——菜品管理_java_41



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

苍穹外卖——菜品管理_大数据_42



修改成功

5.4 代码提交

参照以前步骤