【 苍穹外卖day4 | 套餐管理接口实现 】

前言

套餐管理是后台里一组很典型的业务功能,包含新增套餐、套餐分页查询、删除套餐、修改套餐和起售停售。

这一组功能的实现重点不在单个接口本身,而在于两张表的数据要一起维护完整。套餐基本信息保存在 setmeal 表中,套餐和菜品的对应关系保存在 setmeal_dish 表中,所以这篇文章就顺着这条线,把整套功能实现串起来。

新增套餐

新增套餐的请求交给后台套餐控制器处理,请求方式是 POST,请求路径是 /admin/setmeal。

控制层代码如下:

java 复制代码
@PostMapping
@Operation(summary = "新增套餐接口")
public Result<String> save(@RequestBody SetmealDTO setmealDTO){
    log.info("新增套餐",setmealDTO);
    setmealService.save(setmealDTO);
    return Result.success();
}

这里接收的参数是 SetmealDTO。这个对象里不只有套餐名称、价格、分类这些基本字段,还包含套餐下的菜品集合,所以前端一次提交,就能把整套套餐信息传给后端。

真正的保存逻辑在 Service 层:

java 复制代码
@Override
public void save(SetmealDTO setmealDTO) {
    Setmeal setmeal = new Setmeal();
    BeanUtils.copyProperties(setmealDTO,setmeal);
    setmealMapper.insert(setmeal);

    List<SetmealDish> dishes = setmealDTO.getSetmealDishes();
    if (dishes !=null && dishes.size()>0){
        dishes.forEach(dish ->{
            dish.setSetmealId(setmeal.getId());
        });
        setmealMapper.insertSetmealDish(dishes);
    }
}

这里的执行顺序很清楚。

先把 DTO 中的属性拷贝到套餐实体对象里,再调用 mapper 保存套餐主表数据。主表插入成功以后,套餐 id 会自动回填。拿到这个 id 之后,再遍历套餐菜品集合,把每一条关系数据的 setmealId 补上,最后批量插入关系表。

对应的 SQL 也分成两部分:

xml 复制代码
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    insert into setmeal(name, category_id, price, status , description, image) values
    (#{name}, #{categoryId}, #{price}, #{status}, #{description}, #{image})
</insert>

<insert id="insertSetmealDish">
    insert into setmeal_dish(setmeal_id, dish_id, name, price, copies) values
    <foreach collection="dishes" item="dish" separator=",">
        (#{dish.setmealId}, #{dish.dishId}, #{dish.name}, #{dish.price}, #{dish.copies})
    </foreach>
</insert>

所以新增套餐这一块,实际做的是两件事:先存套餐,再存套餐和菜品的关系。

套餐分页查询

套餐分页查询使用 GET 请求,请求路径是 /admin/setmeal/page。

控制层代码如下:

java 复制代码
@GetMapping("/page")
@Operation(summary = "套餐分页查询")
public Result pagequery(SetmealPageQueryDTO setmealPageQueryDTO) {
    log.info("套餐分页查询");
    PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
    return Result.success(pageResult);
}

分页查询接收的是 SetmealPageQueryDTO,其中包含页码、每页条数、套餐名称、分类 id 和状态等条件。

Service 层代码如下:

java 复制代码
@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());
}

这里先调用分页插件设置分页参数,再执行 mapper 查询,最后把总记录数和当前页数据封装成 PageResult 返回。

分页 SQL 如下:

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

这里返回的不是套餐实体 Setmeal,而是 SetmealVO。原因是分页列表不仅要展示套餐本身的信息,还要显示分类名称,所以查询时把 category 表也关联进来了,直接把 categoryName 一起返回给前端。

这样前端在展示分页列表时,就不需要再单独补一次分类数据。

删除套餐

删除套餐使用 DELETE 请求,请求路径是 /admin/setmeal,而且支持批量删除。

控制层代码如下:

java 复制代码
@DeleteMapping
@Operation(summary = "套餐批量删除")
public Result delete(@RequestParam List<Long> ids){
    log.info("套餐批量删除");
    setmealService.delete(ids);
    return Result.success();
}

Service 层代码如下:

java 复制代码
@Override
@Transactional
public void delete(List<Long> ids) {
    setmealMapper.delete(ids);
    setmealMapper.deleteSetmealDish(ids);
}

这里加了事务,原因很直接。删除套餐时,删的不只是套餐主表数据,还要把套餐和菜品的关系数据一起删掉。如果只删除 setmeal 表中的记录,setmeal_dish 表里还会留下旧关系数据,数据库状态就不干净了。

对应 SQL 如下:

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

<delete id="deleteSetmealDish">
    delete from setmeal_dish where setmeal_id in
    <foreach collection="ids" open="(" close=")" separator="," item="id">
        #{id}
    </foreach>
</delete>

