黑马程序员Java项目实战《瑞吉外卖》,轻松掌握springboot + mybatis plus开发核心技术的真java实战项目——第三部分
- 1. 菜品管理的业务功能
- 1.1 文件的上传和下载🙇♂️
- 1.2 新增菜品
- 1.3 接收页面提交的数据🙇♂️(涉及两张表)
- 1.4 菜品信息分页查询
- 1.5 修改菜品🙇♂️(回显和保存修改都是两张表)
- 1.5.1 菜品信息的回显
- 1.5.2 保存修改🙇♂️
- 1.6 需要自己单独实现的功能
- 1.6.1 菜品启售和停售
- 1.6.2 菜品批量启售和批量停售
- 1.6.3 菜品的批量删除
- 1.6.4 菜品删除逻辑优化
1. 菜品管理的业务功能
1.1 文件的上传和下载🙇♂️
整体介绍:
文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。
文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件.上传功能。
文件上传时,对页面的form表单有如下要求:
method=" post"
——采用post
方式提交数据enctype="multipart/form-data"
——采用multipart
格式上传文件type="file"
——使用input
的file
控件上传
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
commons-fileuplolad
commons-io
Spring框架在spring-web
包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile
类型的参数即可接收.上传的文件,例如:
/*** * @param file* @return*/@PostMapping("/upload")public R<String> upload(MultipartFile file){System.out.println("file = " + file);return null;}
CommonController.java
package com.itheima.reggie.controller;import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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 java.io.File;
import java.io.IOException;/*** 文件上传和下载*/
@RestController
@Slf4j
@RequestMapping("/common")
public class CommonController {@PostMapping("/upload")public R<String> upload(MultipartFile file){log.info(file.toString());try {file.transferTo(new File("F:\\Two or three things on the table\\hello.jpg"));} catch (IOException e) {throw new RuntimeException(e);}return null;}
}
LoginCheckFilter.java
package com.itheima.reggie.filter;import com.alibaba.fastjson.JSON;
import com.itheima.reggie.common.BaseContext;
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {//路径匹配器,支持通配符public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;// 1. 获取本次请求的URIString requestURI = httpServletRequest.getRequestURI();//定义不需要处理的请求路径 比如静态资源(静态页面我们不需要拦截,因为此时的静态页面是没有数据的)String[] urls = new String[]{"/employee/login","/employee/logout","/backend/**","/front/**","/common/**"};//做调试用的//log.info("拦截到请求:{}",requestURI);// 2. 判断本次请求是否需要处理boolean check = check(urls, requestURI);// 3. 如果不需要处理,则直接放行if(check){filterChain.doFilter(httpServletRequest,httpServletResponse);return;}// 4. 判断登录状态,如果已登录,则直接放行if (httpServletRequest.getSession().getAttribute("employee")!=null){//log.info("用户已登录,用户id为:{}",httpServletRequest.getSession().getAttribute("employee"));Long empId = (Long) httpServletRequest.getSession().getAttribute("employee");BaseContext.setCurrentId(empId);filterChain.doFilter(httpServletRequest,httpServletResponse);return;}// 5. 如果未登录则返回未登录结果httpServletResponse.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));return;}public boolean check(String[] urls,String requestURI){for (String url : urls) {//把浏览器发过来的请求和我们定义的不拦截的url做比较,匹配则放行boolean match = PATH_MATCHER.match(url, requestURI);if(match){return true;}}return false;}
}
文件下载
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
- 以
附件形式下载
,弹出保存对话框,将文件保存到指定磁盘目录 直接在浏览器中打开
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
文件上传,页面端可以使用ElementUl提供的.上传组件。
可以直接使用资料中提供的上传页面,位置:资料/文件.上传下载页面/upload.html
upload.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>文件上传</title><!-- 引入样式 --><link rel="stylesheet" href="../../plugins/element-ui/index.css" /><link rel="stylesheet" href="../../styles/common.css" /><link rel="stylesheet" href="../../styles/page.css" />
</head>
<body><div class="addBrand-container" id="food-add-app"><div class="container"><el-upload class="avatar-uploader"action="/common/upload":show-file-list="false":on-success="handleAvatarSuccess":before-upload="beforeUpload"ref="upload"><img v-if="imageUrl" :src="imageUrl" class="avatar"></img><i v-else class="el-icon-plus avatar-uploader-icon"></i></el-upload></div></div><!-- 开发环境版本,包含了有帮助的命令行警告 --><script src="../../plugins/vue/vue.js"></script><!-- 引入组件库 --><script src="../../plugins/element-ui/index.js"></script><!-- 引入axios --><script src="../../plugins/axios/axios.min.js"></script><script src="../../js/index.js"></script><script>new Vue({el: '#food-add-app',data() {return {imageUrl: ''}},methods: {handleAvatarSuccess (response, file, fileList) {this.imageUrl = `/common/download?name=${response.data}`},beforeUpload (file) {if(file){const suffix = file.name.split('.')[1]const size = file.size / 1024 / 1024 < 2if(['png','jpeg','jpg'].indexOf(suffix) < 0){this.$message.error('上传图片只支持 png、jpeg、jpg 格式!')this.$refs.upload.clearFiles()return false}if(!size){this.$message.error('上传文件大小不能超过 2MB!')return false}return file}}}})</script>
</body>
</html>
文件下载代码实现
文件下载,页面端可以使用<img>
标签展示下载的图片
<img v-if = "imageUrl" :src="imageUrl" class="avatar"></img>
后端具体代码的实现:
yml配置文件:配置上传图片的存储位置;
reggie:path: E:\reggie\
package com.itheima.reggie.controller;import com.itheima.reggie.common.R;
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;/*** 文件上传和下载*/
@RestController
@RequestMapping("/common")
public class CommonController {@Value("${reggie.path}")private String basePath;/*** 文件的上传* @param file* @return*/@PostMapping("/upload")public R<String> upload(MultipartFile file){//这个file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除//拿到文件的原始名String originalFilename = file.getOriginalFilename();//拿到文件的后缀名 比如 .png .jpgString 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);}@GetMapping("/download")public void download(String name, HttpServletResponse response){try {//输入流,通过输入流读取文件内容 这里的name是前台用户需要下载的文件的文件名//new File(basePath + name) 是为了从存储图片的地方获取用户需要的图片对象FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));//输出流,通过输出流将文件写回浏览器ServletOutputStream outputStream = response.getOutputStream();//设置写回去的文件类型response.setContentType("image/jpeg");//定义缓存区,准备读写文件int len = 0 ;byte[] buff = new byte[1024];while ((len = fileInputStream.read(buff)) != -1){outputStream.write(buff,0,len);outputStream.flush();}//关流outputStream.close();fileInputStream.close();}catch (Exception e){e.printStackTrace();}}
}
注意:这里上传的文件的文件名要和这个地方的一样,接收文件的参数的名不能随便定义,要和下面的name的值一致;
1.2 新增菜品
需求分析:
后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。
数据模型:
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_ flavor
表插入数据。所以在新增菜品时,涉及到两个表:
dish
菜品表dish_ flavor
菜品口味表
代码开发:
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
- 实体类
DishFlavor
(直接从课程资料中导入即可,Dish
实体前面课程中已经导入过了) - Mapper接口
DishFlavorMapper
- 业务层接口
DishFlavorService
- 业务层实现类
DishFlavorServicelmpl
- 控制层
DishController
创建相关的mapper和service层:
package com.itheima.reggie.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}
package com.itheima.reggie.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.DishFlavor;public interface DishFlavorService extends IService<DishFlavor> {
}
package com.itheima.reggie.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.DishFlavor;
import com.itheima.reggie.mapper.DishFlavorMapper;
import com.itheima.reggie.service.DishFlavorService;
import org.springframework.stereotype.Service;@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}
编写controller:
在开发代码之前,需要梳理-下新增菜品时前端页面和服务端的交互过程:
- 页面(
backend/page/food/add.html
)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中 - 页面发送请求进行图片上传,请求服务端将图片保存到服务器
- 页面发送请求进行图片下载,将上传的图片进行回显
- 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可
先获取和返回菜品分类列表; 前端主要的代码;
// 获取菜品分类列表
const getCategoryList = (params) => {return $axios({url: '/category/list',method: 'get',params})
}if (res.code === 1) {this.dishList = res.data //这里就相当于把所有的category对象的数据赋值给dishList}这是菜品分类和数据双向绑定的前端代码: 我们返回的是一个集合,
</el-form-item><el-form-itemlabel="菜品分类:"prop="categoryId"><el-selectv-model="ruleForm.categoryId"placeholder="请选择菜品分类"><el-option v-for="(item,index) in dishList" :key="index" :label="item.name" :value="item.id" /></el-select></el-form-item>
这个的返回值和参数值 值得多思考一下; 这里之所以返回list集合,是因为这个要展示的数据是引用类型的数据集,集合可以存放任意类型的数据;
/*** 根据条件查询分类数据* @param category* @return*/@GetMapping("/list")//这个接口接收到参数其实就是一个前端传过来的type,这里之所以使用Category这个类来接受前端的数据,是为了以后方便//因为这个Category类里面包含了type这个数据,返回的数据多了,你自己用啥取啥就行private R<List<Category>> list(Category category){//条件构造器LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();//添加查询条件queryWrapper.eq(category.getType() != null,Category::getType,category.getType());//添加排序条件 使用两个排序条件,如果sort相同的情况下就使用更新时间进行排序queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);List<Category> list = categoryService.list(queryWrapper);return R.success(list);}
测试的返回数据:
1.3 接收页面提交的数据🙇♂️(涉及两张表)
点击保存按钮的时候,把前端的json数据提交到后台,后台接收数据,对数据进行处理;要与两张表打交道,一个是dish
一个是dish_flavor
表;
先用前端页面向后端发一次请求,看看前端具体的请求是什么,我们好写controller;然后再看前端提交携带的参数是什么,我们好选择用什么类型的数据来接收!!!
这里传过来的参数比较复杂,所以这里有两种方式进行封装。
- 第一:创建与这些数据对应的实体类(dto)
- 第二:使用map来接收;
package com.itheima.reggie.dto;import com.itheima.reggie.entity.Dish;
import com.itheima.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;
}
DTO,全称为Data Transfer Object, 即数据传输对象,一般用于展示层
与服务层
之间的数据传输。
这里我们选择使用第一种方式;
package com.itheima.reggie.dto;import com.itheima.reggie.entity.Dish;
import com.itheima.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; //后面要用的
}
前端关键代码:
<el-buttontype="primary"@click="submitForm('ruleForm')"
>保存
</el-button>let params = {...this.ruleForm}
// params.flavors = this.dishFlavors
params.status = this.ruleForm ? 1 : 0
params.price *= 100 //存到数据库的时候是以分为单位,所以这里x100
params.categoryId = this.ruleForm.categoryId
params.flavors = this.dishFlavors.map(obj => ({ ...obj, value: JSON.stringify(obj.value) }))if (this.actionType == 'add') {delete params.idaddDish(params).then(res => {if (res.code === 1) {this.$message.success('菜品添加成功!')if (!st) {this.goBack()} else { ....// 新增接口
const addDish = (params) => {return $axios({url: '/dish',method: 'post',data: { ...params }})
}
后端代码:
在DishService中新增一个方法:
//新增菜品,同时插入菜品对应的口味数据,需要同时操作两张表:dish dish_flavor
void saveWithFlavor(DishDto dishDto);
相关的实现:
@Autowired
private DishFlavorService dishFlavorService;
/*** 新增菜品同时保存对应的口味数据* @param dishDto*/
@Override
@Transactional //涉及到对多张表的数据进行操作,需要加事务,需要事务生效,需要在启动类加上事务注解生效
public void saveWithFlavor(DishDto dishDto) {//保存菜品的基本信息到菜品表dish中this.save(dishDto);Long dishId = dishDto.getId();//为了把dishId set进flavors表中//拿到菜品口味List<DishFlavor> flavors = dishDto.getFlavors();//这里对集合进行赋值 可以使用循环或者是stream流flavors = flavors.stream().map((item) ->{//拿到的这个item就是这个DishFlavor集合item.setDishId(dishId);return item; //记得把数据返回去}).collect(Collectors.toList()); //把返回的集合搜集起来,用来被接收//把菜品口味的数据到口味表 dish_flavor 注意dish_flavor只是封装了name value 并没有封装dishId(从前端传过来的数据发现的,然而数据库又需要这个数据)dishFlavorService.saveBatch(dishDto.getFlavors()); //这个方法是批量保存
}
在启动类开启事务: 加上这个注解就行 @EnableTransactionManagement
controller 层的代码:
package com.itheima.reggie.controller;import com.itheima.reggie.common.R;
import com.itheima.reggie.dto.DishDto;
import com.itheima.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {@Autowiredprivate DishService dishService;/*** 新增菜品* @param dishDto* @return*/@PostMappingpublic R<String> save(@RequestBody DishDto dishDto){ //前端提交的是json数据的话,我们在后端就要使用这个注解来接收参数,否则接收到的数据全是nulldishService.saveWithFlavor(dishDto);return R.success("新增菜品成功");}
}
功能测试:记得功能测试!
1.4 菜品信息分页查询
需求分析:
系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
图片下载的请求前面已经写好了,前端也写好了相关的请求,所以第二步的图片下载和展示就不需要我们管了;
代码编写:
controller层的代码:不过这里是有bug的,后面会改善;
/*** 菜品信息分页查询* @param page* @param pageSize* @param name* @return*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){//构造一个分页构造器对象Page<Dish> dishPage = new Page<>(page,pageSize);//构造一个条件构造器LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();//添加过滤条件 注意判断是否为空 使用对name的模糊查询queryWrapper.like(name != null,Dish::getName,name);//添加排序条件 根据更新时间降序排queryWrapper.orderByDesc(Dish::getUpdateTime);//去数据库处理分页 和 查询dishService.page(dishPage,queryWrapper);//因为上面处理的数据没有分类的id,这样直接返回R.success(dishPage)虽然不会报错,但是前端展示的时候这个菜品分类这一数据就为空return R.success(dishPage);
}
功能完善:引入了DishDto
package com.itheima.reggie.dto;import com.itheima.reggie.entity.Dish;
import com.itheima.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; //后面用的
}
/*** 菜品信息分页查询* @param page* @param pageSize* @param name* @return*/@GetMapping("/page")public R<Page> page(int page,int pageSize,String name){//构造一个分页构造器对象Page<Dish> dishPage = new Page<>(page,pageSize);Page<DishDto> dishDtoPage = new Page<>(page,pageSize);//上面对dish泛型的数据已经赋值了,这里对DishDto我们可以把之前的数据拷贝过来进行赋值//构造一个条件构造器LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();//添加过滤条件 注意判断是否为空 使用对name的模糊查询queryWrapper.like(name != null,Dish::getName,name);//添加排序条件 根据更新时间降序排queryWrapper.orderByDesc(Dish::getUpdateTime);//去数据库处理分页 和 查询dishService.page(dishPage,queryWrapper);//获取到dish的所有数据 records属性是分页插件中表示分页中所有的数据的一个集合List<Dish> records = dishPage.getRecords();List<DishDto> list = records.stream().map((item) ->{//对实体类DishDto进行categoryName的设值DishDto dishDto = new DishDto();//这里的item相当于Dish 对dishDto进行除categoryName属性的拷贝BeanUtils.copyProperties(item,dishDto);//获取分类的idLong categoryId = item.getCategoryId();//通过分类id获取分类对象Category category = categoryService.getById(categoryId);if ( category != null){//设置实体类DishDto的categoryName属性值String categoryName = category.getName();dishDto.setCategoryName(categoryName);}return dishDto;}).collect(Collectors.toList());//对象拷贝 使用框架自带的工具类,第三个参数是不拷贝到属性BeanUtils.copyProperties(dishPage,dishDtoPage,"records");dishDtoPage.setRecords(list);//因为上面处理的数据没有分类的id,这样直接返回R.success(dishPage)虽然不会报错,但是前端展示的时候这个菜品分类这一数据就为空//所以进行了上面的一系列操作return R.success(dishDtoPage);}
1.5 修改菜品🙇♂️(回显和保存修改都是两张表)
需求分析:
代码开发:
在开发代码之前,需要梳理一下修改菜品时前端页面(add.html) 和服务端的交互过程:
- 页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
- 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
- 页面发送请求,请求服务端进行图片下载,用于页图片回显
- 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
第一次交互的后端代码已经完成了;菜品分类的信息前面做新增菜品的时候就已经完成了,这里前端发一个相关接口的请求就行;
第三次交互,图片的下载前面也已经写了,所以前端直接发生请求就行;
1.5.1 菜品信息的回显
在service添加自己要实现的方法:
//根据id来查询菜品信息和对应的口味信息
DishDto getByIdWithFlavor(Long id);
方法的 实现:
@Autowiredprivate DishFlavorService dishFlavorService;
/*** 根据id来查询菜品信息和对应的口味信息* @param id* @return*/
@Override
public DishDto getByIdWithFlavor(Long id) {//查询菜品的基本信息 从dish表查询Dish dish = this.getById(id);//查询当前菜品对应的口味信息,从dish_flavor查询 条件查询LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(DishFlavor::getDishId,dish.getId());List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);//然后把查询出来的flavors数据set进行 DishDto对象DishDto dishDto = new DishDto();//把dish表中的基本信息copy到dishDto对象,因为才创建的dishDto里面的属性全是空BeanUtils.copyProperties(dish,dishDto);dishDto.setFlavors(flavors);return dishDto;
}
controller 层的编写:
/*** 根据id来查询菜品信息和对应的口味信息* @param id* @return*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){ //这里返回什么数据是要看前端需要什么数据,不能直接想当然的就返回Dish对象DishDto dishDto = dishService.getByIdWithFlavor(id);return R.success(dishDto);
}
1.5.2 保存修改🙇♂️
保存修改设计两张表的数据的修改:
DishService中添加自己实现的方法:
//更新菜品信息同时还更新对应的口味信息
void updateWithFlavor(DishDto dishDto);
相关的实现:
@Override@Transactionalpublic void updateWithFlavor(DishDto dishDto) {//更新dish表的基本信息 因为这里的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操作List<DishFlavor> flavors = dishDto.getFlavors();//下面这段流的代码我注释,然后测试,发现一次是报dishId没有默认值(先测),两次可以得到结果(后测,重新编译过,清除缓存过),相隔半个小时//因为这里拿到的flavorsz只有name和value(这是在设计数据封装的问题),不过debug测试的时候发现有时候可以拿到全部数据,有时候又不可以... 所以还是加上吧。。。。。flavors = flavors.stream().map((item) -> {item.setDishId(dishDto.getId());return item;}).collect(Collectors.toList());dishFlavorService.saveBatch(flavors);}
1.6 需要自己单独实现的功能
1.6.1 菜品启售和停售
前端发过来的请求(使用的是post方式):
http://localhost:8080/dish/status/1?ids=1516568538387079169
后端接受的请求:
@PostMapping("/status/{status}")
public R<String> status(@PathVariable("status") Integer status,Long ids){log.info("status:{}",status);log.info("ids:{}",ids);return null;
}
接收到前端参数后,开始补全controller层代码:在DishController中添加下面的接口代码;
/*** 对菜品进行停售或者是起售* @return*/
@PostMapping("/status/{status}")
public R<String> status(@PathVariable("status") Integer status,Long ids){log.info("status:{}",status);log.info("ids:{}",ids);Dish dish = dishService.getById(ids);if (dish != null){dish.setStatus(status);dishService.updateById(dish);return R.success("开始启售");}return R.error("售卖状态设置异常");
}
1.6.2 菜品批量启售和批量停售
把上面对单个菜品的售卖状态的方法进行修改;
/*** 对菜品批量或者是单个 进行停售或者是起售* @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("售卖状态修改成功");
}
注意:controller层的代码是不可以直接写业务的,建议把它抽离到service层,controller调用一下service的方法就行;下面的批量删除功能是抽离的,controller没有写业务代码;
1.6.3 菜品的批量删除
前端发来的请求:
在DishController中添加接口:
在DishFlavor实体类中,在private Integer isDeleted;字段上加上@TableLogic注解,表示删除是逻辑删除,由mybatis-plus提供的;
/*** 套餐批量删除和单个删除* @return*/
@DeleteMapping
public R<String> delete(@RequestParam("ids") List<Long> ids){//删除菜品 这里的删除是逻辑删除dishService.deleteByIds(ids);//删除菜品对应的口味 也是逻辑删除LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.in(DishFlavor::getDishId,ids);dishFlavorService.remove(queryWrapper);return R.success("菜品删除成功");
}
DishServicez中添加相关的方法:
//根据传过来的id批量或者是单个的删除菜品
void deleteByIds(List<Long> ids);
在实现类实现该方法:
/***套餐批量删除和单个删除* @param ids*/
@Override
@Transactional
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.6.4 菜品删除逻辑优化
上面写的菜品的删除功能有点小简单,下面完善一下相关的逻辑;
相关的service的注入,这里就不列举出来了,代码中使用了哪个service,你就autowire就行;
下面的代码可能会有点冗余,这里我就不进行抽离了;
/*** 菜品批量删除和单个删除* 1.判断要删除的菜品在不在售卖的套餐中,如果在那不能删除* 2.要先判断要删除的菜品是否在售卖,如果在售卖也不能删除* @return*///遇到一个小问题,添加菜品后,然后再添加套餐,但是套餐可选择添加的菜品选项是没有刚刚添加的菜品的?//原因:redis存储的数据没有过期,不知道为什么redis没有重新刷新缓存// (与DishController中的@GetMapping("/list")中的缓存设置有关,目前不知道咋配置刷新缓存。。。。。// 解决方案,把redis中的数据手动的重新加载一遍,或者是等待缓存过期后再添加相关的套餐,或者改造成使用spring catch@DeleteMappingpublic R<String> delete(@RequestParam("ids") 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("菜品删除成功");}//如果菜品有关联套餐,并且该套餐正在售卖,那么不能删除//得到与删除菜品关联的套餐idArrayList<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);return R.success("菜品删除成功");}