【瑞吉外卖】-项目复盘(四)
课程内容
- 文件上传下载
- 菜品新增(同时操作两张表需要添加事务,引入DTO对象)
- 菜品分页查询
- 菜品修改
1、文件上传下载
文件上传前端:
文件上传时,对页面的form表单有如下要求:
表单属性 | 取值 | 说明 |
---|---|---|
method | post | 必须选择post方式提交 |
enctype | multipart/form-data | 采用multipart格式上传文件 |
type | file | 使用input的file控件上传 |
简单提交html代码如下:
<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类型的参数即可接收上传的文件,例如:
上传代码实现
1)需要在application.yml中定义文件存储路径
reggie:
path: D:\img\
2)CommonController
文件上传的方法,通过MultipartFile类型的参数即可接收上传的文件,方法形参的名称需要与页面的file域的name属性一致。
上传逻辑:
- 获取文件的原始文件名,通过原始文件名获取文件后缀
- 通过UUID重新声明文件名,以免文件名称重复造成文件覆盖
- 创建文件存放目录
- 将上传的临时文件转存到指定位置
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.UUID;
/**
* 文件上传和下载
*/
@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();//abc.jpg
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;//dfsdfdfd.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);
}
}
下载代码实现
前端通过标签展示数据,src为/common/download?name=xxxx.jpg
因此,服务端需要接收页面的name参数,然后读取图片文件的数据,以流的形式写回浏览器
具体实现逻辑:
1)定义输入流,通过输入流读取文件内容
2)通过response对象,获取到输出流
3)通过response对象设置响应数据格式(image/jpeg)
4)通过输入流读取文件数据,然后通过上述的输出流写回浏览器
5)关闭资源
代码实现:
/**
* 文件下载
* @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();
}
}
2、菜品新增
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:dish
和dish_flavor
菜品的新增的逻辑:
1). 点击新建菜品按钮, 访问页面(backend/page/food/add.html), 页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2). 页面发送请求进行图片上传,请求服务端将图片保存到服务器(上传功能已实现)
3). 页面发送请求进行图片下载,将上传的图片进行回显(下载功能已实现)
4). 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
其中图片上传和下载的功能之前已经实现,因此,我们需要额外实现两个功能:
- 菜品分类数据列表查询
- 保存菜品信息
DishDto实体类
保存菜品信息的功能,前端页面传过来的数据除了,Dish的基本信息外,还有flavor等口味信息。而Dish实体类中是没有这些flavor属性的。因此,需要自定义一个实体类dto(data transfer object,数据传输对象),继承自Dish,并对Dish的属性进行拓展,增加flavors集合属性。
由于,我们不仅需要保存菜品的基本信息,还需要保存菜品的口味信息,需要操作两张表,所以我们需要在DishService中定义接口方法,在这个方法中需要保存上述的两部分数据:
/**
* 新增菜品
* @param dishDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
dishService.saveWithFlavor方法的具体逻辑如下:
- 保存菜品基本信息;
- 获取保存的菜品ID;
- 获取菜品口味列表,遍历列表,为菜品口味对象属性dishId赋值
- 批量保存菜品口味列表
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品,同时保存对应的口味数据
* @param dishDto
*/
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId = dishDto.getId();//菜品id
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> {
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
由于在 saveWithFlavor 方法中,进行了两次数据库的保存操作,操作了两张表,那么为了保证数据的一致性,我们需要在方法上加上注解@Transactional来控制事务。
在引导类上加注解@EnableTransactionManagement
Service层方法上加的注解@Transactional要想生效,需要在引导类上加上注解@EnableTransactionManagement,开启对事物的支持
3、菜品分页查询
在菜品列表展示,除了菜品的基本信息,还有两个字段略微特殊,图片字段和菜品分类。
在实体类Dish中,仅仅包含categoryId,不包含categoryName,因此在这里我们可以返回DishDto对象,在该对象中扩展一个属性categoryName,来封装菜品分类名称。
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName; //菜品分类名称
private Integer copies;
}
具体逻辑为:
1). 构造分页条件对象
2). 构建查询及排序条件
3). 执行分页条件查询
4). 遍历分页查询列表数据,根据分类ID查询分类信息,从而获取该菜品的分类名称
5). 封装数据并返回
4、菜品修改
分析步骤:
1)点击修改,携带菜品id参数跳转到add.html
2)进入add.html,页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示(已实现)
3)页面发送请求,获取菜品基本信息以及口味信息,用于回显
4)页面请求图片数据,用于菜品图片回显(已实现)
5)页面发送请求,将修改后菜品基本信息以及口味信息以json提交到服务端
遇到的注解
- @Value:获取yml中的参数,例如@Value(${reggie.path})
- @Transactional:如果同时操作了两张表,或者多次修改操作,并且这些操作涉及到事务的一致性问题。
- @EnableTransactionManagement:修饰引导类,开启对事务的支持