瑞吉外卖学习笔记5

文件上传下载&新增菜品&菜品信息分页查询&修改菜品

目录

1、文件上传下载
2、新增菜品
3、菜品信息分页查询
4、修改菜品

需要实现页面的功能
在这里插入图片描述

文件上传下载_文件上传下载介绍

文件上传介绍

  • 文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程
  • 文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能

文件上传时,对页面的form表单有如下要求:

1、method=“post” 采用post方式提交数据
2、enctype=“multipart/form-data” 采用multipart格式上传文件
3、type=“file” 使用input的file控件上传

<form method="post" action="/common/upload" enctype="multipart/form-data">
	<input name="myFile" type="file"/>
	<input type="submit" value="提交" />
<form>

目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传

例如ElementUI中提供的upload上传组件:
在这里插入图片描述
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:

  • commons-fileupload
  • commons-io

Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:
在这里插入图片描述
文件下载介绍

文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程;

通过浏览器进行文件下载,通常有两种表现形式:
1、以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
2、直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

文件上传下载_文件上传代码实现

文件上传代码实现

文件上传,页面端可以使用ElementUI提供的上传组件。
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 < 2
              if(['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>

在controller包下创建CommonController类,代码内容如下:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
通过前端的网页我们可以发现发送的请求是common/download同时通过response请求可以获取传来的数据。
文件上传代码如下:

package com.itzq.reggie.controller;

import com.itzq.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;

@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件删除
        log.info(file.toString());
        return null;
    }
}

注意:

  1. MultipartFile是spring类型,代表HTML中form data方式上传的文件,包含二进制数据+文件名称。
  2. MultipartFile后面的参数名必须为file,因为需要和前端页面的name保持一致,否则不会生效
    在这里插入图片描述
    启动项目,在浏览器地址栏输入:http://localhost:8080/backend/page/demo/upload.html
    1、点击上传图片,并上传一个符合文件上传格式,符合文件上传大小的图片
    2、后端返回给前端的msg数据为NOTLOGIN,可知被filter过滤器拦截,返还还未登录的信息
    3、因此我们需要先在页面上登录,登录后会在服务端的内存中存储session对象,session的作用域为一次会话范围内
    4、在浏览器地址栏中输入:http://localhost:8080/backend/page/demo/upload.html,即可避免被filter过滤器拦截
    登录后可正常上传文件,在此处添加断点,以debug方式启动项目
    在这里插入图片描述
    来到文件上传页面,点击上传文件,进入断点模式,查看文件的存储位置:
    在这里插入图片描述
    在开发上传文件代码前,先再LoginCheckFilter类的urls数组中添加 “/common/”**
    作用:避免每次上传文件时都需要进行登录操作
    在这里插入图片描述
    将临时文件存储存储到指定位置
package com.itzq.reggie.controller;

import com.itzq.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;

import java.io.File;
import java.io.IOException;

@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件删除
        log.info(file.toString());

        try {
            //将临时文件存储到指定位置
            file.transferTo(new File("D:\\hello.jpg"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

启动项目,浏览器地址栏输入地址:http://localhost:8080/backend/page/demo/upload.html,上传图片,查看指定存储文件的位置是否有上传的文件;

文件转存的位置改为动态可配置的,通过配置文件的方式指定
在这里插入图片描述
1、使用 @Value(“${reggie.path}”)读取到配置文件中的动态转存位置
2、使用uuid方式重新生成文件名,避免文件名重复造成文件覆盖
3、通过获取原文件名来截取文件后缀

package com.itzq.reggie.controller;

import com.itzq.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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;
import java.util.UUID;

@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

    @Value("${reggie.path}")
    private String basePath;

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件删除
        log.info(file.toString());

        //获取原始的文件名
        String originalFilename = file.getOriginalFilename();
        //获取上传的文件后缀
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

        //使用uuid重新生成文件名,防止文件名重复造成文件覆盖
        String fileName = UUID.randomUUID().toString() + suffix;



        try {
            //将临时文件存储到指定位置
            file.transferTo(new File(basePath + fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

重启项目,上传图片,在动态指定的位置上发现上传的文件

但是指定的目录或许不存在于磁盘中,所以我们要为程序添加逻辑代码,若目录不存在于磁盘中,则需要创建该目录,最后服务端需返回文件名给前端,便于后续开发使用;

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;
 
/**
 * @author LJM
 * @create 2022/4/16
 * 文件上传和下载
 */
@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  .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);
    }

文件下载代码实现

前端处理

前端页面ElementUI的upload组件会在上传完图片后,触发img组件发送请求,服务端以流的形式(输出流)将文件写回浏览器,在浏览器中展示图片
在这里插入图片描述
定义前端发送回显图片请求的地址
在这里插入图片描述

在CommonController类中添加download方法

1、通过输入流读取文件内容
2、通过输出流将文件写回浏览器,在浏览器展示图片
3、关闭输入输出流,释放资源

@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){
                //向response缓冲区中写入字节,再由Tomcat服务器将字节内容组成Http响应返回给浏览器。
                outputStream.write(bytes,0,len);
                //所储存的数据全部清空
                outputStream.flush();
            }

            //关闭流
            fileInputStream.close();
            outputStream.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

新增菜品_需求分析&数据模型

需求分析

1、后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品
2、在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片
3、在移动端会按照菜品分类来展示对应的菜品信息

在这里插入图片描述

数据模型

dish表
在这里插入图片描述

dish_flavor表
在这里插入图片描述
新增菜品分类,会将前端传过来的数据保存在这两张表中;

新增菜品_代码开发_查询分类数据

具体实现的功能演示
在这里插入图片描述

代码开发-准备工作
1、在开发业务功能前,先将需要用到的类和接口基本结构创建好:
2、实体类DishFlavor、Dish实体前面已经导入过了
3、Mapper接口DishFlavorMapper
4、业务层接口DishFlavorService
5、业务层实现类DishFlavorServicelmpl
6、控制层DishController

实体类DishFlavor

package com.itzq.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.itzq.reggie.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itzq.reggie.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}


业务层接口DishFlavorService

package com.itzq.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itzq.reggie.entity.DishFlavor;

public interface DishFlavorService extends IService<DishFlavor> {
}


业务层实现类DishFlavorServicelmpl

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.DishFlavor;
import com.itzq.reggie.mapper.DishFlavorMapper;
import com.itzq.reggie.service.DishFlavorService;
import org.springframework.stereotype.Service;

@Service
public class DishFlavorServicelmpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}


控制层DishController

package com.itzq.reggie.controller;

import com.itzq.reggie.service.DishFlavorService;
import com.itzq.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
    @Autowired
    private DishService dishService;

    @Autowired
    private DishFlavorService dishFlavorService;

}


