瑞吉外卖06-用户端+管理后台订单

本笔记源自黑马程序员的视频课程——《瑞吉外卖》,总结了课程笔记、相关知识点以及可能遇到的问题解决方案,并且增加了课程中未实现的功能,供读者参考。笔记全面且条理清晰,希望帮助读者学习和理解这个外卖项目。
本项目全部笔记见:外卖项目笔记合集

1. 个人页面功能开发

1.1 地址管理

1.1.1 需求分析

地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。

图片

1.1.2 数据模型

用户的地址信息会存储在address_book表,即地址簿表中。具体表结构如下:

图片

1.1.3 准备工作

1、实体类AddressBook(直接从课程资料中导入即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@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;
}

2、Mapper接口AddressBookMapper

1
2
3
4
@Mapper
public interface AddressBookMapper extends BaseMapper<AddressBook> {

}

3、Service接口AddressBookService

1
2
3
public interface AddressBookService extends IService<AddressBook> {

}

4、Service实现类AddressBookServicelmpl

1
2
3
4
@Service
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService {

}

5、控制层AddressBookController(直接从课程资料中导入即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {

@Autowired
private AddressBookService addressBookService;

/**
* 新增地址
* 处理请求/addressBook:填写表单后,点"保存地址",将填写的地址信息(json形式{consignee: "abc", phone: "13323234343", sex: "0", detail: "avdfd", label: "公司"})封装到addressBook中,发请求到服务器。
* 需要服务端将信息保存到address_book表
* 1、先获取到此登录用户的id,跟封装的addressBook对象(表中这一条数据)对应上
* 2、将json数据(封装在addressBook对象中)保存到address_book表
*/
@PostMapping
public R<AddressBook> save(@RequestBody AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());//先登录成功,就可以使用BaseContext.getCurrentId()获得当前登录用户id。
// BaseContext是我们基于ThreadLocal封装的工具类,用于保存和获取当前登录用户的id.
log.info("addressBook:{}", addressBook);
addressBookService.save(addressBook);
return R.success(addressBook);
}

/**
* 设置默认地址
* 处理请求/addressBook/default,
* ajax请求携带参数:(需要设为默认地址的那个)地址id,使用AddressBook实体对象接收参数,实际上只传过来地址id
*/
@PutMapping("default")
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
log.info("addressBook:{}", addressBook);
//在address_book表中执行update操作:先将address_book表中,当前登录用户的 所有地址id对应的 is_default字段 都改成0
//SQL: update address_book set is_default = 0 where user_id = ?
LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());//BaseContext.getCurrentId()是当前登录用户的id
wrapper.set(AddressBook::getIsDefault, 0);
addressBookService.update(wrapper); //将当前登录用户的 所有地址信息的 is_default字段 都改成0

addressBook.setIsDefault(1);//将当前要改的这个addressBook的is_default字段 改成1
//SQL:update address_book set is_default = 1 where id = ?
addressBookService.updateById(addressBook);
return R.success(addressBook);
}

/**
* 根据地址id查询地址,以回显到修改地址页面
* 处理请求/addressBook/${id}
*/
@GetMapping("/{id}")
public R get(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);//从address_book表中查出地址id对应的那一条数据(addressBook对象)
if (addressBook != null) {//查到了该地址id对应的一条数据
return R.success(addressBook);//返回给前端,回显到页面
} else {
return R.error("没有找到该对象");
}
}

/**
* 查询默认地址
*/
@GetMapping("default")
public R<AddressBook> getDefault() {
// 想要执行的SQL: select * from address_book where user_id = ? and is_default = 1
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
queryWrapper.eq(AddressBook::getIsDefault, 1);

AddressBook addressBook = addressBookService.getOne(queryWrapper);

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

/**
* 查询指定(当前)用户的全部地址
*/
@GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());//获得当前登录用户的id,并和封装的addressBook对象(表中这一条数据)对应上
log.info("addressBook:{}", addressBook);

//条件构造器
//SQL:select * from address_book where user_id = ? order by update_time desc
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
queryWrapper.orderByDesc(AddressBook::getUpdateTime);

return R.success(addressBookService.list(queryWrapper));//将从address_book表中查出的结果,返回给前端
}
}

1.1.4 修改地址(补充功能)

这一节为自己补充实现的功能,可供参考。如有问题欢迎讨论。

图片

前后端交互过程

1、回显信息:用户点击[某个地址]右侧的“修改”按钮,会执行toAddressEditPage(item)方法,发请求到/front/page/address-edit.html?id=${item.id}。在address-eidt.html页面会执行钩子函数,执行initData()方法,其中执行addressFindOneApi(params.id)方法,向/addressBook/${id}发送GET请求。得到服务端响应的数据后,回显到表单中。/addressBook/${id}请求的处理前面已经给出,此处可以实现。

