springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6

part1
part2
part3
part4
part5
part6(本页)

7. 用户收获地址、菜品展示、购物车、下单 功能开发

7.1 用户地址簿相关功能
7.2 菜品展示
7.3 购物车
7.4 下单

7.1 用户地址簿相关功能

7.1.1 整体分析

  1. 需求分析
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第1张图片
  2. 数据模型springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第2张图片
  3. 需要开发的模块:新增收获地址、设置默认地址

7.1.2 前端代码分析

  1. 把addressList(vue 中的data模块)中的数据展示到页面上代码
<div class="divContent">
    <div class="divItem" v-for="(item,index) in addressList" :key="index" @click.capture="itemClick(item)">
        <div class="divAddress">
            <span :class="{spanCompany:item.label === '公司',spanHome:item.label === '',spanSchool:item.label === '学校'}">{{item.label}}span>
            
            {{item.detail}}
        div>
        <div class="divUserPhone">
            <span>{{item.consignee}}span>
            <span>{{item.sex === '0' ? '女士' : '先生'}}span>
            <span>{{item.phone}}span>
        div>
        
        <img src="./../images/edit.png" @click.stop.prevent="toAddressEditPage(item)"/>
        <div class="divSplit">div>
        <div class="divDefault" >
            
            <img src="./../images/checked_true.png" v-if="item.isDefault === 1">
            <img src="./../images/checked_false.png" @click.stop.prevent="setDefaultAddress(item)" v-else>设为默认地址
        div>
    div>
div>

<div class="divBottom" @click="toAddressCreatePage">+ 添加收货地址div>
div>

页面效果如下:
springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第3张图片
点击添加收获地址之后触发函数发送跳转,类似的编辑也是这样

toAddressCreatePage(){
    window.requestAnimationFrame(()=>{
        window.location.href= '/front/page/address-edit.html'
    })
},
  1. 编辑 address-edit.html
    页面效果
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第4张图片
    钩子函数created() 执行之后,执行了initData(),这里面有一个根据id查询地址的功能,从数据库查询到之后 ,显示到页面上addressFindOneApi;
async initData(){
     /*
         window.location.search 是 JavaScript 中的一个属性,
         它表示当前页面的 URL 中的查询字符串部分。
         例如,如果当前页面的 URL 是 "https://www.example.com/search?q=javascript&page=2",
         那么 window.location.search 的值就是 "?q=javascript&page=2"。
     * */
     const params = parseUrl(window.location.search)
     this.id = params.id
     if(params.id){
         this.title = '编辑收货地址'
         const res = await addressFindOneApi(params.id)
         if(res.code === 1){
             this.form = res.data
         }else{
             this.$notify({ type:'warning', message:res.msg});
         }
     }
 }

增加和删除的前端代码,还是很好理解的.

async saveAddress(){
  const form = this.form
  if(!form.consignee){
      this.$notify({ type:'warning', message:'请输入联系人'});
      return 
  }
  if(!form.phone){
      this.$notify({ type:'warning', message:'请输入手机号'});
      return 
  }
  if(!form.detail){
      this.$notify({ type:'warning', message:'请输入收货地址'});
      return 
  }
  const reg = /^1[3|4|5|7|8][0-9]{9}$/
  if(!reg.test(form.phone)){
      this.$notify({ type:'warning', message:'手机号码不合法'});
      return  
  }
  let res= {}
  if(this.id){
      res = await updateAddressApi(this.form)
  }else{
      res = await addAddressApi(this.form)
  }
  
  if(res.code === 1){
      window.requestAnimationFrame(()=>{
          window.location.replace('/front/page/address.html')
      })
  }else{
      this.$notify({ type:'warning', message:res.msg});
  }
},
deleteAddress(){
  this.$dialog.confirm({
      title: '确认删除',
      message: '确认要删除当前地址吗?',
  })
  .then( async () => {
      const res = await deleteAddressApi({ids:this.id })
      if(res.code === 1){
          window.requestAnimationFrame(()=>{
              window.location.replace('/front/page/address.html')
          })
      }else{
          this.$notify({ type:'warning', message:res.msg});
      }
  })
  .catch(() => {
  });
},

