mthoutai

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Sky-takeout Day03


【前言】 Day03 的 Task 是 公共字段自动填充菜品管理相关功能 的代码开发,主要的菜品管理部分也是基础 CRUD 的练习,另外就是公共字段自动填充这种偏向技术一点的开发技巧的学习。

公共字段自动填充主要涉及到 枚举、注解、Spring的 AOP 、反射 的知识,用自定义注解实现了当执行 insert新增update更新 时在Mapper层对创建人、创建时间和修改人、修改时间的字段的自动填充,避免了在Service设置属性的冗余重复代码。

剩余的菜品管理就是 CRUD 练习了, 只是相比前面的员工管理模块,菜品管理会涉及到菜品Dish与分类Category、套餐Setmeal的多表的联系,分析起来会更比员工管理的单表更复杂一点,要考虑的比较多,但是这部分练习能锻炼到我们对多表的业务功能的分析能力

Contents

  • 公共字段自动填充

  • 新增菜品

  • 菜品分页查询

  • 删除菜品

  • 修改菜品

1. 公共字段自动填充

公共字段问题引入

前面在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段,很多表中都会有这些字段,如下:

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

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

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

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

代码层面,复制方式如下,通过set属性来设置这些公共字段的值

Insert时

Update时

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

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

这样做的好处:

  • 减少重复代码,提升开发效率;

  • 保证字段赋值的一致性(如时间格式、操作人 ID 的来源统一);

  • 降低人为疏忽导致的字段遗漏风险。

实现思路

实现步骤:

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

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

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

实现上述步骤,需掌握以下知识

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

代码编写

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 {
 ​
     // 数据库操作类型,如插入或更新 INSERT, UPDATE
     OperationType value();
 }
 ​

在初始项目的枚举包中已定义了操作类型枚举

2)编写切面类, 自定义AutoFillAspect切面

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

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.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.time.LocalDateTime;
 ​
 /**
  * 自定义切面类,处理公共字段自动填充逻辑
  */
 @Aspect
 @Component
 @Slf4j
 public class AutoFillAspect {
 ​
     /**
      * 切入点,匹配所有标记了 @AutoFill 注解的方法
      */
     // 切入点表达式,匹配所有在 com.sky.mapper 包下的方法,并且这些方法上有 @AutoFill 注解
     // execution(* com.sky.mapper.*.*(..)) 匹配 com.sky.mapper 包及其子包下的所有方法
     // && @annotation(com.sky.annotation.AutoFill) 匹配那些被 @AutoFill 注解标记的方法
     // 这样,任何调用 com.sky.mapper 包下的方法且这些方法被 @AutoFill 注解标记时,
     // 都会触发这个切入点,从而可以在相应的通知中实现自动填充公共字段的逻辑
     @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
     public void autoFillPointCut() {}
 ​
     /**
      * 前置通知,在切入点方法执行前,执行通知--进行公共字段自动填充逻辑 (反射 + AOP)
      * @param joinPoint 连接点信息
      */
     @Before("autoFillPointCut()")
     public void autoFill(JoinPoint joinPoint) {
         log.info("自动填充公共字段...");
 ​
         // 1.获取当前拦截方法的数据库操作类型(插入或更新)
         // 注意这里自动导包的话要导入 org.aspectj.lang.reflect.MethodSignature,
         // 不是 java.lang.reflect.MethodSignature
         // 否则调用不了getMethod()方法
         MethodSignature signature = (MethodSignature) joinPoint.getSignature();  // 获取方法签名
         AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class);// 获取方法上的注解
         OperationType operationType = annotation.value(); // 获取注解的值(操作类型)
 ​
         // 2.获取当前拦截方法的参数对象(实体类对象)
         Object[] args = joinPoint.getArgs(); // 获取方法参数
         if (args == null || args.length == 0) {
             return; // 如果没有参数,直接返回
         }
         Object entity = args[0]; // 项目约定第一个参数是实体类对象
 ​
         // 3.根据操作类型,准备需要填充的公共字段及其值(如创建时间、更新时间、创建人、更新人等)
         LocalDateTime now = LocalDateTime.now();
         Long currentId = BaseContext.getCurrentId();
 ​
         // 4.使用反射机制,给实体类对象的公共字段赋值(如创建时间、更新时间、创建人、更新人等)
         if(operationType == OperationType.INSERT) {
             // 插入操作,准备插入时需要填充的字段及其值 (四个字段)
             try {
                 // 通过反射获取实体类的公共字段的set方法
                 Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                 Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                 Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                 Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                 // 调用set方法,给实体类对象的公共字段赋值
                 setCreateUser.invoke(entity, currentId);
                 setCreateTime.invoke(entity, now);
                 setUpdateUser.invoke(entity, currentId);
                 setUpdateTime.invoke(entity, now);
             } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                 throw new RuntimeException(e);
             }
         } else if (operationType == OperationType.UPDATE) {
             // 更新操作,准备更新时需要填充的字段及其值 (两个字段)
             try {
                 // 通过反射获取实体类的公共字段的set方法
                 Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                 Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                 // 调用set方法,给实体类对象的公共字段赋值
                 setUpdateUser.invoke(entity, currentId);
                 setUpdateTime.invoke(entity, now);
             } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                 throw new RuntimeException(e);
             }
         }
     }
 }
 ​

