瑞吉外卖-Day04
瑞吉外卖-Day04
课程内容
- 管理端
- 文件上传下载 ✏️
- 菜品新增 ✏️
- 菜品分页查询 ✏️
- 菜品修改 ✏️
- 菜品禁用启用 🚀
- 菜品删除 🚀
1. 文件上传下载 ✏️
1.1 上传介绍
1.1.1 概述
文件上传,也称为 upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
文件上传时,对页面的 form 表单有如下要求:👈
表单属性 | 取值 | 说明 |
---|---|---|
method | post | 必须选择 post 方式提交 |
enctype | multipart/form-data | 采用 multipart 格式上传文件 |
type | file | 使用 input 的 file 控件上传 |
注意
在 WebMvcConfig 类中,增加一个 CommonsMultipartResolver 文件上传的解析器 使用 CommonsMultipartResolver 解析器,需要导入 2 个依赖
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
//bean的name一定要是:multipartResolver
//要么 @Bean("multipartResolver") 注解中指定名字
//要么方法的名字为:multipartResolver
@Bean("multipartResolver")
public CommonsMultipartResolver commonsMultipartResolver(){
CommonsMultipartResolver cmr = new CommonsMultipartResolver();
//最大的上传文件尺寸 50M
cmr.setMaxUploadSize(1024*1024*50);
//每个的上传文件尺寸 10
cmr.setMaxUploadSizePerFile(1024*1024*10);
return cmr;
}
}
<!-- -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!-- java代码中使用:IOUtils.copy(输入流,输出流); -->
<!-- 注意包: org.apache.tomcat.util.http.fileupload.IOUtils -->
<!-- 其实不导入下面包也是可以的,fileupload包含commons-io -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
1.1.2 前端介绍
1). 简单 html 页面表单
<form method="post" action="/common/upload" enctype="multipart/form-data">
<input name="myFile" type="file" />
<input type="submit" value="提交" />
</form>
2). ElementUI 中提供的 upload 上传组件
目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于 form 表单的文件上传。

测试前端网页如下:

1.1.3 服务端介绍
服务端要接收客户端页面上传的文件,通常都会使用 Apache 的两个组件:
<!-- 本次用下面的依赖 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!-- java代码中使用:IOUtils.copy(输入流,输出流); -->
<!-- 注意包: org.apache.tomcat.util.http.fileupload.IOUtils -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
1.2 下载介绍
文件下载,也称为 download,是指将文件从服务器传输到本地计算机的过程。通过浏览器进行文件下载,通常有两种表现形式:
1). 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录(如下载电影)

