part 1
第二部分链接part 2
part 3 本文章
part 4
4.1 文件上传(后端为什么要返回文件名给前端、yml中自定义路径值并在类中取出、文件如何转存到指定位置、用UUID防止文件名称重复造成文件覆盖)
4.2 文件下载
4.3 新增菜品 (设计多表操作,事务保证一致性,DTO的使用,自己编写controller,值得学习)
4.4 菜品信息分页查询 (多表联合操作、Dto进一步使用)
4.5 修改菜品 ()
<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>
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>
// 是我们的conrtoller中的方法就拦截,如果不是的话,放行,给加载静态资源
if (!(handler instanceof HandlerMethod)) {
log.info("是静态资源或非controller中的方法,放行");
return true;
}
requestHeader中: Content-Type: multipart/form-data;
@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);
}
}
: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>
就会发送请求。如果返回数据就会通过img标签来展示图片methods: {
handleAvatarSuccess (response, file, fileList) {
//这里直接一个url就是请求,之前用的都是ajax,这里直接请求,
this.imageUrl = `/common/download?name=${response.data}`
},
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);
}
}
}
<el-form-item
label="菜品分类:"
prop="categoryId"
>
<el-select
v-model="ruleForm.categoryId"
placeholder="请选择菜品分类"
>
<el-option v-for="(item,index) in dishList" :key="index" :label="item.name" :value="item.id" />
el-select>
el-form-item>
// 获取菜品分类
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 || '操作失败')
}
})
},
// 获取菜品分类列表
const getCategoryList = (params) => {
return $axios({
url: '/category/list',
method: 'get',
params
})
}
// 按钮 - 添加口味
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':['不辣','微辣','中辣','重辣']}
]
},
<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"
>
/**
* 通过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);
}
Lombok中的@Data : 注在类上,提供类的get、set、equals、hashCode、canEqual、toString方法
List
来将每一条数据封装到一个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 flavorsList = new ArrayList<>();
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
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);
}
}
return RetObj.success(pageInfo);
这个pageInfo实际上是Page
类型里面并没有封装菜品名称这个属性,也就是说这个泛型Dish不满足要求。思路是之前就使用DishDto涵括了:dish(继承了它)、flavor。现在在这个DishDto中再加一个属性categoryName,就满足了。(当然自己写sql多表(dish表和catogory表)联合查询也能直接解决问题,因为mybatis_plus只有单表的 )前端代码平平无奇,和之前的很相似,这一部分难的在后端的处理上。
<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>
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
})
}
return RetObj.success(pageInfo);
这个pageInfo实际上是Page
类型里面并没有封装菜品名称这个属性,也就是说这个泛型Dish不满足要求。思路是之前就使用DishDto涵括了:dish(继承了它)、flavor。现在在这个DishDto中再加一个属性categoryName,就满足了。List records
代表的就是每一条数据,名字需要是record,还有total。dishService.page(pageInfo,queryWrapper);
Page dishDtoPage = new Page<>();
,之后将原来的pageInfo的信息复制给dishDtoPage,但是要排除record,因为record是:List records
,我们希望这个record中的List中的元素类型是DishDto。List records
,在其中new DishDto,把每一条Dish数据复制给DishDto,同时根据categoryId使用category表去查询categoryName,赋值给DishDto,这样我们最需要的字段得到赋值categoryName。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);
}
把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 || '操作失败')
}
})
// 查询详情
const queryDishById = (id) => {
return $axios({
url: `/backend/page/food/add/getInfo.do/${id}`,
method: 'get'
})
}
/**
* 工具前端传过来的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,前端自动展示
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.eq(DishFlavor::getDishId,id);
List<DishFlavor> list = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(list);
return RetObj.success(dishDto);
}
@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("成功修改菜品!");
}