梳理交互过程

在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:
1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显
4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

前端分析

一个vue实例被创建后会调用钩子函数,执行其中的方法
在这里插入图片描述
来到getDishList方法,执行其中getCategoryList方法:
this.dishList = res.data //这里就相当于把所有的category对象的数据赋值给dishList
在这里插入图片描述
执行getCategoryList方法向服务端发送ajax请求,请求方式为get
在这里插入图片描述
先获取和返回菜品分类列表; 前端主要的代码:

// 获取菜品分类列表
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-item
            label="菜品分类:"
            prop="categoryId"
          >
            <el-select
              v-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>

在CategoryController书写查询代码,不过这里的返回值和参数接收值可能和自己想的有点不一样。这个的返回值和参数值 值得多思考一下; 这里之所以返回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);
    }

在这里插入图片描述
启动项目,进入菜品管理,点击菜品分类下拉框,成功获得数据
在这里插入图片描述
一些个人思考:
如果我们将@GetMapping("/list")方法注销或者将返回值数据设置为null,那么重启后我们发现我们是无法进行查询的商品分类的,说白了就是前端返回一个类型type,这个type是固定为1,因为category实体类具有type类型,可以用于接收,同时我们通过这个type可以将所有type为1的所有数据都查出来,然后将不同的菜品类型回显到前端界面。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

接收页面提交的数据(涉及两张表)

点击保存按钮的时候,把前端的json数据提交到后台,后台接收数据,对数据进行处理;要与两张表打交道,一个是dish一个是dish_flavor表;

先用前端页面向后端发一次请求,看看前端具体的请求是什么,我们好写controller;然后再看前端提交携带的参数是什么,我们好选择用什么类型的数据来接收!!!

在这里插入图片描述
注意:
价格在前端已被处理,在点击提交按钮后,先执行前端的submitForm方法,并将price做相应的处理(在页面中单位为元,在数据库中存储的单位为分),再通过ajax请求向后端提供相应的json数据。
在这里插入图片描述
通过传入的参数我们知道:因为Dish实体类不满足接收flavor参数,即需要导入DishDto,用于封装页面提交的数据
在这里插入图片描述

DTO

DTO,全称为Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输。

  • 在reggie包下,创建一个新包为dto
  • 在该包下创建DishDto 数据传输类

在这里插入图片描述

package com.peihj.reggie.dto;


import com.peihj.reggie.entity.Dish;
import com.peihj.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<>();  // 因为口味数据是一个数组,数组里面装的是List类型的DishFlavor对象

    private String categoryName;  // 后面要用的

    private Integer copies;   //后面要用的
}

前端关键代码:

<el-button
  type="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.id
     addDish(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

//新增菜品,同时插入菜品对应的口味数据,需要同时操作两张表:dish  dish_flavor
void saveWithFlavor(DishDto dishDto);

实现类:
@Transactional: 涉及到对多张表的数据进行操作,需要加事务,需要事务生效,需要在启动类加上事务注解生效。

@Autowired
private DishFlavorService dishFlavorService;
/**
 * 新增菜品同时保存对应的口味数据
 * @param dishDto
 */
@Override
@Transactional //涉及到对多张表的数据进行操作,需要加事务,需要事务生效,需要在启动类加上事务注解生效
public void saveWithFlavor(DishDto dishDto) {
    //保存菜品的基本信息到菜品表dish中,这个save方法调用的是ISERVICE方法里的,所以会先将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

package com.peihj.reggie;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/*可以直接使用log*/
@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement
//@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //排除掉数据库自动加载
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class,args);
        log.info("项目成功启动");
        /*http://localhost:8080/backend/page/login/login.html*/
    }
}

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 {
 
    @Autowired
    private DishService dishService;
 
    /**
     * 新增菜品
     * @param dishDto
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody DishDto dishDto){ //前端提交的是json数据的话,我们在后端就要使用这个注解来接收参数,否则接收到的数据全是null
        dishService.saveWithFlavor(dishDto);
        return R.success("新增菜品成功");
    }
}

功能测试:记得功能测试!

菜品信息分页查询(功能完善里面的代码要熟悉,有集合泛型的转换,对象copy)

需求分析

  • 系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看
  • 所以一般的系统中都会以分页的方式来展示列表数据。

图片和菜品分类比较特殊

  • 图片列:会用到文件的下载功能
  • 菜品分类列:只保存了菜品的category_id,需通过查找category_id所对应的菜品分类名称,从而回显数据

在这里插入图片描述

代码开发-梳理交互过程

在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:

  • 页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name),提交到服务端,获取分页数据
  • 页面发送请求,请求服务端进行图片下载,用于页面图片展示

开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
图片下载的请求前面已经写好了,前端也写好了相关的请求,所以第二步的图片下载和展示就不需要我们管了;

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);
            //获取分类的id
            Long 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);
    }

records的值: protected List records
在这里插入图片描述
功能测试

修改菜品_需求分析&梳理交互过程

需求分析

  • 在菜品管理列表页面点击修改按钮,跳转到修改菜品页面
  • 在修改页面回显菜品相关信息并进行修改
  • 最后点击确定按钮完成修改操作

代码开发-梳理交互过程

在开发代码之前,需要梳理一下修改菜品时前端页面( add.html)和服务端的交互过程:

  • 页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示(已完成)
  • 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
  • 页面发送请求,请求服务端进行图片下载,用于页图片回显(已完成)
  • 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端

开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

菜品信息的回显:

在service添加自己要实现的方法:

//根据id来查询菜品信息和对应的口味信息
DishDto getByIdWithFlavor(Long id);

在DishServicelmpl中实现getByIdWithFlavor方法,并添加逻辑代码

  • 根据服务端接收的id,查询菜品的基本信息-dish
  • 创建dishDto对象,并将查询到的dish对象属性赋值给dishDto
  • 根据查询到的dish对象,可以取出对应的菜品id,再通过等值条件查询,查询到DishFlavor数据信息
  • 将查询到的flavor数据信息使用set方法赋值给dishDto对象
  • 返回dishDto对象
	@Autowired
    private 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);
}

在DishController中添加get方法,实现添加在DishServicelmpl中的逻辑代码,返回查询到的数据信息

@GetMapping("/{id}")
    public R<DishDto> get(@PathVariable Long id){
        //查询
        DishDto dishDto = dishService.getByIdWithFlavor(id);
 
        return R.success(dishDto);
    }

保存修改:(重点)

保存修改设计两张表的数据的修改:
DishService中添加自己实现的方法:

前端页面
在这里插入图片描述
发送到服务端的数据为json数据
在这里插入图片描述
在DishService接口中添加updateWithFlavor方法

//更新菜品信息同时还更新对应的口味信息
void updateWithFlavor(DishDto dishDto);

DishServicelmpl类中实现DishService定义的方法,并添加代码逻辑

  • 根据id修改菜品的基本信息
  • 通过dish_id,删除菜品的flavor
  • 获取前端提交的flavor数据
  • 为条flavor的dishId属性赋值
  • 将数据批量保存到dish_flavor数据库
   @Override
    @Transactional
    public 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);
 
    }

在DishController类中添加方法update,并调用updateWithFlavor方法实现表中数据的修改

    @PutMapping
    public R<String> update(@RequestBody DishDto dishDto){

        log.info("接收的dishDto数据:{}",dishDto.toString());

        //更新数据库中的数据
        dishService.updateWithFlavor(dishDto);

        return R.success("新增菜品成功");
    }

功能测试

菜品启售和停售

前端发过来的请求(使用的是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("售卖状态设置异常");
}

菜品批量启售和批量停售

把上面对单个菜品的售卖状态的方法进行修改;

/**
 * 对菜品批量或者是单个 进行停售或者是起售
 * @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没有写业务代码;

菜品的批量删除

前端发来的请求:
在这里插入图片描述
在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("删除菜品中有正在售卖菜品,无法全部删除");
        }
    }
 
}

功能测试:单个删除,批量删除,批量删除中有启售的…

参考

https://blog.csdn.net/eadzsdad/article/details/124265811
https://blog.csdn.net/weixin_53142722/article/details/124371940
https://blog.csdn.net/weixin_53142722/article/details/124356412

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Peihj2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值