其中三个关键的axios请求:

//新增地址
function  addAddressApi(data){
    return $axios({
        'url': '/addressBook',
        'method': 'post',
        data
      })
}

//修改地址
function  updateAddressApi(data){
    return $axios({
        'url': '/addressBook',
        'method': 'put',
        data
      })
}

//删除地址
function deleteAddressApi(params) {
    return $axios({
        'url': '/addressBook',
        'method': 'delete',
        params
    })
}

7.1.3 后端代码分析

这部分比较简单,就是接收前端的数据,从前面看到,提交的都是大多json格式的数据,需要使用@RequestBody 获取。

  1. 页面根据id查询地址,显示出来。

前端请求是这样的

function addressFindOneApi(id) {
  return $axios({
    'url': `/addressBook/${id}`,
    'method': 'get',
  })
}

controller:

@GetMapping("/{id}")
public RetObj get(@PathVariable Long id) {
    AddressBook addressBook = addressBookService.getById(id);
    if (addressBook != null) {
        return RetObj.success(addressBook);
    } else {
        return RetObj.error("没有找到该对象");
    }
}
  1. 把所有地址显示到页面上
    前端:
async initData(){
    const res = await addressListApi()
    if(res.code === 1){
        this.addressList = res.data
    }else{
        this.$message.error(res.msg)
    }
},

后端:

 @GetMapping("/list")
 public RetObj<List> getAddressById(){
     Long userId = BaseContext.getThreadLocal();
     LambdaQueryWrapper<AddressBook> lambdaQueryWrapper = new LambdaQueryWrapper<>();
     lambdaQueryWrapper.eq(AddressBook::getUserId,userId)
             .orderByDesc(AddressBook::getUpdateTime);
     //SQL:select * from address_book where user_id = ? order by update_time desc
     List<AddressBook> addressBooks = addressBookService.list(lambdaQueryWrapper);
     return RetObj.success(addressBooks);
 }
  1. 新增
@PostMapping("")
public RetObj<String> addAddressController(@RequestBody AddressBook addressBook){
    //注意需要 将当前操作插入的用户注入,使用ThreadLocal
    addressBook.setUserId(BaseContext.getThreadLocal());
    boolean res = addressBookService.save(addressBook);
    if (res){
        return RetObj.success("成功新增地址");
    }else {
        return RetObj.error("地址添加失败!");
    }
}
  1. 设置默认地址
    前端,点击按钮,设置为默认,注意参数是json
async setDefaultAddress(item){
   if(item.id){
       const res = await setDefaultAddressApi({id:item.id})
       if(res.code === 1){
       this.initData()
       }else{
           this.$message.error(res.msg)
       }
   }
},

后端,根据id 设置默认地址,有很多注意的点,比如先得把表中所有地址设置为非默认,否则就有两个默认地址

/**
 * 设置默认的地址,需要把其他地址都先设置为非默认的!!(而且是当前user对应的地址)
 * @param addressBook
 * @return
 */
@PutMapping("/default")
public RetObj setDefaultAddress(@RequestBody AddressBook addressBook){
    log.info("addressBook:{}", addressBook);
    LambdaUpdateWrapper<AddressBook> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
    lambdaUpdateWrapper.eq(addressBook != null,AddressBook::getUserId
                    ,BaseContext.getThreadLocal())
            .set(AddressBook::getIsDefault,0);
    boolean res = addressBookService.update(lambdaUpdateWrapper);
    
    //上面先把所有的地址都设置为非默认
    //现在把指定的地址设置为默认
    addressBookService.updateById(addressBook);//会根据非null的字段进行更新!!
    return RetObj.success("成功设置为默认地址");
}
  1. 查询默认地址(一开始把所有地址显示出来之后,要查一下哪个地址是默认的,然后在页面显示,对应地址是默认地址)
