springboot项目:瑞吉外卖 前后端详细分析 part4

part 1
part 2
part 3
part 4 本页

5 套餐管理

5.1 新增套餐
5.2 套餐信息分页查询
5.3 删除套餐
其他小功能都比较简单且类似,不再赘述

5.1 新增套餐

5.1.1 整体分析

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

在这里插入图片描述

在这里插入图片描述

  • 套餐是由菜品构成的,在添加套餐里面,可以添加菜品(会弹出每个菜品分类下的菜,比如主食下面有米饭)。套餐分类那里之前controller已经写好了,前端传不同的type分别查菜品分类、套餐分类,去修改对于的请求url即可。

在这里插入图片描述

  • 类似的,添加菜品的时候有多少菜品分类也列出来了,需要写controller处理菜品分类下对于的菜品。
    在这里插入图片描述

5.1.2 前端分析

  • 获取套餐分类的列表
getDishTypeList() {
  //通过category中的type = 1/2 查出对于的菜品分类或是套餐分类,显示到前端的下拉框中
  //返回值中的data的类型是:List<Category>,赋值给了setMealList属性进行双向绑定
  getCategoryList({ type: 2, page: 1, pageSize: 1000 }).then((res) => {
    if (res.code === 1) {
      this.setMealList = res.data.map((obj) => ({ ...obj, idType: obj.id }))
    } else {
      this.$message.error(res.msg || '操作失败')
    }
  })
},
  • 添加菜品,注意getDishList()方法!
// 添加菜品,之前有按钮点击事件绑定了这个函数
//<div class="addBut" style="margin-bottom: 20px" @click="openAddDish">+ 添加菜品</div>
openAddDish() {
  this.seachKey = ''
  this.dialogVisible = true
  //搜索条件清空,菜品重新查询,菜品类别选第一个重新查询
  this.value = ''
  this.keyInd = 0
  //每个菜品对应有哪些菜,在dishList中循环显示出来
  this.getDishList(this.dishType[0].id)
},
  • 通过套餐ID获取菜品列表分类:getDishList (id)
getDishList (id) {
  queryDishList({categoryId: id}).then(res => {
    if (res.code === 1) {
      if (res.data.length == 0) {
        this.dishAddList = []
        return
      }
      let newArr = res.data;
      newArr.forEach((n) => {
        n.dishId = n.id
        n.copies = 1
        // n.dishCopies = 1
        n.dishName = n.name
      })
      this.dishAddList = newArr
    } else {
      this.$message.error(res.msg)
    }
  })
},

其中queryDishList({categoryId: id}) 发送ajax请求,根据categoryId获取dish(获取当前套餐分类下的菜)。dish表中有category_id(相当于是副键),也就是说,一个category对应很多的dish。

//提交表单,最主要的代码,把整体数据提交上去。
submitForm(formName, st) {
  this.$refs[formName].validate((valid) => {
    if (valid) {
      let prams = { ...this.ruleForm }
      prams.price *= 100
      prams.setmealDishes = this.dishTable.map((obj) => ({
        copies: obj.copies,
        dishId: obj.dishId,
        name: obj.name,
        price: obj.price,
      }))
      prams.status = this.ruleForm ? 1 : 0
      prams.categoryId = this.ruleForm.idType
      if(prams.setmealDishes.length < 1){
        this.$message.error('请选择菜品!')
        return 
      }
      if(!this.imageUrl){
        this.$message.error('请上传套餐图片')
        return 
      }
      // delete prams.dishList
      if (this.actionType == 'add') {
        delete prams.id
        //发送请求
        addSetmeal(prams)
          .then((res) => {
            if (res.code === 1) {
              this.$message.success('套餐添加成功!')
              if (!st) {
                this.goBack()
              } else {
                this.$refs.ruleForm.resetFields()
                this.dishList = []
                this.dishTable = []
                this.ruleForm = {
                  name: '',
                  categoryId: '',
                  price: '',
                  code: '',
                  image: '',
                  description: '',
                  dishList: [],
                  status: true,
                  id: '',
                  idType: '',
                }
                this.imageUrl = ''
              }
            } else {
              this.$message.error(res.msg || '操作失败')
            }
          })
          .catch((err) => {
            this.$message.error('请求出错了:' + err)
          })
      } else {
        delete prams.updateTime
        editSetmeal(prams)
          .then((res) => {
            if (res.code === 1) {
              this.$message.success('套餐修改成功!')
              this.goBack()
            } else {
              this.$message.error(res.msg || '操作失败')
            }
          })
          .catch((err) => {
            this.$message.error('请求出错了:' + err)
          })
      }
    } else {
      return false
    }
  })
},

其中调用的方法

