当前位置: 首页 > news >正文

【项目实战 Day3】springboot + vue 苍穹外卖系统(菜品模块 完结)

目录

【开发中遇到的小知识】

1、idea代码填充快捷键

2、四种类型的请求参数

1. Query String 参数 (查询参数)

2. Body 参数 (请求体参数)

3. Header 参数 (请求头参数)

4. Path 参数 (路径参数)

一、公共字段自动填充

1、AOP面向切面编程

2、通过注解方式实现AOP

(1)自定义注解AutoFill

(2)自定义切面类AutoFillAspect

【1】定义切入点

【2】前置通知

什么是反射?

(3)在Mapper方法上加@AutoFill注解

二、菜品管理模块

1、阿里云OSS对象存储初始化

2、文件上传 - POST接口

3、新增菜品 - POST接口

4、菜品分页查询 - GET接口

5、批量删除菜品 - DELETE接口

6、根据id查询菜品

7、修改菜品 - PUT接口

8、菜品起售停售 - POST接口


【开发中遇到的小知识】

1、idea代码填充快捷键

使用.var快捷键可以在创建对象时自动生成变量名

eg:

LocalDateTime.now().var + 回车

可以自动生成

LocalDateTime now = LocalDateTime.now();

2、四种类型的请求参数

1. Query String 参数 (查询参数)

  • 位置:直接附在 URL 的 ? 后面

  • 格式key1=value1&key2=value2

  • 特点公开可见,有长度限制,会被浏览器记录。

  • 用途:主要用于 GET 请求,传递过滤、分页、搜索等非敏感条件。

  • 示例
    GET /api/products?**category=food&page=2&sort=price**

2. Body 参数 (请求体参数)

  • 位置:在 HTTP 请求的正文部分

  • 格式:多种多样,常见有:

    • JSON (最常用):{"name": "手机", "price": 2999}

    • Form-Data:常用于文件上传

  • 特点不可见,可传输大量复杂数据。

  • 用途:主要用于 POST, PUT, PATCH 请求,传递需要创建或修改的核心数据

  • 示例
    POST /api/users
    Body (JSON){"username": "john", "password": "123456"}

3. Header 参数 (请求头参数)

  • 位置:在 HTTP 请求的头部信息中。

  • 格式Key: Value 键值对。

  • 特点不可见,用于传递协议和上下文信息。

  • 用途:传递元数据,如身份认证、内容格式、客户端信息等。

  • 常见参数

    • Authorization: Bearer <token> (身份令牌)

    • Content-Type: application/json (告知服务器Body的格式)

4. Path 参数 (路径参数)

  • 位置:作为 URL 路径的一部分

  • 格式:通常用 /value 形式直接嵌入路径。

  • 特点用于标识唯一资源,是URL的一部分。

  • 用途:指定要操作的具体资源ID

  • 示例
    GET /api/users/**123** (获取ID为123的用户)
    DELETE /api/products/**456** (删除ID为456的商品)

一、公共字段自动填充

1、AOP面向切面编程

模块中有许多字段重复率较高,如:create_time等,当系统表出现变化时,则需要重新设置这些字段,不利于系统维护

因此我们使用AOP面向切面编程:一种编程范式,主要目的是将那些【散布在应用程序多个地方、与核心业务逻辑无关的代码】分离出来,从而让开发者能更专注于核心业务。

AOP的思路是

  • 把这些功能(日志、事务等)从业务方法中抽取出来,封装成一个个独立的模块,称为 切面(Aspect)
  • 然后,它通过一种方式告诉框架:“请你在每个业务方法执行之前,先帮我运行一下我日志切面里的记录开始日志的方法”。
  • 这个过程就像是“切开”了业务的流程,然后“插入”了新的功能,因此得名“切面”。
  • 使用了AOP之后,代码会变得非常简洁

2、通过注解方式实现AOP

(1)自定义注解AutoFill

自定义注解:用于标识某个方法需要进行功能字段自动填充处理

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

其中OperationType为提前准备好的常量

/*** 数据库操作类型*/
public enum OperationType {/*** 更新操作*/UPDATE,/*** 插入操作*/INSERT}

(2)自定义切面类AutoFillAspect

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