2、更新地址信息到数据表:用户填写表单后,点击“保存地址”按钮,执行saveAddress方法。此时是对某一条地址信息操作,有id,所以执行updateAddressApi(this.form)方法,更新地址,发送ajax请求到/addressBook,请求方式put,携带参数data。需要服务器处理。

代码开发

在AddressBookController中新增update方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 修改地址信息
* 请求/addressBook,修改某地址id对应的那一条数据
* @param addressBook
* @return
*/
@PutMapping
public R<String> update(@RequestBody AddressBook addressBook){
// update address_book set 所有字段 where id = ? (id = addressBook.getId())
addressBookService.updateById(addressBook);
return R.success("修改成功");
}

1.1.5 删除地址(补充功能)

这一节为自己补充实现的功能,可供参考。如有问题欢迎讨论。

用户对某个地址点击编辑按钮后,可以跳转到编辑地址页面,删除该地址。

图片

交互过程

1、用户点击“删除地址”按钮,执行deleteAddress方法,其中执行deleteAddressApi({ids:this.id })方法,向/addressBook发送DELETE请求,参数跟在url后。

2、如果得到服务器执行成功的响应(code=1),则请求/front/page/address.html,跳转回地址列表页面。

代码开发

AddressBookController中新增delete方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 删除地址
* 处理请求/addressBook?ids=1634529822791176194
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(String[] ids){//此页面只会有一个ids,数组中只有一个元素。为了通用性,所以写成数组。
for(String id : ids){
addressBookService.removeById(id);
}
return R.success("删除成功");
}

1.2 退出登录(补充功能)

这一节为自己补充实现的功能,可供参考。如有问题欢迎讨论。

1.2.1 前后端交互过程

图片

在/front/page/user.html页面点击“退出登录”,执行toPageLogin方法,执行loginoutApi()方法,发送请求到/user/loginout,method= post,需要服务端处理。服务端响应后跳转到登录页面/front/page/login.html。

图片

1.2.2 代码开发

在UserController中新增logout方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 退出登录
* 在/front/page/user.html页面点击“退出登录”,发送请求到/user/loginout,method= post。
* 需要服务端处理,从session中清掉用户id。
* @param httpServletRequest
* @return
*/
@PostMapping("/loginout")
public R<String> logout(HttpServletRequest httpServletRequest){
httpServletRequest.getSession().removeAttribute("user");
return R.success("登出成功");
}

2. 菜品展示(用户端首页)

2.1 需求分析

用户登录成功后,跳转到系统首页(/front/index.html)。在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示 [选择规格] 按钮,否则显示 [+] 按钮。

2.2 梳理交互过程&代码开发

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

1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)

具体的过程为:

执行/front/index.html页面的mounted()方法后,执行initData方法。

注:mounted()是在created()函数之后被调用的,即组件实例被创建并初始化后执行的第一个函数。

initData()方法中,执行categoryListApi()方法,和cartListApi({})方法,分别向/category/list/shoppingCart/list发送GET请求。分别是”获取所有的菜品分类”,和”获取购物车内商品的集合”。

/category/list的处理方法已经写过,但由于前端写的是:Promise.all([categoryListApi(),cartListApi({})]).then(),意思是:两个方法同时都成功,页面才能渲染。而/shoppingCart/list请求还没有写,所以展示有问题。

此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时,再修改回来。

front/api/main.js中:

1
2
3
4
5
6
7
8
9
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
// 'url': '/shoppingCart/list', //真实的url
'url':'/front/cartData.json', //暂时给一个静态页面,以展示购物车内商品数据
'method': 'get',
params:{...data}
})
}

front/cartData.json:

1
{"code":1,"msg":null,"data":[],"map":{}}

加了静态文件/front/cartData.json之后,首页可以看到第一个分类的所有菜品了。

2、点“菜品分类”,显示该分类的所有菜品:

用户每点击一个分类,就发送请求/dish/list?categoryId=xxx&status=1,以展示该菜品分类下的所有菜品。

