Sky-takeout Day04
【前言】
Day04 的 Task 是套餐管理相关代码的开发 ,也是对多表业务的分析能力的锻炼
Contents
-
新增套餐
-
套餐分页查询
-
删除套餐
-
修改套餐
-
起售停售套餐
1. 新增套餐
需求分析与接口设计
业务规则:
-
套餐名称唯一
-
套餐必须属于某个分类
-
套餐必须包含菜品
-
名称、分类、价格、图片为必填项
-
添加菜品窗口需要根据分类类型来展示菜品
-
新增的套餐默认为停售状态
接口设计(共涉及到 4 个接口):
-
根据类型查询分类(已完成)
-
根据分类 id 查询菜品
-
图片上传(已完成)
-
新增套餐
产品原型:


可以看到,在新增套餐中还涉及到了菜品的一个接口,就是 根据分类 id 查询菜品并列出菜品,所以需要在 DishController 再加一个接口 来根据分类 id 展示菜品(list)
接口设计:


表设计:
setmeal 表为套餐表,用于存储套餐的信息。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 套餐名称 | 唯一 |
category_id | bigint | 分类 id | 逻辑外键 |
price | decimal(10,2) | 套餐价格 | |
image | varchar(255) | 图片路径 | |
description | varchar(255) | 套餐描述 | |
status | int | 售卖状态 | 1 起售 0 停售 |
create_time | datetime | 创建时间 | |
update_time | datetime | 最后修改时间 | |
create_user | bigint | 创建人 id | |
update_user | bigint | 最后修改人 id |
setmeal_dish 表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
setmeal_id | bigint | 套餐 id | 逻辑外键 |
dish_id | bigint | 菜品 id | 逻辑外键 |
name | varchar(32) | 菜品名称 | 冗余字段 |
price | decimal(10,2) | 菜品单价 | 冗余字段 |
copies | int | 菜品份数 |
代码编写
1)根据分类 id 查询并列出菜品
DishController.java
/**
* 根据分类条件查询列出菜品
* @param categoryId 分类id
* @return 菜品列表
*/
@GetMapping("/list")
@ApiOperation("根据分类条件查询并列出菜品")
public Result<List<Dish>> list(Long categoryId) {
log.info("根据分类条件查询并列出菜品: {}", categoryId);
List<Dish> dishList = dishService.list(categoryId);
return Result.success(dishList);
}
DishServiceImpl.java
这里注意 status 状态是 ENABLE,一开始写成 DISABLE 了,没有菜品列出,后面发现是状态设置错了。
/**
* 根据分类条件查询列出菜品
* @param categoryId 分类id
* @return 菜品列表
*/
@Override
public List<Dish> list(Long categoryId) {
Dish dish = Dish.builder()
.status(StatusConstant.ENABLE) // 注意这里是只查询起售状态的菜品
.categoryId(categoryId)
.build();
return dishMapper.list(dish);
}
DishMapper.java
/**
* 动态条件查询菜品
* @param dish 条件
* @return 菜品列表
*/
List<Dish> list(Dish dish);
DishMapper.xml
<select id="list" resultType="com.sky.entity.Dish">
select * from dish
<where>
<if test="name != null and name != ''">
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>
2)新增套餐
SetmealController.java
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
@Api(tags = "套餐管理")
@RequestMapping("admin/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
/**
* 新增套餐
* @param setmealDTO 套餐信息
* @return 结果
*/
@PostMapping
@ApiOperation("新增套餐")
public Result<Void> save(@RequestBody SetmealDTO setmealDTO) { // 这里要加 @RequestBody 将前端传递的json数据转换为java对象
log.info("新增套餐: {}", setmealDTO);
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
}
SetmealServiceImpl.java
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 java.util.List;
@Service
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
/**
* 新增套餐
* @param setmealDTO 套餐信息
*/
@Override
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
// 1. 向setmeal表插入套餐
setmealMapper.insert(setmeal);
// 2. 向setmeal_dish表插入套餐和菜品的关联关系
// 先获取套餐id
Long setmealId = setmeal.getId();
// 再获取套餐和菜品的关联关系
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
// 设置套餐id
setmealDishes.forEach(setmealDish -> setmealDish.setSetmealId(setmealId));
// 批量插入套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
}
SetmealMapper.java
/**
* 插入套餐, 并且将主键回填到实体类对象中 (useGeneratedKeys = true, keyProperty = "id")
* @param setmeal 套餐
*/
@AutoFill(value = OperationType.INSERT)
void insert(Setmeal setmeal);
SetmealMapper.xml
<?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="insert" 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>
SetmealDishMapper.java
/**
* 批量插入套餐和菜品的关联关系
* @param setmealDishes 套餐和菜品的关联关系
*/
void insertBatch(List<SetmealDish> setmealDishes);
SetmealDishMapper.xml
<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>
功能测试

