苍穹外卖资源点整理+个人错误解析-Day04-套餐模块
一 需求
业务功能
- 新增套餐
- 套餐分页查询
- 删除套餐
- 修改套餐
- 起售停售套餐
要求
1. 根据产品原型进行需求分析,分析出业务规则
2. 设计接口
3. 梳理表之间的关系(分类表、菜品表、套餐表、口味表、套餐菜品关系表)
4. 根据接口设计进行代码实现
5. 分别通过swagger接口文档和前后端联调进行功能测试
二 代码
1.新增套餐
1.产品原型


业务规则:
套餐名称唯一
套餐必须属于某个分类
套餐必须包含菜品
名称、分类、价格、图片为必填项
添加菜品窗口需要根据分类类型来展示菜品
新增的套餐默认为停售状态
接口设计(共涉及到4个接口):
根据类型查询分类(已完成)
根据分类id查询菜品
图片上传(已完成)
新增套餐


根据类型查询分类
这项功能已完成
代码位于categorycontroller层。根据类型,也就是数据库中的type字段进行查询,而type类型定义则是在category实体层:
//类型: 1菜品分类 2套餐分类 private Integer type;
controller层
/*
根据类型查询分类
*/
@GetMapping("/list")
@ApiOperation("根据类型查询分类")
public Result<List<Category>> list(Integer type){
List<Category> list = categoryService.list(type);
return Result.success(list);
}
xml层:
<select id="list" resultType="Category">select * from categorywhere status = 1<if test="type != null">and type = #{type}</if>order by sort asc,create_time desc
</select>根据分类id查询菜品
返回值是query类型。
首先分析查询逻辑可得, 我们想实现的效果大概是选择了一个分类就能显示出所有的菜品。
/*controller* 根据分类id查询菜品*/@GetMapping("/list")@ApiOperation("根据分类id查询菜品")public Result<List<Dish>> list(Long categoryId){List<Dish> list=dishService.list(categoryId);
return Result.success(list);}
@Override//serviceimpl
public List<Dish> list(Long categoryId) {Dish dish =Dish.builder().categoryId(categoryId).status(StatusConstant.ENABLE).build();//// 以上代码创建一个Dish对象,相当于以下的简写//Dish dish = new Dish();//dish.setCategoryId(categoryId);//dish.setStatus(StatusConstant.ENABLE);return dishMapper.list(dish);
}
//mapper.mxl
<select id="list" resultType="com.sky.entity.Dish">select * from dish<where><if test="name != null">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>实现效果:

新增套餐
如果想要新增套餐,需要新增的不只是单独的一个套餐,还要保证套餐与菜品的关系。所以需要写dishmapper以及setmealmapper,并且数据库中还有一个关联表setmealdish,所以:

由于要对多个表进行操作,我们得加上注解@Transactional在方法上以保证在对多个表进行数据操作时保证数据一致性,以下是impl层代码
@Transactionalpublic void saveWithDish(SetmealDTO setmealDTO) {Setmeal setmeal = new Setmeal();BeanUtils.copyProperties(setmealDTO,setmeal);//插入数据setmealMapper.insert(setmeal);//xml
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">insert into setmeal(category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},#{createUser}, #{updateUser})
</insert>插入数据后,会生成一个套餐id,我们得获取这个id并将这个id设置到detmealDish表中每个套餐菜品对象中,并且注意一个条件
//setmealdto中有套餐菜品关系 // private List<SetmealDish> setmealDishes = new ArrayList<>();
实现层代码:
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
//为每个 SetmealDish 对象设置 setmealId 字段,建立与 Setmeal 主表的关联关系
//将前端传入的 SetmealDTO 中的套餐菜品列表(setmealDishes)与刚创建的套餐记录进行关联setmealDishes.forEach(setmealDish -> {setmealDish.setSetmealId(setmealId);});//保存套餐与菜品的关联关系setmealDishMapper.insertBatch(setmealDishes);}//xml
<!-- 批量保存套餐与菜品的关系--><insert id="insertBatch" parameterType="list">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>2.套餐分页查询
1.分析
产品原型:

业务规则:
- 根据页码进行分页展示
- 每页展示10条数据
- 可以根据需要,按照套餐名称、分类、售卖状态进行查询
以下是接口信息

封装成为Result<PageResult>,因为返回数据有这么多,code,msg,data是result对象,而data里的数据是pageresult里面的records对象。
2.代码
//分页查询controller@GetMapping("/page")@ApiOperation("分页查询")public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO){log.info("分页查询:{}",setmealPageQueryDTO);PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);return Result.success(pageResult);}
}//impl
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {PageHelper.startPage(setmealPageQueryDTO.getPage(),setmealPageQueryDTO.getPageSize());Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);return new PageResult(page.getTotal(), page.getResult());
}
<select id="pageQuery" resultType="com.sky.vo.SetmealVO"> selects.*,c.name categoryNamefromsetmeal sleft joincategory cons.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>3.删除套餐
1.分析

业务规则:
可以一次删除一个套餐,也可以批量删除套餐
起售中的套餐不能删除