之前在DishController里写过对于此请求的处理,能够显示分类名称categoryName。但是此处前端还需要flavors数据。因为有口味数据flavors时,页面需要显示“选择规格”,而不仅仅是单纯的“ + ”。所以还需要去dish_flavor表中查每一个dish的口味数据,然后将flavors响应给前端。改造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 根据分类id, 查出该分类的所有菜品(含口味数据)
* 返回值:查出的是多个菜品dish,所以用List集合盛装。此处返回值中还应该包含口味数据,所以用dishDto代替dish,其中包含flavors属性。
* 处理请求:/dish/list?categoryId=xxxx
*
* 后期改造:
* 因为在前台用户端的首页 展示某分类的所有菜品时,如果有口味信息,应该显示"选择规格",而不是单纯的"+"。所以此处返回值中应该包含口味数据。使用DishDto封装
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){

//从表中查询所有的分类:select * from dish where category_id = ? AND status = ?
//构造条件构造器:
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加条件:根据category_id查询
//具体条件 用::的写法 Dish::getCategoryId。具体的值 是传过来的参数dish.getCategoryId()
queryWrapper.eq(Dish::getStatus, 1);//只查启售的菜品,停售(status=0)过滤掉
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());//先判断了一下category_id不为空
//添加排序条件:按照dish表里的sort字段 升序排列,即sort越小越靠前(orderBy),再按UpdateTime升序排列,即越晚添加,越靠前
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);

//现在已经从dish表查出了所有菜品信息,还需要从dish_flavor表查出每个菜品对应的口味信息

List<DishDto> dishDtoList = list.stream().map((item)->{ //item:遍历出的每一个菜品对象dish
DishDto dishDto = new DishDto();

BeanUtils.copyProperties(item, dishDto);//把dish的普通属性 copy给 我们自己创建的dishDto对象

//在dishDto对象中,添加categoryName属性的值
Long categoryId = item.getCategoryId();//获取当前遍历到的 dish对象的 categoryId
//根据id,从数据库查询分类对象,以获取分类名称
Category category = categoryService.getById(categoryId);//根据id查分类对象
if(category!=null){
String categoryName = category.getName();//获取分类名称
dishDto.setCategoryName(categoryName);//把categoryName赋值给 我们自己创建的dishDto对象的 categoryName属性
}

//在dishDto对象中,添加flavors属性的值
Long dishId = item.getId();//获得当前菜品id
// 获取口味数据
// select * from dish_flavor where dish_id = ?
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId, dishId);
List<DishFlavor> flavors = dishFlavorService.list(lambdaQueryWrapper);//返回从dish_flavor表查出的 某个菜品id对应的所有口味数据
dishDto.setFlavors(flavors);//把flavors赋值给 我们自己创建的dishDto对象的 flavors属性

return dishDto;
}).collect(Collectors.toList());

return R.success(dishDtoList);//将查询出的dishDtoList作为data,封装在R对象中
}

3、点“套餐分类”,显示该分类的所有套餐:

用户每点击一个套餐分类,就发送请求/setmeal/list?categoryId=xxxx&status=1,以展示该套餐分类下的所有菜品。需要服务端处理。

在SetmealController中新增list方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 显示套餐信息
* 处理请求/setmeal/list?categoryId=1413342269393674242&status=1
* 形参:用Setmeal实体类接收,含categoryId和status属性
* @param setmeal
* @return
*/
@GetMapping("list")
public R<List<Setmeal>> list(Setmeal setmeal){
//从setmeal表中查该分类id对应的套餐数据
//select * from setmeal where category_id = ? and status = ?
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
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);
}

3. 购物车

3.1 需求分析

移动端用户可以将菜品或者套餐添加到购物车。

对于菜品来说,如果设置了口味信息,则需要选择规格后,才能加入购物车;

对于套餐来说,可以直接点击 [+] ****将当前套餐加入购物车。

在购物车中可以修改菜品和套餐的数量,也可以清空购物车。

图片

3.2 数据模型

购物车对应的数据表为shopping_cart表,具体表结构如下:

图片

3.3 梳理交互过程

在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:

1、点击”选择规格”,再点击“加入购物车”,或者直接点击“+”按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车

这一步的具体过程:

  • 用户在某菜品处,点击“选择规格”,执行flavorClick(flavor,item)方法,显示选择口味的窗口。
  • 选择后,点“加入购物车”,执行dialogFlavorAddCart()方法,其中执行addCart(item)方法,携带dishFlavor口味、amount金额、dishId菜品id或者setmealId套餐id、name菜名、image图片,执行addCartApi(params)方法,向/shoppingCart/add发送请求,method=post。需要服务端处理,将菜品添加到shopping_cart表中。
  • 服务端处理后,响应到前端。执行getCartData()方法,其中执行cartListApi({})方法,展示购物车内商品数据。

2、点击购物车图标,页面发送ajax请求/shoppingCart/list,请求服务端查询购物车中的菜品和套餐

3、(补充功能)对购物车内某菜品点击“+”或“-”,页面发送请求,修改菜品数量

4、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

这一步的具体过程:

在购物车点击“清空”,执行clearCart方法,其中执行clearCartApi()方法,向/shoppingCart/clean发送请求,method = delete,需要服务端处理。

3.4 准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

1、实体类ShoppingCart(直接从课程资料中导入即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@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;
}

