Part03 ☀️
Part03 ☀️
课程内容
- 管理端
- 文件上传下载 ✏️
- 菜品新增 ✏️
- 菜品分页查询 ✏️
- 菜品修改 ✏️
1. 菜品模块介绍 🍐
菜品模块介绍
- 文件上传下载 ✏️
- 菜品新增 ✏️
- 菜品分页查询 ✏️
- 菜品修改 ✏️
开始前,需要下载群里的前端页面,替换项目中的backend代码

2. 菜品新增模块
2.1 图片上传 🍐
图片上传
- 新增菜品,需要上传图片,流程如下:

2. 实际开发中,图片会存到专门的图片服务器,本项目使用阿里云OSS对象存储,来存储图片,流程如下:

1.按住F12进入浏览器开发者模式,观察上传的请求信息

代码操作

- 打开阿里云oss,申请服务,获取如下信息:
- 服务器地址:如武汉机房,北京机房
- accessKeyId 访问keyId
- accessKeySecret:秘钥
- bucketName:桶信息
- 本次焱哥已经申请好服务,大家可以先试用我提供的工具类(已经配置好了权限),感兴趣的可以自行去申请服务!
2.1 在导入工具类之前,需要先导入依赖到pom文件中,,
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
注意:点击刷新,这样会下载依赖!!!
2.2 直接导入工具类,就可以用了

package com.itheima.reggie.common;
@Component // 表明当前类需要被spring进行实例化,放到ioc容器中
public class AliOSSUtils {
private String endpoint="https://oss-cn-wuhan-lr.aliyuncs.com";
private String accessKeyId="LTAI5tGJKWuBifboPQrMQFze";
private String accessKeySecret="PghR2lcWRjJI4UupdqLtPVXCIz9INS";
private String bucketName="java86";
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile multipartFile) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = multipartFile.getInputStream();
// 避免文件覆盖
String originalFilename = multipartFile.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径
String url =endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
根据接口信息,书写CommonController接收请求
package com.itheima.reggie.controller;
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Autowired
AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
public R<String> upload( MultipartFile file) throws IOException {
String url = aliOSSUtils.upload(file);
return R.success(url);
}
}

在测试以前,需要在过滤器中,添加上传链接到白名单,这样不需要登录,也可以上传
点击添加菜品或者添加套餐,都可以上传和回显图片

总结
课堂作业
- 根据上述提示完成图片的上传?🎤
- 大家思考一下,秘钥可以随便传播吗?
2.2 获取分类列表
获取分类列表
1. 点击新增菜品,会自动发起获取菜品分类的列表
2. 按照F12观察请求,获取请求路径和请求方式以及参数类型
代码操作
1. 在CategoryController类中书写请求
/**
* 根据条件查询分类数据
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
log.info("分类type:{}",category.getType());
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加条件 分类类型不等于NUll 1为菜品分类 2为套餐分类
queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
//添加排序条件 修改时间降序
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
//带条件查询
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
1. 点击添加菜品和添加套餐,可以发现,都可以返回列表

2.3 菜品新增 ✏️
菜品新增
后台系统中可以管理菜品信息,通过 新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息 。


新增菜品,其实就是将新增页面录入的菜品信息插入到 dish 表,如果添加了口味做法,还需要向 dish_flavor 表插入数据。所以在新增菜品时,涉及到两个表:
表结构 | 说明 |
---|---|
dish | 菜品表 |
dish_flavor | 菜品口味表 |
1). 菜品表:dish

2). 菜品口味表:dish_flavor

代码操作
步骤
- 导入资料到工程中(焱哥发到群里了)
- DishController 定义方法新增菜品
- DishService 中增加方法 saveWithFlavor
- DishServiceImpl 中实现方法 saveWithFlavor
- 在引导类上加注解 @EnableTransactionManagement
将老师发送在微信群里的压缩包,下载解压后,放到对应的包中!

1). 导入 DishDto 实体类
封装页面传递的请求参数。
所属包: com.itheima.reggie.dto
@Data
public class DishDto extends Dish {
// 口味信息
private List<DishFlavor> flavors = new ArrayList<>();
// private String categoryName;
// private Integer copies;
}
前端传过来的数据:👇
实体模型 | 描述 |
---|---|
DTO | Data Transfer Object(数据传输对象),一般用于展示层与服务层之间的数据传输。 |
Entity | 最常用实体类,基本和数据表一一对应,一个实体类对应一张表。 |
2). DishController 定义方法新增菜品
在该 Controller 的方法中,不仅需要保存菜品的基本信息,还需要保存菜品的口味信息,需要操作两张表,所以我们需要在 DishService 接口中定义接口方法,在这个方法中需要保存上述的两部分数据。
/**
* 新增菜品
* @param dishDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
3). DishService 接口 中增加方法 saveWithFlavor
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavor
public void saveWithFlavor(DishDto dishDto);
4). DishServiceImpl 中实现方法 saveWithFlavor
页面传递的菜品口味信息,仅仅包含 name 和 value 属性,缺少一个非常重要的属性 dishId, 所以在保存完菜品的基本信息后,我们需要获取到菜品 ID,然后为菜品口味对象属性 dishId 赋值。
具体逻辑如下:
- ①. 保存菜品基本信息 ;
- ②. 获取保存的菜品 ID ;
- ③. 获取菜品口味列表,遍历列表,为菜品口味对象属性 dishId 赋值;
- ④. 批量保存菜品口味列表;
由于在 saveWithFlavor 方法中,进行了两次数据库的保存操作,操作了两张表,那么为了保证数据的一致性,我们需要在方法上加上注解 @Transactional来控制事务,一般写在接口 的方法上(如下)
DishServiceImpl 类
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品,同时保存对应的口味数据
@Transactional也可以写在接口的方法上
* @param dishDto
*/
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
dishDto.setCreateTime(LocalDateTime.now());
dishDto.setUpdateTime(LocalDateTime.now());
dishDto.setCreateUser(1L);
dishDto.setUpdateUser(1L);
this.save(dishDto);
//保存口味信息
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
//使用遍历循环给口味表设置dishid
for (DishFlavor flavor : flavors) {
flavor.setDishId(dishDto.getId());
flavor.setCreateTime(LocalDateTime.now());
flavor.setUpdateTime(LocalDateTime.now());
flavor.setCreateUser(1L);
flavor.setUpdateUser(1L);
}
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
- 因为没有写分页查询,因此只能从数据库和控制台输出日志中观察

