苍穹外卖-菜品新增、删除

批量删除用foreach循环来遍历,删除被套餐关联的SQL语句比较复杂。

删除菜品表中的菜品数据这里,每次循环需要执行2次SQL,可能会出现性能问题。应该采用如下的SQL形式:delete from dish where id in (?,?,?)。

在sky-server的mapper中已有的DishMapper类中添加如下代码(负责删除菜品):

public Result save(@RequestBody DishDTO dishDTO) 这 1 行代码看起来简单,其实背后把 SpringMVC 参数解析、JSON 反序列化、校验、统一响应 全串在了一起。下面把它拆成 8 个细节逐条细说:


  1. 访问修饰符与返回类型
    public ------ 必须 public,Spring 才能用反射调用。
    Result ------ 项目里自定义的"统一响应体",通常长这样

    java 复制代码
    public class Result<T> {
        private Integer code;  // 1 成功 0 失败
        private String msg;
        private T data;
        public static <T> Result<T> success(){ ... }
    }

    因此前端收到的总是固定格式,不用再拼 Map。


  1. 方法名
    save 只是方法名,和 URL 没有半毛钱关系;真正的 URL 由类上的 @RequestMapping("/admin/dish") + 方法上的 @PostMapping 拼出:
    POST http://ip:port/admin/dish

  1. @PostMapping 背后做了什么

    1. 限定请求方式 = POST,其他方法(GET/PUT...)进来直接 405。

    2. 继承自 @RequestMapping(method = RequestMethod.POST),Spring 6 之后还支持 @PostMapping(path = "/xxx") 自定义子路径。

    3. 与 REST 风格配套:新增资源 → POST;修改 → PUT;查询 → GET;删除 → DELETE。


  1. @RequestBody 核心流程(面试常问)

    1. 请求进来后,DispatcherServlet 把活交给 RequestResponseBodyMethodProcessor

    2. 处理器先看请求头 Content-Type: application/json(必须是 JSON)。

    3. Jackson (默认)把字节流读成 JsonNode,再按字段名一一映射到 DishDTOsetter/构造器/record 组件

    4. 映射失败(类型对不上、缺失必填字段)会抛 HttpMessageNotReadableException,Spring 默认给 400,项目里可以用 @RestControllerAdvice 捕获后转 Result.error(400,"JSON格式错误")


  1. DishDTO 里常见字段与注解示例

    java 复制代码
    @Data
    public class DishDTO {
        private Long id;                // null 表示新增
        @NotBlank
        private String name;            // 菜品名称
        @NotNull
        @Positive
        private BigDecimal price;       // 价格
        private String image;           // OSS 图片 URL
        private String description;
        @Range(min = 0, max = 1)
        private Integer status;         // 0停售 1起售
        private List<DishFlavor> flavors;  // 口味列表
    }

    可再加 @Valid 开启校验(Controller 上加 @Validated 即可)。


  1. 事务边界

    Service 层方法 saveWithFlavor 上会有 @Transactional

    • INSERT INTO dish

    • 取到自增主键 id

    • INSERT INTO dish_flavor (dish_id, name, value) VALUES (?,?,?)

      两步任何一步失败整体回滚,保证不会出"菜品成功、口味半截"这种脏数据。


  1. 异常流向

    1. 业务校验失败(如同名菜品已存在)→ 抛自定义 BusinessException → 被全局异常处理器捕获 → 返回 Result.error(msg)

    2. 数据库唯一索引冲突 → Spring 转 DuplicateKeyException → 同样进全局处理器 → 前端收到 0/"菜品已存在"。

    3. 未捕获的运行时异常 → Spring 默认 500 → 也可被同一处理器统一包装成 Result.error(500,"系统异常"),防止堆栈泄露。


  1. 接口文档与测试

    @ApiOperation("新增菜品") 后,启动项目 → 访问 http://ip:port/doc.html(Knife4j)即可看到:

    • 请求方法:POST

    • 请求体示例

      复制代码
      {
        "name": "辣子鸡",
        "categoryId": 3,
        "price": 2800,
        "image": "https://xxx.com/a.jpg",
        "description": "微辣",
        "status": 1,
        "flavors": [
          { "name": "辣度", "value": "[\"微辣\",\"中辣\",\"重辣\"]" }
        ]
      }
    • 响应 200:

      复制代码
      { "code": 1, "msg": null, "data": null }