2、Mapper接口ShoppingCartMapper

1
2
3
@Mapper
public interface ShoppingCartMapper extends BaseMapper<ShoppingCart> {
}

3、Service接口ShoppingcartService

1
2
public interface ShoppingCartService extends IService<ShoppingCart> {
}

4、Service实现类ShoppingCartServicelmpl

1
2
3
@Service
public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartService {
}

5、控制层ShoppingCartController

1
2
3
4
5
6
7
8
9
@Slf4j
@RestController
@RequestMapping("/shoppingCart")
public class ShoppingCartController {

@Autowired
private ShoppingCartService shoppingCartService;

}

3.5 代码开发

3.5.1 添加购物车

对应3.3中第1步的需求。

处理请求/shoppingCart/add,将菜品添加到shopping_cart表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 添加购物车
* 处理请求/shoppingCart/add,将菜品添加到shopping_cart表中。
* json数据中有dishFlavor口味、amount金额、dishId菜品id或者setmealId套餐id、name菜名、image图片,都是shopping_cart表的字段,可以由ShoppingCart对象封装
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("购物车数据:{}", shoppingCart);
//设置用户id,指定当前的购物车数据是哪个用户的(传过来的json数据中没有user_id)
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);

// 查询当前菜品或者套餐是否已经在购物车当中
// select * from shopping_cart where user_id = ? and dish_id = ?
// 或者select * from shopping_cart where user_id = ? and setmeal_id = ?
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, currentId);
if(shoppingCart.getDishId() != null){
//添加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
}else if(shoppingCart.getSetmealId() != null){
//添加到购物车的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
//从表中查出的一条数据:(有可能不存在这一条数据,没查到,此时需要新增而非更新)
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
// 判断表中是否已有这个user_id和dish_id。如果已有,则给数量加1。如果没有,则新增一条购物车的数据
if(cartServiceOne == null){
//没有这条数据,则新增一条购物车的数据。且第一次新增,数量默认为1
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartService.save(shoppingCart);//已经给shoppingCart设置了user_id
cartServiceOne = shoppingCart;
}else {//cartServiceOne != null
//已有这条数据,则给原来的数量加1
Integer number = cartServiceOne.getNumber();//从表中查出的这一条已有的数据,获取它的number字段值,并加1
cartServiceOne.setNumber( number + 1);
shoppingCartService.updateById(cartServiceOne);//将更改后的这一条数据,更新至表中
}
return R.success(cartServiceOne);
}

3.5.2 查看购物车

购物车内如果有菜品,就可以点购物车查看。发送请求/shoppingCart/list。

现在可以把之前的前端假数据改回来:

1
2
3
4
5
6
7
8
9
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
'url': '/shoppingCart/list', //真实的url
//'url': '/front/cartData.json', //暂时给一个静态页面,以展示购物车内商品数据
'method': 'get',
params:{...data}
})
}

在ShoppingCartController中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 查看购物车内商品的集合
* 处理请求:/shoppingCart/list
* @return
*/
@GetMapping("/list")
public R<List<ShoppingCart>> list(){
log.info("查看购物车");
//查询当前用户对应的购物车数据
//select * from shopping_cart where user_id = ?
Long currentId = BaseContext.getCurrentId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, currentId);
queryWrapper.orderByDesc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);//从shopping_cart表,查出筛选后的数据
return R.success(list);
}

3.5.3 修改菜品数量(补充功能)

这一节为自己补充实现的功能,可供参考。如有问题欢迎讨论。

分析

  • 菜单中增加菜品数量:在加入购物车处,已经实现(add方法)。

  • 菜单中减少菜品数量:

    • 点击菜单中某菜品右边的“-”,执行subtractCart(item)方法,执行updateCartApi(params)方法,发送请求到/shoppingCart/sub,method=post。需要服务器处理。
    • 得到删除成功的响应后,如果菜品数量number=0,则不再显示该菜品。
    • 完成后再执行getCartData方法,向/shoppingCart/list发送请求,重新获取购物车数据。前面已经实现。
  • 购物车中减少商品数量:

    在购物车内点击某菜品右边的“-”,执行cartNumberSubtract(item)方法,执行updateCartApi(params),后续和“菜单中减少菜品数量”一样。

