目录

【开发中遇到的小知识】

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查询菜品 - GET接口

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、INSERT
    OperationType 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/ObjectName
        StringBuilder 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,这里就不会重复创建
    @ConditionalOnMissingBean
    public 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 {

    @Autowired
    private 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,后端->前端 VO
        Page<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 categoryName
        from sky_take_out.dish d left outer join sky_take_out.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>

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)用动态sql
    List<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查询菜品 - GET接口

为了使修改页面,数据可以回显,我们需要通过菜品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);
    }

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

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