【苍穹外卖笔记】Day04--套餐管理模块

Sky-takeout Day04


【前言】

Day04 的 Task 是套餐管理相关代码的开发 ,也是对多表业务的分析能力的锻炼

Contents

  • 新增套餐

  • 套餐分页查询

  • 删除套餐

  • 修改套餐

  • 起售停售套餐

1. 新增套餐

需求分析与接口设计

业务规则:

  • 套餐名称唯一

  • 套餐必须属于某个分类

  • 套餐必须包含菜品

  • 名称、分类、价格、图片为必填项

  • 添加菜品窗口需要根据分类类型来展示菜品

  • 新增的套餐默认为停售状态

接口设计(共涉及到 4 个接口):

  • 根据类型查询分类(已完成)

  • 根据分类 id 查询菜品

  • 图片上传(已完成)

  • 新增套餐

产品原型:

可以看到,在新增套餐中还涉及到了菜品的一个接口,就是 根据分类 id 查询菜品并列出菜品,所以需要在 DishController 再加一个接口 来根据分类 id 展示菜品(list)

接口设计:


表设计:

setmeal 表为套餐表,用于存储套餐的信息。具体表结构如下:

字段名 数据类型 说明 备注
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

setmeal_dish 表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:

字段名 数据类型 说明 备注
id bigint 主键 自增
setmeal_id bigint 套餐 id 逻辑外键
dish_id bigint 菜品 id 逻辑外键
name varchar(32) 菜品名称 冗余字段
price decimal(10,2) 菜品单价 冗余字段
copies int 菜品份数

代码编写

1)根据分类 id 查询并列出菜品

DishController.java

复制代码
     /**
      * 根据分类条件查询列出菜品
      * @param categoryId 分类id
      * @return 菜品列表
      */
     @GetMapping("/list")
     @ApiOperation("根据分类条件查询并列出菜品")
     public Result<List<Dish>> list(Long categoryId) {
         log.info("根据分类条件查询并列出菜品: {}", categoryId);
         List<Dish> dishList = dishService.list(categoryId);
         return Result.success(dishList);
     }

DishServiceImpl.java

这里注意 status 状态是 ENABLE,一开始写成 DISABLE 了,没有菜品列出,后面发现是状态设置错了。

复制代码
     /**
      * 根据分类条件查询列出菜品
      * @param categoryId 分类id
      * @return 菜品列表
      */
     @Override
     public List<Dish> list(Long categoryId) {
         Dish dish = Dish.builder()
                 .status(StatusConstant.ENABLE) // 注意这里是只查询起售状态的菜品
                 .categoryId(categoryId)
                 .build();
         return dishMapper.list(dish);
     }

DishMapper.java

复制代码
     /**
      * 动态条件查询菜品
      * @param dish 条件
      * @return 菜品列表
      */
     List<Dish> list(Dish dish);

DishMapper.xml

复制代码
     <select id="list" resultType="com.sky.entity.Dish">
         select * from dish
         <where>
             <if test="name != null and name != ''">
                 and name like concat('%', #{name}, '%')
             </if>
             <if test="categoryId != null">
                 and category_id = #{categoryId}
             </if>
             <if test="status != null">
                 and status = #{status}
             </if>
         </where>
         order by create_time desc
     </select>
2)新增套餐

SetmealController.java

复制代码
 package com.sky.controller.admin;
 ​
 import com.sky.dto.SetmealDTO;
 import com.sky.result.Result;
 import com.sky.service.SetmealService;
 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.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 ​
 @RestController
 @Api(tags = "套餐管理")
 @RequestMapping("admin/setmeal")
 @Slf4j
 public class SetmealController {
 ​
     @Autowired
     private SetmealService setmealService;
 ​
     /**
      * 新增套餐
      * @param setmealDTO 套餐信息
      * @return 结果
      */
     @PostMapping
     @ApiOperation("新增套餐")
     public Result<Void> save(@RequestBody SetmealDTO setmealDTO) { // 这里要加 @RequestBody 将前端传递的json数据转换为java对象
         log.info("新增套餐: {}", setmealDTO);
         setmealService.saveWithDish(setmealDTO);
         return Result.success();
     }
 }
 ​

SetmealServiceImpl.java