【1】定义切入点
    /*** 切入点:即对哪些类的哪些方法进行拦截*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}

execution(* com.sky.mapper.*.*(..))  —— 匹配com.sky.mapper包下所有类的所有方法

  • 【第一个*】返回值类型不限
  • 【com.sky.mapper.*】mapper包下的任何类
  • .*(..)】类的任何方法,参数不限

@annotation(com.sky.annotation.AutoFill)  —— 同时要求方法上必须标注有@AutoFill注解
这是组合条件,只有同时满足【在mapper包】且【方法上有@AutoFill注解】的方法才会被拦截

【2】前置通知
    /*** 前置通知,在通知中进行公共字段的赋值*/@Before("autoFillPointCut()")public void autoFill(JoinPoint joinPoint){log.info("开始进行公共字段填充!");
  • @Before —— 表示凡是匹配autoFillPointCut()规则的方法,在执行前都会先执行本方法
  • 通常在这里会有获取方法注解、操作类型判断、反射设置字段值等后续逻辑

1)获取当前被拦截的方法上的数据库操作类型

        // 获取当前被拦截的方法上的数据库操作类型// 1.获取当前被拦截方法的签名信息// 由于Spring AOP使用动态代理,joinPoint.getSignature()默认返回Signature接口类型// 但我们需要获取方法上的注解,所以需要强制转换为更具体的MethodSignature类型MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 2.通过MethodSignature对象获取当前被拦截的Method对象// 然后从该Method对象上获取特定的注解(@AutoFill)// 查询方法上是否标注了@AutoFill注解AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);// 3.如果@AutoFill注解存在,调用其value()方法获取注解中定义的枚举值// 这个枚举值(OperationType)通常表示数据库操作类型,如INSERT, UPDATE等// 最终将获取到的操作类型赋值给operationType变量,供后续逻辑使用OperationType operationType = autoFill.value();
  1. 获取当前被拦截方法的签名信息
  2. 通过被拦截对象签名获取被拦截对象的方法
  3. 查询该方法是否存在@AutoFill注解,若存在,获取其注解中定义的数据库操作类型

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){//如果当前操作为INSERT,则需要为createtime、createuser、updatetime、updateuser赋值try {Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);//通过反射为对象属性赋值setCreateTime.invoke(entity,now);setCreateUser.invoke(entity,currentId);setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {throw new RuntimeException(e);}}else if(operationType == OperationType.UPDATE){//如果当前操作为UPDATE,则需要为updatetime、updateuser赋值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) {throw new RuntimeException(e);}}
什么是反射?
  • 正常调用:就像你去餐厅点菜,你知道菜单上有什么(类的方法),直接告诉服务员“我要一个A套餐”(调用方法orderSetA())。
  • 反射调用:就像你不知道菜单有什么,你告诉服务员:“请把你们菜单上第三个分类下的第五道菜给我来一份”。你不需要在写代码时就知道那道菜具体叫什么,你只需要在运行时根据一些动态的规则(比如“第三个分类下的第五个”)来找到并点它。
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
setCreateTime.invoke(entity, now);

这就是一个典型的反射应用:

  1. entity.getClass():在运行时获取实体对象的Class信息。

  2. getDeclaredMethod(...):动态地查找一个名叫setCreateTimeAutoFillConstant.SET_CREATE_TIME的值)、且接受一个LocalDateTime参数的方法。

  3. method.invoke(...):动态地调用找到的这个方法,并传入具体的实体对象(entity)和参数值(now)。

反射的好处:这段自动填充字段的代码是通用的。无论entityEmployeeCategory还是任何其他实体类,只要它有setCreateTime(LocalDateTime time)这个方法,这段代码就能工作。不需要为每个实体类都写一遍重复的赋值逻辑,这就是反射强大灵活性的体现。

(3)在Mapper方法上加@AutoFill注解

字段自动填充主要应用在insert、update两种方法上

  • insert方法需要填充4个字段内容:createtime、createuser、updatetime、updateuser
  • update方法需要填充2个字段内容:updatetime、updateuser

目前我们开发了EmployeeMapper和CategoryMappper两个模块,因此我们需要在这两个Mapper的insert和update方法上加上@AutoFill注解

例如:

既然已经通过AOP实现createtime、createuser、updatetime、updateuser自动填充,那我们可以把Service层的相关填充代码注释掉

二、菜品管理模块

业务规则

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

1、阿里云OSS对象存储初始化

【1】首先登陆阿里云,搜索OSS,充值完毕后进入控制台,点击【Bucket列表】

【2】创建一个新的Bucket,注意只更改名称,然后保存

【3】读写权限改为公共读