// 新增数据接口
const addSetmeal = (params) => {
  return $axios({
    url: '/setmeal',
    method: 'post',
    data: { ...params }
  })
}

5.1.3 后端分析

  • 对应的套餐分类复用了之前的,根据 category中的type = 1/2 查出对于的菜品分类或是套餐分类,显示到前端的下拉框中

在这里插入图片描述

  1. 现在解决点击某个分类,显示该分类下具体菜品。
    在这里插入图片描述
    代码:
/**
 * 根据菜品分类查菜品比如: 川菜这个选项一点击,就通过这个controller返回一个list(元素就是各种川菜dish)
 * @param dish 参数只有一个categoryId,
 * @return
 */
@GetMapping("list/getDishByCategoryId.do")
public RetObj getDishByCategoryId(Dish dish){
    LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(dish != null,Dish::getCategoryId,dish.getCategoryId())
            .orderByDesc(Dish::getSort);

    List<Dish> dishList = dishService.list(lambdaQueryWrapper);
    return RetObj.success(dishList);
}

在这里插入图片描述
2. 对应菜品分类下的菜品查询到并显示到前端后,整体添加页面提交请求给后端,进行插入数据。这里 肯定需要操作来个表,因为新增套餐这个页面提交的数据就涉及来个表的数据,主要是套餐下有很多菜品。
在这里插入图片描述
分析前端发送过来的数据

{
  "name": "二逼餐饮",
  "categoryId": "1641061323236622337",
  "price": 12300,
  "code": "",
  "image": "abfe28a3-35af-493d-a043-295189807847.jpg",
  "description": "asdf",
  "dishList": [],
  "status": 1,
  "idType": "1641061323236622337",
  
  "setmealDishes": [
    {
      "copies": 1,
      "dishId": "1645433949618958337",
      "name": "二逼2",
      "price": 32100
    },
    {
      "copies": 1,
      "dishId": "1413384757047271425",
      "name": "王老吉",
      "price": 500
    },
    {
      "copies": 1,
      "dishId": "1413385247889891330",
      "name": "米饭",
      "price": 200
    },
    {
      "copies": 1,
      "dishId": "1397860578738352129",
      "name": "白切鸡",
      "price": 6600
    }
  ]
}

注意setmealDishes,这里面只有我们选中的菜品的信息,对setmeal_dish这个表进行操作的时候,肯定还需要setmeal_id,就是这个菜现在属于哪个套餐分类中,这个要自己加。
思路和之前类似,写一个MealDto,把meal的信息(名称、价格、图片等等存进去),还要报错对应的dish的信息,一次封装到MealDto
中。

  • Dto类如下
@Data
public class SetMealDto extends Setmeal {
    //名字一定要和前端一致,否则无法注入
    List<SetmealDish> setmealDishes;
}

  • service的处理,注意需要给setmeal_dish这个表插入具体菜品的时候,需要setmeal_id这个信息。List<SetmealDish>这个前端传来的数据中肯定没有这些菜品对应的套餐的setmeal_id,这个就需要我们重新构造一个list,把前端传来的那个list进行完善,添加setmeal_id的值。
@Transactional
public void saveMealWithFlavor(SetMealDto setMealDto) {
    this.save(setMealDto);
    List<SetmealDish> setmealDishes = setMealDto.getSetmealDishes();
    //setmealDishes中有一个字段:setmeal_id,这个字段在上面那个list中肯定是没有的!
    // 只有每个菜品自己的信息,这个setmeal_id就是setMealDto中的id
    List<SetmealDish> afterCompleteList = setmealDishes.stream().map(item -> {
        SetmealDish setmealDish = new SetmealDish();
        BeanUtils.copyProperties(item,setmealDish);
        setmealDish.setSetmealId(setMealDto.getId().toString());
        return setmealDish;
    }).collect(Collectors.toList());
    boolean res = setmealDishService.saveBatch(afterCompleteList);
    if (res == false){
        throw new RuntimeException("套餐中菜品导入失败!");
    }
}
  • 上面可以优化的点是:在SetMealDish这个类型中直接就有属性id属性,直接给id属性赋值就行(直接把每个List中SetmealDish赋值不就好了,不需要new一个对象,不需要拷贝!)
    在这里插入图片描述
  • controller 简单编写
@PostMapping("/add/setmeal.do")
public RetObj setMealController(@RequestBody SetMealDto setMealDto){
    setmealService.saveMealWithFlavor(setMealDto);
    return RetObj.success("成功新增套餐!");
}

5.2 套餐信息分页查询

5.2.1 整体分析

  • 需求分析
    在这里插入图片描述
    如图,每个套餐都有对应的图片,这些图片展示直接调用下载功能就可以。
    和之前的一样,关键点在于套餐分类这一栏的显示,因为在SetMeal表中,对应的套餐信息是categoryId,使用MP插件查出来其他信息都没问题,就是这个套餐分类不能正常显示,应该封装一个categoryId对应的套餐名字name
  • 交互过程
    在这里插入图片描述