复制代码
 package com.sky.service.impl;
 ​
 import com.sky.dto.SetmealDTO;
 import com.sky.entity.Setmeal;
 import com.sky.entity.SetmealDish;
 import com.sky.mapper.SetmealDishMapper;
 import com.sky.mapper.SetmealMapper;
 import com.sky.service.SetmealService;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 ​
 import java.util.List;
 ​
 @Service
 public class SetmealServiceImpl implements SetmealService {
 ​
     @Autowired
     private SetmealMapper setmealMapper;
     @Autowired
     private SetmealDishMapper setmealDishMapper;
 ​
     /**
      * 新增套餐
      * @param setmealDTO 套餐信息
      */
     @Override
     public void saveWithDish(SetmealDTO setmealDTO) {
         Setmeal setmeal = new Setmeal();
         BeanUtils.copyProperties(setmealDTO, setmeal);
 ​
         // 1. 向setmeal表插入套餐
         setmealMapper.insert(setmeal);
 ​
         // 2. 向setmeal_dish表插入套餐和菜品的关联关系
         // 先获取套餐id
         Long setmealId = setmeal.getId();
         // 再获取套餐和菜品的关联关系
         List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
         // 设置套餐id
         setmealDishes.forEach(setmealDish -> setmealDish.setSetmealId(setmealId));
         // 批量插入套餐和菜品的关联关系
         setmealDishMapper.insertBatch(setmealDishes);
     }
 }
 ​

SetmealMapper.java

复制代码
     /**
      * 插入套餐, 并且将主键回填到实体类对象中 (useGeneratedKeys = true, keyProperty = "id")
      * @param setmeal 套餐
      */
     @AutoFill(value = OperationType.INSERT)
     void insert(Setmeal setmeal);
 ​

SetmealMapper.xml

