瑞吉外卖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”添加上传的文件
目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传。例如ElementUI中提供的upload上传组件:
服务端要接收客户端页面上传的文件,通常都会使用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 |
|
新增CommonController,负责文件上传与下载。
接收上传的文件步骤:
1、新建一个控制器类CommonController,专门负责文件上传和下载
2、在upload()方法的形参位置,声明MultipartFile类型的参数,对象名与「html页面传来的form表单中的name」一致,也就是和浏览器请求体form-data处的name值一致。
1 |
|
注意:MultipartFile定义的file变量必须与name一致
完整代码:
1 |
|
配置文件application.yml中,配置指定位置:
1 | #自定义参数。文件上传时,上传到指定位置 |
1.1.4 文件下载代码实现
通过浏览器进行文件下载,本质就是:将文件从服务端,以流的形式,写入到浏览器的过程。
前端html页面中:
(1)在el-upload标签内,通过此标签展示下载的图片:(用img标签,指定src属性,会向src中的地址发送请求。)
(2)methods中:
:on-success=”handleAvatarSuccess”:文件上传success后,同时回调handleAvatarSuccess方法。如下:
后端controller代码:
文件上传完成后,回调handleAvatarSuccess方法,即紧接着发送/common/download请求,并将name传给服务器。
服务端收到/common/download?name=xxxxx请求后,会执行以下方法。到指定目录下,通过输入流读取name这个文件。再通过输出流写给浏览器。浏览器就可以显示刚上传的图片。
1 | //文件下载 |
说明:
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、Mapper接口DishFlavorMapper
1 |
|
3、Service接口DishFlavorService
1 | public interface DishFlavorService extends IService<DishFlavor> { |
4、Service接口实现类 DishFlavorServicelmpl
1 |
|
5、控制层 DishController
1 |
|
梳理交互过程
在开发代码之前,需要梳理一下新增菜品时,前端页面和服务端的交互过程:
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、管理员填完表单,点击保存,会发送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 |
|
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 |
|
由于涉及多表操作,需要在启动类上开启事务,添加@EnableTransactionManagement
注解。
1 | //加上这个注解后,才会扫描@WebFilter注解,使得过滤器生效 |
在控制器DishController中,新增save()方法,调用刚才写好的Service方法,新增菜品。
1 |
|
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
DishController中:
1 |
|
上面第(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 | /** |
说明:
为什么不直接用Dish dish = dishService.getById(id);
?因为这样只能查到dish表中的数据,还需要显示菜品口味,所以使用dishDto,查dish_flavor表中的数据。
2、在DishService中新增getByIdWithFlavor方法,并在DishServiceImpl中实现
1 | /** |
测试:
第4步:后端需要处理请求/dish
,method=PUT,接收(修改后的)该id的菜品的价格、售卖状态、图片、菜品口味等数据,将修改后的数据保存到数据库,并返回响应信息。
1、在DishController中添加put方法
1 | /** |
说明:
Q:为什么不能复用save()?
A:如果是保存,则填写提交后会新增一条数据。而我们需要根据id,找到表里这条数据,对它进行修改,所以需要重新写一个sercice方法,即updateWithFlavor(dishDto)。
2、在DishService新增updateWithFlavor方法,并在DishServiceImpl中实现
1 | /** |
1.5 停售/启售菜品(支持批量)
1.5.1 需求分析
在list.html菜品管理页面,有”售卖状态“这一列。以及“操作”这一列中,有”启售“/”停售“的按钮。可以便于管理菜品。
Dish实体类中有属性status,可以表示商品的售卖状态:
1 | //0:停售 1:起售 |
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 | // 起售停售---批量起售停售接口 |
最后,根据响应,跳转到分页页面。
1.5.3 后端代码实现
服务端需要处理传来的POST请求/dish/status/${params.status}
,并对数据表的status字段进行修改,最后返回响应信息。
在DishController新增switchStatus方法,通过数组保存ids,实现单个/批量起售停售:
1 | /** |
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 | if (type === '批量' && id === null) {//点了批量删除 |
在用户确认删除后(点“确定”),执行deleteDish(ids)方法。该方法判断type的值是“批量”还是“单删”。
- 如果type的值为’批量’,则将选中的勾选框对应的id放到数组中,再将this.checkList数组中的元素(多个id)以逗号分隔,组成一个字符串,作为参数ids传递给deleteDish(ids)函数。deleteDish()中,向
/dish
发送DELETE请求,在url中携带参数ids(=组成的字符串)。
1 | handleSelectionChange (val){ |
- 如果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 | /** |