成功在表中插入数据

- 注意 这里有误!!后来在写删除套餐的时候才发现,插入时 status 不应该是 1,之前编写新增套餐代码的时候在 mapper 的 xml 映射文件遗漏了 status 字段,而且在 service 层忘记设置了初始状态为停售 DISABLE(也就是 0)

2. 套餐分页查询
需求分析与接口设计
业务规则:
-
根据页码进行分页展示
-
每页展示 10 条数据
-
可以根据需要,按照套餐名称、分类、售卖状态进行查询
产品原型

注意因为视图需要套餐对应的分类名称,所以分页时 Page 的泛型是 VO 对象,即 SetmealVO。

接口设计:

代码编写
SetmealController.java
/**
* 分页查询套餐
* @param setmealPageQueryDTO 分页查询参数
* @return 套餐分页结果
*/
@GetMapping("/page")
@ApiOperation("分页查询套餐")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
log.info("分页查询套餐: {}", setmealPageQueryDTO);
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}
SetmealServiceImpl.java
/**
* 分页查询套餐
* @param setmealPageQueryDTO 分页查询参数
* @return 套餐分页结果
*/
@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());
}
SetmealMapper.java
/**
* 分页查询套餐
* @param setmealPageQueryDTO 分页查询参数
* @return 套餐分页结果
*/
Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
SetmealMapper.xml
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select s.*, c.name as categoryName
from setmeal s left outer join category c
on s.category_id = c.id
<where>
<if test="name != null">
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>
</select>
功能测试


3. 删除套餐
需求分析与接口设计
业务规则:
-
可以一次删除一个套餐,也可以批量删除套餐
-
起售中的套餐不能删除
接口设计:

代码编写
SetmealController.java
/**
* 删除套餐
* @param ids 套餐id集合
* @return 结果
*/
@DeleteMapping
@ApiOperation("删除套餐")
public Result<Void> delete(@RequestParam("ids") List<Long> ids) {
log.info("删除套餐: {}", ids);
setmealService.deleteWithDish(ids);
return Result.success();
}
SetmealServiceImpl.java
/**
* 删除套餐, 同时删除套餐和菜品的关联数据
* @param ids 套餐id集合
*/
@Override
public void deleteWithDish(List<Long> ids) {
// 起售中的套餐不能删除
// 1. 查询套餐状态, 确认是否可以删除
for (Long id : ids) {
Setmeal setmeal = setmealMapper.getById(id);
if (setmeal != null && Objects.equals(setmeal.getStatus(), StatusConstant.ENABLE)) { // 套餐起售中
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}
}
// 2. 删除套餐表中的数据
// delete from setmeal where id in (1,2,3)
setmealMapper.deleteByIds(ids);
// 3. 删除套餐和菜品的关联数据
// delete from setmeal_dish where setmeal_id in (1,2,3)
setmealDishMapper.deleteBySetmealIds(ids);
}
SetmealMapper.java
/**
* 根据id查询套餐 (用于删除套餐时, 查询套餐状态)
* @param id 套餐id
*/
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);
/**
* 根据id删除套餐
* @param ids 套餐id集合
*/
void deleteByIds(List<Long> ids);
SetmealMapper.xml
<delete id="deleteByIds" parameterType="list">
delete from setmeal where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
SetmealDishMapper.java
/**
* 根据套餐id删除对应的套餐和菜品的关联关系
* @param setmealIds 套餐id集合
*/
void deleteBySetmealIds(List<Long> setmealIds);
SetmealDishMapper.xml
<delete id="deleteBySetmealIds">
delete from setmeal_dish where setmeal_id in
<foreach collection="setmealIds" item="setmealId" open="(" separator="," close=")">
#{setmealId}
</foreach>
</delete>
功能测试
起售中无法删除

删除成功


4. 修改套餐
需求分析与接口设计
产品原型:

