1.明确需求

需要完成的业务包括:

1.新增套餐

2.套餐分页查询

3.删除套餐

4.修改套餐

5.起售、停售套餐

2.新增套餐

2.1 分析

添加套餐时需要将套餐中所包含的菜品也要加入到setmeal_dish(套餐-菜品关系表)中。

需要操作两个表(setmeal和setmeal_dish),注意在serivice层的实现类上添加@Transactional注解。

添加响应的Setmeal的controller、service、mapper

2.1.1 补充(根据分类id查询菜品)

在分析过程中,我发现菜品模块缺少了一个接口--通过分类id查询菜品,这个接口用于在添加套餐时,点击添加菜品时的菜品数据回显。

    /**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("通过分类id查询菜品")
    public Result getByCategoryId(Long categoryId){
        log.info("查询分类id:{}",categoryId);
        List<Dish> dishes = dishService.getByCategoryId(categoryId);
        return Result.success(dishes);
    }

注意这里传入的参数时categoryId,如果不是这个,这里的参数会接受不到。

    /**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    @Override
    public List<Dish> getByCategoryId(Long categoryId) {
        List<Dish> dishes = dishMapper.selectByCategoryId(categoryId);
        return dishes;
    }
    /**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    @Select("select  * from dish where category_id = #{categoryId}")
    List<Dish> selectByCategoryId(Long categoryId);

结果图:

2.2  新增套餐

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
@Slf4j
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关操作")
public class SetmealController {

    @Autowired
    SetmealService setmealService;

    @PostMapping
    @ApiOperation("新增套餐")
    public Result addSetmeal(@RequestBody SetmealDTO setmealDTO){
        log.info("新增套餐数据:{}",setmealDTO);
        setmealService.addSetmeal(setmealDTO);
        return Result.success();
    }

}
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 org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
public class SetmealServiceImpl implements SetmealService {
    @Autowired
    SetmealMapper setmealMapper;
    @Autowired
    SetMealDishMapper setMealDishMapper;
    /**
     * 新增套餐
     * @param setmealDTO
     */
    @Override
    @Transactional
    public void addSetmeal(SetmealDTO setmealDTO) {
        //取出DTO中的数据
        Setmeal setmeal = new Setmeal();//套餐数据
        BeanUtils.copyProperties(setmealDTO,setmeal);
        setmealMapper.addSetmeal(setmeal); //调用Mapper层传入套餐数据
        //需要将套餐id回显,用于插入套餐-菜品关系表
        Long setmealId = setmeal.getId();
        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes(); //菜品数据
        //调用Mapper层传入套餐-菜品关系数据
        if(setmealDishes!=null && setmealDishes.size()>0){
            //利用循环将套餐id赋值给setmealDish
            setmealDishes.forEach(setmealDish -> {
                setmealDish.setSetmealId(setmealId);
            });
            //批量插入数据
            setMealDishMapper.insertBatch(setmealDishes);
        }

    }
}
    /**
     * 新增套餐
     * @param setmeal
     */
    @AutoFill(OperationType.INSERT)
    void addSetmeal(Setmeal setmeal);

 

<?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="addSetmeal" 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>
    /**
     * 批量添加套餐菜品数据
     * @param setmealDishes
     */
    void insertBatch(List<SetmealDish> setmealDishes);

 

    <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>

编写代码时我出现的问题以及解决过程:

问题一: 

nested exception is org.apache.ibatis.binding.BindingException: Parameter 'setmealId' not found. Available parameters are [collection, list, setmealDishes]] with root cause

 找不到匹配参数,原因

    <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>

