瑞吉外卖04-菜品管理

本笔记源自黑马程序员的视频课程——《瑞吉外卖》,总结了课程笔记、相关知识点以及可能遇到的问题解决方案,并且增加了课程中未实现的功能,供读者参考。笔记全面且条理清晰,希望帮助读者学习和理解这个外卖项目。
本项目全部笔记见:外卖项目笔记合集

1. 菜品管理业务开发

1.1 文件上传下载

1.1.1 文件上传介绍

文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

文件上传时,需要使用form表单上传文件,且对页面的form表单有如下要求:

  • method=”post” method为post
  • enctype=”multipart/form-data” form表单的enctype属性值为”multipart/form-data”
  • type=”file” form标签中使用input type=”file”添加上传的文件

1

目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传。例如ElementUI中提供的upload上传组件:

2

服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:

  • commons-fileupload
  • commons-io

Spring框架在spring-web包中对“文件上传”进行了封装,大大简化了服务端代码。

我们只需要在Controller的方法中,声明一个MultipartFile类型的参数,即可接收上传的文件。

步骤为:

1、新建一个控制器类CommonController,专门负责文件上传和下载

2、在upload()方法的形参位置,声明MultipartFile类型的参数,对象名与「html页面传来的form表单中的name」一致,也就是和浏览器请求体form-data处的name值一致。

具体实现在1.1.3中。

1.1.2 文件下载介绍

文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。通过浏览器进行文件下载,通常有两种表现形式:

  • 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

1.1.3 文件上传代码实现

文件上传,页面端可以使用ElementUI提供的上传组件。

可以直接使用资料中提供的上传页面,位置:资料/文件上传下载页面/upload.html

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传</title>
<!-- 引入样式 -->
<link rel="stylesheet" href="../../plugins/element-ui/index.css" />
<link rel="stylesheet" href="../../styles/common.css" />
<link rel="stylesheet" href="../../styles/page.css" />
<link rel="shortcut icon" href="../../favicon.ico">
</head>
<body>
<div class="addBrand-container" id="food-add-app">
<div class="container">
<el-upload class="avatar-uploader"
action="/common/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeUpload"
ref="upload">
<img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>
</div>
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="../../plugins/vue/vue.js"></script>
<!-- 引入组件库 -->
<script src="../../plugins/element-ui/index.js"></script>
<!-- 引入axios -->
<script src="../../plugins/axios/axios.min.js"></script>
<script src="../../js/index.js"></script>
<script>
new Vue({
el: '#food-add-app',
data() {
return {
imageUrl: ''
}
},
methods: {
handleAvatarSuccess (response, file, fileList) {
this.imageUrl = `/common/download?name=${response.data}`
},
beforeUpload (file) {
if(file){
const suffix = file.name.split('.')[1]
const size = file.size / 1024 / 1024 < 2
if(['png','jpeg','jpg'].indexOf(suffix) < 0){
this.$message.error('上传图片只支持 png、jpeg、jpg 格式!')
this.$refs.upload.clearFiles()
return false
}
if(!size){
this.$message.error('上传文件大小不能超过 2MB!')
return false
}
return file
}
}
}
})
</script>
</body>
</html>

新增CommonController,负责文件上传与下载。

接收上传的文件步骤

1、新建一个控制器类CommonController,专门负责文件上传和下载

2、在upload()方法的形参位置,声明MultipartFile类型的参数,对象名与「html页面传来的form表单中的name」一致,也就是和浏览器请求体form-data处的name值一致。

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
//文件上传
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file 是一个临时文件,需要转存到指定位置,否则请求完成后临时文件会删除
log.info("file:{}",file.toString());
return null;
}
}

注意:MultipartFile定义的file变量必须与name一致

3

完整代码

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
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}") //注解@Value 是org.springframework.beans.factory.annotation包下的。可以将配置文件中的值赋值给Java对象中的属性。
//此处将配置文件中定义的reggie.path属性值注入给basePath
private String basePath;