总结
课堂作业
- 参考上述步骤,完成菜品的新增🎤
3. 菜品分页查询 🍐 ✏️
菜品分页查询
系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

菜品列表展示数据:
- 菜品的基本信息(名称、售价、售卖状态、更新时间)
- 菜品图片
- 菜品分类(需要展示分类名称,而不是分类 ID )
- 需要根据菜品的分类 ID,去分类表中查询分类信息,然后在页面展示

- 请求信息:路径和请求方式以及参数非常明显
- 返回值不是简单的R类,而是Page类,含有总数量和当前页集合数据
代码操作
上述我们已经分析了分页查询的请求信息,那么接下来,我们就需要在 DishController 中开发方法,来完成菜品的条件分页查询,在分页查询时还需要给页面返回分类的名称,而分类的名称前端在接收的时候是通过 categoryName 属性获取的,那么对应的服务端也应该封装到 categoryName 属性中。
<el-table-column prop="categoryName" label="菜品分类"></el-table-column>
而在我们的实体类 Dish 中,仅仅包含 categoryId, 不包含 categoryName,那么我们应该如何封装查询的数据呢?
其实,这里我们可以返回 DishDto 对象,在该对象中我们可以拓展一个属性 categoryName,来封装菜品分类名称。
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName; //菜品分类名称
private Integer copies;
}
注意: 已经导入成功了,不需要重复导入
逻辑:
- 构造分页条件对象
- 构建查询及排序条件
- 执行分页条件查询
- 遍历分页查询列表数据,根据分类 ID 查询分类信息,从而获取该菜品的分类名称
- 封装数据并返回
注意:
- 数据库查询菜品信息时,获取到的分页查询结果 Page 的泛型为 Dish,而
- 最终需要给前端页面返回的类型为 DishDto,所以这个时候就要进行转换,
- 基本属性我们可以直接通过属性拷贝的形式对 Page 中的属性进行复制
//对象拷贝 //BeanUtils.copyProperties(被拷贝的对象,目标对象,"排除字段"); BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
- 对于结果列表 records 属性,我们是需要进行特殊处理的(需要封装菜品分类名称)(上述代码选中部分);
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//构造分页构造器对象
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>();
//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行分页查询
dishService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
- 重启服务(debug 模式),访问系统,登陆系统,观察是否分页
- 同时查看控制台 MP 的 sql 语句输出