2). 直接在浏览器中打开
今天所需要实现的菜品展示,表现形式为

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
1.3 上传代码实现
服务端实现 ✏️
步鄹
application.yml 定义文件存储路径
CommonController 定义 upload 方法
通过 MultipartFile 类型的参数即可接收上传的文件, 方法形参的名称需要与页面的 file 域的 name 属性一致 所在包: com.itheima.reggie.controller
- 获取文件的原始文件名, 通过原始文件名获取文件后缀
- 通过 UUID 重新声明文件名, 文件名称重复造成文件覆盖
- 创建文件存放目录
- 将上传的临时文件转存到指定位置
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
//bean的name一定要是:multipartResolver
//要么 @Bean("multipartResolver") 注解中指定名字
//要么方法的名字为:multipartResolver
@Bean("multipartResolver")
public CommonsMultipartResolver commonsMultipartResolver(){
CommonsMultipartResolver cmr = new CommonsMultipartResolver();
//最大的上传文件尺寸 50M
cmr.setMaxUploadSize(1024*1024*50);
//每个的上传文件尺寸 10
cmr.setMaxUploadSizePerFile(1024*1024*10);
return cmr;
}
}
# 需要在 application.yml 中定义文件存储路径
reggie:
path: G:/imgs/
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
注意三要素:post、multipart/form-data、file
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
//原始文件名
String originalFilename = file.getOriginalFilename();//abc.jpg
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));//.jpg
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;//sadsadsaedeffeffffsads.jpg
//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if(!dir.exists()){
//目录不存在,需要创建
dir.mkdirs();
}
try {
//将临时文件转存到指定位置
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
}
1.3.3 测试
测试步鄹
- 重启服务(debug 模式),访问系统,登陆系统
- 访问上传页面
http://localhost:8080/backend/page/demo/upload.html
- 点击上传图片,选择图片上传时,(如果图片并不能正常的上传,则需要检查过滤器是否拦截)
- 过滤器中添加白名单/common/**
- 在中观察 upload 方法是否响应,以及 MultipartFile 是否为 null(如果为 null 检查 CommonsMultipartResolver 是否配置)
1.4 下载代码实现
前端代码
文件下载,前端页面可以使用标签展示下载的图片。
代码位置:backend\page\demo\upload.html
<img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
那么,通过标签如何展示图片数据呢,接下来我们来解析一下具体的流程:

在文件上传成功后,在 handleAvatarSuccess 方法中获取文件上传成功之后返回的数据(文件名),然后调用 /common/download?name=xxx 进行文件的下载。在这里,我们想让上传的照片能够在页面展示出来,所以我们就需要在服务端将文件以流的形式写回浏览器。
服务端代码
在 CommonController 中定义方法 download,并接收页面传递的参数 name,然后读取图片文件的数据,然后以流的形式写回浏览器。
逻辑:
- 定义输入流,通过输入流读取文件内容
- 通过 response 对象,获取到输出流
- 通过 response 对象设置响应数据格式(image/jpeg)
- 通过输入流读取文件数据,然后通过上述的输出流写回浏览器
- 关闭资源
代码实现:
/**
* 文件下载
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
//输出流,通过输出流将文件写回浏览器
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpeg");
// 使用工具类
IOUtils.copy(fileInputStream,outputStream)
//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
2. 菜品新增
2.1 需求分析
后台系统中可以管理菜品信息,通过 新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息 。

2.2 数据模型

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

2). 菜品口味表:dish_flavor

2.3 准备工作 ✏️
准备工作
1). 实体类 DishFlavor
直接从课程资料中导入即可,Dish 实体前面课程中已经导入过了 所属包: com.itheima.reggie.entity
2). Mapper 接口 DishFlavorMapper
所属包: com.itheima.reggie.mapper
3). 业务层接口 DishFlavorService
所属包: com.itheima.reggie.service
4). 业务层实现类 DishFlavorServiceImpl
所属包: com.itheima.reggie.service.impl
5). 控制层 DishController
菜品及菜品口味的相关操作,我们统一使用这一个 controller 即可。 所属包: com.itheima.reggie.controller
/**
菜品口味
*/
@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;
}
@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}
public interface DishFlavorService extends IService<DishFlavor> {
}
@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper,DishFlavor> implements DishFlavorService {
}
/**
* 菜品管理
*/
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;
}
2.4 前端页面分析
接下来我们就需要来实现新增菜品功能,在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程。
1). 点击新建菜品按钮, 访问页面(backend/page/food/add.html), 页面加载时发送 ajax 请求,请求服务端获取菜品分类数据并展示到下拉框中

2). 页面发送请求进行图片上传,请求服务端将图片保存到服务器(上传功能已实现)
3). 页面发送请求进行图片下载,将上传的图片进行回显(下载功能已实现)

4). 点击保存按钮,发送 ajax 请求,将菜品相关数据以 json 形式提交到服务端
页面代码:

浏览器抓取请求:

