功能实现:菜品管理
菜品管理效果图:
1. 新增菜品
1.1 需求分析与设计
1.1.1 产品原型
后台系统中可以管理菜品信息,通过 新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片。
新增菜品原型:
当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。
业务规则:
-
菜品名称必须是唯一的
-
菜品必须属于某个分类下,不能单独存在
-
新增菜品时可以根据情况选择菜品的口味
-
每个菜品必须对应一张图片
1.1.2 接口设计
根据上述原型图先粗粒度设计接口,共包含3个接口。
接口设计:
-
根据类型查询分类
-
文件上传
-
新增菜品
接下来细粒度分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。
1. 根据类型查询分类
2. 文件上传
3. 新增菜品
1.1.3 表设计
通过原型图进行分析:
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:
表名 | 说明 |
---|---|
dish | 菜品表 |
dish_flavor | 菜品口味表 |
1). 菜品表:dish
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
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 |
2). 菜品口味表:dish_flavor
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
dish_id | bigint | 菜品id | 逻辑外键 |
name | varchar(32) | 口味名称 | |
value | varchar(255) | 口味值 |
1.2 代码开发
1.2.1 文件上传实现
因为在新增菜品时,需要上传菜品对应的图片(文件),包括后绪其它功能也会使用到文件上传,故要实现通用的文件上传接口。
文件上传,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发抖音、发朋友圈都用到了文件上传功能。
实现文件上传服务,需要有存储的支持,那么我们的解决方案将以下几种:
-
直接将图片保存到服务的硬盘(springmvc中的文件上传)
-
优点:开发便捷,成本低
-
缺点:扩容困难
-
-
使用分布式文件系统进行存储
-
优点:容易实现扩容
-
缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO)
-
-
使用第三方的存储服务(例如OSS)
-
优点:开发简单,拥有强大功能,免维护
-
缺点:付费
-
在本项目选用阿里云的OSS服务进行文件存储。
2.2.2 新增菜品实现
1). 设计DTO类
在sky-pojo模块中
package com.sky.dto;
import com.sky.entity.DishFlavor;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Data
public class DishDTO implements Serializable {
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//口味
private List<DishFlavor> flavors = new ArrayList<>();
}
2). Controller层
进入到sky-server模块
package com.sky.controller.admin;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
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.*;
import java.util.List;
import java.util.Set;
/**
* 菜品管理
*/
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
/**
* 新增菜品
*
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO);//后绪步骤开发
return Result.success();
}
}
3). Service层接口
package com.sky.service;
import com.sky.dto.DishDTO;
import com.sky.entity.Dish;
public interface DishService {
/**
* 新增菜品和对应的口味
*
* @param dishDTO
*/
public void saveWithFlavor(DishDTO dishDTO);
}
4). Service层实现类
package com.sky.service.impl;
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
/**
* 新增菜品和对应的口味
*
* @param dishDTO
*/
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//向菜品表插入1条数据
dishMapper.insert(dish);//后绪步骤实现
//获取insert语句生成的主键值
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);//后绪步骤实现
}
}
}
5). Mapper层 (略)
1.3 功能测试
进入到菜品管理--->新建菜品
dish表:
dish_flavor表:
测试成功。
1.4代码提交
后续步骤和上述功能代码提交一致,不再赘述。
2. 删除菜品
2.1 需求分析和设计
2.1.1 产品原型
在菜品列表页面,每个菜品后面对应的操作分别为修改、删除、停售,可通过删除功能完成对菜品及相关的数据进行删除。
删除菜品原型:
业务规则:
-
可以一次删除一个菜品,也可以批量删除菜品
-
起售中的菜品不能删除
-
被套餐关联的菜品不能删除
-
删除菜品后,关联的口味数据也需要删除掉
2.1.2 接口设计
根据上述原型图,设计出相应的接口。
注意:删除一个菜品和批量删除菜品共用一个接口,故ids可包含多个菜品id,之间用逗号分隔。
2.1.3 表设计
在进行删除菜品操作时,会涉及到以下三张表。
注意事项:
-
在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。
-
setmeal_dish表为菜品和套餐关联的中间表。
-
若删除的菜品数据关联着某个套餐,此时,删除失败。
-
若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。
2.2 代码开发
2.2.1 Controller层
根据删除菜品的接口定义在DishController中创建方法:
/**
* 菜品批量删除
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);//后绪步骤实现
return Result.success();
}
2.2.2 Service层接口
在DishService接口中声明deleteBatch方法:
/**
* 菜品批量删除
*
* @param ids
*/
void deleteBatch(List<Long> ids);
2.2.3 Service层实现类
在DishServiceImpl中实现deleteBatch方法:
@Autowired
private SetmealDishMapper setmealDishMapper;
/**
* 菜品批量删除
*
* @param ids
*/
@Transactional//事务
public void deleteBatch(List<Long> ids) {
//判断当前菜品是否能够删除---是否存在起售中的菜品??
for (Long id : ids) {
Dish dish = dishMapper.getById(id);//后绪步骤实现
if (dish.getStatus() == StatusConstant.ENABLE) {
//当前菜品处于起售中,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//判断当前菜品是否能够删除---是否被套餐关联了??
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds != null && setmealIds.size() > 0) {
//当前菜品被套餐关联了,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品表中的菜品数据
for (Long id : ids) {
dishMapper.deleteById(id);//后绪步骤实现
//删除菜品关联的口味数据
dishFlavorMapper.deleteByDishId(id);//后绪步骤实现
}
}
2.2.4 Mapper层(略)
2.3 功能测试
既可以通过Swagger接口文档进行测试,也可以通过前后端联调测试,接下来,我们直接使用前后端联调测试。
进入到菜品列表查询页面
对测试菜品进行删除操作
同时,进到dish表和dish_flavor两个表查看测试菜品的相关数据都已被成功删除。
再次,删除状态为启售的菜品
点击批量删除
删除失败,因为起售中的菜品不能删除。
2.4 代码提交
后续步骤和上述功能代码提交一致,不再赘述。
项目地址:sky-take-out: springboot+redis+mybatisplus苍穹外卖项目 (gitee.com)