3)在Mapper的编辑(插入、更新)相关方法加上注解

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

1). 将员工管理的新增和编辑方法中的公共字段赋值的代码注释。

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

功能测试

  • 初步测试,发现前置通知成功拦截方法

  • 完善切面类中前置通知的公共字段自动填充逻辑后,进行调试,观察线程和变量捕获情况

2.新增菜品

需求分析与接口设计

新增菜品的产品原型

业务规则:

  • 菜品名称必须是唯一的

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

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

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

接口设计:明确每个接口的请求方式、请求路径、传入参数和返回值。

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

  • 文件上传

  • 新增菜品

1. 根据类型查询分类

2. 文件上传

3. 新增菜品

数据库设计:涉及两张表——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)口味值

代码编写

1. 文件上传

在项目初始代码已有一部分阿里OSS配置文件 AliOssProperties.java (阿里云OSS属性配置JavaBean类)

application.yml (这里读取了配置属性JavaBean类的字段值)

AliOssUtil.java (这里是文件上传的具体实现)

OssConfiguration.java

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;
 ​
 /**
  * OSS配置类, 用于创建AliOssUtil对象, 读取配置文件中的OSS相关配置, 并提供给AliOssUtil使用
  */
 @Configuration // 表示该类是一个配置类, 可以包含Bean定义
 @Slf4j
 public class OssConfiguration {
 ​
     @Bean // 将方法的返回值作为Bean对象注册到Spring容器中
     @ConditionalOnMissingBean // 当容器中没有指定类型的Bean时, 才会创建该Bean
     public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
         log.info("开始创建阿里云文件上传工具类对象AliOssUtil, {}", aliOssProperties);
         return new AliOssUtil(
                 aliOssProperties.getEndpoint(),
                 aliOssProperties.getAccessKeyId(),
                 aliOssProperties.getAccessKeySecret(),
                 aliOssProperties.getBucketName()
         );
     }
 }
 ​

CommonController.java

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.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 ​
 import java.io.IOException;
 import java.util.UUID;
 ​
 @RestController
 @Slf4j
 @RequestMapping("/admin/common")
 @Api(tags = "通用接口")
 public class CommonController {
 ​
     @Autowired
     private AliOssUtil aliOssUtil;
 ​
     /**
      * 文件上传
      *
      * @param file 文件
      * @return 文件访问路径
      */
     @RequestMapping("/upload")
     @ApiOperation("文件上传")
     public Result upload(MultipartFile file) {
         log.info("文件上传: {}", file);
         // 获取原始文件名
         String originalFilename = file.getOriginalFilename();
         // 获取文件后缀名
         String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
         // 构造新文件名(防止文件名重复)
         String objectName = UUID.randomUUID().toString() + extension;
         // 上传文件到OSS, 并接收返回的文件访问路径
         try {
             String fileUrl = aliOssUtil.upload(file.getBytes(), objectName);
             // 返回文件访问路径
             return Result.success(fileUrl);
         } catch (IOException e) {
             log.error("文件上传失败: {}", e.getMessage());
 ​
         }
         return Result.error(MessageConstant.UPLOAD_FAILED); // 上传失败
     }
 }
 ​

2. 新增菜品

DishController.java