开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这 4 次请求(上传、下载已实现)即可。 经过上述的分析,我们还需要在服务端实现两块功能:
A. 菜品分类数据列表查询, 具体请求信息整理如下 :
请求 | 说明 |
---|---|
请求方式 | GET |
请求路径 | /category/list |
请求参数 | ?type=1 |
B. 保存菜品信息, 具体请求信息整理如下:
请求 | 说明 |
---|---|
请求方式 | POST |
请求路径 | /dish |
请求参数 | json 格式 |
2.5 代码实现 ✏️ 👈
2.5.1 菜品分类查询
在 CategoryController 中增加方法实现菜品分类查询,根据分类进行查询,并对查询的结果按照 sort 排序字段进行升序排序,如果 sort 相同,再按照修改时间倒序排序。
/**
* 根据条件查询分类数据
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
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);
}
代码编写完毕之后,我们可以打开新增菜品页面,查看响应的数据,及页面下拉列表的渲染情况:

2.5.2 保存菜品信息
在上述的分析中,我们可以看到在保存菜品时,页面传递过来的是 json 格式数据,格式如下:
页面传递过来 JSON 格式数据
{
"name": "佛跳墙",
"price": 88800,
"code": "",
"image": "da9e1c70-fc32-4781-9510-a1c4ccd2ff59.jpg",
"description": "佛跳墙",
"status": 1,
"categoryId": "1397844357980663809",
"flavors": [
{
"name": "辣度",
"value": "[\"不辣\",\"微辣\",\"中辣\",\"重辣\"]",
"showOption": false
},
{
"name": "忌口",
"value": "[\"不要葱\",\"不要蒜\",\"不要香菜\",\"不要辣\"]",
"showOption": false
}
]
}
问题
在服务端我们应该如何来封装前端传递的数据呢,我们发现,如果使用菜品类 Dish 来封装,只能封装菜品的基本属性,flavors 属性是无法封装的。那么这个时候,我们应该如何处理呢?
解决方案
需要自定义一个实体类,然后继承自 Dish,并对 Dish 的属性进行拓展,增加 flavors 集合属性(内部封装 DishFlavor)。清楚了这一点之后,接下来就进行功能开发。
代码步鄹
- 导入 DishDto 实体类
- 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 | 最常用实体类,基本和数据表一一对应,一个实体类对应一张表。 |
VO | Value Object(值对象), 主要用于封装前端页面展示的数据对象,用一个 VO 对象来封装整个页面展示所需要的对象数据 |
PO | Persistant Object(持久层对象), 是 ORM(Objevt Relational Mapping)框架中 Entity,PO 属性和数据库中表的字段形成一一对应关系 |
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来控制事务,一般写在接口 的方法上(如下)
DishService 接口
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavor
@Transactional
public void saveWithFlavor(DishDto dishDto);
DishServiceImpl 类
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品,同时保存对应的口味数据
@Transactional也可以写在接口的方法上
* @param dishDto
*/
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId = dishDto.getId();//菜品id
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
// 方式1:使用遍历循环给口味表设置dishid
// for (DishFlavor flavor : flavors) {
// flavor.setDishId(dishDto.getId());
// }
// 方式2:使用stream流
flavors = flavors.stream().map((item) -> {
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
5). 在引导类上加注解 @EnableTransactionManagement Service 层方法上加的注解 @Transactional 要想生效,需要在引导类上加上注解 @EnableTransactionManagement, 开启对事务的支持。
@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement //开启对事物管理的支持
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功...");
}
}
2.6 功能测试
测试步鄹
- 重启服务(debug 模式),访问系统,登陆系统
- 新增菜品,输入数据, debug 断点跟踪
- 查询看前端传输的数据,以及 DTO 是否封装成功
- 查看数据库数据是否添加成功

debug 跟踪数据的封装情况:

然后在测试完毕后, 我们可以检查一下数据库中的数据保存情况:

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

菜品列表展示数据:
- 菜品的基本信息(名称、售价、售卖状态、更新时间)
- 菜品图片(需要下载)
- 需要提供图片下载接口
- 菜品分类(需要展示分类名称,而不是分类 ID )
- 需要根据菜品的分类 ID,去分类表中查询分类信息,然后在页面展示
3.2 前端页面分析
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:
1). 访问页面(backend/page/food/list.html)时,发送 ajax 请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2). 页面发送请求,请求服务端进行图片下载,用于页面图片展示