/**
* 文件上传
* @param file 类型为MultipartFile。名字和(浏览器请求体form-data处的)name值一致
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//log.info("file:{}",file.toString());

//文件名:随机UUID名(避免重名造成覆盖) + 后缀
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); //substring(int index)方法:返回从index处(含)至末尾(含)的子串
//使用UUID随机生成文件名,防止因为文件名相同造成文件覆盖
String fileName = UUID.randomUUID().toString()+suffix;

//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if(!dir.exists()){
//目录不存在
dir.mkdirs(); //创建目录(含所有不存在的parent目录)
//mkdir(): 创建目录。如果父目录不存在/当前目录已存在,则创建失败
//mkdirs(): 创建目录。如果父目录不存在,也同时创建父目录。
}

//file 是一个临时文件,需要转存到指定位置,否则请求完成后临时文件会删除
try {
//将临时文件转存到指定位置
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName); //前端需要
}
}

配置文件application.yml中,配置指定位置:

1
2
3
#自定义参数。文件上传时,上传到指定位置
reggie:
path: /Users/kathy/Downloads/img_upload/

1.1.4 文件下载代码实现

通过浏览器进行文件下载,本质就是:将文件从服务端,以流的形式,写入到浏览器的过程。

前端html页面中:

(1)在el-upload标签内,通过此标签展示下载的图片:(用img标签,指定src属性,会向src中的地址发送请求。)

4

(2)methods中:

:on-success=”handleAvatarSuccess”:文件上传success后,同时回调handleAvatarSuccess方法。如下:

5

后端controller代码

文件上传完成后,回调handleAvatarSuccess方法,即紧接着发送/common/download请求,并将name传给服务器。

服务端收到/common/download?name=xxxxx请求后,会执行以下方法。到指定目录下,通过输入流读取name这个文件。再通过输出流写给浏览器。浏览器就可以显示刚上传的图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//文件下载
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream=new FileInputStream(new File(basePath+name)); //basePath + name是要读取的文件
//输出流,通过输出流将文件写回浏览器(用response),在浏览器中展示图片
ServletOutputStream outputStream = response.getOutputStream();

//读到哪去:
int len=0;
byte[] bytes = new byte[1024];
while ((len=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
outputStream.close();//fileInputStream & outputStream是局部变量,只能在try中关。不能在finally中关
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}

说明:

1、返回值是void:通过输出流,向浏览器写回二进制数据即可,不需要返回值

2、参数:name:获取请求参数name。response:输出流需要通过response来获得

1.1.5 业务过程梳理总结

后端

  • 上传

    1、浏览器访问localhost:8080/backend/page/demo/upload.html页面,选择图片上传后,会发送POST请求到http://localhost:8080/common/upload ,由controller处理。

    2、controller中的upload方法处理该post请求。通过MultipartFile类型的形参,接收上传的文件。

    (上传文件时,elementUI会自动生成"Content-Disposition: form-data; name="file"; filename="fileName.jpeg" " 注意此处的name,要和控制器方法形参同名。而类型固定为MultipartFile,这样就可以使用spring框架封装的代码)

    3、upload()方法还要生成文件名fileName、将临时文件file转存到服务器指定位置。在转存前还需要检查该文件目录是否存在(如不存在,就要创建),最后返回文件名给前端(封装在R中)。

  • 下载

    1、前端发出下载请求/common/download?name=${response.data},带着文件名参数来服务器,由controller中的download方法处理。

    2、到服务器的指定目录下(basePath+filename),通过输入流读取文件到输入流对象fileInputStream,再通过响应response得到输出流对象outputStream 将文件写回浏览器。

前端

  • 上传

    el-upload是elementUI提供的【上传组件】

    指定action=”/common/upload”,所以选择图片上传后,会发POST请求到/common/upload,由控制器处理。(控制器处理后,会响应filename文件名回来)

  • 下载

    :on-success="handleAvatarSuccess":文件上传success后,接到服务器的响应 R.success(filename),同时回调handleAvatarSuccess方法,发送下载请求/common/download?name=${response.data}。其中response.data是服务器刚响应回来的R对象中封装的filename。

    接下来由img标签展示下载下来的图片,来源是imageUrl,它是请求/common/download?name=${response.data}由服务器处理后的结果(即通过输出流,响应到浏览器的图片)由img标签展示。

1.2 新增菜品

1.2.1 需求分析

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

图片

1.2.2 数据模型

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

1、dish表(菜品表)

图片

2、dish_flavor表(菜品口味表)

图片

1.2.3 代码开发

准备工作:

在开发业务功能前,先将需要用到的类和接口基本结构创建好。

1、实体类DishFlavor(直接从课程资料中导入。Dish实体类前面课程中已经导入过了)

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
@Data
public class DishFlavor implements Serializable {

private static final long serialVersionUID = 1L;

//主键
private Long id;

//菜品id
private Long dishId;

//口味名称
private String name;

//口味数据list
private String value;

@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT)
private Long createUser;

@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;

//是否删除
private Integer isDeleted;
}

2、Mapper接口DishFlavorMapper

1
2
3
@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {

3、Service接口DishFlavorService

1
2
public interface DishFlavorService extends IService<DishFlavor> {
}

4、Service接口实现类 DishFlavorServicelmpl

1
2
3
@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor>implements DishFlavorService {
}

5、控制层 DishController

1
2
3
4
5
6
7
8
9
10
@Slf4j
@RestController
@RequestMapping("/dish")
public class DishController {
@Autowired
private DishService dishService;

@Autowired
private DishFlavorService dishFlavorService;
}

梳理交互过程

在开发代码之前,需要梳理一下新增菜品时,前端页面和服务端的交互过程:

1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中

这一步的具体过程:

点击“新建菜品”,访问backend/page/food/add.html页面,会执行created()钩子函数,其中执行this.getDishList()方法。getDishList()中,执行getCategoryList(),该方法向/category/list发送ajax请求,请求类型是GET,params是type=1。

所以,页面一加载完,会发送请求:/category/list?type=1,以获取所有的分类信息(之前添加的菜品分类),回显到下拉菜单中。

图片

2、页面发送请求进行图片上传,请求服务端将图片保存到服务器

3、页面发送请求进行图片下载,将上传的图片进行回显

2、3 这两步的具体过程:

菜品图片上传和下载模块:点击+ 进行图片上传,向/common/upload发送POST请求,将图片上传至服务器某路径保存。紧接着在页面回显,向/common/download?name=xxxx发送GET请求,将图片从服务器以流的方式,写入到浏览器。2、3这两步在前面已经写过了,可以直接用。

4、点击“保存”按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

这一步的具体过程:

填完表单,点击“保存”,会先校验,校验通过后,把整个表单的数据收集起来,封装到params。然后执行addDish(params)方法,发送ajax请求到/dish,method=post,将填的params数据以json形式发给服务器,让服务器保存。所以控制器方法应该能够接收提交的表单中的这些数据params,并保存到数据表。

图片

开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

后端程序开发

1、菜品分类下拉框回显

前端要获取所有的分类,以展示在下拉框。前端发送请求:/category/list?type=1,需要服务器处理并给出响应。所以我们在CategoryController控制器新建list()方法,处理该请求。

该方法中,需要在数据库的category表中,根据type查询所有的分类。并将查出的分类list响应给前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 根据条件查询分类数据 为list
* 请求是/category/list?type=1
* 在添加菜品的时候,需要为菜品添加一个分类。需要在下拉菜单中显示所有已经创建好的分类。
* type=1 菜品分类;type=2 套餐分类
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
//从表中查询所有的分类:select * from category where type = ? (type使用参数传来的type值)
//条件构造器
LambdaQueryWrapper<Category> lambdaQueryWrapper=new LambdaQueryWrapper<>();
//添加条件
lambdaQueryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
//添加排序条件
lambdaQueryWrapper.orderByAsc(Category::getSort).orderByAsc(Category::getUpdateTime);
List<Category> list = categoryService.list(lambdaQueryWrapper);
return R.success(list); //将查询出的list作为data,封装在R对象中
}

2、菜品图片上传和下载方法已经写过,可以直接调用。

3、管理员填完表单,点击保存,会发送ajax请求到/dish,method=post,将填的params数据以json形式发给服务器。服务器(控制器方法)如何接收这些参数呢?

浏览器传来的json数据中,name、price、code、image、description、status、categoryId这些属性,在Dish实体类中都有属性与之对应。但是flavors这一项,Dish类中没有该属性对应,无法在形参中用实体类Dish+@RequestBody注解的方式接收请求参数。

解决办法:可以封装另一个类DishDto,继承Dish的所有属性,还另扩展了其他属性(flavors等),接收所有的这些参数。

我们先导入DishDto(位置:资料/dto),用于封装页面提交的数据。

1
2
3
4
5
6
7
8
9
@Data
public class DishDto extends Dish {

private List<DishFlavor> flavors = new ArrayList<>();

private String categoryName;

private Integer copies;
}

DTO,全称为Data Transfer object,即数据传输对象,一般用于展示层与服务层之间的数据传输。

接下来控制器方法就可以声明DishDto类型的形参,接收到dish表需要的参数和flavors参数。然后执行service层的方法。

但由于原本dishService中的save()方法只能操作dish表。我们新增菜品同时插入菜品对应的口味数据,还需要操作dish_flavor表,添加菜品对应的口味信息。所以需要在service层重新声明saveWithFlavor(DishDto dishDto)方法,将Dish类的属性保存到dish表,将DishFlavor类的属性保存到dish_flavor表。

在DishService接口中添加方法saveWithFlavor(DishDto dishDto),在DishServiceImpl实现:

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
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Autowired
private DishFlavorService dishFlavorService;

@Override
@Transactional //涉及多张表,所以使用事务。还需要在主程序加@EnableTransactionManagement,以启用事务
public void saveWithFlavor(DishDto dishDto) {
//保存菜品基本信息到菜品表dish
this.save(dishDto);//DishDto继承自Dish,所以dishDto中封装了菜品表中所需的信息。但是dishDto的扩展部分还没保存
//save(dishDto)保存菜品后,雪花算法自动生成菜品的id(Dish类的id属性),插入到数据库。mybatis plus帮我们自动返回主键。

Long dishid = dishDto.getId();//获得菜品id

//菜品口味
/*
思路:
前端传来的"flavors":[{"name":"甜味","value":"[\"无糖\",\"少糖\",\"半糖\",\"多糖\",\"全糖\"]"
flavors是用List集合盛装DishFlavor对象。前端传来的只有DishFlavor的name和value,还缺DishFlavor类中的dishId。此处必须给flavors信息加上dishId数据,否则不知道添加的口味(name和value)是哪个菜的,无法对应。
dish_flavor表中的dish_id对应dish表中的id。所以可以先获取dish表的id(从dishDto获取),再将这个id加到集合中,并插入到dish_flavor表中。
*/