5.2.2 前端分析

  1. 批量状态修改操作,还是通过axios请求对数据库操作。点击对应的按钮,进行起售等。
//状态更改
statusHandle (row) {
  let params = {}
  if (typeof row === 'string' ){
    if (this.checkList.length == 0){
      this.$message.error('批量操作,请先勾选操作菜品!')
      return false
    }
    params.ids = this.checkList.join(',')
    params.status = row
  } else {
    params.ids = row.id
    params.status = row.status ? '0' : '1'
  }
  this.$confirm('确认更改该套餐状态?', '提示', {
    'confirmButtonText': '确定',
    'cancelButtonText': '取消',
    'type': 'warning'
  }).then(() => {
    // 起售停售---批量起售停售接口
    setmealStatusByStatus(params).then(res => {
      if (res.code === 1) {
        this.$message.success('套餐状态已经更改成功!')
        this.handleQuery()
      } else {
        this.$message.error(res.msg || '操作失败')
      }
    }).catch(err => {
        this.$message.error('请求出错了:' + err)
    })
  })
},

如果是多个id,就发送多个id(对应是批量启售)

const setmealStatusByStatus = (params) => {
  return $axios({
    url: `/setmeal/status/${params.status}`,
    method: 'post',
    params: { ids: params.ids }
  })
}
  1. 当前端页面发送变化时,双向绑定数据,并调用init()方法
// 全部操作
handleSelectionChange (val){
  let checkArr = []
  val.forEach((n) => {
    checkArr.push(n.id)
  })
  this.checkList = checkArr
},
handleSizeChange (val) {
  this.pageSize = val
  this.init()
},
handleCurrentChange (val) {
  this.page = val
  this.init()
}
  1. init方法中,分装后端想要的分页数据,如:pageSize,page,name(模糊查询,前端可能会输入)为json,传输给后端,进行分页查询。主要注意后端要给前端返回的数据类型中包含的属性名要保持一致:this.tableData = res.data.records || [] ;this.counts = res.data.total
async init () {
     const params = {
       page: this.page,
       pageSize: this.pageSize,
       name: this.input ? this.input : undefined
     }
     await getSetmealPage(params).then(res => {
       if (String(res.code) === '1') {
         this.tableData = res.data.records || []
         this.counts = res.data.total
       }
     }).catch(err => {
       this.$message.error('请求出错了:' + err)
     })
   },
   //图片下载直接调用download的controller方法,注意传过去name,以得到对应的image
   getImage (image) {
     return `/common/download?name=${image}`
   },
   handleQuery() {
     this.page = 1;
     this.init();
   },
  1. 后端返回的数据是放到 record中,record数据放到tableData中,tableData又通过el-table控件的prop,把数据显示到对应的栏目中。
    在这里插入图片描述

5.2.3 后端分析

  1. 主要解决套餐分类的问题,把categoryId找到对应的name传给前端去显示,这个之前写过(菜品分类)很类似的。
  2. 拷贝过程中关键是records,这个不能拷贝,原因是泛型不一样!
@GetMapping("/list/page.do")
public RetObj getSetmealPageController(int page, int pageSize, String name){
    LambdaQueryWrapper<Setmeal> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    //要注意,如果前端没有给模糊查询的name,那就不应该查询,否则查到是的0条数据!
    // select count(*) from setmeal where name=null =>查到0条数据
    lambdaQueryWrapper.like(StringUtils.isNotBlank(name),Setmeal::getName,name)
            .orderByDesc(Setmeal::getUpdateTime);//根据跟新时间拍个序,你漏掉的
    Page<Setmeal> pageInfo = new Page<>(page,pageSize);
    setmealService.page(pageInfo,lambdaQueryWrapper);

    //解决两个表的问题,setmeal表只有category_id,而没有菜品对应的name,在setmeal_dish中才有。
    //解决思路,在SetmealDto中加入Name这个成员变量,多查一次在category这个表中的name,进行赋值
    //注意前端那里是categoryName,要的不是name!!
    Page<SetMealDto> pageInfoWithName = new Page<>(page,pageSize);
    BeanUtils.copyProperties(pageInfo,pageInfoWithName,"records");
    List<SetMealDto> recordsWithName = pageInfo.getRecords().stream().map(item -> {
        SetMealDto setMealDto = new SetMealDto();
        Category category = categoryService.getById(item.getCategoryId());
        if (category != null){//没考虑到的,防止出现空指针异常!!
            setMealDto.setCategoryName(category.getName());
            BeanUtils.copyProperties(item,setMealDto);
        }

        return setMealDto;
    }).collect(Collectors.toList());
    pageInfoWithName.setRecords(recordsWithName);
    return RetObj.success(pageInfoWithName);
}