接口设计(共涉及到5个接口):
-
根据id查询套餐
-
根据类型查询分类(已完成)
-
根据分类id查询菜品(已完成)
-
图片上传(已完成)
-
修改套餐
代码编写
SetmealController.java
/**
* 根据id查询套餐信息, 包括套餐和菜品的关联信息
* @param id 套餐id
* @return 套餐信息
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询套餐信息(包括套餐和菜品的关联信息)")
public Result<SetmealVO> getById(@PathVariable Long id) { // 这里要加 @PathVariable 将路径参数中的id参数绑定到方法的id参数上
log.info("根据id查询套餐信息: {}", id);
SetmealVO setmealVO = setmealService.getByIdWithDishes(id);
return Result.success(setmealVO);
}
/**
* 修改套餐(包括套餐和菜品的关联信息)
* @param setmealDTO 套餐信息
* @return 结果
*/
@PutMapping
@ApiOperation("修改套餐")
public Result<Void> update(@RequestBody SetmealDTO setmealDTO) { // 这里要加 @RequestBody 将前端传递的json数据转换为java对象
log.info("修改套餐: {}", setmealDTO);
setmealService.updateWithDish(setmealDTO);
return Result.success();
}
SetmealServiceImpl.java
/**
* 根据id查询套餐信息, 包括套餐和菜品的关联信息
* @param id 套餐id
* @return 套餐信息
*/
@Override
public SetmealVO getByIdWithDishes(Long id) {
// 1. 查询套餐基本信息
Setmeal setmeal = setmealMapper.getById(id);
// 2. 查询套餐和菜品的关联信息
List<SetmealDish> setmealDishes = setmealDishMapper.getByIdWithDishes(id);
// 3. 将套餐基本信息与套餐菜品信息封装到VO对象中并返回
SetmealVO setmealVO = new SetmealVO();
BeanUtils.copyProperties(setmeal, setmealVO);
setmealVO.setSetmealDishes(setmealDishes);
return setmealVO;
}
/**
* 修改套餐(包括套餐和菜品的关联信息)
* @param setmealDTO 套餐信息
*/
@Override
public void updateWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
// 1. 首先,更新setmeal表中的基本信息
setmealMapper.updateById(setmeal);
// 2. 然后,更新setmeal_dish表中的关联信息
Long setmealId = setmeal.getId();
// 2.1 先删除原有的关联信息
setmealDishMapper.deleteBySetmealIds(List.of(setmealId));
// 2.2 再重新插入新的关联信息
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
// 设置套餐id
setmealDishes.forEach(setmealDish -> setmealDish.setSetmealId(setmealId));
// 批量插入套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
SetmealMapper.java
/**
* 根据id动态修改套餐信息
* @param setmeal 套餐信息
*/
@AutoFill(value = OperationType.UPDATE)
void updateById(Setmeal setmeal);
SetmealMapper.xml
<update id="updateById">
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>
update_time = #{updateTime},
update_user = #{updateUser}
</set>
where id = #{id}
</update>
SetmealDishMapper.java
/**
* 根据套餐id查询对应的套餐和菜品的关联关系
* @param setmealId 套餐id
* @return 套餐和菜品的关联关系
*/
@Select("select * from setmeal_dish where setmeal_id = #{setmealId}")
List<SetmealDish> getByIdWithDishes(Long setmealId);
功能测试


5. 起售停售套餐
需求分析与接口设计
业务规则:
-
可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作
-
起售的套餐可以展示在用户端,停售的套餐不能展示在用户端
-
起售套餐时,如果套餐内包含停售的菜品,则不能起售
接口设计:

代码编写
SetmealController
/**
* 起售/停售套餐
* @param status 状态
* @param id 套餐id集合
* @return 结果
*/
@PostMapping("/status/{status}")
@ApiOperation("起售/停售套餐")
public Result<Void> startOrStop(@PathVariable Integer status, Long id) {
log.info("起售/停售套餐: {}, {}", status, id);
setmealService.startOrStop(status, id);
return Result.success();
}
SetmealServiceImpl
/**
* 起售/停售套餐
* @param status 套餐状态
* @param id 套餐id
*/
@Override
public void startOrStop(Integer status, Long id) {
// 如果要设置为起售,需要检查套餐内的菜品是否都为起售状态
if (Objects.equals(status, StatusConstant.ENABLE)) { // 要起售
// select a.* from dish a
// left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?;
List<Dish> dishList = dishMapper.getBySetmealId(id); // 查询套餐内的菜品
dishList.forEach(dish -> {
if (dish.getStatus().equals(StatusConstant.DISABLE)) {
throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
}
}); // 只要有一个菜品是停售状态, 就不能起售
}
// 可以起售,更新套餐状态
Setmeal setmeal = Setmeal.builder()
.id(id)
.status(status)
.build();
setmealMapper.updateById(setmeal);
}
DishMapper
/**
* 根据套餐id查询对应的菜品
* @param id 套餐id
* @return 菜品列表
*/
@Select("select d.* from dish d " +
"left join setmeal_dish sd on d.id = sd.dish_id " +
"where sd.setmeal_id = #{id} ")
List<Dish> getBySetmealId(Long id);
功能测试