/**
 * 查询默认地址,因为可能没有查到地址等情况,所以返回值需要判别一下
 * SQL:select * from address_book where user_id = ? and is_default = 1
 */
@GetMapping("default")
public RetObj<AddressBook> getDefault() {
    LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(AddressBook::getUserId, BaseContext.getThreadLocal());
    queryWrapper.eq(AddressBook::getIsDefault, 1);

    //SQL:select * from address_book where user_id = ? and is_default = 1
    AddressBook addressBook = addressBookService.getOne(queryWrapper);

    if (null == addressBook) {
        return RetObj.error("没有找到该对象");
    } else {
        return RetObj.success(addressBook);
    }
}

7.2 移动端菜品展示

7.2.1 整体分析

  1. 需求分析
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第5张图片
    2.交互过程分析
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第6张图片
    分类数据展示到左边的菜单栏上,在之前的分类管理中,其实已经写好了 (就是之前,选中菜品的分类的时候,下拉框就需要显示有哪些分类,正好现在直接复用) ;
    一打开页面也要默认展示第一个分类下的菜品,不要点击了才展示出来(比如现在的湘菜)。类似的,代码之前也写的差不多了,之前在新增分类中,选择该分类下的菜品(下拉框中)。但是之前返回值是List,现在不仅仅需要Dish,还需要Dish对应的口味信息,后端那边要对这个进行改造(使用DishDto)!
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第7张图片

7.2.2 前端代码分析

  1. 显示分类,循环读取categoryList的内容。如果点击了对应的套餐,显示套餐下的菜品,展示到右边
<div class="divType">
  <ul>
    <li v-for="(item,index) in categoryList" :key="index" @click="categoryClick(index,item.id,item.type)" :class="{active:activeType === index}">{{item.name}}</li>
  </ul>
</div>

显示出套餐下的菜品,调用getDishList()

//分类点击
categoryClick(index,id,type){
  this.activeType = index
  this.categoryId = id
  if(type === 1){//菜品
    this.getDishList()
  }else{
    this.getSetmealData()
  }
},

getDishList()具体方法如下,获取了后端分装好的data数据,保存在dishList中进行数据的双向绑定。