5.3 删除套餐

5.3.1 整体分析

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

5.3.2 前端分析

  1. 删除操作
// 删除
deleteHandle (type, id) {
  if (type === '批量' && id === null) {
    if (this.checkList.length === 0) {
      return this.$message.error('请选择删除对象')
    }
  }
  this.$confirm('确定删除该套餐, 是否继续?', '确定删除', {
    'confirmButtonText': '确定',
    'cancelButtonText': '取消',
  }).then(() => {
    deleteSetmeal(type === '批量' ? this.checkList.join(',') : id).then(res => {
      if (res.code === 1) {
        this.$message.success('删除成功!')
        this.handleQuery()
      } else {
        this.$message.error(res.msg || '操作失败')
      }
    }).catch(err => {
      this.$message.error('请求出错了:' + err)
    })
  })
},

上面代码的checkList就是在勾选中对应套餐的时候,触发以下函数,将对应数据压入

handleSelectionChange (val){
  let checkArr = []
  val.forEach((n) => {
    checkArr.push(n.id)
  })
  this.checkList = checkArr
},

删除操作deleteSetmeal,因为可能是一次删除多个,所以这里用ids,看上面的代码中分装好了多个ids,使用三木运算符判断是删除一个,还是一批。

const deleteSetmeal = (ids) => {
  return $axios({
    url: '/setmeal',
    method: 'delete',
    params: { ids }
  })
}

5.3.3 后端分析

  1. 先是接收参数,使用List接收,因为可能是多个ids,注意和名字对应上,使用@RequestParam

  2. 明确能不能删除(是不是售卖状态),如果不能删除,那就抛出异常

  3. sql 设想: select count(*) from setmeal where id in (id1,id2,id3) and status = 1

  4. 注意不同表的id,主键和副键,在删除的时候removeByIds的时候多注意。

    @Transactional
    public void deleteBatchWithDish(@RequestParam List ids){
    //查询套餐状态,确定是否可用删除,这个是要第一个确定的!!
    LambdaQueryWrapper lambdaQueryWrapper1 = new LambdaQueryWrapper<>();
    lambdaQueryWrapper1.eq(Setmeal::getStatus,2);
    lambdaQueryWrapper1.in(Setmeal::getId,ids);//这个需要重点学习,就不用遍历ids,比如下面删除dish的代码那样。
    //this.remove(lambdaQueryWrapper1); 需要判断!判断能不能删除
    if (this.count(lambdaQueryWrapper1) > 0){
    //如果不能删除,抛出一个业务异常
    throw new CustomException(“套餐正在售卖中,不能删除”);
    }
    //删除
    this.removeBatchByIds(ids);

     //错误代码!!setmealDishService的id是自己的id,是主键,应该根据副键删除!
     //setmealDishService.removeBatchByIds(ids);
     ids.forEach(item -> {
         LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
         lambdaQueryWrapper.eq(ids!= null,SetmealDish::getSetmealId,item);
         setmealDishService.remove(lambdaQueryWrapper);
     });
    

    }

持续更新中

瑞吉外卖项目是一个基于黑马课程资源的项目,该项目包含了许多功能模块。然而,根据引用的描述,黑马课程中并没有完整开发和讲解所有的模块,导致学习者在完成课程后还存在一些不完整的模块。为了解决这个问题,引用提供了一个补充资源,该资源完整实现了瑞吉外卖项目的所有功能模块,并且在后端部分提供了相应的接口。 在引用中提到,套餐管理的启售、停售和修改以及后台订单展示和查询是瑞吉外卖项目中的一些功能。具体的代码可以在作者的博客中找到。 为了更好地管理共同使用的类,引用建议创建一个名为"common"的新包,并将返回结果类放入其中。该结果类名为"R<T>",类中定义了以下属性和方法: - code属性表示编码,值为1表示成功,值为0或其他数字表示失败。 - msg属性表示错误信息。 - data属性表示数据。 - map属性为动态数据的存储。 - success方法用于返回成功结果。 - error方法用于返回失败结果。 - add方法用于向map属性中添加动态数据。 综上所述,瑞吉外卖项目是基于黑马课程资源的项目,但存在一些未开发和讲解的模块。为了解决这个问题,可以参考引用提供的资源来完整实现项目的所有功能模块。套餐管理和后台订单展示与查询是瑞吉外卖项目中的一些功能,相关代码可以在引用提到的作者的博客中找到。为了更好地管理共同使用的类,可以按照引用的建议创建一个新的包,并将返回结果类放入其中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值