复制代码
 <?xml version="1.0" encoding="UTF-8" ?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
 <mapper namespace="com.sky.mapper.SetmealMapper">
 ​
     <insert id="insert" useGeneratedKeys="true" keyProperty="id">
         insert into setmeal (category_id, name, price, description, image, create_time, update_time, create_user, update_user)
         values
         (#{categoryId}, #{name}, #{price}, #{description}, #{image}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
     </insert>
 ​
 </mapper>
 ​

SetmealDishMapper.java

复制代码
    /**
     * 批量插入套餐和菜品的关联关系
     * @param setmealDishes 套餐和菜品的关联关系
     */
    void insertBatch(List<SetmealDish> setmealDishes);

SetmealDishMapper.xml

复制代码
    <insert id="insertBatch">
        insert into setmeal_dish (setmeal_id, dish_id, name, price, copies)
        values
        <foreach collection="setmealDishes" item="sd" separator=",">
            (#{sd.setmealId}, #{sd.dishId}, #{sd.name}, #{sd.price}, #{sd.copies})
        </foreach>
    </insert>

功能测试

成功在表中插入数据

  • 注意 这里有误!!后来在写删除套餐的时候才发现,插入时 status 不应该是 1,之前编写新增套餐代码的时候在 mapper 的 xml 映射文件遗漏了 status 字段,而且在 service 层忘记设置了初始状态为停售 DISABLE(也就是 0)

2. 套餐分页查询

需求分析与接口设计

业务规则:

  • 根据页码进行分页展示

  • 每页展示 10 条数据

  • 可以根据需要,按照套餐名称、分类、售卖状态进行查询

产品原型

注意因为视图需要套餐对应的分类名称,所以分页时 Page 的泛型是 VO 对象,即 SetmealVO。

接口设计:

代码编写

SetmealController.java

复制代码
    /**
     * 分页查询套餐
     * @param setmealPageQueryDTO 分页查询参数
     * @return 套餐分页结果
     */
    @GetMapping("/page")
    @ApiOperation("分页查询套餐")
    public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
        log.info("分页查询套餐: {}", setmealPageQueryDTO);
        PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
        return Result.success(pageResult);
    }

SetmealServiceImpl.java

复制代码
    /**
     * 分页查询套餐
     * @param setmealPageQueryDTO 分页查询参数
     * @return 套餐分页结果
     */
    @Override
    public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
        PageHelper.startPage(setmealPageQueryDTO.getPage(), setmealPageQueryDTO.getPageSize());
        Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

SetmealMapper.java

复制代码
    /**
     * 分页查询套餐
     * @param setmealPageQueryDTO 分页查询参数
     * @return 套餐分页结果
     */
    Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

SetmealMapper.xml

复制代码
    <select id="pageQuery" resultType="com.sky.vo.SetmealVO">
        select s.*, c.name as categoryName
        from setmeal s left outer join category c
        on s.category_id = c.id
        <where>
            <if test="name != null">
                s.name like concat('%', #{name}, '%')
            </if>
            <if test="status != null">
                and s.status = #{status}
            </if>
            <if test="categoryId != null">
                and s.category_id = #{categoryId}
            </if>
        </where>
    </select>

功能测试

3. 删除套餐

需求分析与接口设计

业务规则:

  • 可以一次删除一个套餐,也可以批量删除套餐

  • 起售中的套餐不能删除

接口设计:

代码编写

SetmealController.java

复制代码
    /**
     * 删除套餐
     * @param ids 套餐id集合
     * @return 结果
     */
    @DeleteMapping
    @ApiOperation("删除套餐")
    public Result<Void> delete(@RequestParam("ids") List<Long> ids) {
        log.info("删除套餐: {}", ids);
        setmealService.deleteWithDish(ids);
        return Result.success();
    }

SetmealServiceImpl.java

复制代码
    /**
     * 删除套餐, 同时删除套餐和菜品的关联数据
     * @param ids 套餐id集合
     */
    @Override
    public void deleteWithDish(List<Long> ids) {
        // 起售中的套餐不能删除
        // 1. 查询套餐状态, 确认是否可以删除
        for (Long id : ids) {
            Setmeal setmeal = setmealMapper.getById(id);
            if (setmeal != null && Objects.equals(setmeal.getStatus(), StatusConstant.ENABLE)) { // 套餐起售中
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }
        }

        // 2. 删除套餐表中的数据
        // delete from setmeal where id in (1,2,3)
        setmealMapper.deleteByIds(ids);
        // 3. 删除套餐和菜品的关联数据
        // delete from setmeal_dish where setmeal_id in (1,2,3)
        setmealDishMapper.deleteBySetmealIds(ids);
    }

SetmealMapper.java

复制代码
    /**
     * 根据id查询套餐  (用于删除套餐时, 查询套餐状态)
     * @param id 套餐id
     */
    @Select("select * from setmeal where id = #{id}")
    Setmeal getById(Long id);

    /**
     * 根据id删除套餐
     * @param ids 套餐id集合
     */
    void deleteByIds(List<Long> ids);

SetmealMapper.xml

复制代码
    <delete id="deleteByIds" parameterType="list">
        delete from setmeal where id in
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>

SetmealDishMapper.java

复制代码
    /**
     * 根据套餐id删除对应的套餐和菜品的关联关系
     * @param setmealIds 套餐id集合
     */
    void deleteBySetmealIds(List<Long> setmealIds);

SetmealDishMapper.xml

复制代码
    <delete id="deleteBySetmealIds">
        delete from setmeal_dish where setmeal_id in
        <foreach collection="setmealIds" item="setmealId" open="(" separator="," close=")">
            #{setmealId}
        </foreach>
    </delete>

功能测试

起售中无法删除

删除成功

4. 修改套餐

需求分析与接口设计

产品原型:

接口设计(共涉及到5个接口):

  • 根据id查询套餐

  • 根据类型查询分类(已完成)

  • 根据分类id查询菜品(已完成)

  • 图片上传(已完成)

  • 修改套餐

代码编写

SetmealController.java

复制代码
    /**
     * 根据id查询套餐信息, 包括套餐和菜品的关联信息
     * @param id 套餐id
     * @return 套餐信息
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询套餐信息(包括套餐和菜品的关联信息)")
    public Result<SetmealVO> getById(@PathVariable Long id) {  // 这里要加 @PathVariable 将路径参数中的id参数绑定到方法的id参数上
        log.info("根据id查询套餐信息: {}", id);
        SetmealVO setmealVO = setmealService.getByIdWithDishes(id);
        return Result.success(setmealVO);
    }

    /**
     * 修改套餐(包括套餐和菜品的关联信息)
     * @param setmealDTO 套餐信息
     * @return 结果
     */
    @PutMapping
    @ApiOperation("修改套餐")
    public Result<Void> update(@RequestBody SetmealDTO setmealDTO) {  // 这里要加 @RequestBody 将前端传递的json数据转换为java对象
        log.info("修改套餐: {}", setmealDTO);
        setmealService.updateWithDish(setmealDTO);
        return Result.success();
    }

SetmealServiceImpl.java

复制代码
    /**
     * 根据id查询套餐信息, 包括套餐和菜品的关联信息
     * @param id 套餐id
     * @return 套餐信息
     */
    @Override
    public SetmealVO getByIdWithDishes(Long id) {
        // 1. 查询套餐基本信息
        Setmeal setmeal = setmealMapper.getById(id);
        // 2. 查询套餐和菜品的关联信息
        List<SetmealDish> setmealDishes = setmealDishMapper.getByIdWithDishes(id);

        // 3. 将套餐基本信息与套餐菜品信息封装到VO对象中并返回
        SetmealVO setmealVO = new SetmealVO();
        BeanUtils.copyProperties(setmeal, setmealVO);
        setmealVO.setSetmealDishes(setmealDishes);

        return setmealVO;
    }

    /**
     * 修改套餐(包括套餐和菜品的关联信息)
     * @param setmealDTO 套餐信息
     */
    @Override
    public void updateWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);
        // 1. 首先,更新setmeal表中的基本信息
        setmealMapper.updateById(setmeal);

        // 2. 然后,更新setmeal_dish表中的关联信息
        Long setmealId = setmeal.getId();
        // 2.1 先删除原有的关联信息
        setmealDishMapper.deleteBySetmealIds(List.of(setmealId));

        // 2.2 再重新插入新的关联信息
        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        // 设置套餐id
        setmealDishes.forEach(setmealDish -> setmealDish.setSetmealId(setmealId));
        // 批量插入套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);
    }

