本课程以当前热门的外卖点餐为业务基础,业务真实、实用、广泛。基于流行的Spring Boot、mybatis
plus等技术框架进行开发,带领学员体验真实项目开发流程、需求分析过程和代码实现过程。学完本课程能够收获:锻炼需求分析能力、编码能力、bug调试能力,增长开发经验。
文件上传介绍:
文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。文件上传时,对页面的form表单有如下要求:
- method=“post” 采用post方式提交数据
- enctype=“multipart/form-data” 采用multipart格式上传文件
- type=“file” 使用input的file控件上传
在controller包中创建CommonController类,并创建upload方法
package com.reggie.controller;
import com.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
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;
/**
* 文件上传下载
*/
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@PostMapping("/upload")
public R<String> upload(MultipartFile file) {
log.info("上传文件成功,内容为:{}", file.toString());
return null;
}
}
调用file的方法,将临时文件转存到指定目录
try{
//将文件转存到指定位置
file.transferTo(new File("D:\\hello.jpg"));
}catch(IOException e){
throw new RuntimeException(e);
}
log.info("上传文件成功,内容为:{}",file.toString());
return null;
定义一个变量basePath存储基本路径,并在application.yml中配置初始值
@Value("${reggie.path}")
private String basePath;
使用uuid重新生成文件名,防止文件名重复造成文件覆盖,并通过字符串截取获取源文件拓展名,得到新文件名
String originalFilename=file.getOriginalFilename();
String suffix=originalFilename.substring(originalFilename.lastIndexOf("."));
//使用uuid重新生成文件名,防止文件名重复造成文件覆盖
String fileName=UUID.randomUUID().toString()+suffix;
String fileName=UUID.randomUUID().toString()+suffix;
判断选择的路径是否存在,若不存在则创建此目录
File dir=new File(basePath);
if(!dir.exists()){
dir.mkdirs();
}
upload方法完整代码如下:
@Value("${reggie.path}")
private String basePath;
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
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){
throw new RuntimeException(e);
}
log.info("上传文件成功,内容为:{}",file.toString());
return R.success(fileName);
}
文件下载介绍:
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
- 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
- 直接在浏览器中打开
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
创建输出流和输入流,分别用于下载图片和读取图片信息
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream=new FileInputStream(new File(basePath+name));
//输出流,通过输出流将文件写回浏览器,在浏览器展示图片
ServletOutputStream outputStream=response.getOutputStream();
创建while循环,当读取结束后向客户端返回图片数组bytes,并关闭资源
byte[]bytes=new byte[1024];
response.setContentType("image/jpeg");
int len=0;
while((len=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
fileInputStream.close();
outputStream.close();
文件下载完整代码如下:
@GetMapping("/download")
public void download(String name,HttpServletResponse response){
try{
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream=new FileInputStream(new File(basePath+name));
//输出流,通过输出流将文件写回浏览器,在浏览器展示图片
ServletOutputStream outputStream=response.getOutputStream();
byte[]bytes=new byte[1024];
response.setContentType("image/jpeg");
int len=0;
while((len=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭资源
fileInputStream.close();
outputStream.close();
log.info("文件下载成功,路径为:{}",basePath+name);
}catch(IOException e){
throw new RuntimeException(e);
}
}
需求分析
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,
并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。
数据模型
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。
所以在新增菜品时,涉及到两个表:
- dish 菜品表
- dish_flavor 菜品口味表
代码开发-准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类DishFlavor
package com.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
菜品口味
*/
@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;
}
Mapper接口DishFlavorMapper
package com.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.reggie.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}
业务层接▣DishFlavorService
package com.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.reggie.entity.DishFlavor;
public interface DishFlavorService extends IService<DishFlavor> {
}
业务层实现类DishFlavorServicelmpl
package com.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.reggie.entity.DishFlavor;
import com.reggie.mapper.DishFlavorMapper;
import com.reggie.service.DishFlavorService;
import com.reggie.service.DishService;
import org.springframework.stereotype.Service;
@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}
控制层DishController
package com.reggie.controller;
import com.reggie.service.DishFlavorService;
import com.reggie.service.DishService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 菜品管理
*/
@RestController
@RequestMapping("/dish")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;
}
在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:
1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显
4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
在CategoryController中创建list方法,用于根据条件查询分类数据,在其中创建条件构造器,并添加查询和排序条件
@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);
}
导入DishDto(位置:资料/dto),用于封装页面提交的数据
package com.reggie.DTO;
import com.reggie.entity.Dish;
import com.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
在DishController创建save方法用于新增菜品信息,并添加@PostMapping注解
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info("获取到菜品,内容为:{}",dishDto.toString());
return null;
}
在DishService创建saveWithFlavor方法
package com.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.reggie.DTO.DishDto;
import com.reggie.entity.Dish;
public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:Dish、dish_Flavor
public void saveWithFlavor(DishDto dishDto);
}
在DishServiceImpl中实现方法,调用save方法保存菜品基本信息
@Override
public void saveWithFlavor(DishDto dishDto){
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
}
通过stream流为dishid赋值,最后调用dishFlavorService的saveBatch方法保存flavors
Long dishId=dishDto.getId();
List<DishFlavor> flavors=dishDto.getFlavors();
flavors.stream().map((item)->{
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到口味表dish_Flavor
dishFlavorService.saveBatch(flavors);
为ReggieApplication类添加@EnableAutoConfiguration注解,并为saveWithFlavor方法添加事务处理@Transactional
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto){
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId=dishDto.getId();
List<DishFlavor> flavors=dishDto.getFlavors();
flavors.stream().map((item)->{
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到口味表dish_Flavor
dishFlavorService.saveBatch(flavors);
}
在DishController中调用刚刚创建的saveWithFlavor方法保存新增菜品
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info("获取到菜品,内容为:{}",dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/food/ist.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
在DishController中创建page方法用于获取分页数据,仿照之前的分页方法,添加构造器
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("接收到查询请求,page={},pageSize={},name={}",page,pageSize,name);
//构造分页构造器
Page<Dish> pageinfo=new Page<>(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper=new LambdaQueryWrapper();
//添加过滤条件
queryWrapper.like(StringUtils.isNotEmpty(name),Dish::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行查询
dishService.page(pageinfo,queryWrapper);
return R.success(pageinfo);
}
向DishController中注入CategoryService
@Autowired
private CategoryService categoryService;
创建一个DishDto泛型的list,通过调用BeanUtils的copyProperties方法拷贝pageinfo的属性,并使用stream流,向list中添加分类信息,完整代码如下:
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("接收到查询请求,page={},pageSize={},name={}",page,pageSize,name);
//构造分页构造器
Page<Dish> pageinfo=new Page<>(page,pageSize);
Page<DishDto> dishDtoPage=new Page<>(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper=new LambdaQueryWrapper();
//添加过滤条件
queryWrapper.like(StringUtils.isNotEmpty(name),Dish::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行查询
dishService.page(pageinfo,queryWrapper);
BeanUtils.copyProperties(pageinfo,dishDtoPage,"records");
List<Dish> records=pageinfo.getRecords();
List<DishDto> list=null;
list=records.stream().map((item)->{
DishDto dishDto=new DishDto();
Long categoryId=item.getCategoryId();
Category category=categoryService.getById(categoryId);
String categoryName=category.getName();
BeanUtils.copyProperties(item,dishDto);
dishDto.setCategoryName(categoryName);
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
代码开发-梳理交互过程
在开发代码之前,需要梳理一下修改菜品时前端页面(add.html)和服务端的交互过程:
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送jax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajx请求,将修改后的菜品相关数据以json形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
在DishService中创建方法getByIdWithFlavor,并DishServiceImpl在中实现方法
通过LambdaQueryWrapper查找到对应口味,并通过BeanUtils的copyProperties方法拷贝到dishDto对象中,最后返回dishDto
@Override
public DishDto getByIdWithFlavor(Long id) {
DishDto dishDto = new DishDto();
Dish dish = this.getById(id);
BeanUtils.copyProperties(dish,dishDto);
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(flavors);
return dishDto;
}
在DishService中创建方法updateByIdWithFlavor,并DishServiceImpl在中实现方法
this.updateById(dishDto);
清理当前来品对应口味数据 dish flavor表的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
添加当前提交过来的口味数据 dish flavor表的insert操作
Long dishId = dishDto.getId();
List<DishFlavor> flavors = dishDto.getFlavors();
flavors.stream().map((item) ->{
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
保存菜品口味数据到口味表dish_Flavor,完整代码如下:
@Override
public void updateByIdWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.updateById(dishDto);
//清理当前来品对应口味数据 dish flavor表的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
//添加当前提交过来的口味数据 dish flavor表的insert操作
Long dishId = dishDto.getId();
List<DishFlavor> flavors = dishDto.getFlavors();
flavors.stream().map((item) ->{
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到口味表dish_Flavor
dishFlavorService.saveBatch(flavors);
}