# 新增套餐

# 需求分析和设计

产品原型:

image-20221018135930842

image-20221018140833345

业务规则:

  • 套餐名称唯一
  • 套餐必须属于某个分类
  • 套餐必须包含菜品
  • 名称、分类、价格、图片为必填项
  • 添加菜品窗口需要根据分类类型来展示菜品
  • 新增的套餐默认为停售状态

接口设计(共涉及到 4 个接口):

  • 根据类型查询分类(已完成)
  • 根据分类 id 查询菜品
  • 图片上传(已完成)
  • 新增套餐

image-20221018141521068

image-20221018141606787

数据库设计:

setmeal 表为套餐表,用于存储套餐的信息。具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)套餐名称唯一
category_idbigint分类 id逻辑外键
pricedecimal(10,2)套餐价格
imagevarchar(255)图片路径
descriptionvarchar(255)套餐描述
statusint售卖状态1 起售 0 停售
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人 id
update_userbigint最后修改人 id

setmeal_dish 表为套餐菜品关系表,用于存储套餐和菜品的关联关系。具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
setmeal_idbigint套餐 id逻辑外键
dish_idbigint菜品 id逻辑外键
namevarchar(32)菜品名称冗余字段
pricedecimal(10,2)菜品单价冗余字段
copiesint菜品份数

# 代码实现

# DishController

1
2
3
4
5
6
7
8
9
10
11
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
List<Dish> list = dishService.list(categoryId);
return Result.success(list);
}

# DishService

1
2
3
4
5
6
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
List<Dish> list(Long categoryId);

# DishServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
public List<Dish> list(Long categoryId) {
Dish dish = Dish.builder()
.categoryId(categoryId)
.status(StatusConstant.ENABLE)
.build();
return dishMapper.list(dish);
}

# DishMapper

1
2
3
4
5
6
/**
* 动态条件查询菜品
* @param dish
* @return
*/
List<Dish> list(Dish dish);

# DishMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="list" resultType="Dish" parameterType="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>

# 1.2.6 SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 套餐管理
*/
@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();
}
}

# SetmealService

1
2
3
4
5
6
7
8
public interface SetmealService {

/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
void saveWithDish(SetmealDTO setmealDTO);
}

# SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 套餐业务实现
*/
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;

/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
@Transactional
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);

//向套餐表插入数据
setmealMapper.insert(setmeal);

//获取生成的套餐id
Long setmealId = setmeal.getId();

List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});

//保存套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
}

[!IMPORTANT]

为什么要先插入再获取套餐 id,不能直接获取吗

  1. 自增主键:
    • 很多数据库使用自增主键来自动生成 ID 值。在插入数据之前,这个自增主键还没有被赋值,所以无法直接获取。只有在插入数据后,数据库才会给这个自增主键赋值,此时才能获取到这个 ID。
  2. 数据库事务:
    • 在插入数据的过程中,可能会涉及到数据库事务。如果先获取 ID, 然后插入数据失败,那么这个 ID 就被 "浪费" 了。所以先插入数据,确保数据插入成功后,再获取 ID 更加合理和安全。
  3. 其他情况:
    • 有时候,ID 的生成可能不是自增的,而是通过某种算法或者其他机制生成的。在这种情况下,插入数据后获取 ID 也是一种常见的做法。

# SetmealMapper

1
2
3
4
5
6
/**
* 新增套餐
* @param setmeal
*/
@AutoFill(OperationType.INSERT)
void insert(Setmeal setmeal);

# SetmealMapper.xml

1
2
3
4
5
6
<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>

# SetmealDishMapper

1
2
3
4
5
/**
* 批量保存套餐和菜品的关联关系
* @param setmealDishes
*/
void insertBatch(List<SetmealDish> setmealDishes);

# SetmealDishMapper.xml

1
2
3
4
5
6
7
8
<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>

# 功能测试

# 套餐分页查询

# 需求分析和设计

产品原型:

image-20221018152429246

业务规则:

  • 根据页码进行分页展示
  • 每页展示 10 条数据
  • 可以根据需要,按照套餐名称、分类、售卖状态进行查询

接口设计:

image-20221018152731141

# 代码实现

# SetmealController

1
2
3
4
5
6
7
8
9
10
11
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}