图片

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 购物车商品减少数量
* POST请求: /shoppingCart/sub
* @param shoppingCart
* @return
*/
@PostMapping("/sub")
public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart) {
log.info("购物车数据:{}", shoppingCart);

//判断是菜品还是套餐:看传来的是dishId还是setmealId
//然后需要根据dishId或者setmealId定位到shopping_cart表中的某一条数据
// (如果是菜品)select * from shopping_cart where user_id = ? and dish_id = ?
// 或者(如果是套餐)select * from shopping_cart where user_id = ? and setmeal_id = ?
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());//user_id已经存在域中和BaseContext中,可以取用
if(shoppingCart.getDishId() != null){
//传来的是菜品id,需要减少菜品的数量
queryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
}else if(shoppingCart.getSetmealId() != null){
//传来的是套餐id,需要减少套餐的数量
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
//现在已经找到了表中待修改的一条数据(ShoppingCart对象) - one
ShoppingCart one = shoppingCartService.getOne(queryWrapper);
Integer number = one.getNumber();//表中这条数据对应的菜品数量number
if(number == 1){
//菜品数量为1,则直接删除这条记录。无论菜品数量number为何值,都需要-1,只有返回number为0的shoppingcart,才能在页面正常显示(前端要求)
one.setNumber(number - 1);//
shoppingCartService.remove(queryWrapper);
}else{
//菜品数量非1,则给表中原来的number-1。
one.setNumber(number - 1);
shoppingCartService.updateById(one);
}
return R.success(one);
}

此处可能遇到的问题(已解决):从购物车减少菜品数量,减少至0,会清空购物车。但是从菜单减少菜品数量,减少至0,还会显示“- 1 +”,而不是“选择规格”。

并且从购物车减少菜品后,菜单的数量会刷新,而如果从购物车减菜品减到0,菜单处还会显示“- 1 +”,而不是“选择规格”。

这是因为,前端需要根据number属性的值来显示“选择规格”或者“ - 1 + ”。number属性为0时,显示“选择规格”。所以需要注意:当菜品数量为1时,删除记录之前,也应该给number减1。one.setNumber(number - 1);

1
2
3
4
5
6
this.dishList.forEach(dish=>{
if(dish.id === (item.dishId || item.setmealId)){
dish.number = (res.data.number === 0 ? undefined : res.data.number)
}//更新"dish"对象的"number"属性 为"res.data.number"返回的值。
// 如果"res.data.number"的值为0(当用户减商品数量到0,number会为0),则将"number"属性设置为undefined。前端商品数量处会再次显示"选择规格"
})

3.5.4 清空购物车

处理请求/shoppingCart/clean。将该登录用户对应的所有shopping_cart中的数据都删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 清空购物车
* 处理请求/shoppingCart/clean
* @return
*/
@DeleteMapping("/clean")
public R<String> clean(){
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);

return R.success("清空购物车成功");
}

4. 用户端订单功能

4.1 用户下单

4.1.1 需求分析

用户将菜品或者套餐加入购物车后,可以点击购物车中的“去结算”按钮,页面跳转到订单确认页面,点击“去支付”按钮,完成下单操作。

图片

4.1.2 数据模型

用户下单业务对应的数据表为orders表和order_detail表。

orders表(订单表):

图片

order_detail表(订单明细表):

图片

4.1.3 梳理交互过程

在开发代码之前,需要梳理一下用户下单操作时,前端页面和服务端的交互过程:

1、在购物车中点击“去结算”按钮,页面跳转到订单确认页面

这一步的具体过程为:

用户将菜品或者套餐加入购物车后,点击购物车中的“去结算”按钮,执行toAddOrderPage()方法,跳转到/front/page/add-order.html页面。

2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址

这一步的具体过程为:

/front/page/add-order.html 页面,会执行钩子函数,执行initData()方法,执行defaultAddress()方法,其中执行getDefaultAddressApi()方法,请求/addressBook/default,method = get,获取该用户的默认地址,显示到下单页面上。需要服务端处理,返回默认地址信息。前面已经写过。

如果响应成功,执行getFinishTime()方法,获取送达时间;如果响应失败(没有获取到默认地址),跳转到地址编辑页面/front/page/address-edit.html

3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据

这一步的具体过程为:

/front/page/add-order.html 页面,会执行钩子函数,执行initData()方法,执行getCartData()方法,其中执行cartListApi({})方法,请求/shoppingCart/list,method = get,获取购物车内商品的集合,显示到下单页面上。需要服务端把购物车商品集合数据返回。前面已经写过。

4、在订单确认页面点击“去支付”按钮,发送ajax请求,请求服务端完成下单操作

这一步的具体过程为:

点击“去支付”,执行goToPaySuccess方法,其中执行addOrderApi(params)方法,携带的参数是备注信息remark、付款方式payMethod、addressBookId,请求/order/submit,method=post。需要服务端处理,将订单信息保存至数据库。服务端操作成功并响应后,前端跳转到/front/page/pay-success.html页面。

4.1.4 准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

1、实体类Orders、OrderDetail(直接从课程资料中导入即可)

Orders:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@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;
}

OrderDetail:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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;
}

2、Mapper接口OrderMapper、OrderDetailMapper