//获取菜品数据
async getDishList(){
  if(!this.categoryId){
    return
  }
  const res = await dishListApi({categoryId:this.categoryId,status:1})
  if(res.code === 1){
    let dishList = res.data
    const cartData  = this.cartData
    if(dishList.length > 0 && cartData.length > 0){
      dishList.forEach(dish=>{ //循环为DishList插入cart值
        cartData.forEach(cart=>{
          if(dish.id === cart.dishId){
            dish.number = cart.number
          
      })
    }
    this.dishList = dishList
  }else{
    this.$notify({ type:'warning', message:res.msg});
  }
},

具体发送的axios请求。这里的后端代码也是复用了之前的,之前在添加套餐的时候要选择菜品,下拉框中就是这些菜品。

function dishListApi(data) {
    return $axios({
        'url': '/dish/list',
        'method': 'get',
        params:{...data}
    })
}
  1. 接着上面,点击对应的菜品分类后(数据已经存到dishList中),现在要循环显示出该分类下的菜品,并且能够对菜品进行操作,包括显示月销,选择规格等。同时,如果点击进去,会显示具体的细节,对应@click="dishDetails(item)
<div class="divMenu">
   <div>
     <div class="divItem" v-for="(item,index) in dishList" :key="index" @click="dishDetails(item)">
       <el-image :src="imgPathConvert(item.image)" >
         <div slot="error" class="image-slot">
           <img src="./images/noImg.png"/>
         </div>
       </el-image>
       <div>
         <div class="divName">{{item.name}}</div>
         <div class="divDesc">{{item.description}}</div>
         <div class="divDesc">{{'月销' + (item.saleNum ? item.saleNum : 0)  }}</div>
         <div class="divBottom"><span></span><span>{{item.price/100}}</span></div>
         <div class="divNum">
           <div class="divSubtract" v-if="item.number > 0">
             <img src="./images/subtract.png" @click.prevent.stop="subtractCart(item)"/>
           </div>
           <div class="divDishNum">{{item.number}}</div>
           <div class="divTypes" v-if="item.flavors && item.flavors.length > 0 && !item.number " @click.prevent.stop="chooseFlavorClick(item)">选择规格</div>
           <div class="divAdd" v-else>
             <img src="./images/add.png" @click.prevent.stop="addCart(item)"/>
           </div>
         </div>
       </div>
     </div>
   </div>

进行了后端交互,调用setMealDishDetailsApi()

async dishDetails(item){
    //先清除对象数据,如果不行的话dialog使用v-if
    this.detailsDialog.item = {}
    this.setMealDialog.item = {}
    if(Array.isArray(item.flavors)){
      this.detailsDialog.item = item
      this.detailsDialog.show = true
    }else{
      //显示套餐的数据
      const res = await setMealDishDetailsApi(item.id)
      if(res.code === 1){
        this.setMealDialog.item = {...item,list:res.data}
        this.setMealDialog.show = true
      }else{
        this.$notify({ type:'warning', message:res.msg});
      }
    }
  }

axios交互:

//获取套餐的全部菜品
function setMealDishDetailsApi(id) {
    return $axios({
        'url': `/setmeal/dish/${id}`,
        'method': 'get',
    })
}
  1. 初始化数据,一进来不点击任何东西应该也要显示出对应第一个分类的菜品.
    注意Promise.all必须是购物车和分类都加载完毕了,才把数据初始化到界面上。只有这里面所有的请求都完成了,才会成功执行接下来的代码,才会把categoryList等等数据提交成功。所以没写购物车逻辑的时候,就算分类写好了也显示不出来
mounted(){
  this.initData()
},
methods:{
  //初始化数据
  initData(){
    Promise.all([categoryListApi(),cartListApi({})]).then(res=>{
      //获取分类数据
      if(res[0].code === 1){
        this.categoryList = res[0].data
        if(Array.isArray(res[0].data) && res[0].data.length > 0){
          this.categoryId = res[0].data[0].id
          if(res[0].data[0].type === 1){
            this.getDishList()
          }else{
            this.getSetmealData()
          }
        }
      }else{
        this.$notify({ type:'warning', message:res[0].msg});
      }
      //获取菜品数据
      if(res[1].code === 1){
      this.cartData = res[1].data
      }else
        this.$notify({ type:'warning', message:res[1].msg});
      }
    })
  },
  1. 上面的代码完成所有分类和套餐的名字的展示,没有把具体的套餐和菜品的信息显示出来,上面代码中调用了getDishList()方法,具体如下(对应type === 1)
//获取菜品数据
async getDishList(){
  if(!this.categoryId){
    return
  }
  const res = await dishListApi({categoryId:this.categoryId,status:1})
  if(res.code === 1){
    let dishList = res.data
    const cartData  = this.cartData
    if(dishList.length > 0 && cartData.length > 0){
      dishList.forEach(dish=>{ //为每一条dishList插入cart值
        cartData.forEach(cart=>{
          if(dish.id === cart.dishId){
            dish.number = cart.number
          }
        })
      })
    }
    this.dishList = dishList
  }else{
    this.$notify({ type:'warning', message:res.msg});
  }
},

对应api:

function dishListApi(data) {
    return $axios({
        'url': '/backend/page/food/list/getDishByCategoryId.do',
        'method': 'get',
        params:{...data}
    })
}
  1. 现在type不是1,那就是要展示套餐的具体信息:getSetmealData(),和上面很类似
//获取套餐数据setmealId
async getSetmealData(){
  if(!this.categoryId){
    return
  }
  const res = await setmealListApi({categoryId:this.categoryId,status:1})
  if(res.code === 1){
      let dishList = res.data
      const cartData  = this.cartData
      if(dishList.length > 0 && cartData.length > 0){
        dishList.forEach(dish=>{
          cartData.forEach(cart=>{
            if(dish.id === cart.setmealId){
              dish.number = cart.number
            }
          })
        })
      }
      this.dishList = dishList
  }else{
    this.$notify({ type:'warning', message:res.msg});
  }
},

setmealListApi如下

function setmealListApi(data) {
    return $axios({
        'url': '/setmeal/list',
        'method': 'get',
        params:{...data}
    })
}
  1. 以上完成的是具体菜品或者套餐在页面右边的展示,现在如果点击了 具体菜品或者套餐(如:麻辣兔头、二逼套餐A计划),要显示该菜品的具体信息。
async dishDetails(item){
   //先清除对象数据,如果不行的话dialog使用v-if
   this.detailsDialog.item = {}
   this.setMealDialog.item = {}
   if(Array.isArray(item.flavors)){
     this.detailsDialog.item = item
     this.detailsDialog.show = true
   }else{
     //显示套餐的数据
     const res = await setMealDishDetailsApi(item.id)
     if(res.code === 1){
       this.setMealDialog.item = {...item,list:res.data}
       this.setMealDialog.show = true
     }else{
       this.$notify({ type:'warning', message:res.msg});
     }
   }
 }

7.2.3 后端分析

  1. 查出所有套餐、菜品的分类,显示出来(左边的栏目)。之前写好了,直接复用,注意加上category.getType() != null。
@GetMapping("/food/list/getCategory.do")
public RetObj getCategoryList(Category category){
    LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    //category.getType() != null 加上了这个,因为在移动端front中会直接查询,不带type,把菜品和套餐全部查出来
    lambdaQueryWrapper.eq(category.getType() != null,Category::getType,category.getType())
            .orderByAsc(Category::getSort)
            .orderByDesc(Category::getUpdateTime);
    List<Category> categoryList = categoryService.list(lambdaQueryWrapper);
    //log.info("查询出菜品:{}",categoryList);
    return RetObj.success(categoryList);
}
  1. 点击左边的菜品分类后,查出该菜品分类对应有哪些菜。如川菜有:麻辣兔头等。这个其实能够复用,也是在下拉框。
    这个要注意!菜品需要把口味显示出来!效果如下,后端做法也就是不能够再传输一个DIsh的集合过去,应该传过去一个dishDto的list过去,这个dto内含有口味list的属性。
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第8张图片
/**
* 根据菜品分类查菜品比如: 川菜这个选项一点击,就通过这个controller返回一个list(元素就是各种川菜dish)
* 这里要注意的是,返回值应该不仅包含菜品,还需要保护这些菜品对应的口味,在添加到购物车的时候就可以把口味也添加到其中。
* 主要的思路就是使用dishDto
* @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);
   //先将所有的dish查询出来
   List<Dish> dishList = dishService.list(lambdaQueryWrapper);
   //循环重构每一个元素。
   List<DishDto> dishDtoList = dishList.stream().map(item -> {
       DishDto dishDto = new DishDto();
       BeanUtils.copyProperties(item, dishDto);
       LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
       //注意Id对应,Dish元素的id对应DishFlavor中的DishId的id
       dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, item.getId());
       List<DishFlavor> dishFlavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
       dishDto.setFlavors(dishFlavors);
       return dishDto;
   }).collect(Collectors.toList());

   return RetObj.success(dishDtoList);
}

  1. 点击套餐分类后弹出该套餐分类下有哪些套餐。(setmeal套餐表中有category_id字段,就根据这个字段进行查询即可(左边栏目中点击的就传category_id,如点击二逼套餐,传递二逼套餐对应的category_id,查出二逼套餐A计划),后端开发需要注意state==1,没有被禁用的才能查出。)
    如儿童套餐里面有 儿童套餐A计划、儿童套餐B计划等。每个具体套餐又含有对应的菜品。如二逼套餐中有二逼套餐A计划,二逼套餐A计划中有二逼(二逼这个具体的菜又属于二逼菜这个菜品分类)。 如下图添加套餐管理时就是这样添加的。
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第9张图片
/**
 * 查出套餐分类下有哪些套餐:如二逼套餐中有二逼套餐A计划、B计划等(二逼套餐A计划中有二逼(二逼这个具体的菜又属于二逼菜这个菜品分类)
 * 注意,
 * @param  主要就是categoryID,还有state是1,没被禁用的查出
 * @return
 */
@GetMapping("list/getSetMealByCategoryId.do")
public RetObj getSetMealByCategoryId(Long categoryId,Integer status){
    LambdaQueryWrapper<Setmeal> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(Setmeal::getCategoryId,categoryId)
            .eq(Setmeal::getStatus,status)
            .orderByDesc(Setmeal::getUpdateTime);

    List<Setmeal> setmealList = setmealService.list(lambdaQueryWrapper);
    return RetObj.success(setmealList);
}

做完后效果如下
springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第10张图片
4. 以上是完成左边分类栏目的显示,以及点击分类栏目在右边示出该分类具体的项目,现在点击具体的项目,要显示出详情。比如点击上面的二逼套餐A计划,要能看到这个套餐中有哪些菜品。

  • 这部分一定要搞清楚表的结构! 举例:二逼套餐点击后有二逼套餐A计划,现在就是想点击二逼套餐A计划,看有哪些菜。
    1. 二逼套餐A计划 表中有id是自己的id,有categoryId是二逼套餐的Id。
    1. 在setmeal_dish表中有 setmeal_id、dish_id、id 现在这些id都很清楚明白了
    1. 用二逼套餐A计划自己的Id(主键)去查setmeal_dish中的setmeal_id(副键)
/**
* 获取套餐的全部菜品
* @param id 注意这个id是二逼套餐A计划套餐的id,查表setmeal_dish中的setmeal_id这个字段对应起来
* @return
*/
@GetMapping("setmeal/setMealDishDetails.do/{id}")
public RetObj setMealDishDetails(@PathVariable Long id){
   LambdaQueryWrapper<SetmealDish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
   dishLambdaQueryWrapper.eq(SetmealDish::getSetmealId,id)
           .orderByDesc(SetmealDish::getUpdateTime);
   List<SetmealDish> setmealDishes = setmealDishService.list(dishLambdaQueryWrapper);
   return RetObj.success(setmealDishes);
}

7.3 购物车

7.3.1 整体分析

  1. 数据结构表如下。当添加套餐或者添加菜品的时候,添加到对应的字段中,同时想要设置user_id,这个直接从ThreadLocal中拿就好。想要设置购物车物品的个数number。
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第11张图片

7.3.2 前端代码分析

  1. 获取购物车的数据(从后端获取,可能是之前添加到购物车中的内容)
//获取购物车数据
 async getCartData(){
 	//	async 是异步的意思,而 await 是等待的意思,await 用于等待一个异步任务执行完成的结果。
   const res = await cartListApi({})
   if(res.code === 1){
     this.cartData = res.data
   }else{
     this.$notify({ type:'warning', message:res.msg});
   }
 },

对应的ajax请求

//获取购物车内商品的集合
function cartListApi(data) {
    return $axios({
        'url': '/shoppingCart/list',
        'method': 'get',
        params:{...data}
    })
}
  1. 菜单中往购物车中添加商品
async addCart(item){
  let params = {
    amount:item.price/100,//金额
    dishFlavor:item.dishFlavor,//口味  如果没有传undefined
    dishId:undefined,//菜品id
    setmealId:undefined,//套餐id
    name:item.name,
    image:item.image
  }
  
  //先将菜品id和套餐id置为0,之后再判断到底是菜品还是套餐,进行赋值
  if(Array.isArray(item.flavors)){//表示是菜品
    params.dishId = item.id
  }else{//表示套餐 套餐没有口味
    params.setmealId = item.id
  }
  const res = await addCartApi(params)
  if(res.code === 1){
    this.dishList.forEach(dish=>{
      if(dish.id === item.id){
        dish.number = res.data.number
      }
    })
    if(this.setMealDialog.show){
      item.number = res.data.number
    }
    this.getCartData()
  }else{
    this.$notify({ type:'warning', message:res.msg});
  }
},

调用后端把购物车中的数据存起来,目的就是防止用户离开后,购物车数据消失。

function  addCartApi(data){
    return $axios({
        'url': '/shoppingCart/add',
        'method': 'post',
        data
      })
}
  1. 菜单中减少选中的商品,关键就是把参数传给
async subtractCart(item){
    let params = {
      dishId:item.id,
    }
    if(!Array.isArray(item.flavors)){
      params = {
        setmealId:item.id,
      }
    }
    const res = await updateCartApi(params)
    if(res.code === 1){
    this.dishList.forEach(dish=>{
      if(dish.id === item.id){
        dish.number = (res.data.number === 0 ? undefined : res.data.number)
      }
    })
    if(this.setMealDialog.show){
      item.number = (res.data.number === 0 ? undefined : res.data.number)
    }
    this.getCartData()
    }else{
      this.$notify({ type:'warning', message:res.msg});
    }
},

发送的axios

function  updateCartApi(data){
    return $axios({
        'url': '/shoppingCart/sub',
        'method': 'post',
        data
      })
}

7.3.3 后端代码

  1. 展示购物车的物品
@GetMapping("/list")
public RetObj listShoppingCart(){
    LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
    shoppingCartLambdaQueryWrapper.orderByDesc(ShoppingCart::getCreateTime)
            .eq(ShoppingCart::getUserId, BaseContext.getThreadLocal());
    List<ShoppingCart> shoppingCartList = shoppingCartService.list(shoppingCartLambdaQueryWrapper);
    return RetObj.success(shoppingCartList);
}
  1. 添加菜品或套餐到购物车,这个过程中需要注意几个点。传过去的参数直接就是json形式的,所以后端接收需要使用@RequestMapping。

let params = {amount:item.amount,//金额 dishFlavor:item.dishFlavor,//口味 如果没有传undefined dishId:item.dishId,//菜品id setmealId:item.setmealId,//套餐id name:item.name, image:item.image }

注意,前端在添加菜品的时候,添加多次同一个菜品,应该在原来购物车的基础上,添加该商品的个数,而不是再重复在表中加一个记录。

    /**
     * 添加菜品或套餐到购物车
     * 1.前端传过来的是json的格式,去研究一下如何看前端的数据到底是json还是?的形式
     * 2.注意前端传过来的数据,肯定没有userID,这个是必填字段,从ThreadLocal中获取,或者session
     * 3.需要先查询数据库,看数据库里面有没有这条数据,如果有,再添加东西的时候,只有将这条记录的number字段+1,而不是在数据库里面再加一条数据
     *      具体操作哪个?不应该是userId,也不应该是id本身,因为id本身是自动生成的,每个都不一样,考虑使用菜品、套餐的名称
     * @param shoppingCart 根据前端的意思,把对应的参数接收到这个对象中
     * @return
     */
    @PostMapping("/add")
    public RetObj addShoppingCart(@RequestBody ShoppingCart shoppingCart){
        //填充其对应是哪个用户的购物车
        Long userId = BaseContext.threadLocal.get();
        shoppingCart.setUserId(userId);
        //先查出有没有该条菜品、套餐的记录
        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getName,shoppingCart.getName());
        ShoppingCart shoppingCartDish = shoppingCartService.getOne(shoppingCartLambdaQueryWrapper);
        if (shoppingCartDish != null){
            //如果本来就有记录,记录条数加1就可以了。
            shoppingCartDish.setNumber(shoppingCartDish.getNumber()+1);
            //shoppingCartDish.setAmount(shoppingCartDish.getAmount());
            shoppingCartService.updateById(shoppingCartDish);
            return RetObj.success("成功加添");
        }else { //第一次加入
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            boolean save = shoppingCartService.save(shoppingCart);
            if (save) return RetObj.success("成功加入购物车!");
            else return RetObj.error("加入购物车失败!");
        }
    }
  1. 减少购物车中的商品
   /**
     * 减少购物车中的商品
     * 1. 注意前端 let params = {dishId:item.id,或者套餐的id} 直接把这个param穿过来,所以肯定是json的格式
     * 2. 前端也是根据有没有口味来判断到底要删除的是菜品,还是套餐,从而判断传过来的param到底是dishId,还是setmealId
     *
     * 3. 前端只传了id过来,那现在应该根据id查出最重要的number,进行减1
     * 4.要注意,如果已经是1,应该直接删除这条数据(删除购物车的东西),而不是将number设置为0
     * 5. 忘了最基本的一点:查询本用户的购物车,不是整个表的!!
     * @param shoppingCart
     * @return
     */
    @PostMapping("/sub")
    public RetObj subUpdateShoppingCart(@RequestBody ShoppingCart shoppingCart){
        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getThreadLocal());

/*        //可能是菜品,也可能是套餐.
        if (shoppingCart.getDishId() == null){ //是套餐,queryWrapper应该追加条件
            shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getDIshId,shoppingCart.getDishId())
        }else ...*/
        //优化
        shoppingCartLambdaQueryWrapper.eq(shoppingCart.getDishId() != null, ShoppingCart::getDishId,shoppingCart.getDishId())
                .eq(shoppingCart.getSetmealId() != null, ShoppingCart::getSetmealId,shoppingCart.getSetmealId())
                .eq(ShoppingCart::getUserId,BaseContext.getThreadLocal());//基本的别忘了,不然可能会把别人购物车的东西删除了!

        ShoppingCart shoppingCart1 = shoppingCartService.getOne(shoppingCartLambdaQueryWrapper);

        if (shoppingCart1.getNumber() == 1){ //如果只有一条,直接删除
            shoppingCartService.removeById(shoppingCart1);//是从数据库查出来的,所以有id
        }else{ //如果多份商品,num-1
            shoppingCart1.setNumber(shoppingCart1.getNumber()-1);
            shoppingCartService.updateById(shoppingCart1);

        }
        return RetObj.success("删除成功!");
    }
  1. 直接清空整个购物车
 /**
     * 无参Api,直接清空整个购物车
     * 注意,不是删除整个表!!而是删除该用户下的所有记录条数(删除自己的)
     */
    @DeleteMapping("/clean")
    public RetObj cleanShoppingCart(){
        //SQL:delete from shopping_cart where user_id = ?
        LambdaQueryWrapper<ShoppingCart> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(ShoppingCart::getUserId,BaseContext.getThreadLocal());

        boolean remove = shoppingCartService.remove(lambdaQueryWrapper);
        if (remove) {
            return RetObj.success("成功清除购物车");
        }else {
            return RetObj.error("清除失败!");
        }
    }

7.4 用户下单

7.4.1 整体分析

  1. 需求分析
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第12张图片
  2. 数据结构,订单表插入一条数据,表示一个订单,订单明细表可能是多条,可能有多个菜品、套餐
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第13张图片
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第14张图片
  3. 交互过程
    springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第15张图片

7.4.2 前端分析

该部分前端较为简单,主要就是点击提交订单,进行页面跳转,并进行ajax请求把对应的数据存在数据库就好了
下面为add-order的页面
springboot项目:瑞吉外卖 前后端 代码、思路 详细分析 part6_第16张图片

持续更新中…

你可能感兴趣的:(前后端详细分析,spring,boot,java,后端,spring)