【苍穹外卖笔记】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);

功能测试

相关推荐
神奇小汤圆5 分钟前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生14 分钟前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling15 分钟前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅17 分钟前
springBoot项目有几个端口
java·spring boot·后端
Luke君6079718 分钟前
Spring Flux方法总结
后端
define952722 分钟前
高版本 MySQL 驱动的 DNS 陷阱
后端
清风拂山岗 明月照大江23 分钟前
Redis笔记汇总
java·redis·缓存
未来之窗软件服务25 分钟前
计算机等级考试—高频英语词汇—东方仙盟练气期
数据库·计算机软考·东方仙盟
lekami_兰28 分钟前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
xiaoxue..38 分钟前
合并两个升序链表 与 合并k个升序链表
java·javascript·数据结构·链表·面试