苍穹外卖-day03 - 公共字段自动填充- 新增菜品- 菜品分页查询- 删除菜品- 修改菜品

1.公共字段自动填充

问题

有些字段,如 : create_time,create_user , update_time , update_user是公共的,每次赋值都要重新编写代码,会造成代码冗余 ;

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

实现 :

  • 自定义注解AutoFill,用于表示需要进行公共自读那自动填充的方法

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

  • 在Mapper的方法上加入AutoFill注解。

1.创建枚举类

在common下定义了需要加上注解的数据库操作类型 :

OperationType.java :

package com.sky.enumeration;
​
/**
 * 数据库操作类型
 */
public enum OperationType {
​
    /**
     * 更新操作
     */
    UPDATE,
​
    /**
     * 插入操作
     */
    INSERT
​
}
​
2.自定义注解AutoFill
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 {
    // 指定数据库操作类型
    OperationType value();
}
3.自定义拦截器
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.Signature;
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
@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(); // 获取数据库操作类型
​
        // 获取当前被拦截的方法的参数 -- 实体对象(比如employee)
        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个公共字段赋值 -- 通过反射来进行赋值
            // 获取Set方法
            try {
//                Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime" , LocalDateTime.class);
//                Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser" , Long.class);
//                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
//                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
​
                // 用定义好的常量实现
                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();
            }
        }
​
​
    }
}
​

注意 :

  • 切面是 切入点 + 通知

4.在mapper对应方法上加上注解 :
@AutoFill(value = OperationType.INSERT)
    void insert(Employee employee);
@AutoFill(value = OperationType.UPDATE)
    void update(Employee employee);

CategoryMapper中一样 ;

5.在impl中将之前写的insert和update方法里对公共字段进行赋值的语句注释掉

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-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}
​

注意 :

  • 这里在sky-commom下的properties中的AliOssProperties中配置类中设置了这四个属性 ,封装成了一个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") // 配置属性类 , 读取配置文件的配置项封装成一个java对象
    @Data
    public class AliOssProperties {
    ​
        private String endpoint;
        private String accessKeyId;
        private String accessKeySecret;
        private String bucketName;
    ​
    }

    在这两个之间对于属性的书写不一样,如 : yml中的bucket-name,在AliOssProperties中就是bucketName,采取的是驼峰命名法,这里在spring框架中能够自动进行转换 ;在yml中习惯使用横线来分割单词;

  • 在yml中配置直接引用dev中的值,在dev中配置相应的值 ;

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对象
 */
@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();
    }
}

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("文件上传")
    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 新增菜品实现

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

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;
​
​
@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语句生成的主键值
        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);//后绪步骤实现
        }
    }
​
}

注意 :

  • @Transactional 表示该方法时原子性的,要么全成功,要么全失败,在涉及多个表的操作时加上,要开启注解方式的事务控制,在启动类上已经开启过了 ;(在启动类上加上@EnableTransactionManagement //开启注解方式的事务管理)

5). Mapper层

DishMapper.java中添加

    /**
     * 插入菜品数据
     *
     * @param dish
     */
    @AutoFill(value = OperationType.INSERT)
    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>
​
  • 这里通过useGeneratedKeys="true" keyProperty="id"给后面在impl中获取id值;

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">
    <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 功能测试

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

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

测试成功。

3.菜品分类查询

1.需求分析

业务规则 :

  • 根据页码展示菜品信息

  • 每页展示10条数据

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

接口设计 :

  • Path : /admin/dish/page

  • Method : GET

  • 请求参数 :

    • page : 必须 : 页码

    • pageSize : 必须 : 每页记录数

    • name : 非必须 : 菜品名称

    • categoryId : 非必须 : 分类id

    • status : 非必须 : 菜品售卖状态

  • 返回数据:

注意 : 其中菜品表中只存了分类的id,没有存categoryName,所以要查分类表

2.代码开发

1.设计DTO :

