- 视频:【黑马程序员】Java 项目实战《瑞吉外卖》,轻松掌握 SpringBoot + MybatisPlus 开发核心技术
- 资料:2022 最新版 Java学习 路线图>第 5 阶段一 企业级项目实战>7.黑马程序员 瑞吉外卖平台实战开发(提取码:dor4)
上一篇:学习【瑞吉外卖⑥】SpringBoot单体项目:https://blog.csdn.net/yanzhaohanwei/article/details/125228024
地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。
同一个用户可以有多个地址信息,但是只能有一个默认地址。
用户的地址信息会存储在 address_book 表,即 地址簿表 中。
com/itheima/reggie/entity/AddressBook.java
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 地址簿
*/
@Data
public class AddressBook implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//用户 id
private Long userId;
//收货人
private String consignee;
//手机号
private String phone;
//性别 0 女 1 男
private String sex;
//省级区划编号
private String provinceCode;
//省级名称
private String provinceName;
//市级区划编号
private String cityCode;
//市级名称
private String cityName;
//区级区划编号
private String districtCode;
//区级名称
private String districtName;
//详细地址
private String detail;
//标签
private String label;
//是否默认 0 否 1是
private Integer isDefault;
//创建时间
@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;
}
com/itheima/reggie/mapper/AddressBookMapper.java
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.AddressBook;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AddressBookMapper extends BaseMapper<AddressBook> { }
com/itheima/reggie/service/AddressBookService.java
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.AddressBook;
public interface AddressBookService extends IService<AddressBook> { }
com/itheima/reggie/service/impl/AddressBookServiceImpl.java
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.AddressBook;
import com.itheima.reggie.mapper.AddressBookMapper;
import com.itheima.reggie.service.AddressBookService;
import org.springframework.stereotype.Service;
@Service
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService { }
com/itheima/reggie/controller/AddressBookController.java
package com.itheima.reggie.controller;
import com.itheima.reggie.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 地址簿管理
*/
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;
}
com/itheima/reggie/controller/AddressBookController.java
/**
* 新增地址
*
* @param addressBook
* @return
*/
@PostMapping
public R<AddressBook> save(@RequestBody AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook:{}", addressBook);
addressBookService.save(addressBook);
return R.success(addressBook);
}
com/itheima/reggie/controller/AddressBookController.java
/**
* 设置默认地址
*
* @param addressBook
* @return
*/
@PutMapping("default")
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
log.info("addressBook:{}", addressBook);
LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<AddressBook>();
wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
wrapper.set(AddressBook::getIsDefault, 0);
//SQL:update address_book set is_default = 0 where user_id = ?
addressBookService.update(wrapper);
addressBook.setIsDefault(1);
//SQL:update address_book set is_default = 1 where id = ?
addressBookService.updateById(addressBook);
return R.success(addressBook);
}
com/itheima/reggie/controller/AddressBookController.java
/**
* 根据 id 查询地址
*
* @param id
* @return
*/
@GetMapping("/{id}")
public R get(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
if (addressBook != null) {
return R.success(addressBook);
} else {
return R.error("没有找到该对象");
}
}
com/itheima/reggie/controller/AddressBookController.java
/**
* 查询默认地址
*
* @return
*/
@GetMapping("default")
public R<AddressBook> getDefault() {
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
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 R.error("没有找到该对象");
} else {
return R.success(addressBook);
}
}
com/itheima/reggie/controller/AddressBookController.java
/**
* 查询指定用户的全部地址
*
* @param addressBook
* @return
*/
@GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook:{}", addressBook);
//条件构造器
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
queryWrapper.orderByDesc(AddressBook::getUpdateTime);
//SQL:select * from address_book where user_id = ? order by update_time desc
return R.success(addressBookService.list(queryWrapper));
}
http://localhost:8080/front/index.html
http://localhost:8080/front/page/user.html
http://localhost:8080/front/page/address.html
http://localhost:8080/front/page/address-edit.html
http://localhost:8080/front/page/address.html
显然,上述的地址管理功能是不完整的。
地址管理既然能 增加,也必然可以 修改 和 删除。
同时,官方也给我们写好了前端,那么我们只需写一些后台代码就行了。
官方要是不给前端页面的话,我这种懒人是懒得补未完成的代码功能的。
http://localhost:8080/front/page/address.html
http://localhost:8080/front/page/address-edit.html?id=1535994735380975618
front/page/address-edit.html
(前端资源中命名较为规范)编辑地址簿信息的功能
当我们进入到之前已经添加过的用户的界面时,再次点击保存页面时,前端控制台网络状态为 404。
之前我们在 AddressBookController 类中已经写好了地址簿信息的回显方法。
当前页面地址:http://localhost:8080/front/page/address-edit.html?id=1535994735380975618
com/itheima/reggie/controller/AddressBookController.java
/**
* 修改地址
*
* @param addressBook
* @return
*/
@PutMapping
public R<String> update(@RequestBody AddressBook addressBook) {
log.info("addressBook:{}", addressBook);
if (addressBook == null) {
return R.error("数据异常");
}
addressBookService.updateById(addressBook);
return R.success("修改数据成功");
}
删除地址簿信息的功能
浏览器控制台上显示查询到的 字符串参数 是 ids: 1535994735380975618,传递的数据就是这个 ids。
com/itheima/reggie/controller/AddressBookController.java
/**
* 删除地址
*
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam("ids") Long id) {
if (id == null) {
return R.error("请求异常");
}
/*int addressStatus = addressBookService.getById(id).getIsDefault();
if (addressStatus == 1) {
return R.error("默认地址不可删除");
}*/
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<AddressBook>();
queryWrapper.eq(AddressBook::getId, id);
queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
addressBookService.remove(queryWrapper);
return R.success("删除地址成功");
}
用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。
如果菜品设置了口味信息需要展示 [选择规格] 按钮,否则显示 [+] 按钮。
在开发代码之前,需要梳理一下前端页面和服务端的交互过程
front/index.html
)发送 ajax 请求,获取分类数据(菜品分类和套餐分类)开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这 2 次请求即可。
首页 localhost:8080/front/index.html
加载完成后,还发送了一次 ajax 请求用于加载购物车数据。
以下是 resources
目录下的 front/index.html
中的部分代码
methods:{
//初始化数据
initData(){
Promise.all([categoryListApi(),cartListApi({})]).then(res=>{
... ...
},
... ...
}
相关方法在 resources
目录下的 front/api/main.js
里。
//获取所有的菜品分类
//获取所有的菜品分类
function categoryListApi() {
return $axios({
'url': '/category/list',
'method': 'get',
})
}
... ...
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
'url': '/shoppingCart/list',
'method': 'get',
params: {...data}
})
}
此处可以将这次请求的地址暂时修改一下,从静态 json 文件获取数据,等后续开发购物车功能时再修改回来。
resources
目录下的 front/api/main.js
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
/******************************************************/
//'url': '/shoppingCart/list', // 注释掉的代码
'url':'/front/cartData.json', // 添加的代码
/******************************************************/
'method': 'get',
params: {...data}
})
}
resources
目录下的 front/cartData.json
的内容
{
"code": 1,
"msg": null,
"data": [],
"map": {}
}
进行完以上的操作后,就可以正常展示菜品了。
com/itheima/reggie/controller/CategoryController.java
写的 list 方法(根据条件查询分类数据)com/itheima/reggie/controller/DishController.java
写的 list (根据条件查询对应的菜品数据)但是,这里有个 Bug。
该项目前端处理后的结果是:如果菜品设置了口味信息需要展示 [选择规格] 按钮,否则显示 [+] 按钮。
但这里是什么也没有显示,故我们要修改部分代码。
究其原因还是list 方法返回的对象中的 Dish 类里没有口味的相关属性。
我们可以把其改为 DishDto 类,之前继承了 Dish 类,且拓展了 flavors 等属性。
根据 DishDto 来更改 com/itheima/reggie/controller/DishController.java
里的 list 方法的代码。
改造一下 DishController 类中的 list 方法。
com/itheima/reggie/controller/DishController.java
/**
* 根据条件查询对应的菜品数据 和 菜品口味的数据
*
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish) {
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<Dish>();
//添加查询条件
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
queryWrapper.eq(Dish::getStatus, 1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
//调用 service 方法拿到 list 集合
List<Dish> list = dishService.list(queryWrapper);
/* *********************************************************** */
List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);
Long categoryId = item.getCategoryId();//分类 id
//根据 id 查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//获取的当前菜品的 id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<DishFlavor>();
lambdaQueryWrapper.eq(DishFlavor::getDishId, dishId);
//最终获取到了口味的集合对象:select * from dish_flavor where dish_id = ?
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
/* *********************************************************** */
return R.success(dishDtoList);
}
最终的效果如下
点击 XX套餐 时,浏览器控制台显示 404。
显然,我们根据上图可以推出解决办法:在 setmealController 编写 list 方法(根据分类 id 查询对应的套餐信息)。
com/itheima/reggie/controller/SetmealController.java
/**
* 根据分类 id 查询对应的套餐信息
*
* @param setmeal
* @return
*/
@RequestMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal) {
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<Setmeal>();
queryWrapper.eq(setmeal.getCategoryId() != null, Setmeal::getCategoryId, setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus() != null, Setmeal::getStatus, setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}
最终效果如下
套餐展示问题的修复
我们点击菜品图片时,页面会跳出菜品的具体信息。
但当我们点击套餐时,控制台的网络状态显示:404。
当前页面地址:http://localhost:8080/front/index.html
以下是对应的一些前端代码。
resources
目录下的 front/api/main.js
//获取菜品分类对应的菜品
function dishListApi(data) {
return $axios({
'url': '/dish/list',
'method': 'get',
params: {...data}
})
}
//获取菜品分类对应的套餐
function setmealListApi(data) {
return $axios({
'url': '/setmeal/list',
'method': 'get',
params: {...data}
})
}
resources
目录下的 front/index.html
//获取菜品数据
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=>{
cartData.forEach(cart=>{
if(dish.id === cart.dishId){
dish.number = cart.number
}
})
})
}
this.dishList = dishList
}else{
this.$notify({ type:'warning', message:res.msg});
}
},
//获取套餐数据 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});
}
},
那么接下来我们要做的事情就是在 Controller 层中编写好接口。
此处代码来源地址:https://blog.csdn.net/weixin_53142722/article/details/124371940
com/itheima/reggie/controller/SetmealController.java
@Autowired
private DishService dishService;
/**
* 移动端点击套餐图片查看套餐具体内容
* 这里返回的是 dto 对象,前端需要 copies 这个属性
* 前端主要要展示的信息是:套餐中菜品的基本信息、图片、菜品描述、菜品的份数
*
* @param setmealId
* @return
*/
@GetMapping("/dish/{id}")
public R<List<DishDto>> dish(@PathVariable("id") Long setmealId) {
//构造查询条件
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<SetmealDish>();
//添加查询条件
queryWrapper.eq(SetmealDish::getSetmealId, setmealId);
//获取套餐里面的所有菜品,即 SetmealDish 表里面的数据
List<SetmealDish> list = setmealDishService.list(queryWrapper);
List<DishDto> dishDtos = list.stream().map((setmealDish) -> {
DishDto dishDto = new DishDto();
//其实这个 BeanUtils 的拷贝是浅拷贝,这里要注意一下
BeanUtils.copyProperties(setmealDish, dishDto);
//这里是为了把套餐中的菜品的基本信息填充到 dto 中,比如菜品描述,菜品图片等菜品的基本信息
Long dishId = setmealDish.getDishId();
Dish dish = dishService.getById(dishId);
BeanUtils.copyProperties(dish, dishDto);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtos);
}
最终,套餐也可以正常展示数据(套餐内所有菜品的描述、价格、份量)。
移动端用户可以将菜品或者套餐添加到购物车。
对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;
对于套餐来说,可以直接点击 [+] 将当前套餐加入购物车。
在购物车中可以修改菜品和套餐的数量,也可以清空购物车。
购物车对应的数据表为 shopping_cart 表
在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这 3 次请求即可。
com/itheima/reggie/entity/ShoppingCart.java
package com.itheima.reggie.entity;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 购物车
*/
@Data
public class ShoppingCart implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//名称
private String name;
//用户id
private Long userId;
//菜品id
private Long dishId;
//套餐id
private Long setmealId;
//口味
private String dishFlavor;
//数量
private Integer number;
//金额
private BigDecimal amount;
//图片
private String image;
private LocalDateTime createTime;
}
com/itheima/reggie/mapper/ShoppingCartMapper.java
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.ShoppingCart;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ShoppingCartMapper extends BaseMapper<ShoppingCart> {}
com/itheima/reggie/service/ShoppingCartService.java
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.ShoppingCart;
public interface ShoppingCartService extends IService<ShoppingCart> {}
com/itheima/reggie/service/impl/ShoppingCartServiceImpl.java
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.ShoppingCart;
import com.itheima.reggie.mapper.ShoppingCartMapper;
import com.itheima.reggie.service.ShoppingCartService;
import org.springframework.stereotype.Service;
@Service
public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartService {}
com/itheima/reggie/controller/ShoppingCartController.java
package com.itheima.reggie.controller;
import com.itheima.reggie.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 购物车
*/
@Slf4j
@RestController
@RequestMapping("/shoppingCart")
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
}
在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这 3 次请求即可。
前端分析
http:localhost:8080/shoppingCart/add
,请求方法为 POST{"amount":金额数字, "dishFlavor":"xxx口味", "dishId":"xxx菜品Id", "name":"xxx菜品名", "image":"xxx.jpeg"}
{"amount":金额数字, "setmealId":"xxx套餐Id", "name":"xxx套餐名", "image":"xxx.jpeg"}
经过前端控制台测试可知:菜品、套餐 添加到购物车走的服务端的方法是同一个,但提交的参数略有所差异。
代码编写
com/itheima/reggie/controller/ShoppingCartController.java
/**
* 添加 菜品/套餐 进购物车
*
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
log.info("购物车数据:{}", shoppingCart);
//设置用户 id,指定当前是哪个用户的购物车数据
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<ShoppingCart>();
queryWrapper.eq(ShoppingCart::getUserId, currentId);
if (dishId != null) {
//添加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getDishId, dishId);
} else {
//添加到购物车的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
//查询当前菜品或者套餐是否在购物车中
//SQL:select * from shopping_cart where user_id = ? and dish_id / setmeal_id = ?
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
if (cartServiceOne != null) {
//如果已经存在,就在原来数量基础上加一
Integer number = cartServiceOne.getNumber();
cartServiceOne.setNumber(number + 1);
shoppingCartService.updateById(cartServiceOne);
} else {
//如果不存在,则添加到购物车,数量默认就是一
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartService.save(shoppingCart);
}
return R.success(cartServiceOne);
}
处理之前的前端代码
之前在进行菜品展示的功能代码的开发时,页面初始化的时候会加载 cartListApi()
。
由于尚未设置购物车代码,故采用空数据代替来避免报错,方便代码开发。
此时需要我们将该文件内容改回来,其位置在 resources
目录下的 front/api/main.js
。
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
/***********************************************************************/
'url': '/shoppingCart/list', // 之前注释的代码,现在需要使用的代码
//'url':'/front/cartData.json', // 之前添加的代码,现在需要注释的代码
/***********************************************************************/
'method': 'get',
params: {...data}
})
}
后台代码编写
这里要注意的一点是用户只能看到自己的购物车。
com/itheima/reggie/controller/ShoppingCartController.java
/**
* 查看购物车
*
* @return
*/
@GetMapping("/list")
public R<List<ShoppingCart>> list() {
log.info("查看购物车...");
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
queryWrapper.orderByAsc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list);
}
前端分析
点击 “购物车” 图标上方的 “清空” 按钮
页面请求的 URL 是 http://localhost:8080/shoppingCart/clean
;请求的方式是 DELETE。
实际上就是根据 user_id 来删除 shopping_cart 表内的数据。
代码编写
com/itheima/reggie/controller/ShoppingCartController.java
/**
* 清空购物车
*
* @return
*/
@DeleteMapping("/clean")
public R<String> clean() {
//SQL:delete from shopping_cart where user_id = ?
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);
return R.success("清空购物车成功");
}
减少菜品/套餐功能
当我们点击 [—] 时,我们希望 菜品/套餐 可以正常减少。减少至0时为止,不可变为负数。
根据浏览器控制台的反馈可以知道
http:/ /localhost:8080/shoppingCart/sub
{"dishId":"1397854865672679425","setmealId":null}
此处代码源自 【瑞吉外卖项目 | 剩余功能的补充】 中的一节:手机端减少购物车中的菜品或者套餐数量。
com/itheima/reggie/controller/ShoppingCartController.java
/**
* 减少[菜品/套餐]的数量
*
* @param shoppingCart
* @return
*/
@PostMapping("/sub")
@Transactional
public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart) {
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
//代表数量减少的是菜品数量
if (dishId != null) {
//通过 dishId 查出购物车对象
queryWrapper.eq(ShoppingCart::getDishId, dishId);
//这里必须要加两个条件,否则会出现用户互相修改对方与自己购物车中相同套餐或者是菜品的数量
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
ShoppingCart cart1 = shoppingCartService.getOne(queryWrapper);
cart1.setNumber(cart1.getNumber() - 1);
Integer LatestNumber = cart1.getNumber();
if (LatestNumber > 0) {
//对数据进行更新操作
shoppingCartService.updateById(cart1);
} else if (LatestNumber == 0) {
//如果购物车的菜品数量减为0,那么就把菜品从购物车删除
shoppingCartService.removeById(cart1.getId());
} else if (LatestNumber < 0) {
return R.error("操作异常");
}
return R.success(cart1);
}
Long setmealId = shoppingCart.getSetmealId();
//代表数量减少的是套餐数量
if (setmealId != null) {
//代表是套餐数量减少
queryWrapper.eq(ShoppingCart::getSetmealId, setmealId);
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
ShoppingCart cart2 = shoppingCartService.getOne(queryWrapper);
cart2.setNumber(cart2.getNumber() - 1);
Integer LatestNumber = cart2.getNumber();
if (LatestNumber > 0) {
//对数据进行更新操作
shoppingCartService.updateById(cart2);
} else if (LatestNumber == 0) {
//如果购物车的套餐数量减为0,那么就把套餐从购物车删除
shoppingCartService.removeById(cart2.getId());
} else if (LatestNumber < 0) {
return R.error("操作异常");
}
return R.success(cart2);
}
//如果以上两个判断条件都不满足的话
return R.error("操作异常");
}
移动端用户将 菜品 / 套餐加入购物车后,可以点击购物车中的 [去结算] 按钮
页面会跳转到订单确认页面,点击 [去支付] 按钮则完成下单操作
需要注意的是这里只是把用户的支付订单保存到数据库,并没有真正的实现支付功能。
因为真正的支付功能是需要去申请支付资质的,个人用户是很难申请到的。
com/itheima/reggie/entity/Orders.java
package com.itheima.reggie.entity;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单
*/
@Data
public class Orders implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//订单号
private String number;
//订单状态 1待付款,2待派送,3已派送,4已完成,5已取消
private Integer status;
//下单用户 id
private Long userId;
//地址 id
private Long addressBookId;
//下单时间
private LocalDateTime orderTime;
//结账时间
private LocalDateTime checkoutTime;
//支付方式:1.微信;2.支付宝
private Integer payMethod;
//实收金额
private BigDecimal amount;
//备注
private String remark;
//用户名
private String userName;
//手机号
private String phone;
//地址
private String address;
//收货人
private String consignee;
}
com/itheima/reggie/entity/OrderDetail.java
package com.itheima.reggie.entity;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 订单明细
*/
@Data
public class OrderDetail implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//名称
private String name;
//订单 id
private Long orderId;
//菜品 id
private Long dishId;
//套餐 id
private Long setmealId;
//口味
private String dishFlavor;
//数量
private Integer number;
//金额
private BigDecimal amount;
//图片
private String image;
}
com/itheima/reggie/mapper/OrderMapper.java
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Orders;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper extends BaseMapper<Orders> { }
com/itheima/reggie/mapper/OrderDetailMapper.java
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.OrderDetail;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderDetailMapper extends BaseMapper<OrderDetail> { }
com/itheima/reggie/service/OrderService.java
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Orders;
public interface OrderService extends IService<Orders> { }
com/itheima/reggie/service/OrderDetailService.java
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.OrderDetail;
public interface OrderDetailService extends IService<OrderDetail> { }
com/itheima/reggie/service/impl/OrderServiceImpl.java
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.*;
import com.itheima.reggie.mapper.OrderMapper;
import com.itheima.reggie.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService { }
com/itheima/reggie/service/impl/OrderDetailServiceImpl.java
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.OrderDetail;
import com.itheima.reggie.mapper.OrderDetailMapper;
import com.itheima.reggie.service.OrderDetailService;
import org.springframework.stereotype.Service;
@Service
public class OrderDetailServiceImpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements OrderDetailService { }
com/itheima/reggie/controller/OrderController.java
package com.itheima.reggie.controller;
import com.itheima.reggie.service.OrderService;
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;
/**
* 订单
*/
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
}
com/itheima/reggie/controller/OrderDetailController.java
package com.itheima.reggie.controller;
import com.itheima.reggie.service.OrderDetailService;
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;
/**
* 订单明细
*/
@Slf4j
@RestController
@RequestMapping("/orderDetail")
public class OrderDetailController {
@Autowired
private OrderDetailService orderDetailService;
}
localhost:8080/front/page/add-order.html
)localhost:8080/addressBook/default
),请求服务端获取当前登录用户的默认地址localhost:8080/shoppingCart/list
),请求服务端获取当前登录用户的购物车数据localhost:8080/order/submit
),请求服务端完成下单操作开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求。
因为之前(或是前端)已经处理过前三步的请求了,实际上我们需要实现的只有第四步。
此次操作中前端页面的请求地址是:localhost:8080/order/submit
,请求方式是:POST。
提交过来的参数有:{remark: "备注信息", payMethod: 1, addressBookId: "XXXX"}
。
com/itheima/reggie/service/OrderService.java
//用户下单
public void submit(Orders orders);
com/itheima/reggie/service/impl/OrderServiceImpl.java
@Autowired
private ShoppingCartService shoppingCartService;
@Autowired
private UserService userService;
@Autowired
private AddressBookService addressBookService;
@Autowired
private OrderDetailService orderDetailService;
/**
* 用户下单
* @param orders
*/
@Transactional
public void submit(Orders orders) {
//获得当前用户id
Long userId = BaseContext.getCurrentId();
//查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId,userId);
List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper);
if(shoppingCarts == null || shoppingCarts.size() == 0){
throw new CustomException("购物车为空,不能下单");
}
//查询用户数据
User user = userService.getById(userId);
//查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if(addressBook == null){
throw new CustomException("用户地址信息有误,不能下单");
}
long orderId = IdWorker.getId();//订单号
AtomicInteger amount = new AtomicInteger(0);
List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));
orders.setUserId(userId);//下单用户 ID
orders.setNumber(String.valueOf(orderId));
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setPhone(addressBook.getPhone());
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);
//向订单明细表插入数据,多条数据
orderDetailService.saveBatch(orderDetails);
//清空购物车数据
shoppingCartService.remove(wrapper);
}
com/itheima/reggie/controller/OrderController.java
/**
* 用户下单
*
* @param orders
* @return
*/
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders) {
log.info("订单数据:{}", orders);
orderService.submit(orders);
return R.success("下单成功");
}
如果没有填写默认地址的话,系统会直接跳转到填写地址簿的页面。要求你填写信息。
填完信息,购物车里有菜的话,就会跳转到订单页面。
http://localhost:8080/front/page/add-order.html
下单成功的话,就会跳转到这个页面。
http://localhost:8080/front/page/pay-success.html
用户查看订单功能
此处通过前端控制台来查看其发送的 ajax 请求
根据上图情况来编写后台代码
com/itheima/reggie/controller/OrderController.java
/**
* 用户查看订单
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize) {
Page<Orders> pageInfo = new Page<Orders>(page, pageSize);
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<Orders>();
queryWrapper.orderByDesc(Orders::getOrderTime);
orderService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}
http://localhost:8080/front/page/order.html
显然有数据尚未处理,接下来查看前端的页面上的代码
resources
目录下的 front/page/order.html
中的部分代码
<van-cell v-for="(order,index) in orderList" :key="index" class="item">
<div class="timeStatus">
<span>{{order.orderTime}}span>
<span>{{getStatus(order.status)}}span>
div>
<div class="dishList">
<div v-for="(item,index) in order.orderDetails" :key="index" class="item">
<span>{{item.name}}span>
<span>x{{item.number}}span>
div>
div>
<div class="result">
<span>共{{order.sumNum}} 件商品,实付span><span class="price">¥{{order.amount}}span>
div>
<div class="btn" v-if="order.status === 4">
<div class="btnAgain" @click="addOrderAgain(order)">再来一单div>
div>
van-cell>
由此可知,需要设计一个 DTO 类来封装页面提交的数据
com/itheima/reggie/dto/OrdersDto.java
package com.itheima.reggie.dto;
import com.itheima.reggie.entity.OrderDetail;
import com.itheima.reggie.entity.Orders;
import lombok.Data;
import java.util.List;
@Data
public class OrdersDto extends Orders {
private String userName;
private String phone;
private String address;
private String consignee;
private List<OrderDetail> orderDetails;
}
自此开始,用户查看订单功能的代码自此开始完全摘抄自 【瑞吉外卖项目剩余功能补充】
我这里只是把大佬的控制层代码放到了业务层里。
让我自己想是想不出来的,只能说大佬牛皮。
com/itheima/reggie/service/OrderService.java
//根据订单 id 来得到一个订单明细的集合
public List<OrderDetail> getOrderDetailListByOrderId(Long orderId);
//用户查看自己的订单信息
public void page(Page<Orders> ordersPageInfo, Page<OrdersDto> ordersDtoPageInfo);
com/itheima/reggie/service/impl/OrderServiceImpl.java
/**
* 根据订单 id 来得到一个订单明细的集合
*
* @param orderId
* @return
*/
public List<OrderDetail> getOrderDetailListByOrderId(Long orderId) {
/*
此处为从 [用户查看自己的订单信息] 功能代码中抽离的一个方法
旨在通过订单的 id 来查询订单明细,得到一个订单明细的集合
单独抽离出来是为了避免在 stream 中遍历时
直接使用的构造条件来查询导致 eq 叠加
从而导致后面查询的数据都是 null
*/
LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(OrderDetail::getOrderId, orderId);
List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);
return orderDetailList;
}
/**
* 用户查看自己的订单信息
*
* @param ordersPageInfo
* @param ordersDtoPageInfo
* @return
*/
@Transactional
public void page(Page<Orders> ordersPageInfo, Page<OrdersDto> ordersDtoPageInfo) {
//构造条件查询对象
LambdaQueryWrapper<Orders> queryWrapper_1 = new LambdaQueryWrapper<Orders>();
//这里是直接把当前用户分页的全部结果查询出来,要添加用户 id 作为查询条件,否则会出现用户可以查询到其他用户的订单的情况
queryWrapper_1.eq(Orders::getUserId, BaseContext.getCurrentId());
//添加排序条件,根据更新时间降序排列
queryWrapper_1.orderByDesc(Orders::getOrderTime);
//分页查询(queryWrapper_1 -> ordersPageInfo)
orderService.page(ordersPageInfo, queryWrapper_1);
//对 OrderDto 进行必要的属性赋值
List<Orders> records = ordersPageInfo.getRecords();
List<OrdersDto> orderDtoList = records.stream().map((item) -> {
OrdersDto orderDto = new OrdersDto();
Long orderId = item.getId();//获取订单 id
//通过订单的 id 来查询订单明细,得到一个订单明细的集合
List<OrderDetail> orderDetailList = this.getOrderDetailListByOrderId(orderId);
//为 orderDetails 里面的属性赋值
BeanUtils.copyProperties(item, orderDto);
//对 orderDto 进行 OrderDetails 属性的赋值
orderDto.setOrderDetails(orderDetailList);
return orderDto;
}).collect(Collectors.toList());
BeanUtils.copyProperties(ordersPageInfo, ordersDtoPageInfo, "records");
ordersDtoPageInfo.setRecords(orderDtoList);
}
com/itheima/reggie/controller/OrderController.java
/**
* 用户端展示自己的订单查询
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("/userPage")
public R<Page> page(int page, int pageSize) {
//分页构造器对象
Page<Orders> ordersPageInfo = new Page<Orders>(page, pageSize);
Page<OrdersDto> ordersDtoPageInfo = new Page<OrdersDto>(page, pageSize);
orderService.page(ordersPageInfo, ordersDtoPageInfo);
return R.success(ordersDtoPageInfo);
}
http://localhost:8080/front/page/order.html
如果你的订单没有出现 “再来一单” 这样的字眼,那也是正常的。
因为前段页面设置了只有在订单完成的情况下(status=4
)才能显现出这样的字眼。
所以要想现在显现出 “再来一单” 的话,在数据库中更改 status 的值就行了。
用户再来一单的功能
resources
目录下的 front/page/order.html
的部分代码
<div class="btn" v-if="order.status === 4">
<div class="btnAgain" @click="addOrderAgain(order)">再来一单div>
div>
async addOrderAgain(order) {
const res = await orderAgainApi({id: order.id})
if (res.code === 1) {
window.requestAnimationFrame(() => {
window.location.href = '/front/index.html'
})
} else {
this.$notify({type: 'warning', message: res.msg});
}
},
显然,当 “再来一单” 的操作完成时,其会跳转到最初的页面:/front/index.html
resources
目录下的 front/api/order.js
中的部分代码
//再来一单
function orderAgainApi(data) {
return $axios({
'url': '/order/again',
'method': 'post',
data
})
}
若是想要出现 “再来一单” 的按钮,请先将数据库中的表 order_detail 中的 status 改为 4。
status 值的含义:订单状态(1.待付款,2.待派送,3.已派送,4.已完成,5.已取消)
http://localhost:8080/front/page/order.html
大体思路是
SELECT id,name,order_id,dish_id,setmeal_id,dish_flavor,number,amount,image FROM order_detail WHERE (order_id = ?);
com/itheima/reggie/controller/OrderController.java
/**
* 再来一单
*
* @param order_1
* @return
*/
@PostMapping("/again")
@Transactional
public R<String> again(@RequestBody Orders order_1) {
log.info("再来一单_数据测试:{}", order_1);
//取得[订单id]
Long id = order_1.getId();
//通过[订单id]获得相应的[order]对象
Orders orders = orderService.getById(id);
//在[order]对象中重新设置[订单id]
long orderId = IdWorker.getId();
orders.setId(orderId);
//在[order]对象中重新设置[订单号码]
String number = String.valueOf(IdWorker.getId());
orders.setNumber(number);
//在[order]对象中重新设置[下单时间]
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
//在[order]对象中重新设置[订单状态]
orders.setStatus(2);
//最终,将这条数据插入订单表(orders)
orderService.save(orders);
LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<OrderDetail>();
//设置查询条件
queryWrapper.eq(OrderDetail::getOrderId, id);
List<OrderDetail> list = orderDetailService.list(queryWrapper);
list.stream().map((item) -> {
//订单明细表 id
long detailId = IdWorker.getId();
//设置[订单号码]
item.setOrderId(orderId);
item.setId(detailId);
return item;
}).collect(Collectors.toList());
//向 订单明细表 中插入多条数据
orderDetailService.saveBatch(list);
return R.success("再来一单成功");
}
但是这里仍然有 BUG。
用户若是要修改备注,就必须要跳转到订单的页面才行。
再来一单 的操作完成后,直接跳转到 index.html。(万一用户手抖点到了怎么办 )
com/itheima/reggie/service/ShoppingCartService.java
//清空购物车
public R<String> clean();
com/itheima/reggie/service/impl/ShoppingCartServiceImpl.java
/**
* 清空购物车
*
* @return
*/
@Transactional
public R<String> clean() {
//SQL:delete from shopping_cart where user_id = ?
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
this.remove(queryWrapper);
return R.success("清空购物车成功");
}
以下描述摘抄自博客 【瑞吉外卖项目 | 剩余功能补充】
- 前端点击 “再来一单” 按钮,会直接跳转到 index.html,且购物车中是包含 [菜品/套餐] 信息的。
- 为了避免数据出现问题。再跳转到该页面之前,我们需要清除购物车的数据。
- 具体的代码编写步骤:
- 通过 orderId 获取订单明细;
- 把订单明细的数据的数据塞到购物车表中。
- 但在进行该操作之前需要清除购物车中原有数据,以保证 “再来一单” 的数据不出现问题。
- 其中清除的是当前登录用户的购物车表中的数据
- 虽然这样可能会影响用户体验,但是就外卖而言,该操作对用户体验影响不大。电商项目就不能这么干了。
com/itheima/reggie/service/OrderService.java
//再来一单
public void againSubmit(@RequestBody Map<String, String> map);
com/itheima/reggie/service/impl/OrderServiceImpl.java
/**
* 再来一单
*
* @param map
*/
@Transactional
public void againSubmit(@RequestBody Map<String, String> map) {
String ids = map.get("id");
long id = Long.parseLong(ids);
LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(OrderDetail::getOrderId, id);
//获取该订单对应的所有的订单明细表
//SQL 语句:SELECT id,name,order_id,dish_id,setmeal_id,dish_flavor,number,amount,image FROM order_detail WHERE (order_id = ?);
List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);
//通过用户 id 把原来的购物车给清空
shoppingCartService.clean();//此处的 clean 方法在视频中出现过,建议抽取到 service 层中以方便调用
//获取用户 id
Long userId = BaseContext.getCurrentId();
List<ShoppingCart> shoppingCartList = orderDetailList.stream().map((item) -> {
//把从 order 表中和 order_details 表中获取到的数据赋值给这个购物车对象
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
shoppingCart.setImage(item.getImage());
Long dishId = item.getDishId();
Long setmealId = item.getSetmealId();
if (dishId != null) {
//如果是菜品那就添加菜品的查询条件
shoppingCart.setDishId(dishId);
} else {
//添加到购物车的是套餐
shoppingCart.setSetmealId(setmealId);
}
shoppingCart.setName(item.getName());
shoppingCart.setDishFlavor(item.getDishFlavor());
shoppingCart.setNumber(item.getNumber());
shoppingCart.setAmount(item.getAmount());
shoppingCart.setCreateTime(LocalDateTime.now());
return shoppingCart;
}).collect(Collectors.toList());
//把携带数据的购物车批量插入购物车表
shoppingCartService.saveBatch(shoppingCartList);
}
com/itheima/reggie/controller/OrderController.java
/**
* 再来一单
*
* @param map
* @return
*/
@PostMapping("/again")
public R<String> againSubmit(@RequestBody Map<String, String> map) {
orderService.againSubmit(map);
return R.success("操作成功");
}
由上可知,请求 URL:http://localhost:8080/user/loginout
,请求方法:POST
于此要做的就是在 Controlller 层来处理该请求,清除 session 中保存的用户 id。
com/itheima/reggie/controller/UserController.java
/**
* 用户退出登录
*
* @param request
* @return
*/
@PostMapping("/loginout")
public R<String> logout(HttpServletRequest request) {
//清理 session 中的用户 id
request.getSession().removeAttribute("user");
return R.success("退出成功");
}
但鉴于该功能是后台功能,故放在下一篇博客中详述。
下一篇:学习【瑞吉外卖⑧】SpringBoot单体项目:https://blog.csdn.net/yanzhaohanwei/article/details/125362891