我将” (#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})“这句代码写成了

这样: (#{setmealId},#{dishId},#{name},#{price},#{copies})

导致无法找到参数。

 问题二:

<foreach>标签的openclose属性导致生成了多余的括号,进而引发 SQL 语法错误。

解决办法:去掉open和close标签。

 3.套餐分页查询

    @GetMapping("/page")
    @ApiOperation("套餐分页查询")
    public Result<PageResult> pageQuerySetmeal(SetmealPageQueryDTO setmealPageQueryDTO){
        log.info("套餐分页查询数据:{}",setmealPageQueryDTO);
        PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
        return Result.success(pageResult);
    }
    /**
     * 套餐分页查询
     * @param setmealPageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
        PageHelper.startPage(setmealPageQueryDTO.getPage(),setmealPageQueryDTO.getPageSize());
        Page<SetmealVO> page =  setmealMapper.pageQuery(setmealPageQueryDTO);
        long total = page.getTotal();
        List<SetmealVO>result = page.getResult();
        return new PageResult(total,result);
    }
    <select id="pageQuery" resultType="com.sky.vo.SetmealVO">
        select
            s.*,c.name categoryName
        from
            setmeal s
        left join
            category c
        on
            s.category_id = c.id
        <where>
            <if test="name != null">
                and 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>
        order by s.create_time desc
    </select>

4.套餐起售停售

    @PostMapping("/status/{status}")
    @ApiOperation("套餐起售停售")
    public Result updateStatus(@PathVariable Integer status,Long id){
        log.info("套餐状态:{}",status);
        setmealService.updateStatus(status,id);
        return Result.success();
    }

当套餐中有菜品在停售状态时,改套餐不能起售。

首先要获取该套餐对应的菜品,然后判断各个菜品当前状态。


    /**
     * 套餐起售停售
     * @param status
     * @param id
     */
    @Override
    public void updateStatus(Integer status, Long id) {
        //起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
        if(status == StatusConstant.ENABLE){
            List<Dish> dishList = dishMapper.getBySetmealId(id);
            if(dishList != null && dishList.size() > 0){
                dishList.forEach(dish -> {
                    if(StatusConstant.DISABLE == dish.getStatus()){
                        throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
                    }
                });
            }
        }
        Setmeal setmeal = Setmeal.builder()
                .id(id)
                .status(status)
                .build();
        setmealMapper.update(setmeal);
    }

获取该套餐对应菜品的mapper层。 

    /**
     * 通过套餐id查询菜品
     * @param setmealId
     * @return
     */
    @Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
    List<Dish> getBySetmealId(Long setmealId);
    <update id="update">
        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="image!=null">image = #{image},</if>
            <if test="updateTime!=null">update_time = #{updateTime},</if>
            <if test="updateUser!=null">update_user = #{updateUser},</if>
        </set>
        where id = #{id}
    </update>

5 修改套餐

在修改套餐之前需要先查询一下当前套餐对应的数据,用于前端页面回显

    /**
     * 根据id查询套餐用于修改页面的数据回显
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Result<SetmealVO>selectSetmealById(@PathVariable Long id){
        SetmealVO setmealVO = setmealService.selectById(id);
        return Result.success(setmealVO);
    }

    @PutMapping
    @Transactional
    @ApiOperation("修改套餐")
    public Result updateSetmeal(@RequestBody SetmealDTO setmealDTO){
        log.info("修改套餐数据:{}",setmealDTO);
        setmealService.updateSetmeal(setmealDTO);
        return Result.success();
    }
    /**
     * 根据id查询套餐
     *
     * @param id
     * @return
     */
    @Override
    public SetmealVO selectById(Long id) {
        //获取setmeal
        Setmeal setmeal = setmealMapper.selectByid(id);
        //获取对应菜品
        List<SetmealDish> setmealDishes = setMealDishMapper.selectDishBySetmealId(id);
        //封装成一个VO
        SetmealVO setmealVO = new SetmealVO();
        BeanUtils.copyProperties(setmeal,setmealVO);
        BeanUtils.copyProperties(setmealDishes,setmealVO);
        return setmealVO;
    }

    @Override
    public void updateSetmeal(SetmealDTO setmealDTO) {
        //属性复制
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO,setmeal);
        setmealMapper.update(setmeal);
        //获取套餐id
        Long setmealID = setmealDTO.getId();
        //先删除套餐对应的菜品
        setMealDishMapper.deleteBySetmealId(setmealID);
        //将套餐id赋值给菜品
        List<SetmealDish>setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealID);
        });
        //在套餐菜品关系表中插入数据。
        setMealDishMapper.insertBatch(setmealDishes);
    }
    /**
     * 根据套餐id查询菜品与套餐的关系
     * @param setmealDishId
     * @return
     */
    @Select("select * from setmeal_dish where setmeal_id = #{setmealDishId}")
    List<SetmealDish> selectDishBySetmealId(Long setmealDishId);

6 删除套餐

删除套餐这个操作需要对两张表进行操作--setmeal,setmeal_dish

注意当套餐处于起售状态时无法删除

    @DeleteMapping
    @ApiOperation("批量删除套餐")
    public Result deleteBatch(@RequestParam List<Long> ids){
        log.info("删除套餐的id:{}",ids);
        setmealService.deleteBatch(ids);
        return Result.success();
    }
    @Transactional
    @Override
    public void deleteBatch(List<Long> ids) {
        //通过id获取套餐,然后判断各个套餐是否处于起售状态--起售状态不能删除
        ids.forEach(id->{
            Setmeal setmeal = setmealMapper.selectByid(id);
            if(StatusConstant.ENABLE == setmeal.getStatus()){
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }
        });
        //删除套餐表中数据和套餐菜品关系表中的数据
        ids.forEach(id->{
            setmealMapper.deleteById(id);
            setMealDishMapper.deleteBySetmealId(id);
        });
    }
    /**
     * 根据id删除套餐
     * @param id
     */
    @Delete("delete from setmeal where id = #{id}")
    void deleteById(Long id);

7总结

  1. 功能:围绕套餐业务需求,完成了五大核心功能。新增套餐时通过事务管理(@Transactional)保证 setmeal 表与 setmeal_dish 关联表的数据一致性,同时补充菜品分类查询接口支撑前端菜品回显;分页查询通过 PageHelper 实现高效分页,并关联分类表获取分类名称,提升查询结果可读性;起售停售功能通过校验套餐内菜品状态,确保业务规则(含停售菜品的套餐不可起售);修改套餐采用 "先删后增" 策略更新关联表数据,保证数据准确性;删除套餐前校验状态(起售状态不可删除),并同步删除主表与关联表数据,避免数据残留。

  2. 业务规则保障:多处体现业务逻辑校验,如起售时检查套餐内菜品状态、删除时检查套餐是否在售,通过自定义异常(如 SetmealEnableFailedException、DeletionNotAllowedException)及时反馈错误信息,增强系统健壮性。

  3. 数据关联处理:针对套餐与菜品的多对多关系,在新增、修改、删除操作中均同步处理 setmeal_dish 表,通过批量插入、批量删除等方式优化数据库交互,同时利用 MyBatis 的动态 SQL 和 foreach 标签实现高效批量操作。

  4. 分层设计规范:严格遵循 Controller-Service-Mapper 分层架构,Controller 接收请求并返回结果,Service 处理业务逻辑与事务,Mapper 负责数据持久化,各层职责清晰;通过 DTO 传输数据、VO 封装响应结果,降低层间耦合,提升代码可维护性。

Logo

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

更多推荐