苍穹外卖-菜品新增、删除
批量删除用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 个细节逐条细说:
访问修饰符与返回类型
public
—— 必须 public,Spring 才能用反射调用。Result
—— 项目里自定义的“统一响应体”,通常长这样public class Result<T> {private Integer code; // 1 成功 0 失败private String msg;private T data;public static <T> Result<T> success(){ ... } }
因此前端收到的总是固定格式,不用再拼 Map。
方法名
save
只是方法名,和 URL 没有半毛钱关系;真正的 URL 由类上的@RequestMapping("/admin/dish")
+ 方法上的@PostMapping
拼出:POST http://ip:port/admin/dish
@PostMapping
背后做了什么限定请求方式 = POST,其他方法(GET/PUT...)进来直接 405。
继承自
@RequestMapping(method = RequestMethod.POST)
,Spring 6 之后还支持@PostMapping(path = "/xxx")
自定义子路径。与 REST 风格配套:新增资源 → POST;修改 → PUT;查询 → GET;删除 → DELETE。
@RequestBody
核心流程(面试常问)请求进来后,
DispatcherServlet
把活交给RequestResponseBodyMethodProcessor
。处理器先看请求头
Content-Type: application/json
(必须是 JSON)。用 Jackson(默认)把字节流读成
JsonNode
,再按字段名一一映射到DishDTO
的 setter/构造器/record 组件。映射失败(类型对不上、缺失必填字段)会抛
HttpMessageNotReadableException
,Spring 默认给 400,项目里可以用@RestControllerAdvice
捕获后转Result.error(400,"JSON格式错误")
。
DishDTO
里常见字段与注解示例@Data public class DishDTO {private Long id; // null 表示新增@NotBlankprivate String name; // 菜品名称@NotNull@Positiveprivate BigDecimal price; // 价格private String image; // OSS 图片 URLprivate String description;@Range(min = 0, max = 1)private Integer status; // 0停售 1起售private List<DishFlavor> flavors; // 口味列表 }
可再加
@Valid
开启校验(Controller 上加@Validated
即可)。
事务边界
Service 层方法saveWithFlavor
上会有@Transactional
:先
INSERT INTO dish
取到自增主键
id
再
INSERT INTO dish_flavor (dish_id, name, value)
VALUES (?,?,?)
两步任何一步失败整体回滚,保证不会出“菜品成功、口味半截”这种脏数据。
异常流向
业务校验失败(如同名菜品已存在)→ 抛自定义
BusinessException
→ 被全局异常处理器捕获 → 返回Result.error(msg)
。数据库唯一索引冲突 → Spring 转
DuplicateKeyException
→ 同样进全局处理器 → 前端收到 0/"菜品已存在"。未捕获的运行时异常 → Spring 默认 500 → 也可被同一处理器统一包装成
Result.error(500,"系统异常")
,防止堆栈泄露。
接口文档与测试
加@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
/*** 菜品管理*/
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {@Autowiredprivate 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
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
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 {@Autowiredprivate DishMapper dishMapper;@Autowiredprivate DishFlavorMapper dishFlavorMapper;@Autowiredprivate SetmealDishMapper setmealDishMapper;/*** 新增菜品,同时保存对应的口味数据*/@Transactionalpublic 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*/@Overridepublic PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);return new PageResult(page.getTotal(),page.getResult());}/*** 菜品批量删除*/@Transactionalpublic 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
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
public interface SetmealDishMapper {/*** 根据菜品id查询对应套餐id* @param setmealDishes*/List<Long> getsetmealIdsByDishIds(List<Long> dishIds);}
setMealDishMapper.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
<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>