SetmealMapper.java

复制代码
    /**
     * 根据id动态修改套餐信息
     * @param setmeal 套餐信息
     */
    @AutoFill(value = OperationType.UPDATE)
    void updateById(Setmeal setmeal);

SetmealMapper.xml

复制代码
    <update id="updateById">
        update setmeal
        <set>
            <if test="categoryId != null">category_id = #{categoryId},</if>
            <if test="name != null">name = #{name},</if>
            <if test="price != null">price = #{price},</if>
            <if test="status != null">status = #{status},</if>
            <if test="description != null">description = #{description},</if>
            <if test="image != null">image = #{image},</if>
            update_time = #{updateTime},
            update_user = #{updateUser}
        </set>
        where id = #{id}
    </update>

SetmealDishMapper.java

复制代码
    /**
     * 根据套餐id查询对应的套餐和菜品的关联关系
     * @param setmealId 套餐id
     * @return 套餐和菜品的关联关系
     */
    @Select("select * from setmeal_dish where setmeal_id = #{setmealId}")
    List<SetmealDish> getByIdWithDishes(Long setmealId);

功能测试

5. 起售停售套餐

需求分析与接口设计

业务规则:

  • 可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作

  • 起售的套餐可以展示在用户端,停售的套餐不能展示在用户端

  • 起售套餐时,如果套餐内包含停售的菜品,则不能起售

接口设计:

代码编写

SetmealController

复制代码
    /**
     * 起售/停售套餐
     * @param status 状态
     * @param id 套餐id集合
     * @return 结果
     */
    @PostMapping("/status/{status}")
    @ApiOperation("起售/停售套餐")
    public Result<Void> startOrStop(@PathVariable Integer status, Long id) {
        log.info("起售/停售套餐: {}, {}", status, id);
        setmealService.startOrStop(status, id);
        return Result.success();
    }

SetmealServiceImpl

复制代码
    /**
     * 起售/停售套餐
     * @param status 套餐状态
     * @param id 套餐id
     */
    @Override
    public void startOrStop(Integer status, Long id) {
        // 如果要设置为起售,需要检查套餐内的菜品是否都为起售状态
        if (Objects.equals(status, StatusConstant.ENABLE)) { // 要起售
            // select a.* from dish a
            // left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?;
            List<Dish> dishList = dishMapper.getBySetmealId(id); // 查询套餐内的菜品
            dishList.forEach(dish -> {
                if (dish.getStatus().equals(StatusConstant.DISABLE)) {
                    throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
                }
            }); // 只要有一个菜品是停售状态, 就不能起售
        }

        // 可以起售,更新套餐状态
        Setmeal setmeal = Setmeal.builder()
                .id(id)
                .status(status)
                .build();
        setmealMapper.updateById(setmeal);
    }

DishMapper

复制代码
    /**
     * 根据套餐id查询对应的菜品
     * @param id 套餐id
     * @return 菜品列表
     */
    @Select("select d.* from dish d " +
            "left join setmeal_dish sd on d.id = sd.dish_id " +
            "where sd.setmeal_id = #{id} ")
    List<Dish> getBySetmealId(Long id);

功能测试

相关推荐
间彧4 小时前
Java NPE异常详解
后端
无责任此方_修行中4 小时前
我的两次 Vibe Coding 经历,一次天堂,一次地狱
后端·node.js·vibecoding
想想就想想4 小时前
深度分页介绍及优化建议:从原理到实战的全链路解决方案
后端
程序员清风4 小时前
Dubbo RPCContext存储一些通用数据,这个用手动清除吗?
java·后端·面试
低音钢琴4 小时前
【SpringBoot从初学者到专家的成长15】MVC、Spring MVC与Spring Boot:理解其差异与联系
spring boot·spring·mvc
摇滚侠4 小时前
Spring Boot 3零基础教程,条件注解,笔记09
java·spring boot·笔记
一枚正在学习的小白4 小时前
PG数据文件位置迁移
linux·运维·服务器·数据库
南瓜小米粥、4 小时前
从可插拔拦截器出发:自定义、注入 Spring Boot、到生效路径的完整实践(Demo 版)
java·spring boot·后端
Huangmiemei9114 小时前
Spring Boot项目的常用依赖有哪些?
java·spring boot·后端