package com.sky.controller.admin;
 ​
 import com.sky.dto.DishDTO;
 import com.sky.result.Result;
 import com.sky.service.DishService;
 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.*;
 ​
 @RestController
 @RequestMapping("/admin/dish")
 @Slf4j
 @Api(tags = "菜品管理")
 public class DishController {
 ​
     @Autowired
     private DishService dishService;
 ​
     /**
      * 新增菜品
      * @param dishDTO 菜品信息
      * @return 操作结果
      */
     @PostMapping
     @ApiOperation("新增菜品")
     public Result save(@RequestBody DishDTO dishDTO) {  // 这里DTO信息记得要加RequestBody,否则接受不到数据 @RequestBody 将请求体中的json数据转换为对应的Java对象
         log.info("新增菜品: {}", dishDTO);
         dishService.saveWithFlavor(dishDTO);
         return Result.success();
     }
 }
 ​

DishServiceImpl.java

这里注意涉及多表要开启事务

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;
 ​
 @Service
 public class DishServiceImpl implements DishService {
 ​
     @Autowired
     private DishMapper dishMapper;
     @Autowired
     private DishFlavorMapper dishFlavorMapper;
 ​
     /**
      * 新增菜品,同时保存对应的口味数据, 所以方法名字WithFlavor体现出来, 需要操作两张表:dish、dish_flavor, 需要开启事务, 保证数据一致性
      * @param dishDTO 菜品信息
      */
     @Override
     @Transactional  // 涉及多表操作,开启事务
     public void saveWithFlavor(DishDTO dishDTO) {
         Dish dish = new Dish();
         BeanUtils.copyProperties(dishDTO, dish);
 ​
         // 1. 向菜品表中插入 1 条数据,保存菜品的基本信息到菜品表dish
         dishMapper.insert(dish);
 ​
         // 2. 向菜品口味表中插入 n 条数据(一个菜品可能有0 或多个以上口味),保存菜品的口味数据到菜品口味表dish_flavor
         Long dishId = dish.getId(); // 获取菜品id
         List flavors = dishDTO.getFlavors(); // 获取口味数据
         // 需要判断flavors是否为空,不为空才进行插入
         if (flavors != null && !flavors.isEmpty()) {
             // 遍历口味数据,逐个设置口味对应的菜品id
             flavors.forEach(flavor -> flavor.setDishId(dishId));
             // 批量插入口味数据
             dishFlavorMapper.insertBatch(flavors);
         }
     }
 }
 ​

DishMapper.java

package com.sky.mapper;
 ​
 import com.sky.annotation.AutoFill;
 import com.sky.entity.Dish;
 import com.sky.enumeration.OperationType;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Select;
 ​
 @Mapper
 public interface DishMapper {
 ​
     /**
      * 根据分类id查询菜品数量
      * @param categoryId 分类Id
      * @return 数量
      */
     @Select("select count(id) from dish where category_id = #{categoryId}")
     Integer countByCategoryId(Long categoryId);
 ​
     /**
      * 插入菜品, 这里记得需要在xml设置属性:useKeyProperty:主键自增, keyProperty:插入后会将生成的主键回填到实体类对象中的id属性
      * @param dish 菜品
      */
     @AutoFill(OperationType.INSERT)
     void insert(Dish dish);
 }
 ​

DishFlavorMapper.java

package com.sky.mapper;
 ​
 import com.sky.entity.DishFlavor;
 import org.apache.ibatis.annotations.Mapper;
 ​
 import java.util.List;
 ​
 @Mapper
 public interface DishFlavorMapper {
 ​
     /**
      * 批量插入口味数据
      * @param flavors 口味数据
      */
     void insertBatch(List flavors);
 }
 ​

DishMapper.xml


 
 
 ​
     
         insert into dish
         (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
         values
         (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
     
 
 ​

DishFlavorMapper.xml


 
 
 ​
     
         insert into dish_flavor (dish_id, name, value) values
         
             (#{df.dishId}, #{df.name}, #{df.value})
         
     
 
 ​

Debug问题发现记录

1)在Controller那里,DTO是json传来的,需要加RequestBody注解,否则会导致接收不到数据,全为null

2)在dishMapper.xml那里不要忘记加两个属性值配置:useGeneratedKeys = true 和 keyProperty = ”id”

3)dish_flavor表没有公共字段需要填充,不用加AutoFill注解