# SetmealService

1
2
3
4
5
6
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

# SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
int pageNum = setmealPageQueryDTO.getPage();
int pageSize = setmealPageQueryDTO.getPageSize();

PageHelper.startPage(pageNum, pageSize);
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}

# SetmealMapper

1
2
3
4
5
6
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

# SetmealMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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>

# 功能测试

# 删除套餐

# 需求分析和设计

产品原型:

image-20221018153756531

业务规则:

  • 可以一次删除一个套餐,也可以批量删除套餐
  • 起售中的套餐不能删除

接口设计:

image-20221018154541067

# 代码实现

# SetmealController

1
2
3
4
5
6
7
8
9
10
11
/**
* 批量删除套餐
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids){
setmealService.deleteBatch(ids);
return Result.success();
}

# SetmealService

1
2
3
4
5
/**
* 批量删除套餐
* @param ids
*/
void deleteBatch(List<Long> ids);

# SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 批量删除套餐
* @param ids
*/
@Transactional
public void deleteBatch(List<Long> ids) {
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);
});
}

# SetmealMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 根据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

1
2
3
4
5
6
/**
* 根据套餐id删除套餐和菜品的关联关系
* @param setmealId
*/
@Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
void deleteBySetmealId(Long setmealId);

# 功能测试

# 修改套餐

# 需求分析和设计

产品原型:

image-20221018160214225

接口设计(共涉及到 5 个接口):

  • 根据 id 查询套餐
  • 根据类型查询分类(已完成)
  • 根据分类 id 查询菜品(已完成)
  • 图片上传(已完成)
  • 修改套餐

image-20221018160915177

image-20221018160949864

image-20221018161046352

image-20221018161117780

image-20221018161139861

# 代码实现

# SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 根据id查询套餐,用于修改页面回显数据
*
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询套餐")
public Result<SetmealVO> 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();
}

# SetmealService

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 根据id查询套餐和关联的菜品数据
* @param id
* @return
*/
SetmealVO getByIdWithDish(Long id);

/**
* 修改套餐
* @param setmealDTO
*/
void update(SetmealDTO setmealDTO);

# SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 根据id查询套餐和套餐菜品关系
*
* @param id
* @return
*/
public SetmealVO getByIdWithDish(Long id) {
Setmeal setmeal = setmealMapper.getById(id);
List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id);

SetmealVO setmealVO = new SetmealVO();
BeanUtils.copyProperties(setmeal, setmealVO);
setmealVO.setSetmealDishes(setmealDishes);

return setmealVO;
}

/**
* 修改套餐
*
* @param setmealDTO
*/
@Transactional
public void update(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);

//1、修改套餐表,执行update
setmealMapper.update(setmeal);

//套餐id
Long setmealId = setmealDTO.getId();

//2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
setmealDishMapper.deleteBySetmealId(setmealId);

List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
setmealDishMapper.insertBatch(setmealDishes);
}

# SetmealDishMapper

1
2
3
4
5
6
7
/**
* 根据套餐id查询套餐和菜品的关联关系
* @param setmealId
* @return
*/
@Select("select * from setmeal_dish where setmeal_id = #{setmealId}")
List<SetmealDish> getBySetmealId(Long setmealId);

# SetmealMapper

1
2
3
4
5
6
/**
* 根据Id修改套餐
* @param setmeal
*/
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);

# SetmealMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<update id="update" parameterType="Setmeal">
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>

# 功能测试

# 起售停售套餐

# 需求分析和设计

产品原型:

image-20221018163720881

业务规则:

  • 可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作
  • 起售的套餐可以展示在用户端,停售的套餐不能展示在用户端
  • 起售套餐时,如果套餐内包含停售的菜品,则不能起售

接口设计:

image-20221018165055208

# 代码实现

# SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 套餐起售停售
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
public Result startOrStop(@PathVariable Integer status, Long id) {
setmealService.startOrStop(status, id);
return Result.success();
}

# SetmealService

1
2
3
4
5
6
/**
* 套餐起售、停售
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);

# SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 套餐起售、停售
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
//起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
if(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);
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);
}

# DishMapper

1
2
3
4
5
6
7
/**
* 根据套餐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);

# 功能测试