【4】在右上角找到AccessKey点击进入,并创建一个新AccessKey(注意保存好!!!)

【5】根据官方SDK文件,在项目pom.xml文件加载依赖项

【6】在application.yml和application-dev.yml文件中配置阿里云oss,其中application.yml引入,application-dev.yml进行具体配置

  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}

endpoint节点在:历史访问路径 - 概览 - 外网访问节点处获取

 【7】创建一个专门用于存储阿里云OSS相关配置的Java对象,并自动从应用的配置文件中读取对应的配置值

@Component
//指定此类绑定配置文件中的前缀为'sky.alioss'的属性
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName;}

【8】引入文件上传官方代码,在阿里云oss的文档中获取,而本项目已经提前配置好文件上传相关代码

https://help.aliyun.com/zh/oss/developer-reference/simple-upload-11?spm=a2c4g.11186623.0.0.46ac7924UAueor#concept-84781-zh

@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/ObjectNameStringBuilder stringBuilder = new StringBuilder("https://");stringBuilder.append(bucketName).append(".").append(endpoint).append("/").append(objectName);log.info("文件上传到:{}", stringBuilder.toString());return stringBuilder.toString();}

【9】创建阿里云属性配置类对属性进行初始化

前边我们在application文件中提前配置好了阿里云oss所需的四个属性endpoint、accessKeyId、accessKeySecret、bucketName

而上述代码中的四个属性未初始化

所以现在需要通过配置类的方式,对四个属性进行初始化(赋值)

/*** 配置类,用于创建AliossUtil对象*/
@Configuration
@Slf4j
public class OssConfiguration {@Bean// 只有在当前Spring容器中不存在AliOssUtil类型的Bean时,才会创建这个Bean// 这确保了如果其他地方已经配置了AliOssUtil,这里就不会重复创建@ConditionalOnMissingBeanpublic AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);return new AliOssUtil(aliOssProperties.getEndpoint(),aliOssProperties.getAccessKeyId(),aliOssProperties.getAccessKeySecret(),aliOssProperties.getBucketName());}
}

【10】四个属性值是如何通过apllication.yml、配置类等方法进行属性绑定的?

  • Spring Boot启动,加载application.yml
  • 发现AliOssProperties有@ConfigurationProperties,进行属性绑定
  • 将AliOssProperties注册为Spring Bean
  • 发现OssConfiguration需要创建AliOssUtil Bean,自动注入AliOssProperties到aliOssUtil方法
  • 调用方法创建AliOssUtil实例,传入配置值
  • AliOssUtil实例被注册到Spring容器中,可供其他组件使用

2、文件上传 - POST接口

【1】controller层

@RestController
@Api(tags = "通用接口")
@RequestMapping("/admin/common")
@Slf4j
public class CommonController {@Autowiredprivate AliOssUtil aliOssUtil;@ApiOperation(value = "文件上传")@PostMapping("/upload")//因为要返回的是文件名路径,因此函数类型为Result<String>public Result<String> upload(MultipartFile file){ //注意MultipartFile后的参数名和前端传参参数名一致log.info("文件上传:{}",file);try {//原始文件名String originalFilename = file.getOriginalFilename();//获取原始文件名的后缀// 1. originalFilename.lastIndexOf(".")//    - 查找字符串中最后一个点号('.')的位置(索引)// 2. originalFilename.substring(位置)//    - 从指定的索引位置开始截取字符串,直到字符串末尾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.info("文件上传失败:{}",e);}return Result.error(MessageConstant.UPLOAD_FAILED);}
}

上传成功!!

3、新增菜品 - POST接口

【1】controller层

    /*** 新增菜品* @param dishDTO* @return*/@PostMapping@ApiOperation("新增菜品")public Result save(@RequestBody DishDTO dishDTO){log.info("新增菜品:{}",dishDTO);dishService.saveWithFlavor(dishDTO);return Result.success();}