所以删除套餐时,主表和关系表必须一起处理,这样这组数据才算删干净。

修改套餐

修改套餐使用 PUT 请求,请求路径还是 /admin/setmeal。

控制层代码如下:

java 复制代码
@PutMapping
@Operation(summary = "更新套餐信息")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result update(@RequestBody SetmealDTO setmealDTO){
    log.info("更新套餐信息");
    setmealService.update(setmealDTO);
    return Result.success();
}

修改接口接收的还是 SetmealDTO,因为前端提交回来的不只是套餐本身的字段,还有重新选择后的菜品集合。

Service 层代码如下:

java 复制代码
@Override
public void update(SetmealDTO setmealDTO) {
    Setmeal setmeal = new Setmeal();
    BeanUtils.copyProperties(setmealDTO,setmeal);
    setmealMapper.update(setmeal);

    setmealDishMapper.deleteBySetmealId(setmeal.getId());

    List<SetmealDish> dishes = setmealDTO.getSetmealDishes();
    if (dishes !=null && dishes.size()>0){
        dishes.forEach(dish ->{
            dish.setSetmealId(setmeal.getId());
        });
        setmealMapper.insertSetmealDish(dishes);
    }
}

这里的处理流程可以拆成三步。

第一步,更新套餐主表信息。

第二步,根据套餐 id 删除原来的套餐菜品关系。

第三步,取出新的菜品集合,补全 setmealId,再重新插入关系表。

更新主表的 SQL 如下:

xml 复制代码
<update id="update">
    update setmeal set name=#{name}, category_id=#{categoryId}
   , price=#{price}, status=#{status} , description=#{description}, image=#{image} where id=#{id}
</update>

这种写法很适合一对多关系的编辑场景。因为套餐里包含的菜品可能已经整体变化了,这时候直接删除旧关系,再重新插入新关系,逻辑会更清晰。

起售停售套餐

起售停售套餐使用 POST 请求,请求路径是 /admin/setmeal/status/{status},套餐 id 通过请求参数传递。

控制层代码如下:

java 复制代码
@PostMapping("/status/{status}")
@Operation(summary = "启用禁用套餐")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result startOrStop(@PathVariable Integer status,@RequestParam Long id)
{
    log.info("启用禁用套餐: {},{}",status,id);
    setmealService.startOrStop(status,id);
    return Result.success();
}

Service 层代码如下:

java 复制代码
public void startOrStop(Integer status, Long id) {
    setmealMapper.startOrStop(status,id);
}

对应 SQL 如下:

xml 复制代码
<update id="startOrStop">
    update setmeal set status=#{status} where id=#{id}
</update>

这一块的业务很直接,就是更新套餐的状态字段。

状态字段的值也很明确,0 表示停售,1 表示起售。后台修改状态以后,前端在查询套餐时就会根据状态决定是否展示这条数据,所以这个接口虽然简单,但作用很明确。

查询套餐详情

修改套餐之前,前端通常会先根据套餐 id 查询详情,把页面中的表单数据回显出来。

控制层代码如下:

java 复制代码
@GetMapping("/{id}")
@Operation(summary = "根据id查询套餐信息和菜品信息")
public Result<SetmealVO> getById(@PathVariable Long id) {
    log.info("根据id查询套餐信息和菜品信息");
    SetmealVO setmealVO = setmealService.getByIdWithDish(id);
    return Result.success(setmealVO);
}

Mapper 中的查询 SQL 如下:

xml 复制代码
<select id="getByIdWithDish" resultMap="setmealWithDishMap">
    select s.id, s.category_id, s.name, s.price, s.status, s.description, s.image, s.update_time,
           sd.id as setmeal_dish_id, sd.setmeal_id, sd.dish_id,
           d.name as setmeal_dish_name, d.price as setmeal_dish_price, sd.copies
    from setmeal s
    left join setmeal_dish sd on s.id=sd.setmeal_id
    left join dish d on sd.dish_id=d.id
    where s.id=#{id}
</select>

这段查询把套餐表、套餐菜品关系表和菜品表一起查出来,再通过 resultMap 封装成 SetmealVO 返回。

这样前端进入编辑页面时,不只可以看到套餐名称、价格、图片这些基础信息,还能拿到套餐中已经关联好的菜品列表,直接完成回显。

总结

套餐管理这一组功能,整体实现思路是统一的。

新增套餐时,先保存套餐主表,再保存套餐和菜品关系。

分页查询时,根据条件查套餐列表,同时把分类名称一起带出来。

删除套餐时,套餐主表和关系表一起删除。

修改套餐时,先更新主表,再删除旧关系,最后重建新关系。

起售停售时,直接更新状态字段。

把这条链路理顺以后,套餐管理这部分代码就很清楚了,后面再接用户端套餐展示或者购物车里的套餐处理,也更容易串起来。