OrderMapper:

1
2
3
@Mapper
public interface OrderMapper extends BaseMapper<Orders> {
}

OrderDetailMapper:

1
2
3
@Mapper
public interface OrderDetailMapper extends BaseMapper<OrderDetail> {
}

3、业务层接口OrderService、OrderDetailService

OrderService:

1
2
public interface OrderService extends IService<Orders> {
}

OrderDetailService:

1
2
public interface OrderDetailService extends IService<OrderDetail> {
}

4、业务层实现类OrderServicelmpl、OrderDetailServicelmpl

OrderServicelmpl:

1
2
3
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
}

OrderDetailServicelmpl:

1
2
3
@Service
public class OrderDetailServiceImpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements OrderDetailService {
}

5、控制层OrderController、OrderDetailController

OrderController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
private OrderService orderService;

@Autowired OrderDetailService orderDetailService;

@Autowired
ShoppingCartService shoppingCartService;

@Autowired
UserService userService;

}

OrderDetailController:

1
2
3
4
5
6
7
8
9
@Slf4j
@RestController
@RequestMapping("/orderDetail")
public class OrderDetailController {

@Autowired
private OrderDetailService orderDetailService;

}

4.1.5 代码开发

在OrderController新增submit方法,将信息保存至数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 请求/order/submit,method=post
* 携带的参数是备注信息remark、付款方式payMethod、addressBookId,都在Orders类中
* 用户id是不需要传来的。因为用户登录后,可以从session获得,或者从BaseContext(ThreadLocal)中获得
* 下单菜品不需要传。因为下单了什么菜品/套餐,可以根据用户id,从购物车表里查到
* @param orders
* @return
*/
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
orderService.submit(orders);
return R.success("支付成功");
}

在OrderService新增submit方法,并在OrderServicelmpl中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* 用户下单
* 在service中扩展功能
* @param orders
*/
@Override
@Transactional //多表操作
public void submit(Orders orders) {
//获得当前用户的id
Long currentId = BaseContext.getCurrentId();

//查询该用户的购物车数据(可能有很多条数据)
// select * from shopping_cart where user_id = ?
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, currentId);
List<ShoppingCart> shoppingCartList = shoppingCartService.list(queryWrapper); //购物车所有商品的集合

//为了安全进行校验
if(shoppingCartList == null || shoppingCartList.size() == 0){
throw new CustomException("购物车为空,不能下单");
}

//向orders表插入数据之前,先准备好要插入的数据:
// (1)从user表查该user_id的用户信息(可以填充user_name);
User user = userService.getById(currentId); //currentId就是user表中的主键,所以可以用此方法。如果不是主键,只能构造queryWrapper来查

// (2)从address表查该用户的地址信息(可以填充address, consignee, phone)
AddressBook addressBook = addressBookService.getById(orders.getAddressBookId());
if(addressBook == null){
throw new CustomException("用户地址有误,不能下单");//为了安全进行校验
}
// (3)填数据
long orderId = IdWorker.getId();//用mybatis-plus的IdWorker生成订单号,类型Long
AtomicInteger amount = new AtomicInteger(0);//订单总金额初始化(不是最终金额,还需要获取订单详情,再计算总金额)
//原子操作,线程安全。保证多线程时也没问题。但是只支持整型Integer、Long和Boolean型。所以此处金额只能是整数,否则损失精度

//想把ShoppingCart对象组成的集合shoppingCartList,变为OrderDetail对象组成的集合。
// OrderDetail对象需要的id, name, image, dish_id, setmeal_id, number, amount, dish_flavor是ShoppingCart对象已经有的。但是amount需要修改
List<OrderDetail> orderDetails = shoppingCartList.stream().map( (item) -> {// item是该登录用户的一个个ShoppingCart对象
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(item, orderDetail, "userId", "createTime");//这几个属性是ShoppingCart对象独有,OrderDetail对象没有,需要ignore。
// 还差什么属性:order_id
orderDetail.setOrderId(orderId);
// amount需要修改为总金额(原来表中是单价,前端帮我们计算了总金额。此处需要后端自己再计算总金额)
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());// amount += (单价*数量)
// amount是BigDecimal类型,需要将multiply()的参数也变为BigDecimal类型,才能用这个方法。也就是:大数.multiply(大数),得到一个大数
// 通过.intValue()转化为Integer,才能使用addAndGet方法(AtomicInteger类提供的方法)
return orderDetail;
}).collect(Collectors.toList());

