苍穹外卖-day04
课程内容
新增套餐套餐分页查询删除套餐修改套餐起售停售套餐
要求:
根据产品原型进行需求分析,分析出业务规则设计接口梳理表之间的关系(分类表、菜品表、套餐表、口味表、套餐菜品关系表)根据接口设计进行代码实现分别通过swagger接口文档和前后端联调进行功能测试
1. 新增套餐
1.1 需求分析和设计
产品原型:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/4916c405026f40c98121c4c8d6e8170c.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/596fb58d5a54470488ed89fcc9a1f007.png)
业务规则:
套餐名称唯一套餐必须属于某个分类套餐必须包含菜品名称、分类、价格、图片为必填项添加菜品窗口需要根据分类类型来展示菜品新增的套餐默认为停售状态
接口设计(共涉及到4个接口):
根据类型查询分类(已完成)根据分类id查询菜品图片上传(已完成)新增套餐
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/009545a73b024f95b0783629e4aaf14c.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/909d51d76bcd4813bfe7ec865a976462.png)
数据库设计:
setmeal表为套餐表,用于存储套餐的信息。具体表结构如下:
字段名数据类型说明备注idbigint主键自增namevarchar(32)套餐名称唯一category_idbigint分类id逻辑外键pricedecimal(10,2)套餐价格imagevarchar(255)图片路径descriptionvarchar(255)套餐描述statusint售卖状态1起售 0停售create_timedatetime创建时间update_timedatetime最后修改时间create_userbigint创建人idupdate_userbigint最后修改人id
setmeal_dish表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:
字段名数据类型说明备注idbigint主键自增setmeal_idbigint套餐id逻辑外键dish_idbigint菜品id逻辑外键namevarchar(32)菜品名称冗余字段pricedecimal(10,2)菜品单价冗余字段copiesint菜品份数
1.2 代码实现
1.2.1 DishController(根据分类id查询菜品)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/31d4672b08784f71802b6ab9abc69d84.png)
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result list(Long categoryId){
List list = dishService.list(categoryId);
return Result.success(list);
}
1.2.2 DishService
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
List list(Long categoryId);
1.2.3 DishServiceImpl(请求参数封装的不同情况)
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
public List list(Long categoryId) {
Dish dish = Dish.builder()
.categoryId(categoryId)
.status(StatusConstant.ENABLE)
.build();
return dishMapper.list(dish);
}
1.2.4 DishMapper
/**
* 动态条件查询菜品
* @param dish
* @return
*/
List list(Dish dish);
1.2.5 DishMapper.xml
select * from dish
and name like concat('%',#{name},'%')
and category_id = #{categoryId}
and status = #{status}
order by create_time desc
前后端联调:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/7a188f7f8b434d629c15eccf0ac2b9a5.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/66b2ea0581264eb5b5d8fd4c95394b18.png)
1.2.6 SetmealController(新增套餐)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/ef08a9cb3b3b404e8d50f1ddef012213.png)
/**
* 套餐管理
*/
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
/**
* 新增套餐
* @param setmealDTO
* @return
*/
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
}
1.2.7 SetmealService
public interface SetmealService {
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
void saveWithDish(SetmealDTO setmealDTO);
}
1.2.8 SetmealServiceImpl(新增业务逻辑 类似新增菜品逻辑)
/**
* 套餐业务实现
*/
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
@Transactional
@Override
public void saveWithDish(SetmealDTO setmealDTO) {
//请求参数用的是SetmealDTO类封装的,包含套餐数据以及套餐菜品关系表数据,
// 这个地方只需要插入套餐的基本信息,所以进行属性拷贝。
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
//向套餐表插入数据
setmealMapper.insert(setmeal);
//获取生成的套餐id 通过sql中的useGeneratedKeys="true" keyProperty="id"获取插入后生成的主键值
//套餐菜品关系表的setmealId页面不能传递,它是向套餐表插入数据之后生成的主键值,也就是套餐菜品关系表的逻辑外键setmealId
Long setmealId = setmeal.getId();
//获取页面传来的套餐和菜品关系表数据
List setmealDishes = setmealDTO.getSetmealDishes();
//遍历关系表数据,为关系表中的每一条数据(每一个对象)的setmealId赋值,
// 这个地方不需要像之前写新增菜品时多写个if判断,因为之前的口味数据是非必须的,
// 这个地方要求套餐必须包含菜品是必须的,所以不需要if判断,不存在套餐不包含菜品得情况
setmealDishes.forEach(setmealDish -> {
//将Setmeal套餐类的id属性赋值给SetmealDish套餐关系类的setmealId
//套餐表的id保存在套餐关系表充当外键为setmealId
setmealDish.setSetmealId(setmealId);
});
//保存套餐和菜品的关联关系 动态sql批量插入
setmealDishMapper.insertBatch(setmealDishes);
}
}
1.2.9 SetmealMapper
/**
* 新增套餐
* @param setmeal
*/
@AutoFill(OperationType.INSERT)
void insert(Setmeal setmeal);
1.2.10 SetmealMapper.xml
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})
1.2.11 SetmealDishMapper
/**
* 批量保存套餐和菜品的关联关系
* @param setmealDishes
*/
void insertBatch(List setmealDishes);
1.2.12 SetmealDishMapper.xml
insert into setmeal_dish
(setmeal_id,dish_id,name,price,copies)
values
(#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
1.3 功能测试
略,还没有写分页查询,所以暂时不进行前后端联调。
2. 套餐分页查询(字段和属性不一致问题)
2.1 需求分析和设计
产品原型:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/94b0eceb17154a8cba45e5350e23a798.png)
业务规则:
根据页码进行分页展示每页展示10条数据可以根据需要,按照套餐名称、分类、售卖状态进行查询
接口设计:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/00da1d42c7ee4dd080fcf67937409eb0.png)
2.2 代码实现
2.2.1 SetmealController
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/833288e9d35e433a9fa7eab807ef5e3a.png)
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分页查询")
public Result page(SetmealPageQueryDTO setmealPageQueryDTO) {
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}
2.2.2 SetmealService
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
2.2.3 SetmealServiceImpl
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
int pageNum = setmealPageQueryDTO.getPage();
int pageSize = setmealPageQueryDTO.getPageSize();
//需要在查询功能之前开启分页功能:当前页的页码 每页显示的条数
PageHelper.startPage(pageNum, pageSize);
//这个方法有返回值为Page对象,里面保存的是分页之后的相关数据
Page page = setmealMapper.pageQuery(setmealPageQueryDTO);
//封装到PageResult中:总记录数 当前页数据集合
return new PageResult(page.getTotal(), page.getResult());
}
2.2.4 SetmealMapper
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
Page pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
2.2.5 SetmealMapper.xml
问题:类似于菜品分页查询,在套餐分页查询中,套餐表setmeal保存的字段是category_id分类id,而接口文档要求返回的数据是分类名称。 解决:使用左外连接查询。查询套餐表以及套餐对应的分类名称。 注意:此时查询出套餐表中的字段 套餐名称为name,分类表中的字段分类名称也是name,那这样的话我们在封装数据的时候就会出现问题,通过mybatis框架去封装数据的时候由于这2个字段名相同,封装VO这个数据的时候就会对应错,分类表中的字段分类名称是name字段,SetmealVO是categoryName属性,字段名和属性名不一致所以封装不了数据。------------通过起字段别名方式解决c.name as categoryName 名称习惯使用模糊查询而不是等号。
select
s.*,c.name categoryName
from
setmeal s
left join
category c
on
s.category_id = c.id
and s.name like concat('%',#{name},'%')
and s.status = #{status}
and s.category_id = #{categoryId}
order by s.create_time desc
2.3 功能测试
测试:分页查询 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/23b533c3845d40fab67191b1fa69d0cf.png)
测试:新增套餐
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/a351a5208afb439595a17f6693370dc1.png)
3. 删除套餐(删除的业务逻辑)
3.1 需求分析和设计
产品原型:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/5703623d9619408cb2a6c361483bdfd6.png)
业务规则:
可以一次删除一个套餐,也可以批量删除套餐起售中的套餐不能删除
接口设计:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/b259031dec10492aa736009df632a2b7.png)
3.2 代码实现
3.2.1 SetmealController
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/86ea519235ed48a2ba0a1a38782f471d.png)
/**
* 批量删除套餐
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List ids){
setmealService.deleteBatch(ids);
return Result.success();
}
3.2.2 SetmealService
/**
* 批量删除套餐
* @param ids
*/
void deleteBatch(List ids);
3.2.3 SetmealServiceImpl
/**
* 批量删除套餐
* @param ids
*/
@Transactional
@Override
public void deleteBatch(List ids) {
//判断当前套餐是否能够删除---是否存在起售中的套餐??
//思路:遍历获取传入的id,根据id查询套餐setmeal中的status字段,0 停售 1 起售,
// 如果是1代表是起售状态不能删除
ids.forEach(id -> {
Setmeal setmeal = setmealMapper.getById(id);
if(StatusConstant.ENABLE == setmeal.getStatus()){
//起售中的套餐不能删除
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}
});
//思路:套餐表和菜品表是多对多关系,把整个套餐都删除了,那么关系表中保存的套餐对应
// 的菜品关系就没有意义了,所以此时也应该删除关系表中的数据。
ids.forEach(setmealId -> {
//删除套餐表中的数据
setmealMapper.deleteById(setmealId);
//删除套餐菜品关系表中的数据
setmealDishMapper.deleteBySetmealId(setmealId);
});
}
类似于删除菜品,删除套餐的sql存在缺点:
上述代码每一次for循环遍历都会发生2条sql(删除套餐、删除套餐菜品关系表数据),如果遍历的次数比较多,那么发出的sql数量也很多,可能会引发性能一些方面的问题。解决:减少sql语句的数量,使用动态sql批量删除,只需要1条sql就可以把需要删除的菜品删除掉,同样1条sql删除口味表的数据这里不在演示,详情查看day03-删除菜品。这里使用的是有缺点的方式做的删除。
3.2.4 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);
3.2.5 SetmealDishMapper
/**
* 根据套餐id删除套餐和菜品的关联关系
* @param setmealId
*/
@Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
void deleteBySetmealId(Long setmealId);
3.3 功能测试
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/f55a4ffe675e476599a6691e0753e8d0.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/bafdeed5f1d04df490ef1fd703fb4f7a.png)
4. 修改套餐(修改的业务逻辑)
4.1 需求分析和设计
产品原型:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/0712e452819e46fea81d4d47f8a8627a.png)
接口设计(共涉及到5个接口):
根据id查询套餐根据类型查询分类(已完成)根据分类id查询菜品(已完成)图片上传(已完成)修改套餐
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/78642e82458a46cab8ec2bec8c67989c.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/80d2883feda245a2a36075b34d37d31a.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/9431f9af186e42519202174ece3c2f18.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/31e04dee100d45c88b2692d217f6a029.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/ba6f3036fa6f41e8a7bee6aae802d0a2.png)
4.2 代码实现
4.2.1 SetmealController
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/77ee61f20da04369b9a27d7318574e25.png)
/**
* 根据id查询套餐,用于修改页面回显数据
*
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询套餐")
public Result getById(@PathVariable Long id) {
SetmealVO setmealVO = setmealService.getByIdWithDish(id);
return Result.success(setmealVO);
}
/**
* 修改套餐
*
* @param setmealDTO
* @return
*/
@PutMapping
@ApiOperation("修改套餐")
public Result update(@RequestBody SetmealDTO setmealDTO) {
setmealService.update(setmealDTO);
return Result.success();
}
4.2.2 SetmealService
/**
* 根据id查询套餐和关联的菜品数据
* @param id
* @return
*/
SetmealVO getByIdWithDish(Long id);
/**
* 修改套餐
* @param setmealDTO
*/
void update(SetmealDTO setmealDTO);
4.2.3 SetmealServiceImpl
/**
* 根据id查询套餐和套餐菜品关系
*
* @param id
* @return
*/
@Override
public SetmealVO getByIdWithDish(Long id) {
//根据id查询套餐表数据
Setmeal setmeal = setmealMapper.getById(id);//删除菜品时写过了
//根据id查询餐菜品关系表数据
List setmealDishes = setmealDishMapper.getBySetmealId(id);
//封装返回结果
SetmealVO setmealVO = new SetmealVO();
BeanUtils.copyProperties(setmeal, setmealVO);
setmealVO.setSetmealDishes(setmealDishes);
return setmealVO;
}
/**
* 修改套餐
*
* 思路分析:套餐表修改直接使用update语句即可,对于这个套餐菜品关系表,
* 套餐和菜品关系的修改比较复杂,因为它的情况有很多 有可能关系没有修改 有可能
* 关系是追加的 也有可能关系是删除了,那么这个地方我们有没有一种比较
* 简单的处理方式呢???
* 可以先把你当前这个套餐菜品关系数据全都统一删掉,然后在按照你当前
* 传过来的这个套餐菜品关系,重新再来插入一遍这个数据就可以了。
*
* @param setmealDTO
*/
@Transactional
public void update(SetmealDTO setmealDTO) {
//说明:SetmealDTO含有套餐菜品关系表数据,当前只是修改套餐的基本信息,所以直接传递SetmealDTO不合适,
// 可以把SetmealDTO的数据拷贝到套餐的基本信息类Setmeal中更合适。
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
//1、修改套餐表,执行update
setmealMapper.update(setmeal);
//获取生成的套餐id 通过sql中的useGeneratedKeys="true" keyProperty="id"获取插入后生成的主键值
//套餐菜品关系表的setmealId页面不能传递,它是向套餐表插入数据之后生成的主键值,也就是套餐菜品关系表的逻辑外键setmealId
Long setmealId = setmealDTO.getId();//新增套餐时的sql获取主键值
//2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
setmealDishMapper.deleteBySetmealId(setmealId); //删除套餐时已经实现了
//获取页面传来的套餐和菜品关系表数据
List setmealDishes = setmealDTO.getSetmealDishes();
//遍历关系表数据,为关系表中的每一条数据(每一个对象)的setmealId赋值,
// 这个地方不需要像之前写新增菜品时多写个if判断,因为之前的口味数据是非必须的,
// 这个地方要求套餐必须包含菜品是必须的,所以不需要if判断,不存在套餐不包含菜品得情况
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
// 动态sql批量插入
setmealDishMapper.insertBatch(setmealDishes);//新增套餐时已经实现了
}
4.2.4 SetmealMapper
/**
* 根据id查询套餐
* @param id
* @return
*/
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);
//修改套餐表,执行update
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);
4.2.5 SetmealMapper.xml
update setmeal
category_id = #{categoryId},
name = #{name},
price = #{price},
status = #{status},
description = #{description},
image = #{image},
update_time = #{updateTime},
update_user = #{updateUser},
where id = #{id}
4.2.6 SetmealDishMapper
/**
* 根据套餐id查询套餐和菜品的关联关系
* @param setmealId
* @return
*/
@Select("select * from setmeal_dish where setmeal_id = #{setmealId}")
List getBySetmealId(Long setmealId);
/**
* 根据套餐id删除套餐和菜品的关联关系
* @param setmealId
*/
@Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
void deleteBySetmealId(Long setmealId);
/**
* 批量保存套餐和菜品的关联关系
* @param setmealDishes
*/
void insertBatch(List setmealDishes);
4.2.7 SetmealDishMapper
insert into setmeal_dish
(setmeal_id,dish_id,name,price,copies)
values
(#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
4.3 功能测试
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d6171bbb55674ec7ae5aa21e662e6204.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/88903cfc77a54ab6b4353d6405993236.png)
5. 起售停售套餐(业务逻辑)
5.1 需求分析和设计
产品原型:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/184ab72a43b540d1b7b1904339c86064.png)
业务规则:
可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作起售的套餐可以展示在用户端,停售的套餐不能展示在用户端起售套餐时,如果套餐内包含停售的菜品,则不能起售
接口设计:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/3b7a1a0a20124a70866db59c7fdb58bf.png)
5.2 代码实现
5.2.1 SetmealController
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d8079e7b9325429ca6b60068104aef94.png)
/**
* 套餐起售停售
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
public Result startOrStop(@PathVariable Integer status, Long id) {
setmealService.startOrStop(status, id);
return Result.success();
}
5.2.2 SetmealService
/**
* 套餐起售、停售
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
5.2.3 SetmealServiceImpl
/**
* 套餐起售、停售
* @param status
* @param id
*/
@Override
public 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 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
}
5.2.4 DishMapper
/**
* 根据套餐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 getBySetmealId(Long setmealId);
5.2.5 SetmealMapper
//修改套餐表,执行update
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);
5.2.6 SetmealMapper.xml
update setmeal
category_id = #{categoryId},
name = #{name},
price = #{price},
status = #{status},
description = #{description},
image = #{image},
update_time = #{updateTime},
update_user = #{updateUser},
where id = #{id}
5.3 功能测试
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/fef3436e09854927a21bea76faaecf0b.png)
|