4. 菜品修改
4.1 根据 ID 查询菜品信息 ✏️
根据 ID 查询菜品信息
页面发送 ajax 请求,请求服务端,根据 id 查询当前菜品信息和对应的口味信息,用于修改页面中菜品信息回显。
在 DishService 接口中扩展 getByIdWithFlavor 方法
在 DishService 实现类中实现此方法 具体逻辑为:
- A. 根据 ID 查询菜品的基本信息
- B. 根据菜品的 ID 查询菜品口味列表数据
- C. 组装数据并返回
在 DishController 中创建 get 方法
- 根据 ID 查询菜品及菜品口味信息
请求 说明 请求方式 GET 请求路径 /dish/{id}
修改菜品以及口味的回显 JSON 数据
{
"id": "1422783914845487106",
"name": "佛跳墙",
"categoryId": "1397844357980663809",
"price": 88800,
"code": "",
"image": "da9e1c70-fc32-4781-9510-a1c4ccd2ff59.jpg",
"description": "佛跳墙",
"status": 1,
"sort": 0,
"createTime": "2021-08-04 12:58:14",
"createUser": "1412578435737350122",
"updateUser": "1412578435737350122",
"flavors": [
{
"id": "1422783914883235842",
"dishId": "1422783914845487106",
"name": "辣度",
"value": "[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]",
"createTime": "2021-08-04 12:58:14",
"updateTime": "2021-08-04 12:58:14",
"createUser": "1412578435737350122",
"updateUser": "1412578435737350122",
"isDeleted": 0,
"showOption": false
},
{
"id": "1422783914895818754",
"dishId": "1422783914845487106",
"name": "忌口",
"value": "[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]",
"createTime": "2021-08-04 12:58:14",
"updateTime": "2021-08-04 12:58:14",
"createUser": "1412578435737350122",
"updateUser": "1412578435737350122",
"isDeleted": 0,
"showOption": false
}
]
}
代码操作
DishController
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
DishService接口
//根据id查询菜品信息和对应的口味信息
public DishDto getByIdWithFlavor(Long id);
DishServiceImpl 实现类
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息,从dish表查询
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
// 复制属性
BeanUtils.copyProperties(dish,dishDto);
//查询当前菜品对应的口味信息,从dish_flavor表查询
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(flavors);
return dishDto;
}
功能测试
编写完代码后,重启服务进行测试,点击列表的修改按钮,查询数据回显情况。

4.2 修改菜品信息 🍐 ✏️
修改菜品信息

点击保存按钮,页面发送 ajax 请求,将修改后的菜品相关数据以 json 形式提交到服务端。在修改菜品信息时需要注意,除了要更新 dish 菜品表,还需要更新 dish_flavor 菜品口味表。
在 DishService 接口中扩展方法 updateWithFlavor
在 DishServiceImpl 中实现方法 updateWithFlavor
- 该方法中,我们既需要更新 dish 菜品基本信息表,还需要更新 dish_flavor 菜品口味表。
- 页面再操作时,关于菜品的口味,有修改,有新增,也有可能删除,我们应该如何更新菜品口味信息呢?
- 无论菜品口味信息如何变化,我们只需要保持一个原则:先删除,后添加。
- 页面再操作时,关于菜品的口味,有修改,有新增,也有可能删除,我们应该如何更新菜品口味信息呢?
- 该方法中,我们既需要更新 dish 菜品基本信息表,还需要更新 dish_flavor 菜品口味表。
在 DishController 中创建 update 方法
- 修改菜品及菜品口味信息
请求 说明 请求方式 PUT 请求路径 /dish
请求参数 json 格式数据
代码操作
DishController 代码
/**
* 修改菜品
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
log.info("修改菜品,菜品信息:{}",dishDto);
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功");
}
DishService 代码
//更新菜品信息,同时更新对应的口味信息
public void updateWithFlavor(DishDto dishDto);
DishServiceImpl 代码
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
dishDto.setUpdateTime(LocalDateTime.now());
dishDto.setUpdateUser(1L);
//1. 更新菜品基本信息
this.updateById(dishDto);
//2. 更新菜品口味信息
//思路:先按照dish_id将口味数据全部删除,然后在重新添加,起到修改的目的
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
//dish_id要等于dishDto中的id
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
//删除所属菜品的全部口味后,重新添加
//2. 保存菜品口味数据到菜品口味表dish_flavor
List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor flavor : flavors) {
flavor.setDishId(dishDto.getId());
flavor.setCreateTime(LocalDateTime.now());
flavor.setCreateUser(1L);
flavor.setUpdateTime(LocalDateTime.now());
flavor.setUpdateUser(1L);
}
dishFlavorService.saveBatch(flavors);
}
代码编写完成之后,重启服务,然后按照前面分析的操作流程进行测试,查看数据是否正常修改即可。