//准备数据:
orders.setNumber(String.valueOf(orderId));//订单号,类型String
orders.setId(orderId);//订单表的主键,也用订单号
orders.setStatus(2);//订单状态2,待派送
orders.setUserId(currentId);
//addressBookId 已经由json请求传过来,已在orders里
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
//payMethod 已经由json请求传过来,已在orders里
orders.setAmount(new BigDecimal(amount.get()));//计算总金额
//remark 已经由json请求传过来,已在orders里
orders.setPhone(addressBook.getPhone());
String address = (addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
+ (addressBook.getDetail() == null ? "" : addressBook.getDetail());
orders.setAddress(address);
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());

//向orders表插入数据。只有一个订单,插入一条数据
this.save(orders);

//向order_detail表插入数据。一个订单里有多个菜品,插入多条数据,共享同一个order_id
orderDetailService.saveBatch(orderDetails);

//清空购物车数据
shoppingCartService.remove(queryWrapper);
}

4.2 用户查看订单&历史订单

4.2.1 梳理交互过程和需求

1、下单成功后,在/front/page/order.html页面点击“查看订单”,发送请求到/order/userPage?page=1&pageSize=5

图片

具体过程为:

用户下单后,跳转到/front/page/pay-success.html页面。点“查看订单”,执行toOrderPage方法,请求/front/page/order.html

/front/page/order.html页面的钩子函数中,执行getList()方法,执行orderPagingApi(this.paging)方法,请求/order/userPage?page=1&pageSize=5。需要服务端处理,响应回分页信息records。

4.2.2 代码开发

目标:处理请求/order/userPage?page=1&pageSize=5并响应回分页信息records。

在OrderController中新增userPage方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 点“查看订单”,会显示订单详情
* 请求/order/userPage?page=1&pageSize=5
* 使用OrdersDto,它除了Orders类的属性之外,额外扩展了userName、phone、address、consignee、orderDetails属性。并给OrdersDto添加属性private int sumNum;
* @param page
* @param pageSize
* @return
*/
@GetMapping("/userPage")
public R<Page> userPage(int page, int pageSize){

//分页构造器对象
Page<Orders> pageInfo = new Page<>(page, pageSize);
Page<OrdersDto> ordersDtoPage = new Page<>();
// 方法结束时,需要响应一个Page类型的分页构造器对象。
// 且该对象中含有"orderDetails"商品详情,所以还需要构造一个OrdersDto实体对应的Page对象

// select * from orders where user_id = ?
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Orders::getUserId, BaseContext.getCurrentId());
queryWrapper.orderByDesc(Orders::getOrderTime);
//执行分页查询(用orderService,查orders表)
orderService.page(pageInfo, queryWrapper); //处理好又会封装到Page类型的pageInfo中,pageInfo中就有值了,含有数据列表records、总记录数total等信息

// 前端需要:orderTime, status, orderDetails(含name, number), sumNum, amount
// 现在已经查出了orderTime, status, amount
// 还差orderDetails(含name, number), sumNum,所以我们需要将它俩封装到OrdersDto中,再传回给前端return R.success(ordersDtoPage)

// (1) 将已经从orders表中查出的分页信息pageInfo的各属性值 复制给ordersDtoPage(除records)
BeanUtils.copyProperties(pageInfo, ordersDtoPage,"records");//不拷贝records属性(页面上的数据信息就是records,需要处理)
// (2) 再处理records:把records转换为OrdersDto类型的:添加上orderDetails(含name, number), sumNum,设置给ordersDtoPage
List<Orders> records = pageInfo.getRecords();//把pageInfo里的records部分拿出来。
// 需要处理该List集合:原本是List集合盛装Orders,即List<Orders>类型的records,不含订单详情和总商品数;需要变成List集合盛装OrdersDto,即List<OrdersDto>类型的records,含订单详情和总商品数。
// 然后设置ordersDtoPage的records为新的集合list:ordersDtoPage.setRecords(list);
List<OrdersDto> list = records.stream().map((item)->{ //item:遍历出的每一个订单对象orders
OrdersDto ordersDto = new OrdersDto();
BeanUtils.copyProperties(item, ordersDto);//把orders的普通属性 copy给 我们自己创建的ordersDto对象
// ordersDto现在还差orderDetails(含name, number), sumNum
Long orderId = item.getId();//获取当前遍历到的 orders对象的 id,凭此订单id,去order_details表中查找该订单对应的订单详情
// select * from order_details where order_id = ?
LambdaQueryWrapper<OrderDetail> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(OrderDetail::getOrderId, orderId);
List<OrderDetail> orderDetails = orderDetailService.list(lambdaQueryWrapper);//将查询出的*(即某订单id对应的所有商品详情)封装为一个List<OrderDetail>集合
ordersDto.setOrderDetails(orderDetails); //给当前ordersDto对象填上orderDetails属性值

//ordersDto现在还差sumNum商品数量,需要去ordersDetail中获取每一商品项的数量,并累加
Integer sumNum = 0;//初始化
for (OrderDetail orderDetail : orderDetails) {
Integer number = orderDetail.getNumber();
sumNum += number;
}
ordersDto.setSumNum(sumNum);

return ordersDto;
}).collect(Collectors.toList());