//此处用流的方式(也可以用for i循环),将信息封装到item中,给item添加上菜品id,再转成集合,重新赋值给flavors这个集合
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map( (item) ->{ //item:遍历出的每一项,对应一个DishFlavor实体
item.setDishId(dishId); //加工每一个item,把item的属性dishId赋好值
return item;
}).collect(Collectors.toList());

//保存菜品口味到菜品数据表dish_flavor
dishFlavorService.saveBatch(flavors);
}
}

由于涉及多表操作,需要在启动类上开启事务,添加@EnableTransactionManagement注解。

1
2
3
4
5
6
7
8
9
10
@ServletComponentScan   //加上这个注解后,才会扫描@WebFilter注解,使得过滤器生效
@Slf4j
@SpringBootApplication
@EnableTransactionManagement //和@Transactional一起,开启事务
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(TimingFoodDeliveryApplication.class, args);
log.info("项目启动成功");
}
}

在控制器DishController中,新增save()方法,调用刚才写好的Service方法,新增菜品。

1
2
3
4
5
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}

1.3 菜品信息分页查询

1.3.1 需求分析

系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般系统中都会以分页的方式来展示列表数据。

图片

1.3.2 梳理交互过程

在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:

1、页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据。

这一步的具体过程:

点“菜品管理”,访问backend/page/food/list.html,页面加载完会立即发送ajax请求,向服务器发请求/dish/page?page=xx&pageSize=x&name=xxx。由服务器controller处理、查询后,响应回来,得到菜品分页数据(含菜品名称、菜品分类、售价、售卖状态、最后操作时间、操作)。

