part1
part2
part3
part4
part5
part6(本页)
7. 用户收获地址、菜品展示、购物车、下单 功能开发
7.1 用户地址簿相关功能
7.2 菜品展示
7.3 购物车
7.4 用户下单
7.1 用户地址簿相关功能
7.1.1 整体分析
- 需求分析
- 数据模型
- 需要开发的模块:新增收获地址、设置默认地址
7.1.2 前端代码分析
- 把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>
<!--把addressList中的数据循环出来-->
{{item.detail}}
</div>
<div class="divUserPhone">
<span>{{item.consignee}}</span>
<span>{{item.sex === '0' ? '女士' : '先生'}}</span>
<span>{{item.phone}}</span>
</div>
<!--
@click.stop.prevent 是 Vue.js 中的指令,用于阻止事件的默认行为和事件冒泡。
当该指令绑定在一个元素上时,当该元素被点击时,
会阻止事件的默认行为(如链接跳转)和事件冒泡(即不会向父元素传递事件)。
toAddressEditPage,点击后跳转到对应的页面,编辑,后面的添加也一样
-->
<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>
页面效果如下:
点击添加收获地址之后触发函数发送跳转,类似的编辑也是这样
toAddressCreatePage(){
window.requestAnimationFrame(()=>{
window.location.href= '/front/page/address-edit.html'
})
},
- 编辑 address-edit.html
页面效果
钩子函数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 获取。
- 页面根据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("没有找到该对象");
}
}
- 把所有地址显示到页面上
前端:
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);
}
- 新增
@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("地址添加失败!");
}
}
- 设置默认地址
前端,点击按钮,设置为默认,注意参数是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("成功设置为默认地址");
}
- 查询默认地址(一开始把所有地址显示出来之后,要查一下哪个地址是默认的,然后在页面显示,对应地址是默认地址)
/**
* 查询默认地址,因为可能没有查到地址等情况,所以返回值需要判别一下
* 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 整体分析
- 需求分析
2.交互过程分析
分类数据展示到左边的菜单栏上,在之前的分类管理中,其实已经写好了 (就是之前,选中菜品的分类的时候,下拉框就需要显示有哪些分类,正好现在直接复用) ;
一打开页面也要默认展示第一个分类下的菜品,不要点击了才展示出来(比如现在的湘菜)。类似的,代码之前也写的差不多了,之前在新增分类中,选择该分类下的菜品(下拉框中)。但是之前返回值是List<Dish>
,现在不仅仅需要Dish,还需要Dish对应的口味信息,后端那边要对这个进行改造(使用DishDto)!
7.2.2 前端代码分析
- 显示分类,循环读取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}
})
}
- 接着上面,点击对应的菜品分类后(数据已经存到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',
})
}
- 初始化数据,一进来不点击任何东西应该也要显示出对应第一个分类的菜品.
注意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});
}
})
},
- 上面的代码完成所有分类和套餐的名字的展示,没有把具体的套餐和菜品的信息显示出来,上面代码中调用了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}
})
}
- 现在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}
})
}
- 以上完成的是具体菜品或者套餐在页面右边的展示,现在如果点击了 具体菜品或者套餐(如:麻辣兔头、二逼套餐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 后端分析
- 查出所有套餐、菜品的分类,显示出来(左边的栏目)。之前写好了,直接复用,注意加上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);
}
- 点击左边的菜品分类后,查出该菜品分类对应有哪些菜。如川菜有:麻辣兔头等。这个其实能够复用,也是在下拉框。
这个要注意!菜品需要把口味显示出来!效果如下,后端做法也就是不能够再传输一个DIsh的集合过去,应该传过去一个dishDto的list过去,这个dto内含有口味list的属性。
/**
* 根据菜品分类查菜品比如: 川菜这个选项一点击,就通过这个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);
}
- 点击套餐分类后弹出该套餐分类下有哪些套餐。(setmeal套餐表中有category_id字段,就根据这个字段进行查询即可(左边栏目中点击的就传category_id,如点击二逼套餐,传递二逼套餐对应的category_id,查出二逼套餐A计划),后端开发需要注意state==1,没有被禁用的才能查出。)
如儿童套餐里面有 儿童套餐A计划、儿童套餐B计划等。每个具体套餐又含有对应的菜品。如二逼套餐中有二逼套餐A计划,二逼套餐A计划中有二逼(二逼这个具体的菜又属于二逼菜这个菜品分类)。 如下图添加套餐管理时就是这样添加的。
/**
* 查出套餐分类下有哪些套餐:如二逼套餐中有二逼套餐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);
}
做完后效果如下
4. 以上是完成左边分类栏目的显示,以及点击分类栏目在右边示出该分类具体的项目,现在点击具体的项目,要显示出详情。比如点击上面的二逼套餐A计划,要能看到这个套餐中有哪些菜品。
- 这部分一定要搞清楚表的结构! 举例:二逼套餐点击后有二逼套餐A计划,现在就是想点击二逼套餐A计划,看有哪些菜。
-
- 二逼套餐A计划 表中有id是自己的id,有categoryId是二逼套餐的Id。
-
- 在setmeal_dish表中有 setmeal_id、dish_id、id 现在这些id都很清楚明白了
-
- 用二逼套餐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 整体分析
- 数据结构表如下。当添加套餐或者添加菜品的时候,添加到对应的字段中,同时想要设置user_id,这个直接从ThreadLocal中拿就好。想要设置购物车物品的个数number。
7.3.2 前端代码分析
- 获取购物车的数据(从后端获取,可能是之前添加到购物车中的内容)
//获取购物车数据
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}
})
}
- 菜单中往购物车中添加商品
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
})
}
- 菜单中减少选中的商品,关键就是把参数传给
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 后端代码
- 展示购物车的物品
@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);
}
- 添加菜品或套餐到购物车,这个过程中需要注意几个点。传过去的参数直接就是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. 注意前端 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("删除成功!");
}
- 直接清空整个购物车
/**
* 无参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 整体分析
- 需求分析
- 数据结构,订单表插入一条数据,表示一个订单,订单明细表可能是多条,可能有多个菜品、套餐
- 交互过程
7.4.2 前端分析
该部分前端较为简单,主要就是点击提交订单,进行页面跳转,并进行ajax请求把对应的数据存在数据库就好了
下面为add-order的页面
7.4.3 后端分析
- 该部分后端代码其实比较复杂,设计先查出购物车的数据,封装为订单详细信息,再清空购物车;还要从用户地址中查出用户的基本信息,比如姓名,电话,最后插入到订单表中,信息注解代码如下
/**
*1. 前端:const params = {remark:this.note,payMethod:1,addressBookId:this.address.id}
* 前端这个地址只有一个id,需要根据这个id查询具体的地址信息,插入到订单详细表中
* 2.orderDetail表:id、name、image、order_id、dish_id、dish_id、setmeal_id、dish_flavor、number、amount
* order表:id、number、userId、status、user_id、address_book_id、orderTime,CheckoutTime,amount,phone,address,userName
* 3. 插入的东西:
* 3.1 userId, payMethod、address(具体的地址,不是一个id)、note
* 3.2 购物车中的菜品或套餐,如果是菜品,还要加入口味;当然还有amount金额,number数量、image、name
* 3.3 在orderDetail表中插入orderId
* 4. 注意插入到orderDetail中的数据类型应该为OrderDetail的类,这个类的成员变量的补充应该大部分从购物车中查出来的list进行填充
* 5. 订单总的amount是前端自己算的(根据每个菜品、套餐的amount和number);orderDetail的订单存的是每个具体菜品等信息,amount可以是单独的
* @param orders
*/
@Transactional
public void submitOrderService(Orders orders){
Long userId = BaseContext.getThreadLocal();
long orderId = IdWorker.getId(); //订单号
//订单总的金额,根据购物车中每个商品的价格和number进行累加,保证多线程操作下安全性,用AtomicInteger乐观锁配合validate使用
AtomicInteger amount = new AtomicInteger(0);
//查出用户购物车的内容
LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId,userId);
List<ShoppingCart> shoppingCartList = shoppingCartService.list(shoppingCartLambdaQueryWrapper);
//将shoppingCartList中的元素的数据都用OrderDetail类型存储,之后存到OrderDetail表中
List<OrderDetail> orderDetailList = shoppingCartList.stream().map(item -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setAmount(item.getAmount());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setNumber(item.getNumber());
orderDetail.setDishId(item.getDishId());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setSetmealId(item.getSetmealId());
//根据每一个购物车中的商品,算出订单总价格
amount.set(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
orderDetailService.saveBatch(orderDetailList);
//准备用户,地址(地址这个你没有想到这么做,前端orders是会存addressBookId的)
User user = userService.getById(userId);
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
//完善order的基本信息
orders.setUserId(userId);
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));//总金额
orders.setUserName(user.getName());
orders.setPayMethod(2);///1是微信,2是支付宝
orders.setPhone(addressBook.getPhone());
orders.setConsignee(addressBook.getConsignee()); // 收货人
orders.setCheckoutTime(LocalDateTime.now());//结账时间
orders.setStatus(2);//1:待付款; 2 待派送....
orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
+ (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
this.save(orders);
//非常重要的一点!需要清空购物车,是当前用户的!!
shoppingCartService.remove(shoppingCartLambdaQueryWrapper);
}