根据菜品分业务查询接口定义设计对应的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;
​
}

2.设计VO

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

在VO中扩展了categoryName属性 ;

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

直接把vo交给前端 ;

3.controller
/**
 * 菜品分页查询
 * @param dishPageQueryDTO
 * @return
 */
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){// 不是json格式,是querry地址栏传参格式,不用加@RequestBody注解
    log.info("菜品分页查询 : {}",dishPageQueryDTO);
    // 调用service方法 , 用PageResult接收
    PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
    return Result.success(pageResult) ;
}
4.service层
/**
 * 菜品分页查询
 * @param dishPageQueryDTO
 * @return
 */
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
5.impl :
/**
 * 菜品分页查询
 * @param dishPageQueryDTO
 * @return
 */
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
    PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
    Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
    return new PageResult(page.getTotal(),page.getResult());
}

注意 :

  • 这里使用分页插件PageHelper,原理是ThreadLocal ;

6.Mapper
/**
 * 菜品分页查询
 * @param dishPageQueryDTO
 * @return
 */
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
7.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>

这里使用分页查询 ,用动态sql来拼接可能为空的属性;

4.删除菜品

1.需求设计与分析

  • 产品原型 :

删除菜品

  • 业务规则 :

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

    • 起售中的菜品不能删除

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

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

  • 接口设计 :

    批量删除 : 可以只设计一个接口

    Path : /admin/dish

    Method : Delete

    请求参数 : query

    • ids : 必须 : 菜品id,之间用都好分割

    返回数据 :

    • code : 必须

    • data : 非必须

    • msg : 非必须

2.代码开发

controller
/**
 * 菜品批量删除
 * @param ids
 * @return
 */
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids){//使用mvc框架将用,分割ids中的id数据封装到List集合中去
    log.info("菜品批量删除 , {}",ids);
    dishService.deleteBatch(ids) ;// 后续步骤实现
    return Result.success();
}
service
/**
 * 菜品批量删除
 * @param ids
 */
void deleteBatch(List<Long> ids);

impl

    /**
     * 菜品批量删除
     * @param ids
     */
    @Transactional // 事务注解
    public void deleteBatch(List<Long> ids) {
        // 判断当前菜品是否能够删除 --- 是否存在起售中的菜品?
            // 根据status
        for(Long id : ids){
            Dish dish = dishMapper.getById(id);
            if(dish.getStatus() == StatusConstant.ENABLE){
                // 当前菜品在起售中 , 不能删除 , 直接抛出异常
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE) ;
            }
        }
​
        // 判断当前菜品是否能够删除 --- 是否被套餐关联了?
        // 获取setmealIds
        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);//后绪步骤实现
//        }
​
        // 根据菜品id集合批量删除菜品数据
        // sql : delete from dish where id in (?,?,?)
        dishMapper.deleteByIds(ids);
​
        // 根据菜品id集合批量删除口味数据
        // sql : delete from dish_flavor where dish_id in (?,?,?)
        dishFlavorMapper.deleteByDishIds(ids);
​
    }
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);

/**
 * 根据菜品id查找菜品
 * @param id
 * @return
 */
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
优化 :
  • 在删除菜品和相关口味时,遍历的方式会多次访问数据库,这样造成性能的消耗,所以可以采用批量删除的操作来优化

// 根据菜品id集合批量删除菜品数据
// sql : delete from dish where id in (?,?,?)
dishMapper.deleteByIds(ids);
​
// 根据菜品id集合批量删除口味数据
// sql : delete from dish_flavor where dish_id in (?,?,?)
dishFlavorMapper.deleteByDishIds(ids);

DishMapper :

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

DishFlavorMapper :

/**
 * 根据菜品id集合批量删除关联的口味数据
 * @param dishIds
 */
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.修改菜品

1.需求设计与分析

接口设计 :

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

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

  • 文件上传(已实现)

  • 修改菜品

2.代码开发

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>

  • 28
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值