该系列将记录一份完整的实战项目的完成过程,该篇属于第四天
案例来自B站黑马程序员Java项目实战《瑞吉外卖》,请结合课程资料阅读以下内容
该篇我们将完成以下内容:
- 文件上传下载
- 新增菜品
- 菜品信息分页查询
- 修改菜品
文件上传下载
由于是第一次接触文件上传下载,我们分为五个小阶段讲解
文件上传介绍
文件上传,也称为upload,是指将本地图片,视频,音频等文件上传到服务器上,可以供其他用户浏览下载的过程
首先我们介绍文件上传对前端的要求:
- 以POST方法提交数据
- 采用Multipart格式上传文件
- 使用input的file空间上传
尽管前端组件库提供了相应的上传组件,但这些组件底层仍旧采用上述要求的格式构造
然后我们介绍文件上传在后端的实现:
- 通常采用Apache的两个组件:commons-fileupload 和 commons-io
目前我们的Spring框架在Spring-web包下对文件上传进行了封装,简化了服务端代码
我们只需要在Controller中的方法声明一个MultipartFile类型的参数即可接收上传的数据
文件下载介绍
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程
通过浏览器进行文件下载,通常有两种表现形式:
- 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
- 直接在浏览器中打开
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程
文件上传代码实现
测试用例:
- 资料中为我们提供了一个文件上传下载的示例页面:upload.html
- 可以复制在backend/page/demo下,在我们的代码实现过程中进行调试
我们先来思考一下文件上传的逻辑:
- 我们会获得一个文件File,但这个文件是临时文件,所以我们需要将他储存在电脑中
- 我们的储存位置需要固定,但是这个文件的名称和后缀名需要重新书写,但还要保证名称随机不会重复,后缀名和原文件相同
接下来我们正式开始文件上传代码的实现:
- 书写一个CommonController服务层并构造基本框架:
package com.qiuluo.reggie.controller;
import com.qiuluo.reggie.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.CoyoteOutputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
/**
* 下载操作
* @param MultipartFile file 是文件上传的唯一必备代码,代表上传的数据
* 注意:file只是一个临时文件,当我们的request请求结束时,file也会消失,所以我们需要将它保存起来
* @return
*/
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
// 我们可以直接记录日志查看是否收集到file
log.info(file.toString());
}
}
- 书写内部代码,阐明书写思路:
package com.qiuluo.reggie.controller;
import com.qiuluo.reggie.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.CoyoteOutputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String BasePath;
/**
* 下载操作
* @param file 注意需要与前端传来的数据名一致
* @return
*/
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
// 由于我们的file是临时文件,我们需要将该文件保存在计算机上
try {
// 这个方法可以转载文件到指定目录
file.transferTo(new File("D:/hello.jpg"));
} catch (IOException e) {
e.printStackTrace();
}
// 注意:我们需要返回文件名,因为我们还需要让图片回显,回显就需要再次传入文件名来查询文件
return Result.success(fileName);
}
}
- 在yaml配置文件中书写全局变量,使存放路径固定:
# 我们定义了一个reggie.path的参数来存放我们存放文件的地址
reggie:
path: E:\imgs\
- 回到主函数中书写代码:
package com.qiuluo.reggie.controller;
import com.qiuluo.reggie.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.CoyoteOutputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;
/*
目前我们的文件存放已经有了固定的位置
但是我们的文件名以及后缀文件名还没有获得,我们将把它们变为动态的
*/
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
// 定义主路径
@Value("${reggie.path}")
private String BasePath;
/**
* 下载操作
* @param file 注意需要与前端传来的数据名一致
* @return
*/
@PostMapping("/upload")
public Result<String> upload(MultipartFile file){
// 注意:file只是一个临时文件,当我们的request请求结束时,file也会消失,所以我们需要将它保存起来
// 这个方法可以获得文件的原名称,但不推荐设置为文件名保存(因为可能出现重复名称导致文件覆盖)
String originalFilename = file.getOriginalFilename();
// 将原始文件的后缀截取下来
String substring = originalFilename.substring(originalFilename.lastIndexOf("."));
// UUID生成随机名称,文件名设置为 UUID随机值+源文件后缀
String fileName = UUID.randomUUID().toString() + substring;
// 判断文件夹是否存在,若不存在需创建一个
File dir = new File(BasePath);
if (!dir.exists()){
dir.mkdirs();
}
// 这个方法可以转载文件到指定目录
try {
file.transferTo(new File(BasePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return Result.success(fileName);
}
}
文件下载代码实现
同样我们来思考一下文件下载的逻辑:
- 首先我们根据前端传来的名字与我们设置的固定路径得到文件
- 然后我们直接将文件的内容采用输出流的形式输出到前端即可
我们的文件下载同时在CommonController中实现:
- 书写主体框架
package com.qiuluo.reggie.controller;
import com.qiuluo.reggie.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.CoyoteOutputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String BasePath;
/**
*
* @param name
* @param response
* @return
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
// 我们将在里面实现,读取文件,再返回文件的操作
}
}
- 实现内部逻辑
package com.qiuluo.reggie.controller;
import com.qiuluo.reggie.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.CoyoteOutputStream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String BasePath;
/**
* 文件下载
* @param name
* @param response
* @return
*/
@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();
}
// 关闭数据
fileInputStream.close();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
实际测试
我们打开之前转载过来的upload.html页面,传进照片后照片显示在页面中即为成功
新增菜品
我们的功能开发通常分为三部分
需求分析
当我们点击页面的菜品的新添时,会弹出页面,我们根据这个页面进行分析:
我们需要注意,这里还有一个菜品分类的下拉框,其中菜品分类的数据是通过请求到后台最后到数据库查询所得到的:
此外我们点击保存时,还会将一个菜品的相关信息请求返回后台:
我们需要注意的是:这里返回的并不仅仅只有菜品Dish的数据表,还包括了调料Dish Flavor的数据表
所以我们得到数据时不能采用Dish来获得数据
这里我们提出一个新的概念:
- DTO(Data Transfer Object)
这是一种设计模式之间传输数据的软件应用系统,它用于储存一些不属于同一个数据库中的一些信息
我们会新创一种DishDto实体类来继承Dish实体类并且新添一些关于DishFlavor的属性或方法来接收整个数据
在最后我们简单查看一下Dish和DishFlavor的数据表:
其中category_id表示所属分类的id
其中dish_id表示该调味的菜的id
代码实现
首先我们来实现一个简单的功能,获得菜品分类并返回前端页面:
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryServiceImpl categoryService;
/**
* 返回所有分类名称
*/
@GetMapping("/list")
public Result<List<Category>> list(Category category){
// 创造一个LambdaQueryWrapper来判断Type=1并设置排序顺序
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 Result.success(list);
}
}
接下来我们来实现存储双数据表到数据库的操作:
- 基本准备
# 首先我们来完成一些基本准备(我们的DishFlavor的操作也会放在DishController中实现)
实体类DishFlavor
数据层DishFlavorMapper
业务层接口DishFlavorService
业务层DishFlavorServiceImpl
服务层DishController
- 创建实体类DishDto(资料已提供)
// 我们通常单独创建一个包dto来装在DTO类
package com.qiuluo.reggie.dto;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class DishDto extends Dish {
// 在里面设置了List<DishFlavor> flavors 来装载 flavors
private List<DishFlavor> flavors = new ArrayList<>();
// categoryName 表示属于的分类名
private String categoryName;
private Integer copies;
}
- 新方法业务层接口实现
package com.qiuluo.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.DishFlavor;
import com.qiuluo.reggie.dto.DishDto;
public interface DishService extends IService<Dish> {
// 由于当前的默认方法无法满足我们的需求,我们创建新的方法来实现该操作
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavor
public void saveWithFlavor(DishDto dishDto);
}
- 新方法业务层实现
package com.qiuluo.reggie.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.domain.DishFlavor;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.mapper.DishMapper;
import com.qiuluo.reggie.service.DishService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
// 调入dishFlavor的业务层实现类
@Autowired
private DishFlavorServiceImpl dishFlavorService;
public void saveWithFlavor(DishDto dishDto){
// 1. 将菜品数据导入
this.save(dishDto);
// 2. 将Flavor导入(注意:Flavor传入时没有传入dishID,需要我们手动设置)
List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor flavor:flavors) {
flavor.setDishId(dishDto.getId());
}
dishFlavorService.saveBatch(flavors);
}
}
- 服务层实现
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/dish")
public class DishController {
@Autowired
private DishServiceImpl dishService;
/**
* 新增菜品
* @param dishDto
* @return
*/
@PostMapping
public Result<String> save(@RequestBody DishDto dishDto){
dishService.saveWithFlavor(dishDto);
return Result.success("新创成功");
}
}
实际测试
点击打开新建菜品,填写数据点击后数据库出现相关菜品即可
菜品信息分页查询
我们的功能开发通常分为三部分
需求分析
系统中菜品很多时,我们需要采用分页查询,使菜品多批出现防止拥挤导致页面阅读不方便
这里我们先给出完成操作后的图片:
我们需要注意的是这里的菜品分类直接出了分类的名称
但是我们的Dish中只给出了菜品分类的id,所以我们还需要借用id去查出所属分类的名称并重新赋值
代码实现
首先我们来完成简单的分页查询操作:
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/dish")
public class DishController {
@Autowired
private DishServiceImpl dishService;
@Autowired
private CategoryServiceImpl categoryService;
/**
* 分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public Result<Page> page(int page,int pageSize,String name){
// 构造基本Page
Page<Dish> pageImpl = new Page<>(page,pageSize);
// 进行查询
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(name != null,Dish::getName,name);
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
// 查询赋值,此时pageImpl里有值
dishService.page(pageImpl,queryWrapper);
return Result.success(pageImpl);
}
}
此时我们出来的页面中是无法查看到分类所属的:
所以我们需要设置包含有菜品分类名称的实体类作为Page的实现类参数才可以将菜品分类的名称传递到前端
我们只需要到前端代码中查看就可以注意到,商品分类这行上的数据属性名称为categoryName,所以我们采用DishDto来完成操作
我们将对代码进行修改:
package com.qiuluo.reggie.controller;标签:org,springframework,全攻略,瑞吉,外卖,import,reggie,qiuluo,com From: https://blog.51cto.com/u_12617333/5785732
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/dish")
public class DishController {
@Autowired
private DishServiceImpl dishService;
@Autowired
private CategoryServiceImpl categoryService;
/**
* 分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public Result<Page> page(int page,int pageSize,String name){
// 构造基本Page
Page<Dish> pageImpl = new Page<>(page,pageSize);
// 因为返回类型中多了一个categoryName,我们还需要构造一个DishDto类型的Page
Page<DishDto> dishDtoPage = new Page<>();
// 进行查询
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(name != null,Dish::getName,name);
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
// 查询赋值,此时pageImpl里有值
dishService.page(pageImpl,queryWrapper);
// 但是我们需要返回DishDto类型的Page,我们将pageImpl的值赋值到dishDtoPage中(不要赋值records,这个值是数据,我们需要单独处理)
// 我们借助工具类实现
BeanUtils.copyProperties(pageImpl,dishDtoPage,"records");
// 然后我们来处理dishDtoPage中的records值,我们首先将pageImpl的records提取出来
List<Dish> records = pageImpl.getRecords();
// 将该值除了CategoryName全都赋值给dishDtoPage的records(我们这里使用stream流进行局内单个赋值,采用foreach方法也相同)
List<DishDto> dishDtoList = records.stream().map((item) -> {
// 创建一个新dishDto作为返回实体
DishDto dishDto = new DishDto();
// 将正常属性赋值进去
BeanUtils.copyProperties(item,dishDto);
// 将CategoryName复制进去
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
// 完成dishDtoPage的results的内容封装
dishDtoPage.setRecords(dishDtoList);
return Result.success(dishDtoPage);
}
}