页面加载完立即发送ajax请求是如何实现的:页面加载完,执行钩子函数。执行init()方法。init()方法中,(1)封装了params;(2)执行getDishPage(params),发送ajax请求(请求”/dish/page?page=xx&pageSize=x&name=xxx”,需要服务器处理,并响应page对象给前端)

图片

2、页面发送请求/common/download?name=${image},请求服务端进行图片下载,用于页面图片展示。此功能前面已经写过了。

1.3.3 代码开发

需要处理请求/dish/page?page=xx&pageSize=x&name=xxx,从数据库查询出分页数据pageInfo,并封装到R对象中,响应给前端。

但是pageInfo是Page类型的,只包含dish表的字段,只有”category_id”字段,没有“菜品分类”名称,“category_name”只在category表中。因此还需要通过”category_id”,去category表中查询“category_name”。

DishController中:

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
44
45
@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<>();
//添加查询条件(模糊查询):只要name不为空,就在Dish表中根据name字段查询,要查的值是传来的参数name
queryWrapper.like(StringUtils.isNotEmpty(name), Dish::getName, name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行分页查询(用dishService查dish表)
dishService.page(pageInfo, queryWrapper); //处理好会封装到Page类型的pageInfo中,pageInfo中含有数据列表records、总记录数total等信息

//原先:return R.success(pageInfo); 此分页对象是关于dish的,dish表中的菜品分类只是category_id,而页面需要的是分类名字,因此无法正常展示"菜品分类"。
//所以此处不能直接返回。需要改造,额外返回category_id对应的分类名categoryName。查看list.html也能看出,需要后端响应名为"categoryName"的字段,才能显示"菜品分类"这一项

// dishDtoPage构建后没有赋值,如何赋值?
//(1)可以直接把 从dish表中查出的pageInfo各属性值 拷贝给dishDtoPage(除records)
//(2)再把records转换为DishDto类型的,添加上categoryName,设置给dishDtoPage
//第(1)步:对象拷贝
BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");//不拷贝records属性(页面上的数据信息就是records,需要处理)

//第(2)步:处理records
List<Dish> records = pageInfo.getRecords(); //把pageInfo里的records属性的值拿出来。

List<DishDto> list = records.stream().map((item)->{ //item:遍历出的每一个菜品对象dish
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);//把dish的普通属性copy给我们自己创建的dishDto对象
Long categoryId = item.getCategoryId();//获取当前遍历到的dish对象的categoryId
//根据id,从数据库查询分类对象,以获取分类名称
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();//获取分类名称
dishDto.setCategoryName(categoryName);//把categoryName赋值给 我们自己创建的dishDto对象的 categoryName属性
}
return dishDto;
}).collect(Collectors.toList());