【2】service层

  1. 先将DTO->实体类,向菜品表dish插入菜品数据
  2. 因为insert操作不返回主键id,所以需要在mybatis中设置相关属性让其返回该菜品id,接收菜品id用于填充口味表dish_flavor(插入操作后,才自动生成id,且id不会主动返回,因此需要设置相关属性令其主动返回菜品id)
  3. 因为口味是一个list集合,所以需要通过遍历将列表中每一个口味都绑定上对应的菜品id,最后在口味表进行批量插入
    /*** 新增菜品及对应口味* @param dishDTO*/@Transactional //因为涉及多表,因此需要保证事务一致性,要不然全成功,要不然全失败public void saveWithFlavor(DishDTO dishDTO){//因为DTO数据中包含菜品信息+口味,而我们只需要向菜品表中插入菜品信息//因此只需要创建一个实体对象,并把相关属性复制进来即可Dish dish = new Dish();BeanUtils.copyProperties(dishDTO,dish);//向菜品表插入1条数据dishMapper.insert(dish);//获取insert语句返回的主键值Long dishId = dish.getId();//向口味表中插入n条数据List<DishFlavor> flavorList = dishDTO.getFlavors();if(flavorList != null && flavorList.size() > 0){//为flavorList列表里的每一个口味对象,设置它们的菜品ID(dishId)flavorList.forEach(dishFlavor -> {dishFlavor.setDishId(dishId);});dishFlavorMapper.insertBatch(flavorList);}}

【3】mapper层

1)DishMapper

    /*** 插入菜品* @param dish*/@AutoFill(value = OperationType.INSERT)void insert(Dish dish);

2)DishFlavorMapper

        /*** 批量插入菜品口味* @param flavorList*/void insertBatch(List<DishFlavor> flavorList);

【4】mybatis文件

1)DishMapper

  • useGeneratedKeys="true" keyProperty="id"含义:执行完插入语句后,会将主键值存入id这个属性
  • 为什么要加这两个属性?
  • 因为口味表dish_flavor中需要菜品id,而菜品插入操作并不会返回主键id,我们却需要让其返回,因此需要加这两个属性,让其返回主键id
<!--    useGeneratedKeys="true" keyProperty="id"含义:执行完插入语句后,会将主键值存入id这个属性-->
<!--    为什么要加这两个属性?因为口味表dish_flavor中需要菜品id,而菜品插入操作并不会返回主键id,我们却需要让其返回,因此需要加这两个属性,让其返回主键id--><insert id="insert" useGeneratedKeys="true" keyProperty="id">insert into sky_take_out.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})</insert>

2)DishFlavorMapper

  • collection="flavorList": 指定要遍历的集合参数的名字。这里表示遍历一个名为 flavorList 的集合。

  • item="df": 为集合中的每一个元素定义一个临时变量名。这里每个元素都可以通过 df 来引用。

  • separator=",": 指定每次循环生成的内容之间的分隔符。这里用逗号 , 分隔,因为在 SQL 的 VALUES 子句中,多条记录就是用逗号分隔的。

    <insert id="insertBatch">insert into sky_take_out.dish_flavor(dish_id, name, value)values<foreach collection="flavorList" item="df" separator=",">(#{df.dishId},#{df.name},#{df.value})</foreach></insert>

4、菜品分页查询 - GET接口

【1】controller层

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

【2】service层

为什么返回的是DishVO,而不是DishPageDTO或Dish?

因为需要返回联合查询的结果

  • 你的 DishVO 中有一个关键字段:private String categoryName;

  • 数据库中的 dish 表只存储了 category_id,而前端列表需要展示的是分类的名称,而不是一个冰冷的ID数字。

  • 因此,在分页查询时,你的SQL语句很可能是一个多表关联查询(例如 dish LEFT JOIN category),这样才能一次性查出菜品及其对应的分类名称。

  • Dish 实体类只映射了 dish 表的结构,它没有 categoryName 这个字段,无法接收联查的结果。

  • DishVO 在这里的作用就是接收这个联查的结果集,它包含了来自多个表的数据。

    /*** 菜品分页查询* @param dishPageQueryDTO* @return*/public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());//前端->后端 DTO,后端->数据库(需要处理时)entity,后端->前端 VOPage<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);return new PageResult(page.getTotal(),page.getResult());}

【3】mapper层

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

【4】mybatis层

分页查询目的:查询匹配【菜品名name、分类id、菜品状态】中任意若干项的菜品,要求对应菜品展示其【所有菜品信息+对应分类名称】

1)菜品展示其【所有菜品信息+对应分类名称】

select d.*, c.name as categoryName 
from dish d 
left outer join category c on d.category_id = c.id

上述sql语句含义:查询所有菜品信息+对应分类名称

1、select d.*, c.name as categoryName

  •  d.*:查询 dish 表的所有字段(d 是 dish 表的别名)。     
  • c.name as categoryName:查询 category 表的 name 字段,并为其起一个别名 categoryName

