前言
套餐管理是后台里一组很典型的业务功能,包含新增套餐、套餐分页查询、删除套餐、修改套餐和起售停售。
这一组功能的实现重点不在单个接口本身,而在于两张表的数据要一起维护完整。套餐基本信息保存在 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 返回。
这样前端进入编辑页面时,不只可以看到套餐名称、价格、图片这些基础信息,还能拿到套餐中已经关联好的菜品列表,直接完成回显。
总结
套餐管理这一组功能,整体实现思路是统一的。
新增套餐时,先保存套餐主表,再保存套餐和菜品关系。
分页查询时,根据条件查套餐列表,同时把分类名称一起带出来。
删除套餐时,套餐主表和关系表一起删除。
修改套餐时,先更新主表,再删除旧关系,最后重建新关系。
起售停售时,直接更新状态字段。
把这条链路理顺以后,套餐管理这部分代码就很清楚了,后面再接用户端套餐展示或者购物车里的套餐处理,也更容易串起来。