ordersDtoPage.setRecords(list);//把加了categoryName的新list 赋值给dishDtoPage的records属性

return R.success(ordersDtoPage);
}

5. 管理后台订单功能

5.1 商家后台订单明细

图片

5.1.1 需求分析

在管理后台首页/backend/index.html,点击“订单明细”,跳转到/backend/page/order/list.html。

该页面会首先执行钩子函数created(),执行init()方法,其中执行getOrderDetailPage({ page: this.page, pageSize: this.pageSize, number: this.input || undefined, beginTime: this.beginTime || undefined, endTime: this.endTime || undefined })方法,通过url带着page、pageSize、number(订单号)、beginTime、endTime等参数,发送请求到/order/page,method=get。需要服务器处理,并响应回来分页数据。

5.1.2 代码开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 后台管理页面-查看"订单明细"分页数据
* 通过url带着page、pageSize、number(订单号)、beginTime、endTime等参数,发送请求到/order/page,method=get。需要服务器处理,并响应给前端分页数据。
* 返回类型:R,泛型是Page。因为需要传给浏览器records、total数据,以显示分页页面
* 参数:请求中带着参数(/order/page?page=1&pageSize=10&number=xxxx&beginTime=xxx&endTime=xxx)发过来,声明和请求参数同名的形参
* 其中number、beginTime、endTime是在搜索框搜索时才会有
* @param page
* @param pageSize
* @param number
* @param beginTime
* @param endTime
* @return
*/
@GetMapping("/page")
public R<Page> page(Integer page, Integer pageSize, String number, String beginTime, String endTime){

//分页构造器对象
Page<Orders> pageInfo = new Page<>(page, pageSize);
// 页面需要的信息包含订单号number、订单status对应的中文字符串、用户userName、手机号phone、地址address、下单时间orderTime、实收金额amount
// 这些都在orders表中,所以用Page<Orders>封装即可,前端都可以展示,不需要再使用OrdersDto。

// select * from orders like '%number值%' and order_time in (beginTime, endTime)
// 只要number不为空,就根据number字段模糊查询。只要beginTime, endTime不为空,就根据beginTime, endTime字段查询
//包装查询对象queryWrapper
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件(模糊查询):只要number不为空,就在orders表中,根据number字段模糊查询
queryWrapper.like(StringUtils.isNotEmpty(number), Orders::getNumber, number);//like方法的参数:执行条件,字段,值。相当于:LIKE '%值%'
// 添加查询条件(区间):只要beginTime和endTime不为空,就根据beginTime, endTime字段查询
if (beginTime != null && endTime != null) { //beginTime和endTime要么都传来,要么都为null
queryWrapper.ge(Orders::getOrderTime, beginTime);
queryWrapper.le(Orders::getOrderTime, endTime);
}
// 添加排序条件:按照Orders表里的下单时间 倒序排列,即新下单的排在前面(orderBy)
queryWrapper.orderByDesc(Orders::getOrderTime);

//执行分页查询(用orderService,查orders表)
orderService.page(pageInfo, queryWrapper); //处理好又会封装到Page类型的pageInfo中,pageInfo中就有值了,含有数据列表records、总记录数total等信息

return R.success(pageInfo);
}

5.2 商家订单派送状态更改

5.2.1 需求分析

图片

在backend/page/order/list.html页面点击“派送”,会执行cancelOrDeliveryOrComplete (status, id)方法,要求确认是否更改订单状态,如果确认,会执行editOrderDetail(params)方法,发送ajax请求至/order,method=put,携带数据是id(订单id)和status(新的订单状态)。需要服务端处理该请求,根据订单id,修改orders表中的status,并响应回前端。

前端判断status是不是3,如果是3,就在页面显示“订单已派送”,否则显示“订单已完成”。

5.2.2 代码开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 后台管理页面-修改订单状态
* 处理请求/order,method=put。携带数据是id(订单id)和status(新的订单状态)。
* 需要服务端处理该请求,根据订单id,修改orders表中的status,并响应回前端。
* @param orders
* @return
*/
@PutMapping
public R<Orders> editStatus(@RequestBody Orders orders){
Long orderId = orders.getId();//获取订单编号

//根据订单编号,在orders表中查到这一条数据,并修改订单状态status
Orders ordersInDB = orderService.getById(orderId);//该id对应的 数据库中的订单记录
ordersInDB.setStatus(orders.getStatus());
orderService.updateById(ordersInDB);

return R.success(ordersInDB);
}