dishDtoPage.setRecords(list);//把加了categoryName的新list 赋值给dishDtoPage的records属性

return R.success(dishDtoPage);
}

上面第(2)步的解释梳理:

records是List类型(盛装Dish对象),那就可以用流来表示。records.stream()将List类型的records转换为流,流中有多个Dish对象。遍历流中的每一个Dish对象,称为item。

我们想让流中存放的一个个对象中,不仅含dish表中查出的数据,还含category表中查出的category_name数据。于是在DishDto类中添加categoryName属性,以便封装category_name数据。

可以理解为,我们想将流中的每一个dish对象 换成一个dishDto对象,其中存有dish的属性值,还存了category_name值。

所以对于每个item,我们新创建一个dishDto对象去替换原来的dish对象。

  • 先将item(dish对象)的属性值copy给新造的dishDto对象。此时dishDto中除了categoryName属性,其他都有了
  • 接下来去数据库查该对象。通过item.getCategoryId(),获取dish对象中的category_id属性值,用这个id去category表中查出对应的category对象,再获取到该category对象的name。
  • 将获取到的categoryName赋值给dishDto对象的categoryName属性。
  • 最后将dishDto对象返回,则流中都是一个个dishDto对象了。

最后使用collect(Collectors.toList())方法,将流转换回List集合list,再赋给dishDtoPage分页对象的records属性。这样records中装的是DishDto对象,包含categoryName数据。