2、from dish d

  • 从 dish 表进行查询,并给这个表起了一个简短的别名 d

3、left outer join category c on d.category_id = c.id

  • left outer join左外连接。这意味着会返回左表 (dish) 的所有记录,即使右表 (category) 中没有匹配的记录
  • category c:连接 category 表,并为其起别名 c
  • on d.category_id = c.id:连接的条件是 dish 表的 category_id 字段等于 category 表的 id 字段。

2)查询匹配【菜品名name、分类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>

完整mybatis代码

    <select id="pageQuery" resultType="com.sky.vo.DishVO">select d.* ,c.name as categoryNamefrom sky_take_out.dish d left outer join sky_take_out.category con 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>

5、批量删除菜品 - DELETE接口

业务规则

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

【1】controller层

    /*** 批量删除菜品* @param ids* @return*/@DeleteMapping@ApiOperation("批量删除菜品")public Result detele(@RequestParam List<Long> ids){log.info("批量删除:{}",ids);dishService.deleteBatch(ids);return Result.success();}

【2】service层

  • 起售的菜品不能删除:需要历获取所有菜品的状态,若为起售,则抛出异常
  • 被套餐关联的菜品不能删除:多表联查获取关联套餐的菜品数,若关联数不为空,则说明不能删除
    /*** 批量删除菜品* @param ids*/public void deleteBatch(List<Long> ids){//1.起售中的菜品不能删除for (Long id : ids) {//查询前端提交的每一个菜品的状态Dish dish = dishMapper.getById(id);if(dish.getStatus() == StatusConstant.ENABLE){throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);}}//2.被套餐关联的菜品不能删除List<Long> setmealIdsByDishIds = setmealDishMapper.getSetmealIdsByDishIds(ids);if(setmealIdsByDishIds != null && setmealIdsByDishIds.size() > 0){//说明有关联的套餐throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);}//3.批量删除菜品数据dishMapper.deleteByIds(ids);//4.批量删除菜品关联的口味数据dishFlavorMapper.deleteByDishIds(ids);}

【3】mapper层

1)DishMapper

    /*** 根据ids批量删除菜品* @param ids*/void deleteByIds(List<Long> ids);

2)DishFlavorMapper

    /*** 根据ids批量删除对应口味* @param dishIds*/void deleteByDishIds(List<Long> dishIds);

3)SetmealDishMapper

    /*** 根据菜品id查询对应套餐id* @param dishIds* @return*/// select setmeal_id from setmeal_dish where dish_id in (1,2,3)用动态sqlList<Long> getSetmealIdsByDishIds(List<Long> dishIds);//因为一个菜品可能关联多个套餐,因此用list集合接收

【4】mybatis层

1)DishMapper

    <delete id="deleteByIds">delete from sky_take_out.dish where id in<foreach collection="ids" item="id" open="(" close=")" separator=",">#{id}</foreach></delete>

2)DishFlavorMapper

    <delete id="deleteByDishIds">delete from sky_take_out.dish_flavor where dish_id in<foreach collection="dishIds" item="dishId" open="(" close=")" separator=",">#{dishId}</foreach></delete>

3)SetmealDishMapper

<!--    从前端提供的菜品id集合中获取--><select id="getSetmealIdsByDishIds" resultType="java.lang.Long">select setmeal_id from sky_take_out.setmeal_dish where dish_id in<foreach collection="dishIds" item="dishId" open="(" close=")" separator=",">#{dishId}</foreach></select>

6、根据id查询菜品

为了使修改页面,数据可以回显,我们需要通过菜品id查询菜品信息+对应口味信息

【1】controller层

    /*** 根据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层

为什么根据id查询菜品信息不使用sql多表联查?

因为口味表和菜品表是多对多关系,即一个菜品对应多条口味,一个口味也会对应多个菜品,如果使用多表联查,会出现菜品id重复的问题

select d.*,
f.name as flavor_name,
f.value as flavor_value
from dish d 
left outer join dish_flavor f on d.id = f.dish_id

【3】mapper层

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

菜品信息成功回显!

7、修改菜品 - PUT接口

【1】controller层

    /*** 修改菜品* @param dishDTO* @return*/@PutMapping@ApiOperation("修改菜品")public Result update(@RequestBody DishDTO dishDTO){log.info("修改菜品:{}",dishDTO);dishService.updateWithFlavor(dishDTO);return Result.success();}

