最新Springboot+mp核心技术外卖入门实战项目(二)
编码阶段二 —— 菜品管理
此阶段重要掌握的是菜品管理,包括增删改查等,需要特别要关注的是数据的回显,文件上传下载功能显示图片。当然其中使用DTO来传输数据的方式得比较熟练掌握,以后到公司对于我们初级程序员就做的增删改查这些东西。另外我也在这一阶段有一个bug,就是关于动态代理的问题,因为我通过@Autowired自动注入service的实现类而不是接口的方式来产生bean,这就会导致spring底层不知道你是用哪种方式的动态代理而报错,大家在编码的时候记得养成良好的编码习惯。我会在下一篇博客中好好整理jdk动态代理和cglib代理的区别以及spring底层使用动态代理的原理,大家可以点个赞收藏关注一波,一起进步。
菜品分类实体类
@Datapublic class Category implements Serializable { private static final long serialVersionUID = 1L; private Long id; //类型 1 菜品分类 2 套餐分类 private Integer type; //分类名称 private String name; //顺序 private Integer sort; //创建时间 @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;}
公共字段自动填充,例如修改时候的updateTime,updateUser等等,配置之后就可以使得带有注解的公共字段在插入或修改时进行自动填充,减少代码的冗余。
/** * 自定义元数据对象处理器 * @author William * @create 2022-04-22 13:26 */@Component@Slf4jpublic class MyMetaObjectHandler implements MetaObjectHandler { /** * 插入操作,自动填充 *@Param [metaObject] *@Return */ @Override public void insertFill(MetaObject metaObject) { log.info("公共字段自动填充[insert]..."); log.info(metaObject.toString()); metaObject.setValue("createTime", LocalDateTime.now()); metaObject.setValue("updateTime", LocalDateTime.now()); metaObject.setValue("createUser", BaseContext.getCurrentId()); metaObject.setValue("updateUser", BaseContext.getCurrentId()); } /** * 更新操作,自动填充 *@Param [metaObject] *@Return */ @Override public void updateFill(MetaObject metaObject) { log.info("公共字段自动填充[update]..."); log.info(metaObject.toString()); long id = Thread.currentThread().getId(); log.info("线程id为:{}", id); metaObject.setValue("updateTime", LocalDateTime.now()); metaObject.setValue("updateUser", BaseContext.getCurrentId()); }}
菜品分类实体类
@Datapublic class Category implements Serializable { private static final long serialVersionUID = 1L; private Long id; //类型 1 菜品分类 2 套餐分类 private Integer type; //分类名称 private String name; //顺序 private Integer sort; //创建时间 @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;}
菜品分类Service层
public interface ICategoryService extends IService { Page page(int page, int pageSize);//分页展示 void remove(Long ids);}/** * @author William * @create 2022-04-22 14:54 */@Servicepublic class CategoryServiceImpl extends ServiceImpl implements ICategoryService { @Autowired private IDishService dishService; @Autowired private ISetmealService setmealService; /** * 分页展示 *@Param [page, pageSize] *@Return */ @Override public Page page(int page, int pageSize) { Page pageInfo = new Page(page, pageSize); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); queryWrapper.orderByAsc(Category::getSort); return this.page(pageInfo, queryWrapper); } /** * 根据id删除分类,删除之前需要进行判断 *@Param [ids] *@Return */ @Override public void remove(Long ids) { LambdaQueryWrapper dishLambdaQueryWrapper = new LambdaQueryWrapper(); //查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常 dishLambdaQueryWrapper.eq(Dish::getCategoryId, ids);//因为前端传来的是ids int count1 = dishService.count(dishLambdaQueryWrapper); if(count1 > 0){ throw new CustomException("当前分类下关联了菜品,不能删除"); } //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常 LambdaQueryWrapper setmealLambdaQueryWrapper = new LambdaQueryWrapper(); setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, ids); int count2 = setmealService.count(setmealLambdaQueryWrapper); if(count2 > 0){ throw new CustomException("当前分类下关联了套餐,不能删除"); } //正常删除分类 this.removeById(ids); }}
菜品分类Controller层
/** * @author William * @create 2022-04-22 14:56 */@RestController@RequestMapping("/category")@Slf4jpublic class CategoryController { @Autowired private CategoryServiceImpl categoryService; @ApiOperation(value = "添加", notes = "新增分类") @PostMapping public R save(@RequestBody Category category){ log.info("category:{}", category); boolean save = categoryService.save(category); if(save){ return R.success("新增分类成功"); } return R.error("新增分类失败"); } @ApiOperation(value = "分页", notes = "菜品分类分页") @GetMapping("/page") public R page(int page, int pageSize){ Page pageInfo = categoryService.page(page, pageSize); return R.success(pageInfo); } @ApiOperation(value = "删除", notes = "根据id删除分类") @DeleteMapping public R delete(Long ids){ log.info("删除分类,ids为:{}", ids); categoryService.removeById(ids); return R.success("分类信息删除成功"); } @ApiOperation(value = "修改", notes = "根据id修改分类信息") @PutMapping public R update(@RequestBody Category category){ log.info("修改分类信息:{}" , category); categoryService.updateById(category); return R.success("修改分类信息成功"); }}
文件上传下载
文件上传时,对页面的form表单有如下要求:
method="post" 采用post方式提交数据 entype="multipart/form-data" 采用multipart格式上传文件 type="file" 使用input的file控件上传
服务端要接收客户端上传的文件,通常会采用Apache的两个组件:
commons-fileupload commons-io
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件。
文件下载,将文件从服务器传输到本地计算机的过程。通过浏览器进行文件下载,通常有两种表现形式:
以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录 直接在浏览器中打开
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
在yml文件中设置path
reggie:
path: D:\img\
上传下载文件写common中的controller层
/** * @author William * @create 2022-04-22 17:32 */@RestController@RequestMapping("/common")@Slf4jpublic class CommonController { @Value("${reggie.path}") private String basePath; @ApiOperation(value = "文件上传", notes = "文件上传") @PostMapping("/upload") public R 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; //创建一个目录对象 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); } @ApiOperation(value = "下载", notes = "文件下载") @GetMapping(value = "/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(); } }}
新增菜品
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。涉及到dish和dish_flavor表
先在category中加入一个列表查询方法
List list(Category category);//根据条件查询分类数据/** * 根据条件查询分类数据 *@Param [category] *@Return */ @Override public List list(Category category) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); queryWrapper.eq(category.getType() != null, Category::getType, category.getType()); queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime); List list = this.list(queryWrapper); return list; }@ApiOperation(value = "查询", notes = "根据条件查询分类数据") @GetMapping("/list") public R<List> list(Category category){ List list = categoryService.list(category); return R.success(list); }
因为新增菜品时数据传输还需要有dish_flavor中没有的数据,所以我们需要使用DTO来进行数据传输。
DTO(Data Transfer Object):即数据传输对象,一般用于展示层与服务层之间的数据传输。
因为到这里业务就可能比较复杂,有时候需要操作两张表及以上,这时候我们实现类中就需要使用到事务的注解,另外想要开启事务得在启动类加
@EnableTransactionManagement注解
@Datapublic class DishDto extends Dish { private List flavors = new ArrayList(); private String categoryName; private Integer copies;}
菜品的接口
/** * @author William * @create 2022-04-22 16:04 */public interface IDishService extends IService { //新增菜品,同时插入菜品对应的口味数据,需要操作dish和dish_flavor两张表 void saveWithFlavor(DishDto dishDto); Page page(int page, int pageSize, String name); //根据id查询菜品信息和对应的口味信息 DishDto getByIdWithFlavor(Long id); void updateWithFlavor(DishDto dishDto);}
@RestController@RequestMapping("/dish")@Slf4jpublic class DishController { @Autowired private IDishService dishService;// @Autowired// private DishFlavorServiceImpl dishFlavorService; @ApiOperation(value = "新增", notes = "新增菜品") @PostMapping public R save(@RequestBody DishDto dishDto){ log.info(dishDto.toString()); dishService.saveWithFlavor(dishDto); return R.success("新增菜品成功"); }}
/** * 菜品信息分页查询 *@Param [page, pageSize, name] *@Return */ @Override public Page page(int page, int pageSize, String name) { Page pageInfo = new Page(page, pageSize); Page dishDtoPage = new Page(); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); queryWrapper.like(name != null, Dish::getName, name); queryWrapper.orderByDesc(Dish::getUpdateTime); this.page(pageInfo, queryWrapper); //对象拷贝 BeanUtils.copyProperties(pageInfo, dishDtoPage, "records"); List records = pageInfo.getRecords(); List list = records.stream().map((item) -> { DishDto dishDto = new DishDto(); BeanUtils.copyProperties(item, dishDto); Long categoryId = item.getCategoryId();//分类id //根据id查询分类对象 Category category = categoryService.getById(categoryId); if(category != null){//防止空指针 String categoryName = category.getName(); dishDto.setCategoryName(categoryName); } return dishDto; }).collect(Collectors.toList()); dishDtoPage.setRecords(list); return dishDtoPage; } /** * 根据id查询菜品信息和对应的口味信息 *@Param [id] *@Return */ @Override public DishDto getByIdWithFlavor(Long id) { Dish dish = this.getById(id); DishDto dishDto = new DishDto(); BeanUtils.copyProperties(dish, dishDto); //查询当前菜品对应的口味信息,从fish_flavor表查询 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); queryWrapper.eq(DishFlavor::getDishId, dish.getId()); List flavors = dishFlavorService.list(queryWrapper); dishDto.setFlavors(flavors); return dishDto; } /** * *@Param [dishDto] *@Return */ @Override @Transactional public void updateWithFlavor(DishDto dishDto) { //更新dish表基本信息 this.updateById(dishDto); //清理当前菜品对应口味数据——dish_flavor表的delete操作 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); queryWrapper.eq(DishFlavor::getDishId, dishDto.getId()); dishFlavorService.remove(queryWrapper); //插入当前提交过来的口味数据——dish_flavor表的insert操作 List flavors = dishDto.getFlavors(); flavors = flavors.stream().map((item) -> { item.setDishId(dishDto.getId()); return item; }).collect(Collectors.toList()); dishFlavorService.saveBatch(flavors); }
菜品controller层部分代码
@ApiOperation(value = "分页", notes = "菜品信息分页查询") @GetMapping(value = "/page") public R page(int page, int pageSize, String name){ Page dishDtoPage = dishService.page(page, pageSize, name); return R.success(dishDtoPage); } @ApiOperation(value = "查询", notes = "根据id查询菜品信息和对应的口味信息") @GetMapping(value = "/{id}") public R get(@PathVariable Long id){ DishDto dishDto = dishService.getByIdWithFlavor(id); return R.success(dishDto); } @ApiOperation(value = "修改", notes = "修改菜品") @PutMapping public R update(@RequestBody DishDto dishDto){ log.info(dishDto.toString()); dishService.updateWithFlavor(dishDto); return R.success("更新菜品成功"); }
在我这阶段编写的时候报了一个动态代理的bug,因为我在服务层和controller层都是用自动注入service的实现类而不是service的接口,这里我们就可以多去了解一下Spring中的动态代理。注意前面不要用实现类,这样会导致spring报动态代理的异常