1.4 修改菜品

1.4.1 需求分析

在菜品管理列表页面点击“修改”按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息,并进行修改,最后点击“确定”按钮完成修改。

图片

1.3.2 梳理交互过程

在开发代码之前,需要梳理一下修改菜品时,前端页面(add.html)和服务端的交互过程:

1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示。

这一步的具体过程:

“修改菜品”的页面复用了“新建菜品”时的页面,访问backend/page/food/add.html?id=(传入的参数id)后,页面加载完会执行getDishList()方法,其中执行getCategoryList()方法,发送ajax请求,向服务器发请求/category/list?type=1,请求类型是GET,以获取所有的分类(之前添加的菜品分类)回显到下拉菜单中。

图片

此处服务器的处理已在CategoryController中实现过了。可以获取所有的分类信息,回显到下拉菜单中。

2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显

这一步的具体过程:

与第1步同时,在访问backend/page/food/add.html?id=(传入的参数id)后,页面加载完还会立即发送ajax请求,向服务器发请求/dish/${id},method为GET,以获取该id对应的菜品数据。以回显到前端修改页面add.html?id=(传入的参数id)中。

具体实现方法是:created()钩子函数中,取出url中参数id的值,如果url中有id,说明当前是修改的请求,执行init()方法。其中执行queryDishById(this.id)方法,向/dish/${id} 发送get请求。需要服务器处理,查询该id的菜品信息,并需要服务器响应该id的菜品的价格、售卖状态、图片、菜品口味等数据,将服务器响应回来的菜品数据回显到修改的页面中。

图片

3、页面发送请求,请求服务端进行图片下载,用于页图片回显

4、点击“保存”按钮,页面发送ajax请求到/dish,method=PUT,将(修改后的)该id的菜品的价格、售卖状态、图片、菜品口味等数据,以json形式提交到服务端由服务器处理该请求,将修改后的数据保存到数据库,并返回响应信息。

1.4.3 代码开发

上述第1、3步已经在前面实现了。下面开发第2、4步。

第2步:后端需要从表中查询该id对应的菜品数据(该id的菜品的 价格、售卖状态、图片、菜品口味 等),并返回这些数据给前端回显。

1、DishController处理GET请求

1
2
3
4
5
6
7
8
9
10
11
/**
* 菜品修改-查询id对应的菜品信息(封装到dishDto对象),以回显到前端修改页面
* 处理前端发来的GET请求/dish/{id}
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> queryDish(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}

说明:

为什么不直接用Dish dish = dishService.getById(id); ?因为这样只能查到dish表中的数据,还需要显示菜品口味,所以使用dishDto,查dish_flavor表中的数据。

2、在DishService中新增getByIdWithFlavor方法,并在DishServiceImpl中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 根据id,从dish表和dish_flavor表中查询菜品信息(含菜品口味)
* @param id
* @return
*/
@Override
@Transactional //涉及多张表,所以使用事务。还需要在主程序加@EnableTransactionManagement,以启用事务
public DishDto getByIdWithFlavor(Long id) {
Dish dish = this.getById(id);//根据id查出dish表中该id的菜品数据,封装到dish对象(不含菜品口味)
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish, dishDto);

//现在dishDto中还差菜品口味。从dish_flavor表中,根据dish_id获取name和value,封装到flavors对象中

//查询菜品口味信息:select * from dish_flavor where dish_id = ?
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId, id);
//调用该表对应的service的方法,查出结果,以List形式封装
List<DishFlavor> list = dishFlavorService.list(queryWrapper);//查询为列表