4)还有一个Bug是因为dishFlavor中insert into的字段顺序和foreach里面的顺序没对应上,导致insertBatch操作一直有问题但是没报错,因为一开始那里是用代码补全来写的,后面仔细看才发现顺序没对上

5)写分页查询发现遗漏了一点,这里做个补充,涉及多表要开启事务@Transactional注解保证原子性(在Service那里)

功能测试

文件上传测试

在文件上传时,若文件成功上传到阿里云,但是前端没有回显,需要在阿里云做如下配置 : 设置文件管理的 “阻止公共访问” 和 ”读写权限”

1)关闭阻止公共访问

2)读写权限设为公共读

设置好后发现图片显示就没问题了

前端图片显示正常

成功上传到阿里云OSS

新增菜品测试

注意:在测试新增菜品时,名字不要设置成在表中已有的,我一开始拿“北冰洋”来测试发现一直失败,后面才发现是名字要唯一,表中初始自带了北冰洋所以测试不了

测试成功图,因为还没做分页查询,所以直接查看数据库是否添加成功新数据

菜品添加成功

口味添加成功

3.菜品分页查询

需求分析与接口设计

业务规则:

  • 根据页码展示菜品信息

  • 每页展示10条数据

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

接口设计:

  • 这里大体上步骤跟前面员工分页查询一致,但是需要注意的是由于前端展示Dish时还需要分类名称,所以多封装了一个 DishVO (除了多分类名称,还有口味数组)实体类作为分页结果Page的泛型

  • 此外就是这个分页查询会涉及到dish和category两张表的,用了一个左外连接来查询二表信息。

代码编写

DishController.java

/**
      * 分页查询菜品
      * @param dishPageQueryDTO 分页查询参数
      * @return 分页查询结果
      */
     @GetMapping("/page")
     @ApiOperation("分页查询菜品")
     public Result page(DishPageQueryDTO dishPageQueryDTO) {
         log.info("分页查询菜品: {}", dishPageQueryDTO);
         PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
         return Result.success(pageResult);
     }

DishServiceImpl.java

/**
      * 分页查询菜品
      * @param dishPageQueryDTO 分页查询参数
      * @return 分页查询结果
      */
     @Override
     public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
         PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
         // 需要注意的是这里 Page 的泛型是 DishVO, 因为返回给前端的视图,除了要有菜品信息Dish,还要有分类名称categoryName,而项目中的 DishVO 中已经做好了分类名称的封装
         // 具体分类名称的封装是在 mapper.xml 中通过 left join 关联查询的 (c.name as categoryName), 见 dishMapper.xml
         Page page = dishMapper.pageQuery(dishPageQueryDTO);
         return new PageResult(page.getTotal(), page.getResult());
     }

DishMapper.xml

功能测试

接口文档测试

前后端联调测试(图片不在我的阿里云所以显示不了,这是正常的)

4.删除菜品

需求分析与接口设计

业务规则:

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

  • 起售中的菜品不能删除

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

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

接口设计:

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

表设计:

菜品删除会涉及三张表

注意事项:

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

  • setmeal_dish表为菜品和套餐关联的中间表。

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

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

代码编写

DishController.java

/**
      * (批量)删除菜品
      * @param ids 菜品id,多个id用逗号分隔
      * @return 操作结果
      */
     @DeleteMapping
     @ApiOperation("删除菜品")
     public Result delete(@RequestParam("ids") List ids) { // 这里用@RequestParam注解接收ids参数
         log.info("(批量)删除菜品: {}", ids);
         dishService.deleteBatch(ids);
         return Result.success();
     }

DishServiceImpl.java

/**
      * (批量)删除菜品
      * @param ids 菜品id,多个id用逗号分隔
      */
     @Override
     @Transactional  // 涉及多表操作,开启事务
     public void deleteBatch(List ids) {
         // 菜品处于起售状态,不能删除
         for(Long id : ids) {
             Dish dish = dishMapper.getById(id);
             if (Objects.equals(dish.getStatus(), StatusConstant.ENABLE)) {
                 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
             }
         }
         // 菜品被套餐关联,不能删除
         List setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
         if(setmealIds != null && !setmealIds.isEmpty()) {
             throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
         }
 ​
         // 这里有个优化,为了避免循环删除多次sql操作,可以直接批量删除,用in语句 (多次SQL语句执行效率低)
 ​
         // 删除菜品数据
         // SQL: delete from dish where id in (1, 2, 3);
         dishMapper.deleteBatchByIds(ids);
         // 删除菜品关联的口味数据
         // SQL: delete from dish_flavor where dish_id in (1, 2, 3);
         dishFlavorMapper.deleteBatchByDishIds(ids);
     }

