JAVA项目实战瑞吉外卖—day4

文件上传下载

文件上传介绍

文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程 文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友國都用到了文件上传功能

文件上传时,对页面的form表单有如下要求:

  • method=”post” 采用post方式提交数据 使用页面进行文件上传必须是Post请求
  • enctype=”multipart/form-data” 采用multipart格式上传文件
  • type=”file” 使用input的file控件上传

​ 举例:

1
2
3
4
<form method= "post" action="/common/upload" enctype="multipart/form-data"> 
<input name="myFile" type "file" />
<input type="submit" value="提交"/>
</form>

服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:

  • commons-fileupload
  • commons-io

Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声 一个MultipartFile类型的参数即可接收上传的文件,file就是我们上传的文件

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    /**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){

//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
//名称必须和前端命名相同 否则无法接收到数据
log.info(file.toString());
return null;
}
}

文件下载介绍

文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。

通过浏览器进行文件下载,通常有两种表现形式:

  • 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

@Value(“${reggie.path}”)是yml配置的资源路径

1
2
3
# 图片转存位置
reggie:
path: D:\project\ruijiwaimai\imgs\

控制层代码编写

前端读取数据接口路径

image-20220809205018348

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

@Value("${reggie.path}")
private String basePath;

/**
* 文件上传
*
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file) {
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());

//原始文件名
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;

//创建一个目录对象
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);
}

/**
* 文件下载
*
* @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");

int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush();
}

//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}

}
}

因为是根据前端页面的请求路径写的接口,必须要前端约定的配置一样才能完成io流的读取和写入

新增菜品

这里进行菜品信息的操作是需要进行两张表的操作

  • 菜品信息表Dish
  • 菜品口味表DishFlavor

因为是操作两张表,未防止数据库出错,所以需要开启事务控制

@Transactional:在需要事务控制的方法上添加注解

注意在springboot启动类添加开启事务的功能:@EnableTransactionManagement

在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:

  • 1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据井展示到下拉框中
  • 2、页面发送请求进行国片上传,请求服务端将图片保存到服务器
  • 3、页面发送请求进行图片下载,将上传的国片进行回显
  • 4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

开发新增菜品功能,其实就是服务端编写代码去处理前端发送的四次请求即可

第一步

请求服务端获取菜品分类数据井展示到下拉框中 ,根据前端页面返回的Type属性将菜品分类在展示出来

image-20220809212050613

1
2
3
4
5
6
7
8
9
10
// 获取菜品分类
getDishList () {
getCategoryList({ 'type': 1 }).then(res => {
if (res.code === 1) {
this.dishList = res.data
} else {
this.$message.error(res.msg || '操作失败')
}
})
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 根据条件查询分类数据
* 根据前端传递的type属性进行查询
* type:1---菜品
* type:2---套餐
* @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);
}

第二步,第三步同上文件上传与下载 因为写的接口一样可以直接调用

第四步

将菜品相关数据以json形式提交到服务端

因为前端返回的数据中含有flavors的数据且是数组类型,原本封装的Dish实体不能将其一起封装成对象,但是其他属性可以封装为Dish对象属性,所以这里需要新一个DTO实体一起接收flavors和Dish属性

DTO:全称为Data Transfer Object,即数据传输对象,一般用于展示层和服务层之间的数据传输

数据格式

封装dto
1
2
3
4
5
6
7
8
9
@Data
public class DishDto extends Dish {
//里面存放的是菜品口味信息
private List<DishFlavor> flavors = new ArrayList<>();

private String categoryName;

private Integer copies;
}
编写接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {


@Resource
private DishFlavorService dishFlavorService;

/**
* 新增菜品,同时保存口味数据
* @param dishDto
*/
@Transactional
public void saveWithFlavor(DishDto dishDto) {

//保存菜品信息到dish
this.save(dishDto);

Long disId = dishDto.getId();//菜品id

//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();

//使用stream流处理,将菜品口味对应到新建立的菜品id上
flavors = flavors.stream().map((item)->{
item.setDishId(disId);
return item;
}).collect(Collectors.toList());

//保存口味信息到dish_flavor
dishFlavorService.saveBatch(flavors);

}
}

分页查询

这部分的分页查询较为麻烦

我整理一下自己的思路在注解上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 菜品信息分页查询
* @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);
//因为在前端页面上需要得到CategoryName信息,要使用dto来封装
Page<DishDto> dishDtoPage = new Page<>(page,pageSize);

//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<Dish>();

//添加过滤条件
queryWrapper.like(name!=null,Dish::getName,name);

//添加排序
queryWrapper.orderByDesc(Dish::getUpdateTime);

//进行分页查询
dishService.page(pageInfo,queryWrapper);

//对象拷贝
//将pageInfo的属性信息拷贝到dishDtoPage中,但是要忽视records列表覆盖,records列表记录着菜品详情信息
//records的详情信息我们需要自己整理,需要多添加一个CategoryName属性,才能在前端页面显示出菜品分类名
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");