【2】service层

  • 因为口味修改包含:口味删除与修改,而用户操作时可能会同时涉及这两个操作,因此我们直接统一先删除所有口味,再重新插入口味

为什么这里不需要xml文件获取菜品id?

  • 因为修改菜品接口传入的【菜品id】为必须项,而插入菜品接口传入的【菜品id】为非必须项 所以修改菜品接口的菜品id可以直接从DTO中获取
  • 而插入接口的菜品id则需要插入后生成id后返回
    /*** 修改菜品* @param dishDTO*/public void updateWithFlavor(DishDTO 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 -> {//为什么这里不需要xml文件获取菜品id?//因为修改菜品接口传入的【菜品id】为必须项,而插入菜品接口传入的【菜品id】为非必须项//所以修改菜品接口的菜品id可以直接从DTO中获取//而插入接口的菜品id则需要插入后生成id后返回dishFlavor.setDishId(dishDTO.getId());});}dishFlavorMapper.insertBatch(flavors);}

【3】mapper层

    /*** 修改菜品* @param dish*/@AutoFill(value = OperationType.UPDATE)void update(Dish dish);

【4】mybatis文件

    <update id="update">update sky_take_out.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>

8、菜品起售停售 - POST接口

【1】controller层

    /*** 菜品起售停售* @param status* @param id* @return*/@PostMapping("/status/{status}")@ApiOperation("起售停售菜品")public Result startOrStop(@PathVariable Integer status, Long id){log.info("起售停售菜品:{},{}",status,id);dishService.startOrStop(status,id);return Result.success();}

【2】service层

修改菜品状态其实就是把状态和id值填进去,让其动态更新
注:xml文件update是动态更新,像name、price这种没有修改的,其原来的值就不会变,只修改更新的status状态

    /*** 菜品起售停售* @param status* @param id*/public void startOrStop(Integer status, Long id) {//修改菜品状态其实就是把状态和id值填进去,让其动态更新//注:xml文件update是动态更新,像name、price这种没有修改的,其原来的值就不会变//只修改更新的status状态Dish dish = Dish.builder().status(status).id(id).build();dishMapper.update(dish);}

菜品模块开发结束~撒花!

http://www.dtcms.com/a/390668.html

相关文章:

  • 华为 ai 机考 编程题解答
  • Docker多容器通过卷共享 R 包目录
  • 【保姆级教程】MasterGo MCP + Cursor 一键实现 UI 设计稿还原
  • Unity 性能优化 之 理论基础 (Culling剔除 | Simplization简化 | Batching合批)
  • react+andDesign+vite+ts从零搭建后台管理系统
  • No007:构建生态通道——如何让DeepSeek更贴近生产与生活的真实需求
  • 力扣Hot100--206.反转链表
  • Java 生态监控体系实战:Prometheus+Grafana+SkyWalking 整合全指南(三)
  • 生活琐记(3)
  • 在 Elasticsearch 和 GCP 上的混合搜索和语义重排序
  • 借助Aspose.HTML控件,使用 Python 将 HTML 转换为 DOCX
  • 设计测试用例的万能公式
  • 黑马头条_SpringCloud项目阶段三:HTML文件生成以及素材文章CRUD
  • 精准模拟,实战赋能-比亚迪秦EV整车检测与诊断仿真实训系统
  • 学习路之PHP--生成测试数据:fakerphp的使用
  • 《UE5_C++多人TPS完整教程》学习笔记54 ——《P55 旋转根骨骼(Rotate Root Bone)》
  • go资深之路笔记(五)用系统信号实现优雅关机
  • C++实战㉔】解锁C++ STL魔法:list与deque实战秘籍
  • Linux 系统指令——助力信奥初赛
  • LVS详解:构建高性能Linux负载均衡集群
  • 【Linux网络编程】网络层协议-----IP协议
  • 电池AH的定义与WH关系
  • 谙流 ASK 技术解析(四):负载均衡引擎
  • 乾元通渠道商中标国家华中区域应急救援中心应急救援装备采购项目
  • 网络原理补充——NAT/NAPT、代理服务、内网穿透、交换机
  • 深入 HTTP 协议:剖析 1688 商品详情 API 的请求构造与签名机制
  • 共用体union和大小端模式
  • 2022年下半年 系统架构设计师 案例分析
  • LeetCode 面试经典 150_哈希表_有效的字母异位词(42_242_C++_简单)
  • go webrtc - 3 工程演示