一、文件上传——upload
1.1 介绍
** 将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程**
** 文件上传时,对页面的form表单有如下要求:**
**method="post" ** 采用post方式提交数据
** encttype=""multipart/form-data" ** 采用multipart格式上传文件
**type="file" **使用input的file空间上传
服务端接收客户端页面上传的文件时,通常会使用Apache的两个组件:
- commons-fileupload
- **commons-io ** (对文件的操作本质上还是对流的操作)
Spring框架在sprin-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller方法中声明一个MultipartFile类型的参数即可接收上传的文件
1.2 前端代码实现
使用的element-ui组件进行实现,这段代码的编写既有上传,也有下载,其中:src就是下载图片
<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>
**action="/common/upload" 代表controller层的请求路径**
1.3 后端代码实现
我们可以先发一个请求查看一下MultipartFile对象里面包括什么
我们仔细看发现了一个存储路径,也就是说当前传过来的文件被存储在了一个临时的位置(临时目录),我们要做的就是把这个文件存储到一个指定的位置。如果不转存的话,当我们此次请求结束后,这个文件就不存在了。
我们在请求成功后返回了图片的文件名+后缀名,目的就是为了下载图片的时候将图片名称传过去然后找到对应的图片
/**
* 文件的上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
*
* @param file 是一个临时文件,需要转存的指定的位置,否则本次请求后此文件就会消失
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file) throws IOException {
log.info("上传文件:{}", file.toString());
// 1.拼接转存的位置
// 获取文件的原始名
String originalFilename = file.getOriginalFilename(); //abc.jpg
// 文件名后缀
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
// 也可以使用UUID生成文件名,防止文件名称重复导致覆盖原文件
String filename = UUID.randomUUID().toString(); //缺少后缀
String url= basePath + filename+suffix;
// 2. 判断转存的目录是否存在
// File既可以代表一个目录,又可以代表一个文件
File dir = new File(basePath);
// 判断当前目录是否存在
if (!dir.exists()){
// 不存在的时候进行创建
dir.mkdirs();
}
// 转存临时文件到指定的位置 参数是一个URL路径
file.transferTo(new File(url));
return R.success(filename+suffix);
}
}
二、文件下载——download
2.1 介绍
指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
- ** 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录**
- ** 直接在浏览器中打开**
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写会浏览器的过程。
2.2 前端代码编写
<img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
2.3 后端代码编写
/**
* 文件下载
*
* @param name 图片名
* @param response 图片要通过响应流获得
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response) throws IOException {
// 0. 设置文件格式
response.setContentType("image/jpeg");
// 1. 输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
// 2. 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();
}
// 3.关闭流
outputStream.close();
fileInputStream.close();
}
三、 前端总代码
<!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" />
</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>
四、 应用场景
4.1 数据库表
4.1.1 菜品表
4.1.2 菜品口味表
4.1.3 菜品分类及菜品套餐表
4.2 实体类
4.2.1 菜品实体类
/**
菜品
*/
@Data
public class Dish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//商品码
private String code;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//顺序
private Integer sort;
@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;
}
4.2.2 菜品口味实体类
@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;
}
4.2.3 菜品分类及菜品套餐实体类
@Data
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//类型 1 菜品分类 2 套餐分类
private Integer type;
//分类名称
private String name;
//顺序
@TableField("sort")
private Integer sort;
//创建时间
@TableField(value = "create_time",fill = FieldFill.INSERT)
private LocalDateTime createTime;
//更新时间
@TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
//创建人
@TableField(value = "create_user",fill = FieldFill.INSERT)
private Long createUser;
//修改人
@TableField(value = "update_user",fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
//是否删除
// @TableField(value = "is_deleted")
// private Integer isDeleted;
}
4.3 新增菜品
说明:
** 1. 页面发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中**
** 2. 页面发送请求进行图片上传,请求服务端将图片保存到服务器**
** 3. 页面发送请求进行图片下载,将上传的图片进行回显**
** 4. 点击保存按钮,发送ajax请求将菜品相关信息保存到数据库**
** 重要点: 价格是以分为单位!!!!!!!! 防止精度缺失!!!!!!!**
4.3.1 菜品分类下拉框
不是说看见对象就用@RequestBody注解,而是看前端是否提交过来的是json数据,后端用对象来接收才用@RequestBody注解
前端提交过来的得是json格式的数据才用@RequestBody注解
因为这个是前端页面直接通过请求地址提交过来的type,所以不需要使用@RequestBody注解
** 那我们这里为什么要使用一个Category对象来进行接收呢?**
防止前端以后再传入其他参数,所以我们直接用整个Category进行接收
/**
* @param category 这个地方我们没有选择声明一个String的type接收,而是选择一个实体类,
* 原因是防止以后前端再转过来其他的参数,我们还要再修改这个地方
* 总的来说就是通用性更强
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category) {
log.info("获取菜品分类的下拉列表");
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);
}
4.3.2 封装DTO
** DTO:全称Data Transfer Object ,即数据传输对象,一般用于展示层与服务层之间的数据传输。**
我们可以自己看一下这些请求参数,我们并没有一个类能完整的接收这些参数,所以我们要创建一个类来进行接收
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
**子类不能继承父类的私有属性,但是如果子类中公有的方法影响到了父类私有属性,那么私有属性是能够被子类使用的**。
4.3.3 添加菜品
因为同时操作两张表,开启事务可以保证两张表,同时操作成功或者失败,不然第一个表操作成功后如果出现异常,第二张会操作失败,这样数据就出问题了
@Override
@Transactional // 操作多张表,加一个事物的处理 ,记得在启动类添加一个EnableTransactionManagement
public void saveWithFlavor(DishDto dishDto) {
// 保存菜品的基本信息到菜品表dish
this.save(dishDto);
// 保存菜品口味数据到菜品口味表dish_flavor
// 但是我们刚刚分析的时候知识封装了name与value, id与dishId没有封装
Long dishId = dishDto.getId();
List<DishFlavor> dishFlavorList = dishDto.getFlavors();
dishFlavorList = dishFlavorList.stream().map((item) -> {
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
// 传入集合,批量保存
dishFlavorService.saveBatch(dishFlavorList);
}
4.4 菜品分页查询
与之前分页查询的不同就是加入了图片下载
1. 页面发送ajax请求,将分页查询参数提交到服务端,获取分页数据
2. 页面发送请求,请求服务端进行图片下载,用于图片展示
** 总共两次请求**
4.4.1 构建DTO
** 注意看下图中标红的地方,我们需要的是一个菜品分类的名称,而不是一个id,所以我们现有的Dish实体类无法直接返回到前端,要进行处理**
** 我们之前创建的DishDTO就很符合要求**
4.4.2 菜品分页查询
这个地方有两个点,一个是数据的Copy,另一个是数据Copy要忽略records,具体原因代码中有详细解释
@GetMapping("/page")
public R<Page<DishDto>> page(int page, int pageSize, String name) {
Page<Dish> pageInfo = new Page<>(page, pageSize);
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();
queryWrapper.like(Strings.isNotEmpty(name),Dish::getName,name);
queryWrapper.orderByDesc(Dish::getUpdateTime);
dishService.page(pageInfo, queryWrapper);
Page<DishDto> dishDtoPage = new Page<>();
// 进行对象的拷贝 如果再加入第三个参数,就是在拷贝时要忽略的参数,比如我们这里就要忽略 "records"参数
// 这个地方我们为什么要忽略呢?
// 不忽略的话,我们下面运行会出现 com.reggie_take_out.entity.Dish cannot be cast to com.reggie_take_out.dto.DishDto
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
// 将菜品的id改为菜品名
List<Dish> records = pageInfo.getRecords();
List<DishDto> dishDtoList = records.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
dishDto.setCategoryName(category.getName());
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(dishDtoList);
return R.success(dishDtoPage);
}
4.5 修改菜品功能
** 1. 页面发送ajax请求,获取分类数据,用于分类下拉框数据展示 (之前已经实现 )**
** 2. 页面发送ajax请求,根据id查询当前菜品的信息,用于菜品信息回显 **
** 3. 页面发送请求,请求服务端进行图片下载,用于图片回显(之前已经实现)**
** 4. 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以JSON形式提交到服务端**
4.5.1 数据回显操作
在方框内的数据都需要回显,明显Dish实体类不够用,所以不能返回Dish实体类。
依旧使用我们之前的DishDto类。 新增功能时的请求体的接收类与数据回显操作时返回数据的类是同一个。
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
@Override
public DishDto getByIdWithFlavor(Long id) {
//备用
DishDto dishDto = new DishDto();
//查询菜品基本信息,从dish表查询
Dish dish = this.getById(id);
BeanUtils.copyProperties(dish,dishDto);
//查询当前菜品对应的口味信息,从dish_flavor表查询
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> dishFlavorList = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}
4.5.2 修改菜品信息
参照新增菜品进行修改即可
@Override
public void updateWithFlavor(DishDto dishDto) {
// 1.更新菜品表基本信息 因为DishDTO是Dish的子类,所以传进去是没有问题的
this.updateById(dishDto);
// 2. 更新口味表基本信息
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
// 有个简便方式,将当前菜品的口味数据删除,然后将此次修改后的直接添加即可
// 从dto中获取口味表
List<DishFlavor> flavors = dishDto.getFlavors();
// 因为从前端传过来的时候只有name与value ,所以我们需要再改变一下flavors
flavors = flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
// 传入集合,批量保存
dishFlavorService.saveBatch(flavors);
}
五、 新增套餐功能
5.1 数据库对应表
5.1.1 套餐表 setmeal
5.1.2 套餐菜品关系表
5.2 实体类
5.2.1 套餐实体类
/**
* 套餐
*/
@Data
public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//分类id
private Long categoryId;
//套餐名称
private String name;
//套餐价格
private BigDecimal price;
//状态 0:停用 1:启用
private Integer status;
//编码
private String code;
//描述信息
private String description;
//图片
private String image;
@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;
}
5.2.2 套餐菜品关系类
/**
* 套餐菜品关系
*/
@Data
public class SetmealDish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//套餐id
private Long setmealId;
//菜品id
private Long dishId;
//菜品名称 (冗余字段)
private String name;
//菜品原价
private BigDecimal price;
//份数
private Integer copies;
//排序
private Integer sort;
@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;
}
5.3 新增菜品
梳理前后端交互的过程:
1. 页面发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中 2. 页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中 3. 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中 4. 页面发送请求图片上传 5. 页面发送请求图片下载 6. 保存按钮发送ajax请求,将套餐相关数据以JSON形式提交到服务端
5.3.1 根据分类查询菜品
/**
* 根据条件查询对应的菜品数据
* <p>
* 传入一个Dish对象 比传入一个 categoryId通用性更好
*
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish) {
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId())
// 起售
.eq(Dish::getStatus, 1)
.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> dishList = dishService.list(queryWrapper);
return R.success(dishList);
}
5.3.2 封装DTO
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName;
}
5.3.3 新增套餐操作
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
// 保存套餐的基本信息,操作setmeal,执行insert操作
this.save(setmealDto);
// 保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
// 还是和之前一样,这个里面存的只有dishId,setmealId是没有的,所以我们要加上
List<SetmealDish> setmealDishList = setmealDto.getSetmealDishes();
List<SetmealDish> collect = setmealDishList.stream().map((item) -> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
boolean b = setmealDishService.saveBatch(collect);
}
5.4 分页查询
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name !=null,Setmeal::getName,name)
.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
// 对线拷贝
Page<SetmealDto> setmealDtoPage = new Page<>();
BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> collect = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(item, setmealDto);
setmealDto.setCategoryName(item.getName());
return setmealDto;
}).collect(Collectors.toList());
setmealDtoPage.setRecords(collect);
return R.success(setmealDtoPage);
}
**这个地方的套餐分类得益于我们在数据库中的冗余字段,不必再让我们查询数据库 **
5.5 删除套餐与批量删除套餐
/**
* 删除套餐,同时删除套餐和菜品的关联数据
* 只有停售的套餐才能够删除
* @param ids
*/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
// 查询套餐的状态,看是否可以删除
LambdaQueryWrapper<Setmeal> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.in(Setmeal::getId,ids)
// 说明在售卖中
.eq(Setmeal::getStatus,1);
int count = this.count(lambdaQueryWrapper);
// 不能删除抛出异常
if (count>0){
throw new CustomException("正在售卖中,不能删除");
}
// 可以删除,先删除套餐表中的数据
this.removeByIds(ids);
// 删除关系表中的数据
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(queryWrapper);
}
版权归原作者 我爱布朗熊 所有, 如有侵权,请联系我们删除。