dishDto.setFlavors(list);//将查出的口味结果,赋给dishDto对象的flavors属性
return dishDto;
}

测试:

图片

第4步:后端需要处理请求/dish,method=PUT,接收(修改后的)该id的菜品的价格、售卖状态、图片、菜品口味等数据,将修改后的数据保存到数据库,并返回响应信息。

1、在DishController中添加put方法

1
2
3
4
5
6
7
8
9
10
/**
* 修改菜品信息。处理前端发来的PUT请求/dish(JSON请求)
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功");
}

说明:

Q:为什么不能复用save()?

A:如果是保存,则填写提交后会新增一条数据。而我们需要根据id,找到表里这条数据,对它进行修改,所以需要重新写一个sercice方法,即updateWithFlavor(dishDto)。

2、在DishService新增updateWithFlavor方法,并在DishServiceImpl中实现

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
/**
* 根据菜品的id,用前端用户填写的数据,对dish和dish_flavor表进行修改
* @param dishDto
*/
@Override
@Transactional //涉及多张表,所以使用事务。还需要在主程序加@EnableTransactionManagement,以启用事务
public void updateWithFlavor(DishDto dishDto) {
//更新dish表的部分。该id对应的name、category_id、price、code、image、description数据
this.updateById(dishDto);

// (1)根据dish_id,清理当前id对应菜品的口味数据--对dish_flavor表的delete操作
// 想执行的是:delete from dish_flavor where dish_id = ?
// 注意:不可以用dishFlavorService.removeById();因为removeById用的是dish_flavor的id(主键),此处需要根据"dish_id"字段来查找
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId, dishDto.getId());
dishFlavorService.remove(queryWrapper);

// (2)添加当前提交的口味数据--对dish_flavor表的insert操作
//此处用流的方式,将信息封装到item中,给item添加上菜品id,再转成集合,重新赋值给flavors集合(和上面的saveWithFlavor方法一样)
List<DishFlavor> flavors = dishDto.getFlavors();//此时的flavors集合只有name、value这两项,还要加上dish_id才行
flavors = flavors.stream().map( (item) ->{ //item:遍历出的每一项,对应一个DishFlavor实体
item.setDishId(dishDto.getId()); //加工每一个item,把item的属性dishId赋好值
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);//把dishDto中取出的flavors数据保存到dish_service表中
}

