Sky-takeout Day03
【前言】 Day03 的 Task 是 公共字段自动填充 与 菜品管理相关功能 的代码开发,主要的菜品管理部分也是基础 CRUD 的练习,另外就是公共字段自动填充这种偏向技术一点的开发技巧的学习。
公共字段自动填充主要涉及到 枚举、注解、Spring的 AOP 、反射 的知识,用自定义注解实现了当执行 insert新增 与 update更新 时在Mapper层对创建人、创建时间和修改人、修改时间的字段的自动填充,避免了在Service设置属性的冗余重复代码。
剩余的菜品管理就是 CRUD 练习了, 只是相比前面的员工管理模块,菜品管理会涉及到菜品Dish与分类Category、套餐Setmeal的多表的联系,分析起来会更比员工管理的单表更复杂一点,要考虑的比较多,但是这部分练习能锻炼到我们对多表的业务功能的分析能力。
Contents
公共字段自动填充
新增菜品
菜品分页查询
删除菜品
修改菜品
1. 公共字段自动填充
公共字段问题引入
前面在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段,很多表中都会有这些字段,如下:
| 序号 | 字段名 | 含义 | 数据类型 |
|---|---|---|---|
| 1 | create_time | 创建时间 | datetime |
| 2 | create_user | 创建人id | bigint |
| 3 | update_time | 修改时间 | datetime |
| 4 | update_user | 修改人id | bigint |
对于这些字段,我们的赋值方式为:
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
| 字段名 | 数据类型 | 说明 | 备注 |
|---|---|---|---|
| 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) | 口味值 |
代码编写
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查询菜品功能测试:
菜品信息及口味信息正常回显

修改菜品信息功能测试:


浙公网安备 33010602011771号