//将菜品信息拿出来,根据records流信息查询到分类id,由分类id获得属性名
List<Dish> records = pageInfo.getRecords();
//将查询的数据封装到DishDto属性的list中,含有菜品详细信息以及菜品分类名信息
//使用lambda表达式对菜品信息进行整理,一一对应上分类菜品的名称,列如:川菜....
List<DishDto> list = records.stream().map((item)->{
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);

Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象 因为需要查询到菜品分类的名称所以要调用categoryService来查询到对应的数据
Category category = categoryService.getById(categoryId);
if (category!=null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//返回封装好的dto数据
return dishDto;
}).collect(Collectors.toList());

//给原本dishDtoPage的records赋值并且携带CategoryName
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}

修改菜品

操作两张表需要进行事务处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 更新菜品信息 同时更新菜品的口味信息
* @param dishDto
*/
@Transactional
public void updateWithFlavor(DishDto dishDto) {

//更新dish表信息,菜品基本信息更新
this.updateById(dishDto);

//更新菜品口味信息
//先清理原来菜品的口味信息---dishFlavor表的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);

//添加当前提交的口味信息---dishFlavor表的insert操作
List<DishFlavor> flavors = dishDto.getFlavors();

//使用stream流处理,将菜品口味对应到新建立的菜品id上
flavors = flavors.stream().map((item)->{
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());

dishFlavorService.saveBatch(flavors);
}

删除菜品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
   /**
* 逻辑删除
* 关联到套餐
* 如果套餐在售卖且含有菜品,则菜品不能够删除
* @param ids
* @return
*/
public R<String> deleteByIdWithFlavor(List<Long> ids) {

//根据菜品id在stemeal_dish表中查出哪些套餐包含该菜品
LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealDishLambdaQueryWrapper.in(SetmealDish::getDishId,ids);

List<SetmealDish> SetmealDishList = setmealDishService.list(setmealDishLambdaQueryWrapper);
//如果菜品没有关联套餐,直接删除就行
if (SetmealDishList.size() == 0){
//这个deleteByIds中已经做了菜品起售不能删除的判断力
dishService.deleteByIds(ids);
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(DishFlavor::getDishId,ids);
dishFlavorService.remove(queryWrapper);
return R.success("菜品删除成功");
}

//如果菜品有关联套餐,并且该套餐正在售卖,那么不能删除
//得到与删除菜品关联的套餐id
ArrayList<Long> Setmeal_idList = new ArrayList<>();
for (SetmealDish setmealDish : SetmealDishList) {
Long setmealId = setmealDish.getSetmealId();
Setmeal_idList.add(setmealId);
}

//查询出与删除菜品相关联的套餐
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.in(Setmeal::getId,Setmeal_idList);
List<Setmeal> setmealList = setmealService.list(setmealLambdaQueryWrapper);
//对拿到的所有套餐进行遍历,然后拿到套餐的售卖状态,如果有套餐正在售卖那么删除失败
for (Setmeal setmeal : setmealList) {
Integer status = setmeal.getStatus();
if (status == 1){
return R.error("删除的菜品中有关联在售套餐,删除失败!");
}
}


//要删除的菜品关联的套餐没有在售,可以删除
//这下面的代码并不一定会执行,因为如果前面的for循环中出现status == 1,那么下面的代码就不会再执行
dishService.deleteByIds(ids);
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(DishFlavor::getDishId,ids);
dishFlavorService.remove(queryWrapper);


//删除菜品口味信息
LambdaQueryWrapper<DishFlavor> queryWrapper1 = new LambdaQueryWrapper<>();
queryWrapper1.in(DishFlavor::getDishId,ids);
dishFlavorService.remove(queryWrapper1);

return R.success("菜品删除成功");
}


/**
* 查询菜品有没有在售卖的状态
*
* @param ids
*/
public void deleteByIds(List<Long> ids) {
//构造条件查询器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//先查询该菜品是否在售卖,如果是则抛出业务异常
queryWrapper.in(ids != null, Dish::getId, ids);
List<Dish> list = this.list(queryWrapper);
for (Dish dish : list) {
Integer status = dish.getStatus();
//如果不是在售卖,则可以删除
if (status == 0) {
this.removeById(dish.getId());
} else {
//此时应该回滚,因为可能前面的删除了,但是后面的是正在售卖
throw new CustomException("删除菜品中有正在售卖菜品,无法全部删除");
}
}

}

启售停售

启售停售功能是一个更新操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 对菜品批量或者是单个 进行停售或者是起售
* @return
*/
@PostMapping("/status/{status}")
//这个参数这里一定记得加注解才能获取到参数,否则这里非常容易出问题
public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids){
//log.info("status:{}",status);
//log.info("ids:{}",ids);
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ids !=null,Dish::getId,ids);
//根据数据进行批量查询
List<Dish> list = dishService.list(queryWrapper);

for (Dish dish : list) {
if (dish != null){
dish.setStatus(status);
dishService.updateById(dish);
}
}
return R.success("售卖状态修改成功");
}