1.5 停售/启售菜品(支持批量

1.5.1 需求分析

在list.html菜品管理页面,有”售卖状态“这一列。以及“操作”这一列中,有”启售“/”停售“的按钮。可以便于管理菜品。

图片

Dish实体类中有属性status,可以表示商品的售卖状态:

1
2
//0:停售 1:起售
private Integer status;

1.5.2 交互过程分析

”启售“/”停售“按钮的实现(前端):

1、”售卖状态“这一列的显示:

”售卖状态“这一列中,会将status的值映射为文字,显示在列表中。页面获取到这一行的status,如果为0,”售卖状态“就显示”停售”,如果是1,”售卖状态“显示”启售”。

图片

2、“操作”这一列”启售“/”停售“按钮:

(1) 按钮的显示

当前菜品在数据库中status为1(启售),则“操作”就显示“停售”按钮。当前菜品在数据库中status为0(停售),“操作”就显示“启售”按钮。

图片

(2) 修改状态数据

点击”启售“/”停售“按钮后,执行statusHandle(scope.row)方法,发送请求到服务器,由服务器修改数据库中status的值。具体如下:

  • 如果是对单个菜品操作:

    点击”启售“/”停售“(单个菜品)的按钮后,带着当前这一行的数据,执行statusHandle(scope.row)方法。此时row不是String类型,所以会更改params.status的值。

    图片

    例如,如果数据库中status为0,这行数据的status就是0,则此处执行params.status = row.status ? '0' : '1'之后,params.status为1。

  • 如果是批量操作:

    • 如果想批量启售(当前是停售状态,status=0),选中多个勾选框,点“批量启售”,会执行statusHandle(‘1’)方法。参数为String型,将参数row的值“1”赋给status。
    • 如果想批量停售(当前是启售状态,status=1),选中勾选框,点“批量停售”,会执行statusHandle(‘0’)方法。参数为String型,将参数row的值“0”赋给status。

    图片

    图片

这样一来,不管是批量操作还是单一操作,params中封装的status值是更改过的,服务器直接把数据库中的值修改成该status即可。

而对于params中封装的ids,在批量操作时,是逗号隔开的字符串;在单一操作时是当前菜品的id。

在确认修改之后,执行dishStatusByStatus()方法。向/dish/status/${params.status}发POST请求,并将ids通过url带过去给服务器,需要服务器对数据表的status字段进行修改,并需要服务器返回响应信息。

1
2
3
4
5
6
7
8
// 起售停售---批量起售停售接口
const dishStatusByStatus = (params) => {
return $axios({
url: `/dish/status/${params.status}`,
method: 'post',
params: { ids: params.id }
})
}

最后,根据响应,跳转到分页页面。

1.5.3 后端代码实现

服务端需要处理传来的POST请求/dish/status/${params.status},并对数据表的status字段进行修改,最后返回响应信息。

在DishController新增switchStatus方法,通过数组保存ids,实现单个/批量起售停售:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 修改启售/停售状态,支持批量修改和单独修改
* 处理请求/dish/status/{status}?ids=xxxxxx,xxxxxx
* @param status 前端传来的status已经是修改过的,后端只要保存即可
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> switchStatus(@PathVariable Integer status, String[] ids){
for(String id : ids){
Dish dish = dishService.getById(id);//通过遍历到的当前id,获取dish表中的一条记录,即一个dish对象
dish.setStatus(status); //将该记录的status改为url路径参数中的status
dishService.updateById(dish);
}
return R.success("修改成功");
}

1.6 删除菜品(支持批量

1.6.1 需求分析

需要实现删除和批量删除。

图片

1.6.2 交互过程分析

单删:对某个菜品,点击“删除”,执行deleteHandle(type, id)方法,type为‘单删’,id为scope.row.id。如下:

图片

批量删除:如果是批量删除,选中一些勾选框,再点“批量删除”,执行deleteHandle(type, id)方法,type为‘批量’,id为null。如下:

图片

在deleteHandle(type, id)方法中,首先判断是否是「批量删除且0个勾选框被勾选」的情况,需要提示用户信息“请选择删除对象”。

1
2
3
4
5
if (type === '批量' && id === null) {//点了批量删除
 if (this.checkList.length === 0) {//且0个勾选框被勾选
   return this.$message.error('请选择删除对象')
}
}

在用户确认删除后(点“确定”),执行deleteDish(ids)方法。该方法判断type的值是“批量”还是“单删”。

  • 如果type的值为’批量’,则将选中的勾选框对应的id放到数组中,再将this.checkList数组中的元素(多个id)以逗号分隔,组成一个字符串,作为参数ids传递给deleteDish(ids)函数。deleteDish()中,向/dish发送DELETE请求,在url中携带参数ids(=组成的字符串)。
1
2
3
4
5
6
7
handleSelectionChange (val){
 let checkArr = []
 val.forEach((n) => {
   checkArr.push(n.id)
})
 this.checkList = checkArr //将选中的勾选框对应的id放到checkArr数组中,再给checkList数组赋值
}

图片

  • 如果type的值为’单删’,将单个id的值作为参数传递给deleteDish(ids)函数。deleteDish()中,向/dish发送DELETE请求,在url中携带参数ids(=单个id)
1
deleteDish(type === '批量' ? this.checkList.join(',') : id).then()

需要服务器处理请求/dish,删除dish数据表中id对应的菜品,并返回响应。最后,根据响应,跳转到分页页面。

1.6.3 后端代码实现

处理请求/dish?ids=xxx(单删)或者/dish?ids=xxxx,xxxx(批量删除),请求方式为DELETE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 删除菜品
* 请求/dish?ids=xxx
或者/dish?ids=xxxx,xxxx
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(String[] ids){
for (String id : ids){
dishService.removeById(id);
}
return R.success("删除成功");
}