part 1
第二部分链接part 2
part 3 本文章
part 4
文章目录
4 菜品管理
4.1 文件上传(后端为什么要返回文件名给前端、yml中自定义路径值并在类中取出、文件如何转存到指定位置、用UUID防止文件名称重复造成文件覆盖)
4.2 文件下载
4.3 新增菜品 (设计多表操作,事务保证一致性,DTO的使用,自己编写controller,值得学习)
4.4 菜品信息分页查询 (多表联合操作、Dto进一步使用)
4.5 修改菜品 ()
4.1 文件上传
4.1.1 整体思路分析
- 知识点介绍
- 具体实现介绍
上面那段代码会动态地改变元素,生成下面这个input标签
- 后面要做的就是写一个controller,接收前端发来的请求
4.1.2 前端分析
- upload.html文件上传页面
<body>
<div class="addBrand-container" id="food-add-app">
<div class="container">
<!--
el-upload的upload组件:
action:通过提交表单的形式来发送请求,和controller对应起来
method:必须是post
文件必须是form-data的形式,这个从浏览器F12就可以观察出来
-->
<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>
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>
- 上传文件后,404错误,因为controller没写。同时也注意拦截器的配置,当时配置的是只拦截controller(除了登入和登出的),其他均放行
// 是我们的conrtoller中的方法就拦截,如果不是的话,放行,给加载静态资源
if (!(handler instanceof HandlerMethod)) {
log.info("是静态资源或非controller中的方法,放行");
return true;
}
requestHeader中: Content-Type: multipart/form-data;
- 注意观察写好后端成功上传后的form-data
- 在element中可以观察到,原来的
<el-upload>
标签动态生成了input 标签(原来的el-upload标签在页面上有样式,执行之后其实也是input标签)
4.1.3 后端代码分析
- MultipartFile下的transferTo()方法。注意下载到的地址,文件名的设置不要使用上传端的,否则重名之后会覆盖,思路是使用UUID
- 将路径配置到yml文件中,这里取出; 注意!!!不是lombok下的@Value,而是org.springframework.beans.factory.annotation.Value;解决一直报错问题
- 知识点二,资源转存,注意观看每一步需要的类型是什么;file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
- 知识点三、获取原文件的名字,并取出后缀,使用UUID修改文件名,防止覆盖
- 知识点四、判断目录是否存在,不存在就要创建
@Slf4j
@RestController
@RequestMapping("/backend/page/")
public class UploadDownloadController {
//知识点1、将路径配置到yml文件中,这里取出; 注意!!!不是lombok下的@Value,而是org.springframework.beans.factory.annotation.Value;解决一直报错问题
@Value("${custom.download-path}")
private String basePath;
/**
* 文件上传
* @param file 注意形参是MultipartFile类型
* @return
*/
@PostMapping("upload/upload.do")
public RetObj<String> upload(MultipartFile file){
log.info("MultipartFile的值 = {}",file.toString());
//知识点三、获取原文件的名字,并取出后缀,使用UUID修改文件名,防止覆盖
String originalFilename = file.getOriginalFilename();// asd.jsp
String suffix = originalFilename.substring(originalFilename.indexOf(".")); // .jpg
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String newFileName = UUID.randomUUID().toString() + suffix; // UUID.jpg
//知识点四、判断目录是否存在,不存在就要创建
File dir = new File(basePath);//理解为把当前文件读进来
if (!dir.exists()){ //如果之前读的文件不存在
dir.mkdirs();
}
try {
//知识点二,资源转存,注意观看每一步需要的类型是什么
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
file.transferTo(new File(basePath + newFileName));
} catch (IOException e) {
throw new RuntimeException(e);
}
return RetObj.success(newFileName);
}
}
4.2 文件下载
4.2.1 整体逻辑
- 简介
- 代码实现思路
upload组件中有img标签,用于专门展示一个图片。过程是先把图片上传到服务端,通过:src=“imageUrl” 发送一个请求,请求我们的服务端,再把图片下载回来
4.2.2 前端代码分析
- 重点在 el-upload 中中有img标签,用于专门展示一个图片
<img v-if="imageUrl" :src="imageUrl" class="avatar"/>
过程是先把图片上传到服务端,:on-success="handleAvatarSuccess"
上传完成后就会回调这个方法填写imageUrl
的值,通过:src="imageUrl"
发送一个请求,请求我们的服务端,再把图片下载回来,在这个img标签上展示。
<el-upload class="avatar-uploader"
action="/backend/page/upload/upload.do"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeUpload"
ref="upload">
<img v-if="imageUrl" :src="imageUrl" class="avatar"/>
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
- handleAvatarSuccess()方法。
- response.data 是前面上传文件到服务器端,RetObj中的data属性,就是文件名。通过文件名,请求文件数据
- 可以通过response.data中的文件名拿到后端的数据(后端通过输出流向浏览器写回二进制数据)所以后端就不用返回值了
- 输出流通过HttpServletResponse获得,是一个对浏览器的响应;同时还要接收name
- 这里是回调函数,给imageUrl赋值,
- 在前面的
<img v-if="imageUrl" :src="imageUrl" class="avatar"/>
就会发送请求。如果返回数据就会通过img标签来展示图片
methods: {
handleAvatarSuccess (response, file, fileList) {
//这里直接一个url就是请求,之前用的都是ajax,这里直接请求,
this.imageUrl = `/common/download?name=${response.data}`
},
4.2.3 后端代码分析
- 接收参数 name,就是接收要下载的文件名
- 接收到名字之后,在服务器中对应的路径用名字去找这个图片。通过输入流,通过输入流读取文件内容
new FileInputStream(new File(path))
配合读:fileInputStream.read(bytes)
- 找到后通过输出流以二进制的形式打给前端。
response.getOutputStream();
配合写:outputStream.write(bytes,0,len);
@GetMapping("upload/download.do")
public void downloadController(String name, HttpServletResponse response){
FileInputStream fis = null;
ServletOutputStream outputStream = null;
try {
//1、输入流,通过文件输入流,把目标图片先读到
fis = new FileInputStream(new File(basePath + name));
//2、输出流,把图片以流的形式展示到前端
outputStream = response.getOutputStream();
byte[] bytes = new byte[1024];
int len = 0;
while((len = fis.read(bytes)) != -1){
outputStream.write(bytes,0,len);
outputStream.flush();//输出流记得刷新
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
4.3 新增菜品
4.3.1 整体分析
-
需求分析
-
数据模型
-
代码开发 ,先用mybatis_plus弄好,之后交互过程弄明白
4.3.2 前端思路分析
4.3.2.1 选择菜品分类对应的下拉框
- dishList,在vue中定义,双向绑定,后端查询数据后封装到RetObj.success(data属性中),然后在html页面中使用vue组件把dishList中的数据循环出来,展示到页面上。
<el-form-item
label="菜品分类:"
prop="categoryId"
>
<el-select
v-model="ruleForm.categoryId"
placeholder="请选择菜品分类"
>
<!--注意这里的dishList,在vue中定义,双向绑定,后端查询数据后封装到RetObj.success(data属性中)-->
<el-option v-for="(item,index) in dishList" :key="index" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
- vue 中的ajax与后端交互,将category表中所有的type=1的数据查出来(把所有菜品查出来),并存为一个list(vue中定义为一个list,然后使用v-for去遍历这个list显示到下拉框中),存在RetObj.sucess(List data)
// 获取菜品分类
getDishList () {
//下面这个方法封装到js文件中,在表中,1是菜品分类,2是套餐分类。
//把type传给后端来查询(直接用一个对象接住,就会自动为这个对象的某些属性赋值),后端可以用一个对象来接收,因为以后可能不止一个type,还有其他参数
getCategoryList({ 'type': 1 }).then(res => {
if (res.code === 1) {
this.dishList = res.data
} else {
this.$message.error(res.msg || '操作失败')
}
})
},
- getCategoryList()
// 获取菜品分类列表
const getCategoryList = (params) => {
return $axios({
url: '/category/list',
method: 'get',
params
})
}
4.3.2.2 口味管理
// 按钮 - 添加口味
addFlavore () {
this.dishFlavors.push({'name': '', 'value': [], showOption: false}) // JSON.parse(JSON.stringify(this.dishFlavorsData))
},
// 按钮 - 删除口味
delFlavor (ind) {
this.dishFlavors.splice(ind, 1)
},
// 按钮 - 删除口味标签
delFlavorLabel (index, ind) {
this.dishFlavors[index].value.splice(ind, 1)
},
//口味位置记录
flavorPosition (index) {
this.index = index
},
// 添加口味标签
keyDownHandle (val,index) {
console.log('keyDownHandle----val',val)
console.log('keyDownHandle----index',index)
console.log('keyDownHandle----this.dishFlavors',this.dishFlavors)
if (event) {
event.cancelBubble = true
event.preventDefault()
event.stopPropagation()
}
if (val.target.innerText.trim() != '') {
this.dishFlavors[index].value.push(val.target.innerText)
val.target.innerText = ''
}
},
<!--
再次复习,placeholder就是默认显示的灰色字,点击就没了
@focus="selectFlavor(true,index)"
@blur="outSelect(false,index)"失去焦点就触发 这两个大多都是记录日志,可以看对应的函数,在vue下面那里
-->
<el-input
v-model="item.name"
type="text"
style="width: 100%"
placeholder="请输入口味"
@focus="selectFlavor(true,index)"
@blur="outSelect(false,index)"
@input="inputHandle(index)"
/>
// 获取口味列表
getFlavorListHand () {
// flavor flavorData
this.dishFlavorsData = [
{'name':'甜味','value':['无糖','少糖','半糖','多糖','全糖']},
{'name':'温度','value':['热饮','常温','去冰','少冰','多冰']},
{'name':'忌口','value':['不要葱','不要蒜','不要香菜','不要辣']},
{'name':'辣度','value':['不辣','微辣','中辣','重辣']}
]
},
4.3.2.3 图片上传下载
- 这里直接使用之前的图片上传下载功能。
<el-form-item
label="菜品图片:"
prop="region"
class="uploadImg"
>
<el-upload
class="avatar-uploader"
action="/backend/page/upload/upload.do"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:on-change="onChange"
ref="upload"
>
4.3.3 后端代码
- 获取分类列表
/**
* 通过category中的type = 1/0 查出对于的菜品分类或是套餐分类,显示到前端的下拉框中
*
* 需求:查出所有菜品分类,并以优先级sort排序、再以updatetime排序
*
* 注意,lambdaQueryWrapper.eq(R column, Object val);这里是两个参数,一个是字段,一个是要匹配的值
* lambdaQueryWrapper.orderByDesc(R column) ,只要指定那个列就行了!因为不和上面那样,需要比较
*/
@GetMapping("/food/list/getCategory.do")
public RetObj getCategoryList(Category category){
LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Category::getType,category.getType())
.orderByAsc(Category::getSort)
.orderByDesc(Category::getUpdateTime);
List<Category> categoryList = categoryService.list(lambdaQueryWrapper);
log.info("查询出菜品:{}",categoryList);
return RetObj.success(categoryList);
}
- 对数据进行插入,前端提交的表单如下。涉及到两张表的操作。每个数据都是以json的形式提交的,注意使用注解@RequestBody 其中flavors里面的数据是一个一个的数组,每个数组中存的是json0。不可以直接封装到Dish实体类中,因为Dish实体类不包含favors对应的字段。
- 使用DTO构建一个类,这个类保护两个表的字段。
Lombok中的@Data : 注在类上,提供类的get、set、equals、hashCode、canEqual、toString方法
- 要充分理解DishFlavor这个表,每一条数据就是对应的name比如甜味,然后value对应“无糖…”,因此思路就是后端使用
List<DishFlavor>
来将每一条数据封装到一个DishFlavor对象中去,最后构成一个List集合。
package cn.edu.uestc.ruijitakeout.backend.dto;
import cn.edu.uestc.ruijitakeout.backend.domain.Dish;
import cn.edu.uestc.ruijitakeout.backend.domain.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class DishDto extends Dish {
/*
flavors中的所有信息都是通过一个数组传给后端,数组中每个元素是json类型
name、value 就是DishFlavor中最主要的信息,当然,还要绑定上对应的分类id,
后面使用注解,就可以将数组中json解析到DishFlavor对象中,因此,
[
{
"name": "甜味",
"value": "[\"无糖\",\"少糖\",\"半糖\",\"多糖\",\"全糖\"]",
"showOption": false
},
{
"name": "温度",
"value": "[\"热饮\",\"常温\",\"去冰\",\"少冰\",\"多冰\"]",
"showOption": false
}
]
*/
//错误的点:这里属性名要和前端的保持一致,否者无法注入!
//private List<DishFlavor> flavorsList = new ArrayList<>();
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
- 后端接收到前端数据并封装到DishDto类中后,应该编写service,使用DishDto对象同时操作两张表。dish、dish_flavor。其中dish_f表中的字段dish_id,使用DishDto中的dish_id来插入(口味肯定是描述某道菜的口味,相当于主键副键)。另外,前端一次性传过来多条数据,就是多个dish_flavor (表中行数),对应的是一道菜,多个口味选项。
public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavor
public void saveWithFlavor(DishDto dishDto);
...
- 加入事务控制,保证两张表的一致性
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish>
implements DishService{
@Resource
private DishFlavorService dishFlavorService;
@Override
//这个方法对多张表进行了操作,需要保证数据一致性!,在Application中也要加上对应的注解 @EnableTransactionManagement
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//为dish表添加数据
this.save(dishDto);
Long dishId = dishDto.getId();
List<DishFlavor> flavorsList = dishDto.getFlavorsList();
flavorsList.forEach(data -> data.setDish_id(dishId));
//为dishFlavor表添加数据
dishFlavorService.saveBatch(flavorsList);
}
}
4.4 菜品信息分页查询
4.4.1 思路整理
- 不止涉及一张表。主要是菜品表,要显示菜品名称、售价、售卖状态、最后操作时间、操作等。除此之外,还需要显示图片(之前已经写好了,会自己调用,发送完ajax请求后会自动在发一个请求进行图片下载,注意改下载的请求地址)、菜品分类的名称。
- 解决菜品分类的名称问题:这里菜品分类名称通过dish表中,只有一个category_id,而没有这个id对应的菜品名字),在后端返回的时候
return RetObj.success(pageInfo);
这个pageInfo实际上是Page<Dish>
类型里面并没有封装菜品名称这个属性,也就是说这个泛型Dish不满足要求。思路是之前就使用DishDto涵括了:dish(继承了它)、flavor。现在在这个DishDto中再加一个属性categoryName,就满足了。(当然自己写sql多表(dish表和catogory表)联合查询也能直接解决问题,因为mybatis_plus只有单表的 ) - 交互过程。其中name是模糊查询的条件
4.4.2 前端分析
前端代码平平无奇,和之前的很相似,这一部分难的在后端的处理上。
- 页面展示都是类似的。以图片下载为例,主要就是使用vue的组件
<el-table-column prop="image" label="图片" align="center">
<template slot-scope="{ row }">
<el-image style="width: auto; height: 40px; border:none;cursor: pointer;"
:src="getImage(row.image)"
:preview-src-list="[ `/backend/page/upload/download.do?name=${row.image}` ]" >
<div slot="error" class="image-slot">
<img src="./../../images/noImg.png" style="width: auto; height: 40px; border:none;" >
</div>
</el-image>
</template>
</el-table-column>
- 发送请求的vue代码,page和pageSize都有了初始值。返回值pageInfo下的records,就是封装好的每一条数据
methods: {
async init () {
//先构造一个对象
const params = {
page: this.page,
pageSize: this.pageSize,
name: this.input ? this.input : undefined
}
await getDishPage(params).then(res => {
if (String(res.code) === '1') {
this.tableData = res.data.records || []
this.counts = res.data.total
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
},
getImage (image) {
return `/backend/page/upload/download.do?name=${image}`
},
handleQuery() {
this.page = 1;
this.init();
},
// 查询列表接口
const getDishPage = (params) => {
return $axios({
url: 'backend/page/food/list/page.do',
method: 'get',
params
})
}
4.2.3 后端代码分析
- 解决菜品分类的名称问题:这里菜品分类名称通过dish表中,只有一个category_id,而没有这个id对应的菜品名字),在后端返回的时候
return RetObj.success(pageInfo);
这个pageInfo实际上是Page<Dish>
类型里面并没有封装菜品名称这个属性,也就是说这个泛型Dish不满足要求。思路是之前就使用DishDto涵括了:dish(继承了它)、flavor。现在在这个DishDto中再加一个属性categoryName,就满足了。 - 自己写sql、service等:可以自己写sql实现多表联合查询,返回的数据需要严格按照前端要求分装好,比如
List<T> records
代表的就是每一条数据,名字需要是record,还有total。 - 如果使用MP,由于MP无法进行多表联合的查询,思路:
- 先使用 dishService执行分页查询,注意,既然是dishService,在pageInfo中就只能把结果封装到Dish实体类中,不能够直接封装给DishDto类。
dishService.page(pageInfo,queryWrapper);
- 创建出
Page<DishDto> dishDtoPage = new Page<>();
,之后将原来的pageInfo的信息复制给dishDtoPage,但是要排除record,因为record是:List<Dish> records
,我们希望这个record中的List中的元素类型是DishDto。 - 遍历
List<Dish> records
,在其中new DishDto,把每一条Dish数据复制给DishDto,同时根据categoryId使用category表去查询categoryName,赋值给DishDto,这样我们最需要的字段得到赋值categoryName。 - 把上面的一个个对象dishDto,放入到list中,之后:
dishDtoPage.setRecords(list);
,就可以完成dishDtoPage的封装。
@GetMapping("list/page.do")
public RetObj pageController(int page, int pageSize, String name){
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>(page,pageSize);
LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(StringUtils.isNotBlank(name),Dish::getName, name)
.orderByDesc(Dish::getUpdateTime);
dishService.page(pageInfo,lambdaQueryWrapper);
//pageInfo中的record,里面的list存的类型是Dish,我们要DishDto,所以整个record就不拷贝了
BeanUtils.copyProperties(pageInfo,dishDtoPage,"record");
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map(item ->{
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Category category = categoryService.getById(item.getCategoryId());
//dishDto.setCategoryName(category.getName());
if (category != null){ //很重要
dishDto.setCategoryName(category.getName());
}
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return RetObj.success(dishDtoPage);
}
4.5 修改菜品信息
4.5.1 整体分析
- 和之前的修改是类似的,使用的还是add.html,还是需要回显,回显麻烦的是口味也要回显,又要使用DishDto
4.5.2 前端分析
把url中的id取出来,只有0和1,对于编辑和添加
(add这个界面是复用的,在修改的时候也是用这个页面,而且进行数据回显)
有id说明是根据id查询数据进行回显,不是添加,而是修改。
this.id = requestUrlParam('id')
this.actionType = this.id ? 'edit' : 'add'
if (this.id) {
this.init() //是修改页面,才执行这个init方法,主要就是去回显用的,看queryDishById方法
}
},
mounted() {
},
methods: {
async init () {
queryDishById(this.id).then(res => {
console.log(res)
if (String(res.code) === '1') {
this.ruleForm = { ...res.data }
this.ruleForm.price = String(res.data.price/100)
this.ruleForm.status = res.data.status == '1'
this.dishFlavors = res.data.flavors && res.data.flavors.map(obj => ({ ...obj, value: JSON.parse(obj.value),showOption: false }))
console.log('this.dishFlavors',this.dishFlavors)
// this.ruleForm.id = res.data.data.categoryId
// this.imageUrl = res.data.data.image
this.imageUrl = `/common/download?name=${res.data.image}`
} else {
this.$message.error(res.msg || '操作失败')
}
})
- 注意是一杠一值,后端记得要使用路径变量注解,并且使用括号{name}
// 查询详情
const queryDishById = (id) => {
return $axios({
url: `/backend/page/food/add/getInfo.do/${id}`,
method: 'get'
})
}
4.5.3 后端分析
- 回显:前端传来id,我们要根据id查询到以下这么多信息
- 显然,dish表中是没有口味相关信息的,需要把dish和dish_flavor中的信息都查出来,封装到dishDto中,传给前端进行回显
- 用户在前端修改好信息后,返回给后端的也是DishDto,也是要分步来操作:先跟新dish表,为了方便清空所有dishId对应的dishFlavor的记录条数,之后再根据DishDto新的dish_flavor进行插入。
-
回显的一个错误注意! 一个空格的错误会导致404,不要多空格这个地方
-
注意强转问题,可以使用类的复制来代替强制类型转换
-
回显代码
/**
* 工具前端传过来的id,查询信息进行回显,当然,还要同时查询口味表,把口味表的数据也进行回显
* @param id ,注意这里的id就是dish的id,不是categoryId,也不是flavor的id,而flavor中有dish——id
* @return
*/
@GetMapping("/add/getInfo.do/{id}")
public RetObj showBack(@PathVariable Long id){
//查到的信息直接向下转型----为啥不行,而要进行复制?
//编译器:拒绝了父类强转为子类
//DishDto dishDto = (DishDto) dishService.getById(id);
Dish dish = dishService.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//现在还需要把口味信息封装到DishDto中:List<DishFlavor>,前端自动展示
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.eq(DishFlavor::getDishId,id);
List<DishFlavor> list = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(list);
return RetObj.success(dishDto);
}
- 修改代码,注意考虑事务,测试的时候就发现,口味删除了,其他没修改成功,最好放在service中,加入事务控制!
@Transactional
@PutMapping("add/edit.do")
public RetObj editController(@RequestBody DishDto dishDto){
//Dish dish = (Dish)dishDto;
dishService.updateById(dishDto); //多态,dishDto也是dish!!!
//对于flavor,思考,还是把原来的全部删掉,再插入新的数据比较好处理。
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(lambdaQueryWrapper);
//dishService.saveWithFlavor(dishDto);不能用这个的原因是这个是新增,新增是插入数据,不是更新
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map(item -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
return RetObj.success("成功修改菜品!");
}