文章目录
前言
开发之前,我们先来明确一下开发需求,大致有分页查询,新增菜品,修改菜品,批量删除及批量停售菜品功能。
1. 文件上传与下载
1.1 文件上传介绍
文件上传,也成为upload,是指将本地文件,视频,音频等文件上传到服务器上,可以供用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发朋友圈都用到了文件上传功能。
文件上传时,对form表单有如下要求:
- 要有一个 form 标签,method=post 请求
- form 标签的 encType 属性值必须为 multipart/form-data 值
- 在 form 标签中使用 input type=file 添加上传的文件
- 编写服务器代码接收,处理上传的数据。
eg:
<body>
<form action="http://172.25.11.14:8080/09_EL_JSTL/uploadServlet" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username" /> <br>
头像:<input type="file" name="photo" /> <br>
<input type="submit" value="上传" />
</form>
</body>
服务端要接收客户端的页面上传的文件,通常会使用apache的两个组件
- commons-fileupload
- commons-io
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在controller的方法中声明一个MultipartFile类型的参数就可以接收上传 的文件
1.2 文件下载介绍
文件下载也称download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式化:
- 以附件形式下载,弹出保存对话框,保存到指定磁盘目录
- 直接在浏览器中打开
通过浏览器下载文件,本质就是服务端将文件以流的形式写回浏览器的过程
文件下载,页面端可以使用<img>标签来展示下载的图片
1.3 文件上传下载代码实现
我们通过客户端页面向服务端发送ajax请求,在下面请求的URL中我们可以看到路径以及请求方法,这里特别注意,我们文件上传下载的方法名称的参数名需要与客户端页面的表单数据中的name保持一致。
我们看一下文件下载请求的URL及请求方法,可以看到,下载请求的方法时GET,并且携带请求图片的名称。
文件上传代码:
这里需要注意一下,我们文件上传的路劲是我们在yml核心配置文件中配置的,然后使用@Value注解将其映射到我们的basePath。
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonPicController {
@Value("${bingbing.path}")
private String basePath;
/**
* 图片上传功能
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
String originalFilename = file.getOriginalFilename();//abc.jpg
//截取.jpg,但类型不局限于jpg
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 (Exception 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];
//将读到的内容放到byte数组里面,当len!=-1时,说明还没读完
while ((len=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
说明一下:文件上传与下载是为在接下来的菜品管理和套餐管理中图片展示及上传下载而准备的功能。现在我们将这两个接口实现,在接下去的开发中就能直接使用。我们可以提前看一下效果。
2. 新增菜品
2.1 需求分析
后台系统可以进行菜品的管理。通过新增菜品功能来添加一个新的菜品,在添加菜品的时候需要选择当前菜品所属的菜品分类,并且需要上传图片信息。
这里需要重点注意一个点:菜品的新增涉及两张表,分别是菜品表及菜品口味表
2.2 数据模型
新增菜品,其实就是将新增页面录入的信息添加到菜品dish表,如果添加了口味做法,则还需要向菜品口味表dish_flavor插入口味数据。
2.3 代码开发
2.3.1 准备工作
- 创建实体类DishFlavor
- 编写DishFlavorDao接口
- 编写DishFlavorService接口及其实现类
- 编写DishController
这几部分代码很常规,我们不做演示
2.3.2 梳理交互过程
- 页面发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器
- 页面发送请求进行图片下载,将上传的图片进行回显
- 填写新增表单信息,点击保存按钮,发送ajax请求,将菜品以json形式提交服务端。
第二第三部我们在文件上传下载的时候已经将功能写好了,到时候页面会调用相对应的方法进行上传和下载。
接下来我们完成第一和第四部分开发
2.3.3 获取菜品分类数据
我们点击新增菜品,页面就会发送ajax请求,注意,这里获取的分类数据在我们的category表中,因此我们请求的URL是category/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> categoryList = categoryService.list(queryWrapper);
return R.success(categoryList);
}
我们看一下新增菜品页面分类的下拉框,可以看到,菜品分类已经成功展示。
2.3.4 菜品保存
因为我们的菜品保存设计两张表,操作业务稍微复杂一点,因此,我们使用将业务写在业务层,然后controller调service。
这里我们先看一下页面给服务端提供的数据
可以看到,数据中包含菜品信息,又包含菜品口味信息,这时,我们需要用什么类型的对象去接收这些参数呢?用DIsh对象吗?还是Dish_Flavor对象???显然,这两个对象都不能满足需要,这时,我们就需要引入一个Dto对象,里面即包含菜品基本属性,又有菜品口味信息,我们将这个类取名叫DishDto.
DishDto
这个DIshDto类继承了Dish类,同时又将DishFlavor封装成一个集合对象。因此我们就能使用DIshDto来接收客户端返回的参数
/**
* dto:数据传输对象
* 用来封装客户端传递过来的参数信息
* 注意,这个类继承了Dish
*/
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
DishService
public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish和dishFlavor
public void saveWithFlavor(DishDto dishDto);
}
DishServiceImpl
@Service
public class DishServiceImpl extends ServiceImpl<DishDao, Dish> implements DishService {
@Autowired
private DishFlavorService dishFlavorService;
/**
* 添加菜品功能
* @param dishDto
*/
//因为涉及对表的插入操作,要注意是否插入成功,需要添加事务控制
//注意,需要在启动类添加事务控制开关@EnableTransactionManagement
@Transactional
@Override
public void saveWithFlavor(DishDto dishDto) {
//保存基本信息到dish表
this.save(dishDto);
//获取dishId,因为插入dishFlavor表中需要dishId
Long dishId = dishDto.getId();
List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor flavor : flavors) {
flavor.setDishId(dishId);
}
dishFlavorService.saveBatch(flavors);
}
在DishServiceImpl 中,我们需要重点注意两个点
- 因为是涉及对两张表的操作,因此需要开启事务注解开关,表明事务要么都成功,要么都失败。即在方法上添加 @Transactional,同时,需要在启动类上添加@EnableTransactionManagement事务开关注解,很重要。
- 我们在添加菜品信息时,只填写了口味表中的name和value属性,而dishId是没有添加的,因此,我们向dish_flavor插入数据的时候,需要对每个口味添加菜品id属性。
3. 分页查询
3.1 需求分析
不多废话,直接看界面效果,页面传递回来的参数有三个,分别是page,pageSize,name三个参数。
3.2 代码开发
代码开发之前,我们先梳理一下菜品分页查询时前端与服务端的交互过程
- 页面发送ajax请求,将分页参数提交给服务端,获取分页数据
- 页面发送请求,请求服务端进行图片下载,用于图片展示
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(Strings.isNotEmpty(name),Dish::getName,name);
queryWrapper.orderByDesc(Dish::getCreateTime);
Page<Dish> pageInfo = new Page<>(page,pageSize);
//因为菜品表中只有分类id,没有分类名称,因此需要使用dishDto类,里面含有分类名称
Page<DishDto> dishDtoPage = new Page<>();
dishService.page(pageInfo,queryWrapper);
//对象拷贝,但是不拷贝pageInfo中的records数据,因为我们要重新处理一下records
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
List<DishDto> dishDtoList = records.stream().map((record)->{
DishDto dishDto = new DishDto();
//将record中的属性拷贝到dishDto对象中
BeanUtils.copyProperties(record,dishDto);
Long categoryId = record.getCategoryId();//获取分类id去查询分类名称
Category category = categoryService.getById(categoryId);//拿到分类对象
if(category!=null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(dishDtoList);
return R.success(dishDtoPage);
}
这里需要说明两个点:
- 分页查询这里使用了一个对象拷贝 BeanUtils.copyProperties(a,b,“c”),即将a拷贝到b对象中,c是忽略条件。比如上述代码我们需要忽略records属性,因为我们需要对records属性进行改造。通过下图我们可以看到,records属性里面包含菜品id,而我们需要的是菜品名称,因此我们需要通过菜品id去获取菜品名称并进行展示。
- 这里的分页查询不能使用简单的Dish对象,因为在我们页面展示中有菜品分类这个列表属性,需要使用DishDto对象,因为DishDto里面才有菜品分类名称。
4. 修改菜品
4.1 需求分析
当我们点击修改按钮时,页面能根据我们所选的菜品进行基本的信息回显,其中包括
菜品分类,菜品图片显示以及菜品口味。
其中,菜品分类以及菜品图片显示我们在之前的开发中已经写完了,接下去我们就需要根据选择的菜品id进行菜品口味以及其他信息的数据回显。
4.2 代码开发
开发之前,我们先梳理一下前端界面与服务端的交互过程
- 页面发送ajax请求获取分类数据以及图片信息。
- 页面发送ajax请求,请求服务端根据选择的菜品id进行基本信息回显
- 点击保存按钮,发送ajax请求,将修改后的菜品相关数据以json格式提交到服务端
接下来我们就需要在服务端响应这三次的请求过程
而第一个请求我们之前开发中已经完成,我们需要完成下面两个请求功能
4.2.1 基本数据回显
基本信息回显,我们点击修改按钮,查看页面发送的ajax请求
这时,我们需要根据页面传递回来的id去查询对应的菜品以及口味信息
DishService
public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish和dishFlavor
public void saveWithFlavor(DishDto dishDto);
//修改菜品,根据id获取菜品及口味信息,进行数据回显
public DishDto getByIdWithFlavor(Long id);
}
DishServiceImpl : 首先我们根据id去获取菜品基本信息,然后将菜品基本信息拷贝到dishDto对象,因为dishDto封装了dishFlavor对象,最后根据dishId查询dishFlavor中相对应的口味信息,返回dishDto对象。
/**
* 根据id获取菜品及口味信息,进行数据回显
* @param id
* @return
*/
@Transactional
@Override
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息,从dish表中查询
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//查询菜品口味信息,从dishFlavor中查询
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> dishFlavorList = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}
dishController
/**
* 修改菜品功能的数据回显
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> getById(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
测试:数据能够进行回显
4.2.2 修改功能实现
修改功能涉及两张表,一个是dish表,一个是dishFlavor表,修改同时我们需要对两张表都进行操作
DishService接口:添加修改方法
public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish和dishFlavor
public void saveWithFlavor(DishDto dishDto);
//修改菜品,根据id获取菜品及口味信息,进行数据回显
public DishDto getByIdWithFlavor(Long id);
//修改菜品,接收客户端请求,修改菜品信息
public void updateWithFlavor(DishDto dishDto);
}
DishServiceImpl实现类:根据提交的信息对两张表进行操作。其中,对口味表的操作我们可以分为两步,一是根据id删除口味表中的数据,二是根据页面提交的数据对口味表进行插入操作。
/**
* 更新菜品信息
* @param dishDto
* @return
*/
@Override
public void updateWithFlavor(DishDto dishDto) {
//更新dish表中的菜品信息
this.updateById(dishDto);
//删除当前菜品中的口味信息--针对dish_Flavor表的删除操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
//添加当前提交过来的口味数据--针对dish_Flavor表的插入操作
Long dishId = dishDto.getId();
List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor flavor : flavors) {
flavor.setDishId(dishId);
}
dishFlavorService.saveBatch(flavors);
}
最后DishController调用service方法进行修改操作
/**
* 更新菜品信息
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.updateWithFlavor(dishDto);
return R.success("菜品修改成功");
}
这里我们将米饭改为白米饭
5. 删除和批量删除菜品
5.1 需求分析
菜品管理中,我们可能对已经不再售卖的菜品进行删除操作,甚至停售的菜品很多时,我们还需要进行批量删除,因此,我们就需要完善这两个功能。
5.2 代码开发
开发之前,我们梳理一下页面交互过程
- 页面发送ajax请求,将需要删除的id提交服务端
- 服务端根据提交的id,寻找对应的菜品,删除菜品并且删除菜品对应的口味信息
页面发送ajax请求并将需要删除的菜品id提交服务端,服务端就需要响应该次请求。
因为删除和批量删除发送的都是delete请求,就是传递的参数是单个或数组类型,因此我们可以定义一个数据去接收页面传递过来的参数。
public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish和dishFlavor
public void saveWithFlavor(DishDto dishDto);
//修改菜品,根据id获取菜品及口味信息,进行数据回显
public DishDto getByIdWithFlavor(Long id);
//修改菜品,接收客户端请求,修改菜品信息
public void updateWithFlavor(DishDto dishDto);
//批量删除菜品及口味表中的口味
public void deleteAll(Long[] ids);
}
@Override
@Transactional
public void deleteAll(Long[] ids) {
for (Long id : ids) {
//删除菜品表中的信息
this.removeById(id);
//删除口味表中的信息
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,id);
dishFlavorService.remove(queryWrapper);
}
}
/**
* 批量删除和批量删除
* @param id
* @return
*/
@DeleteMapping
public R<String> deleteByIds(Long[] id){
dishService.deleteAll(id);
return R.success("删除成功");
}
需要注意的是,删除的时候需要同时删除菜品及菜品id对应的口味信息,也是操作两张表。
6. 停售和批量停售启售
6.1 需求分析
对应售卖完的菜品,我们需要即时的更新其状态,以便在客户端展示的时候避免售卖完的菜品还显示能销售。
6.2 代码开发
开发之前,我们分析一下页面交互过程
- 页面发送ajax请求,将需要修改的状态id传递给服务端
- 服务端响应该次请求,修改其状态信息。
我们可以看到其请求路径有两级,因此我们的PostMapping就需要设置对应的路径信息。
首先我们需要在参数上使用@PathVariable进行路径参数占位,其次,传递的参数可能是单个id也可能是数组,因此我们使用集合去接收页面返回的参数信息,需要使用@RequestParam进行接收。
@PostMapping("/status/{status}")
public R<String> statusEdit(@PathVariable Integer status, @RequestParam List<Long> id){
log.info(id.toString());
Dish dish = new Dish();
for (Long dishId : id) {
dish.setId(dishId);
dish.setStatus(status);
dishService.updateById(dish);
}
return R.success("修改成功");
}
测试一下吧
OK,菜品开发到这里就全部开发完毕,接下去的套餐开发根菜品开发大同小异,只是操作的数据表不同,因此,套餐开发我们就不再讲解,感兴趣自己动手实践一下。
写了整整一下午,肝了快2w字,希望对小伙伴们能有帮助,看到这里的小伙伴记得点赞评论支持一下,蟹蟹啦!!!