一句话总结

这行代码把"HTTP 请求 → JSON → Java 对象 → 业务 → 统一响应"全链路打包好,开发者只需关心真正的业务逻辑,其余由 SpringMVC + Jackson + 统一异常处理自动完成。

DishController.java

java 复制代码
/**
 * 菜品管理
 */
@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();
    }

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

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

DishService.java

java 复制代码
package com.sky.service;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.result.PageResult;
import org.springframework.stereotype.Service;

import java.util.List;


@Service
public interface DishService {

    /**
     * 新增菜品,同时口味
     * @param dishDTO
     */
    public void saveWithFlavor(DishDTO dishDTO);

    /**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);

    /**
     * 删除菜品
     * @param ids
     */
    void deleteBatch(List<Long> ids);
}

DishServiceImpl.java

java 复制代码
package com.sky.service.impl;


import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.result.PageResult;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;


@Service
@Slf4j
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;

    /**
     * 新增菜品,同时保存对应的口味数据
     */
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO){
        log.info("新增菜品:{}",dishDTO);
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);
        dishMapper.insert(dish);

        Long dishId = dish.getId();
        //口味
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if(flavors != null && flavors.size() > 0){
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
        }
        //插入
        dishFlavorMapper.insertBatch(flavors);
    }


    /**
     * 菜品分页查询
     * @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());
    }

    /**
     * 菜品批量删除
     */
    @Transactional
    public void deleteBatch(List<Long> ids){
        //判断是否删除是否起售中
        for(Long id :ids){
            Dish dish = dishMapper.getById(id);
            if(dish.getStatus() == StatusConstant.ENABLE){
                //起售
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }
        //是否关联套餐
        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);
        }

    }
}

DishMapper.java

java 复制代码
package com.sky.mapper;


import com.sky.entity.DishFlavor;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface DishFlavorMapper {

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

    /**
     * 根据菜品id删除菜品口味数据
     * @param dishId
     */
    @Delete("delete from dish_flavor where dish_id = #{dishId}")
    void deleteByDishId(Long dishId);
}

setMealDishMapper

java 复制代码
public interface SetmealDishMapper {

    /**
     * 根据菜品id查询对应套餐id
     * @param setmealDishes
     */


    List<Long> getsetmealIdsByDishIds(List<Long> dishIds);

}

setMealDishMapper.xml

XML 复制代码
<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.xml

XML 复制代码
<mapper namespace="com.sky.mapper.DishMapper">


    <insert id="insert" useGeneratedKeys="true" keyProperty="id"
            parameterType="com.sky.entity.Dish">
        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})
    </insert>
    <select id="pageQuery" resultType="com.sky.vo.DishVO">
        select d.*,c.name as categoryName from dish d left outer join category c on c.id = d.category_id
        <where>
            <if test = "name != null">
                and d.name like concat('%', #{name},'%')
            </if>
            <if test = "categoryId != null">
                and d.categoryId like concat('%', #{categoryId},'%')
            </if>
            <if test = "status != null">
                and d.status like concat('%', #{status},'%')
            </if>
        </where>
        order by d.update_time desc
    </select>
</mapper>
相关推荐
寻星探路2 小时前
Java EE初阶启程记04---线程的状态
java·开发语言·jvm·java-ee
努力也学不会java2 小时前
【Java并发】揭秘Lock体系 -- 深入理解ReentrantLock
java·开发语言·人工智能·python·机器学习·reentrantlock
haokan_Jia2 小时前
【MyBatis-Plus 动态数据源的默认行为】
java·开发语言·mybatis
_院长大人_2 小时前
IDEA 实现SpringBoot热部署(HotSwap和DevTools混用)
java·spring boot·intellij-idea
小信丶3 小时前
Spring 6 的 @HttpExchange 注解:声明式 HTTP 客户端的现代化利器
java·spring·http
野犬寒鸦6 小时前
多级缓存架构:性能与数据一致性的平衡处理(原理及优势详解+项目实战)
java·服务器·redis·后端·缓存
帧栈8 小时前
开发避坑指南(58):Java Stream 按List元素属性分组实战指南
java
Da Da 泓8 小时前
LinkedList模拟实现
java·开发语言·数据结构·学习·算法
海琴烟Sunshine8 小时前
Leetcode 14. 最长公共前缀
java·服务器·leetcode