DishMapper.java

/**
      * 根据id查询菜品
      * @param id 菜品id
      * @return 菜品
      */
     @Select("select * from dish where id = #{id}")
     Dish getById(Long id);
 ​
     /**
      * 批量删除菜品
      * @param ids 菜品id,多个id用逗号分隔
      */
     void deleteBatchByIds(List ids);

DishMapper.xml


         delete from dish
         where id in
         
             #{id}
         
     

SetmealDishMapper.java

package com.sky.mapper;
 ​
 import org.apache.ibatis.annotations.Mapper;
 ​
 import java.util.List;
 ​
 @Mapper
 public interface SetmealDishMapper {
 ​
     /**
      * 根据菜品id查询对应的套餐id
      * @param dishIds 菜品id
      * @return 套餐id
      */
     List getSetmealIdsByDishIds(List dishIds);
 }

SetmealDishMapper.xml


 
 
     
 

DishFlavorMapper.java

/**
      * 根据菜品集合ids批量删除对应的口味数据
      * @param dishIds 菜品id,多个id用逗号分隔
      */
     void deleteBatchByDishIds(List dishIds);

DishFlavorMapper.xml


         delete from dish_flavor
         where dish_id in
         
             #{dishId}
         
     

功能测试

5. 修改菜品

需求分析与接口设计

修改菜品原型

接口设计:

  • 根据id查询菜品

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

  • 文件上传(已实现)

  • 修改菜品

这里要注意修改菜品 ,id字段是必须的

代码编写

DishController.java

/**
      * 修改菜品, 同时更新对应的口味数据
      * @param dishDTO 菜品信息
      * @return 操作结果
      */
     @PutMapping
     @ApiOperation("修改菜品")
     public Result update(@RequestBody DishDTO dishDTO) {
         log.info("修改菜品: {}", dishDTO);
         dishService.updateWithFlavor(dishDTO);
         return Result.success();
     }

DishServiceImpl.java

/**
      * 修改菜品,同时更新对应的口味数据
      * @param dishDTO 菜品信息
      */
     @Override
     @Transactional  // 涉及多表操作,开启事务
     public void updateWithFlavor(DishDTO dishDTO) {
         // 1. 更新菜品基本信息到菜品表dish
         Dish dish = new Dish();
         BeanUtils.copyProperties(dishDTO, dish);
         dishMapper.update(dish);
 ​
         List ids = List.of(dishDTO.getId());
         // 删除原有口味数据
         dishFlavorMapper.deleteBatchByDishIds(ids);
 ​
         // 重新插入口味数据
         List flavors = dishDTO.getFlavors(); // 获取口味数据
         // 需要判断flavors是否为空,不为空才进行插入
         if (flavors != null && !flavors.isEmpty()) {
             // 遍历口味数据,逐个设置口味对应的菜品id
             flavors.forEach(flavor -> flavor.setDishId(dishDTO.getId()));
             // 批量插入口味数据
             dishFlavorMapper.insertBatch(flavors);
         }
     }

DishMapper.java

/** 
      * 修改菜品 
      * @param dish 菜品
      */
     @AutoFill(OperationType.UPDATE) // 修改操作,自动填充
     void update(Dish dish);

DishMapper.xml


         update dish
         
             name = #{name},
             category_id = #{categoryId},
             price = #{price},
             image = #{image},
             description = #{description},
             status = #{status},
             update_time = #{updateTime},
             update_user = #{updateUser}
         
         where id = #{id}
     

DishFlavorMapper.java

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

功能测试

根据id查询菜品功能测试:

菜品信息及口味信息正常回显

修改菜品信息功能测试:

posted on 2025-11-08 12:11  mthoutai  阅读(32)  评论(0)    收藏  举报