2.代码
//删除controller
@DeleteMapping
@ApiOperation("批量删除")
public Result delete(@RequestParam List<Long> ids){setmealService.deleteBatch(ids);return Result.success();//serviceimpl
@Transactionalpublic void deleteBatch(List<Long> ids) {//起售中的套餐不能删除for (Long id : ids) {Setmeal setmeal= setmealMapper.getById(id);if (setmeal.getStatus()==StatusConstant.ENABLE){throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);}}//删除套餐表中数据for (Long id : ids) {setmealMapper.deleteById(id);setmealDishMapper.deleteBySetmealId(id);}}
//SetmealMapper
/*** 根据id查询套餐* @param id* @return*/
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);
/*** 根据id删除套餐* @param setmealId*/
@Delete("delete from setmeal where id = #{id}")
void deleteById(Long setmealId);
//SetmealDishMapper
/** 根据套餐id删除套餐和菜品的关联关系* @param setmealId*/
@Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
void deleteBySetmealId(Long setmealId);4.修改套餐
1.分析

接口设计(共涉及到5个接口):
根据id查询套餐
根据类型查询分类(已完成)
根据分类id查询菜品(已完成)
图片上传(已完成)
修改套餐


为什么修改会有一个根据id查询菜品?
因为没有这个接口的话,进入修改界面信息是无法显示的,如图所示:



2.代码
根据id查询
由于需要对前端进行数据的回显,进行套餐查询时,也需要将菜品回显。使用setmealVO。
controller:
//根据id查询套餐@GetMapping("/{id}")@ApiOperation("根据id查询套餐")public Result<SetmealVO> getById(@PathVariable Long id){SetmealVO setmealVO = setmealService.getByIdWithDish(id);return Result.success(setmealVO);}impl:
写出方法后需要明确,我们进行根据id查询套餐,其实需要查询套餐以及套餐里的菜品。
所以现根据套餐id查询套餐,这一步的方法在之前已经存在getById,可以直接使用。并封装在一个setmeal对象中(因为查询的就是setseal数据)
随后需要根据套餐id查询相关联的菜品id,关联表为setmeal_Dish,在 setmealDishmapper进行查询,根据套餐id进行查询,并由于一个套餐可以关联多个菜品,因此为集合对象,并为setmealdish
最后为了让前端显示,需要将数据传入vo格式对象中,使用属性拷贝,然后由于setmealVO中有
//套餐和菜品的关联关系 private List<SetmealDish> setmealDishes = new ArrayList<>();
这一行属性,直接使用set进行赋值。
//根据id查询套餐@Overridepublic SetmealVO getByIdWithDish(Long id) {//根据id查询套餐和关联的菜品数据//查询套餐idSetmeal setmeal = setmealMapper.getById(id);//查询关联的菜品List<SetmealDish> setmealDishes= setmealDishMapper.getBySetmealId(id);SetmealVO setmealVO = new SetmealVO();BeanUtils.copyProperties(setmeal,setmealVO);setmealVO.setSetmealDishes(setmealDishes);return setmealVO;}
//setmealdishmapper:
//根据餐id查询菜品@Select("select * from setmeal_dish where setmeal_id = #{setmealId}")List<SetmealDish> getBySetmealId(Long id);修改套餐
controller:
//修改套餐
@PutMapping
@ApiOperation("修改套餐")
public Result update(@RequestBody SetmealDTO setmealDTO){
log.info("修改套餐:{}",setmealDTO);
setmealService.update(setmealDTO);
return Result.success();
}impl:需要拆分步骤,首先第一步是修改套餐,需要注意的是使用的为setmealdto,然后给setmeal对象赋值,因为不需要dto里面的菜品集合。
修改完套餐之后需要考虑菜品问题,此处使用的是day03同款方法,即先将菜品统一删除在重新插入。
但是感觉可以自己删除想删除的菜品id并新增或者不新增。
@Override
@Transactional
public void update(SetmealDTO setmealDTO) {Setmeal setmeal = new Setmeal();BeanUtils.copyProperties(setmealDTO,setmeal);//修改套餐setmealMapper.update(setmeal);//针对套餐里的菜品,删除套餐菜品关联关系Long id = setmeal.getId();setmealDishMapper.deleteBySetmealId(id);//重新插入关联关系List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();for (SetmealDish setmealDish : setmealDishes) {setmealDishMapper.insertBatch(setmealDishes);}}mapper.xml:
<!-- 修改--><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="description != null">description = #{description},</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.起售停售套餐
1.设计
产品原型:

业务规则:
- 可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作
- 起售的套餐可以展示在用户端,停售的套餐不能展示在用户端
- 起售套餐时,如果套餐内包含停售的菜品,则不能起售
2.代码
//启用禁用套餐@PostMapping("/status/{status}")@ApiOperation("套餐启用禁用")public Result startOrStop(@PathVariable Integer status,Long id){
log.info("启用禁用:{}{}",status,id);
setmealService.startOrStop(status,id);
return Result.success();}@Overridepublic void startOrStop(Integer status, Long id) {//起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"if(status.equals(StatusConstant.ENABLE)){ //1 启用//select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?//左外连接查询,根据套餐id查询菜品以及对应的菜品套餐关系数据,a.*所以返回所有菜品数据List<Dish> dishList = dishMapper.getBySetmealId(id);if(dishList != null && dishList.size() > 0){//判断套餐中是否包含的有菜品,有才走if判断dishList.forEach(dish -> {//套餐中包含菜品,如果这个菜品的状态为禁用,则抛出异常if(StatusConstant.DISABLE.equals(dish.getStatus())){throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);}});}}//执行流程: 如果是起售套餐,套餐内有停售菜品,则抛出异常 不能起售// 如果是起售套餐,套餐内没有停售菜品,if执行完后跳出继续向下执行,执行更新// 如果是停售套餐,不走上面的if,直接进行更新状态。Setmeal setmeal = Setmeal.builder().id(id).status(status).build();setmealMapper.update(setmeal);//修改套餐时写了通用的修改sql}
}