开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这 2 次请求即可,而前端发起的请求中, 图片文件下载回显的代码,我们已经实现了。所以,我们只需要开发分页查询的功能即可,分页查询具体的请求信息如下:
请求 | 说明 |
---|---|
请求方式 | GET |
请求路径 | /dish/page |
请求参数 | ?page=1&pageSize=10&name=xxx |
3.3 代码实现 ✏️ ⚠️
3.3.1 分析
上述我们已经分析了分页查询的请求信息,那么接下来,我们就需要在 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;
}
3.3.2 方式 1,纯 MP 实现
提示
逻辑:
- 构造分页条件对象
- 构建查询及排序条件
- 执行分页条件查询
- 遍历分页查询列表数据,根据分类 ID 查询分类信息,从而获取该菜品的分类名称
- 封装数据并返回
注意:
- 数据库查询菜品信息时,获取到的分页查询结果 Page 的泛型为 Dish,而
- 最终需要给前端页面返回的类型为 DishDto,所以这个时候就要进行转换,
- 基本属性我们可以直接通过属性拷贝的形式对 Page 中的属性进行复制
//对象拷贝 //BeanUtils.copyProperties(被拷贝的对象,目标对象,"排除字段"); BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
- 对于结果列表 records 属性,我们是需要进行特殊处理的(需要封装菜品分类名称)(上述代码选中部分);
点击查看 Controller 代码
/**
* 菜品信息分页查询
* @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);
}
说明
3.3.3 方式 2,Mybatis+sql 实现 🚀
提示
MP 是对 mybatis 进行增强,因此使用 MP 也可以使用 Mybatis
Mybatis 的 Sql 语句可以用注解,也可以写在 mapper.xml 中
- 第一步:创建一个 DishMapper.xml 文件,文件位置如下图
- 第二步:在 DishMapper.xml 中
<mapper namespace="接口的全类名">
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.reggie.mapper.DishMapper">
</mapper>
第三步:定义的
<select id='接口中的方法'>
标签,id 对应接口中的方法第四步:接口中方法的返回值类型和
<select id="方法名" resultType="类型">
要一致(如果是集合,就填写泛型)这样做的目的是:编译后,Mapper 接口和 Mapper.xml 的位置在一起
- 如果不清楚使用 Mybatis 代理模式开发,可以安装 MybatisX 插件进行辅助
- 本插件能检测是否符合 Mybatis 代理开发的要求,现象如下:
- 本插件能检测是否符合 Mybatis 代理开发的要求,现象如下:
@GetMapping("/page")
public R<Page<DishDto>> page2(@RequestParam("page") int page, int pageSize,String name) {
// page = 1,pageSize = 10,name=长沙
log.info("page = {},pageSize = {},name={}", page, pageSize,name);
//构造分页构造器
Page<DishDto> dishDtoPage = new Page<DishDto>(page, pageSize);
dishDtoPage = dishService.pageDishDto(dishDtoPage,name);
return R.success(dishDtoPage);
}
Page<DishDto> pageDishDto(Page<DishDto> dishDtoPage, String name);
@Override
public Page<DishDto> pageDishDto(Page<DishDto> dishDtoPage, String name) {
return this.getBaseMapper().pageDishDto(dishDtoPage,name);
}
Page<DishDto> pageDishDto(Page<DishDto> page,String name);
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.reggie.mapper.DishMapper">
<select id="pageDishDto" resultType="com.itheima.reggie.entity.dto.DishDto">
SELECT
dish.id,
category.`name` as category_name,
dish.`name`,
dish.category_id,
dish.price,
dish.`code`,
dish.image,
dish.description,
dish.`status`,
dish.sort,
dish.create_time,
dish.update_time,
dish.create_user,
dish.update_user,
dish.is_deleted
FROM
dish
LEFT JOIN category ON dish.category_id=category.id
<where>
<if test="name!=null and name!='' ">
dish.name LIKE CONCAT('%',#{name},'%')
</if>
</where>
</select>
</mapper>
3.4 功能测试
测试步鄹
- 重启服务(debug 模式),访问系统,登陆系统
- 通过 debug 端点的形式跟踪,整个分页查询过程中,数据的转换和封装
- 同时查看控制台 MP 的 sql 语句输出
注意
在测试的过程中,我们发现有一些菜品图片并没有展示出来,如下:

这是因为我们在导入 sql 脚本时,导入了一部分菜品的数据,而,所以才会出现图片展示不出来的情况,而我们后续自己添加的菜品信息是没有问题的。
4. 菜品修改
4.1 需求分析
在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作。

4.2 前端页面分析
在开发代码之前,需要梳理一下修改菜品时前端页面(add.html)和服务端的交互过程:
1). 点击菜品列表的中的修改按钮,携带菜品 id 跳转至 add.html

2). 进入 add.html,页面发送 ajax 请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示

3). add.html 获取 id, 发送 ajax 请求,请求服务端,根据 id 查询当前菜品信息,用于菜品信息回显

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

5). 点击保存按钮,页面发送 ajax 请求,将修改后的菜品相关数据以 json 形式提交到服务端

经过上述的分析:
菜品分类下拉框的展示(已完成)
图片的下载回显功能(已完成)
根据 ID 查询菜品及菜品口味信息
请求 说明 请求方式 GET 请求路径 /dish/{id}
修改菜品及菜品口味信息
请求 说明 请求方式 PUT 请求路径 /dish
请求参数 json 格式数据
修改菜品以及口味的回显 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
}
]
}
4.3 功能实现 ✏️ ⚠️
4.3.1 根据 ID 查询菜品信息
页面发送 ajax 请求,请求服务端,根据 id 查询当前菜品信息和对应的口味信息,用于修改页面中菜品信息回显。
在 DishService 接口中扩展 getByIdWithFlavor 方法
在 DishService 实现类中实现此方法 具体逻辑为:
- A. 根据 ID 查询菜品的基本信息
- B. 根据菜品的 ID 查询菜品口味列表数据
- C. 组装数据并返回
在 DishController 中创建 get 方法
代码实现:方式1 简单实用
//根据id查询菜品信息和对应的口味信息
public DishDto getByIdWithFlavor(Long id);
/**
* 根据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;
}
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
代码实现:方式2 加强能力
使用Mybatis的一对多查询
1️⃣ 书写sql语句以及映射
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.reggie.mapper.DishMapper">
<resultMap id="dishflavor" type="com.itheima.reggie.entity.dto.DishDto">
<id column="id" property="id"></id>
<result column="name" property="name"></result>
<result column="category_id" property="categoryId"></result>
<result column="price" property="price"></result>
<result column="image" property="image"></result>
<result column="description" property="description"></result>
<!-- 一对多的关系 -->
<!-- property 说的是集合属性的值,集合中泛型类型对应oftype -->
<collection property="flavors" ofType="com.itheima.reggie.entity.DishFlavor">
<id column="dfid" property="id"></id>
<result column="dfvalue" property="value"></result>
<result column="dfname" property="name"></result>
</collection>
</resultMap>
<!--多表查询语句-->
<select id="getByIdWithFlavor" resultMap="dishflavor">
SELECT
dish.id,
dish. NAME,
dish.category_id,
dish.price,
dish.image,
dish.description,
dish_flavor.id dfid,
dish_flavor.`name` dfname,
dish_flavor.`value` dfvalue
FROM
dish
LEFT JOIN dish_flavor ON dish.id = dish_flavor.dish_id
WHERE
dish.id = #{id}
</select>
</mapper>
2️⃣ 书写Mapper方法
@Mapper
public interface DishMapper extends BaseMapper<Dish> {
DishDto getByIdWithFlavor(@Param("id") Long id);
}
3️⃣ 书写Service的接口
public interface DishService extends IService<Dish> {
DishDto getByIdWithFlavor(Long id);
}
4️⃣ 书写Service的实现类
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Override
public DishDto getByIdWithFlavor(Long id) {
return getBaseMapper().getByIdWithFlavor(id);
}
}
5️⃣ 在控制类中书写接口
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
功能测试
编写完代码后,重启服务进行测试,点击列表的修改按钮,查询数据回显情况。

4.3.2 修改菜品信息
思路
点击保存按钮,页面发送 ajax 请求,将修改后的菜品相关数据以 json 形式提交到服务端。在修改菜品信息时需要注意,除了要更新 dish 菜品表,还需要更新 dish_flavor 菜品口味表。
在 DishService 接口中扩展方法 updateWithFlavor
在 DishServiceImpl 中实现方法 updateWithFlavor
- 该方法中,我们既需要更新 dish 菜品基本信息表,还需要更新 dish_flavor 菜品口味表。
- 页面再操作时,关于菜品的口味,有修改,有新增,也有可能删除,我们应该如何更新菜品口味信息呢?
- 无论菜品口味信息如何变化,我们只需要保持一个原则:
- 页面再操作时,关于菜品的口味,有修改,有新增,也有可能删除,我们应该如何更新菜品口味信息呢?
- 该方法中,我们既需要更新 dish 菜品基本信息表,还需要更新 dish_flavor 菜品口味表。
在 DishController 中创建 update 方法
代码实现
/**
* 修改菜品
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功");
}
//更新菜品信息,同时更新对应的口味信息
public void updateWithFlavor(DishDto dishDto);
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//更新dish表基本信息
this.updateById(dishDto);
//清理当前菜品对应口味数据---dish_flavor表的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
//添加当前提交过来的口味数据---dish_flavor表的insert操作
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
功能测试
代码编写完成之后,重启服务,然后按照前面分析的操作流程进行测试,查看数据是否正常修改即可。
5. 菜品启用禁用 🚀
5.1 需求分析

5.2 前端页面分析
1). 点击菜品列表的中的按钮,弹出对话框进行禁用或者启用

5.3 代码实现
步鄹
- 依据分析 Network 中的请求,在 DishController 中定义 updateStatus 方法
- 通过菜品 Id 获得所有的菜品(有可能是批量禁用和启用)
- 通过 Stream 流的形式,设置状态值
- 调用 updateBatchById 批量进行修改
- 观察控制台 sql 语句的输出
- 重启服务器,登陆,进行测试
点击查看代码
@PostMapping("/status/{status}")
public R<String> updateStatus(@PathVariable("status") Integer status,@RequestParam("ids") List<Long> ids){
log.info("status:{},停售起售的ids:{}",status,ids);
//通过ids批量获取dish 菜品
List<Dish> dishes = dishService.listByIds(ids);
//利用stream流,设置dish的status字段为 传过来的status值
dishes=dishes.stream().map(s->{s.setStatus(status) ; return s;}).collect(Collectors.toList());
//批量修改
boolean b = dishService.updateBatchById(dishes);
return b ?R.success("操作成功"):R.error("操作失败");
}
6. 菜品删除 🚀
6.1 需求分析

6.2 前端页面分析
1). 点击菜品列表的中的按钮,弹出对话框进行删除或者批量删除

6.3 代码实现
步鄹
- 依据分析 Network 中的请求,在 DishController 中定义 delete 方法
- 通过菜品 Id 删除菜品(有可能是批量禁用和启用)
- 观察控制台 sql 语句的输出
- 重启服务器,登陆,进行测试
1️⃣ 方式 1:真删除,数据库直接删除
点击查看代码
@DeleteMapping
public R<String> delete(@RequestParam("ids") List<Long> ids){
log.info("删除的ids:{}",ids);
boolean deleteStatus = dishService.removeByIds(ids);
return deleteStatus?R.success("删除成功"):R.error("删除失败");
}
注意
删除菜品后是否要删除口味?[1]
2️⃣ 方式 2:逻辑删除,数据库不删除,只改变状态
@DeleteMapping
public R<String> delete(@RequestParam("ids") List<Long> ids){
log.info("删除的ids:{}",ids);
// 可以使用逻辑,删除,打开Dish中的逻辑删除字段
boolean deleteStatus = dishService.removeByIds(ids);
return deleteStatus?R.success("删除成功"):R.error("删除失败");
}
mybatis-plus:
global-config:
db-config:
logic-delete-field: isDeleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
// 注意:数据库中要有对应的字段,用来存储删除状态
@TableLogic(value = "0",delval = "1")
private Integer isDeleted;
注意
删除菜品后是否要删除口味?[1:1]
删除完整逻辑
@DeleteMapping
public R<String> deleteByIds(@RequestParam("ids") List<Long> ids){
log.info("deleteByIds:ids:{}",ids);
boolean b = dishService.removeByIdsWithFlavors(ids);
return b?R.success("删除成功"):R.error("删除失败");
}
boolean removeByIdsWithFlavors(List<Long> ids);
@Override
@Transactional
public boolean removeByIdsWithFlavors(List<Long> ids) {
//先判断是否在售,如果在售不能删除
LambdaQueryWrapper<Dish> slqw = new LambdaQueryWrapper<>();
slqw.in(Dish::getId,ids);
List<Dish> list = list(slqw);
for (Dish dish : list) {
if (dish.getStatus()==1){
throw new CustomException("此菜品正在热卖中,,不能删除");
}
}
// 再次判断是否在套餐中,是否是,则删除不了,如果不在可以删除?
LambdaQueryWrapper<SetmealDish> smdlqw = new LambdaQueryWrapper<>();
smdlqw.in(SetmealDish::getDishId,ids);
int count = setmealDishService.count(smdlqw);
if (count>0){
throw new CustomException("此菜品在套餐中,不能删除;");
}
//先删除菜品基本信息
boolean b = removeByIds(ids);
LambdaUpdateWrapper<DishFlavor> luw = Wrappers.lambdaUpdate();
luw.in(DishFlavor::getDishId,ids);
boolean remove = dishFlavorService.remove(luw);
return b&&remove;
}