苍穹外卖学习记录04--套餐管理
本套餐模块实现新增、分页查询、删除、修改及起售停售功能。新增时通过事务管理同步操作套餐表与菜品关联表;分页查询关联分类表提升可读性;起售停售校验菜品状态保障规则;修改采用 “先删后增” 更新关联数据;删除前验状态防误操作。分层架构清晰,业务校验严格,确保数据一致与功能可靠。
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>标签的open和close属性导致生成了多余的括号,进而引发 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总结
功能:围绕套餐业务需求,完成了五大核心功能。新增套餐时通过事务管理(@Transactional)保证 setmeal 表与 setmeal_dish 关联表的数据一致性,同时补充菜品分类查询接口支撑前端菜品回显;分页查询通过 PageHelper 实现高效分页,并关联分类表获取分类名称,提升查询结果可读性;起售停售功能通过校验套餐内菜品状态,确保业务规则(含停售菜品的套餐不可起售);修改套餐采用 "先删后增" 策略更新关联表数据,保证数据准确性;删除套餐前校验状态(起售状态不可删除),并同步删除主表与关联表数据,避免数据残留。
业务规则保障:多处体现业务逻辑校验,如起售时检查套餐内菜品状态、删除时检查套餐是否在售,通过自定义异常(如 SetmealEnableFailedException、DeletionNotAllowedException)及时反馈错误信息,增强系统健壮性。
数据关联处理:针对套餐与菜品的多对多关系,在新增、修改、删除操作中均同步处理 setmeal_dish 表,通过批量插入、批量删除等方式优化数据库交互,同时利用 MyBatis 的动态 SQL 和 foreach 标签实现高效批量操作。
分层设计规范:严格遵循 Controller-Service-Mapper 分层架构,Controller 接收请求并返回结果,Service 处理业务逻辑与事务,Mapper 负责数据持久化,各层职责清晰;通过 DTO 传输数据、VO 封装响应结果,降低层间耦合,提升代码可维护性。
更多推荐
所有评论(0)