外卖平台及移动端开发(移动端)

一、手机验证码登陆

1、短信服务介绍

​ 阿里云短信服务:https://cn.aliyun.com/

2、处理业务逻辑

 1. 在登录页面(front/page/login.html)输入手机号,点击[获取验证码]按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信
 2. 在登录页面输入验证码,点击[登录]按钮,发送ajax请求,在服务端处理登录请求

3、编码处理

3.1 准备工作

2.1.1 导入maven坐标
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
3.1.2 创建utils包并导入工具类
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
package com.itreggie.reggie.utils;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.zhenzi.sms.ZhenziSmsClient;

import java.util.HashMap;
import java.util.Map;

/**
* 短信发送工具类
*/

public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/

public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);

SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}

}



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
package com.itreggie.reggie.utils;

import java.util.Random;

/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}

/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}

3.2 创建所需的类和接口的基本结构

image-20220910103346257

3.3 在全局拦截器LoginCheckFilter中添加代码

1
2
"/user/sendMsg",//移动端发送短信
"/user/login"//移动端登录
1
2
3
4
5
6
7
8
9
10
//4-2、移动端判断登陆状态,如果已登陆,则直接放行
if (request.getSession().getAttribute("user") != null) {
log.info("用户已登陆,用户id为:{}", request.getSession().getAttribute("user"));

Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);

chain.doFilter(request, response);
return;
}

3.4 controller代码开发

在UserController控制层中,添加sendMsg方法和login方法

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
package com.itreggie.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itreggie.reggie.common.R;
import com.itreggie.reggie.entity.User;
import com.itreggie.reggie.service.UserService;
import com.itreggie.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import java.util.Map;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;

/**
* 发送手机验证码
* @param user
* @return
*/
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user,HttpSession session){
//获取手机号
String phone = user.getPhone();

if (StringUtils.isNotEmpty(phone)){
//随机生成4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("code={}",code);

//调用阿里云(or榛子云)提供的短信服务API完成发送短信
//SendSms.send(phone,code);

//需要将生成的验证码保存到session中
session.setAttribute(phone,code);

return R.success("手机验证码短信发送成功");
}

return R.error("手机验证码短信发送失败");
}

/**
* 移动端用户登录
* @param map
* @param session
* @return
*/
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session){
log.info(map.toString());
//获取手机号
String phone = map.get("phone").toString();

//获取验证码
String code = map.get("code").toString();

//从session中获取保存的验证码
Object codeInSession = session.getAttribute(phone);

//进行验证码比对(页面提交的验证码和session中保存的验证码比对)
if (codeInSession != null && codeInSession.equals(code)){
//如果比对成功,说明登录成功
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);

User user = userService.getOne(queryWrapper);

if (user == null){
//判断当前手机号对应的用户是否为新用户,如果为新用户,自动完成注册
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user",user.getId());
return R.success(user);
}

return R.error("登录失败");
}

}

4、功能测试

  • 重启项目,在用户登录页面输入手机号,和获取到的验证码,点击登录
  • 页面成功跳转到服务用户界面
  • user表中成功添加上测试的手机号码(未注册的手机号码)

二、导入用户地址簿相关功能

1、数据模型

​ 地址簿指移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。每一个用户可以有多个地址,但只能拥有一个默认地址。用户的地址信息会存储在address_book表中。

image-20220912202759223

2、编码处理

2.1 创建地址簿所需类和接口

​ 实体类AddressBook,接口AddressBookMapper,service方法AddressBookService以及其实现类。

2.2 controller代码实现

2.2.1 新增地址
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
package com.itreggie.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.itreggie.reggie.common.BaseContext;
import com.itreggie.reggie.common.R;
import com.itreggie.reggie.entity.AddressBook;
import com.itreggie.reggie.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* 地址簿
*/
@RestController
@RequestMapping("/addressBook")
@Slf4j
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;

/**
* 新增
* @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);
}

}

2.2.2 设置默认地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 设置默认地址
* @param addressBook
* @return
*/
@PutMapping("default")
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook){
log.info("addressBook: {}",addressBook);

LambdaUpdateWrapper<AddressBook> queryWrapper = new LambdaUpdateWrapper<>();
queryWrapper.eq(AddressBook::getUserId,BaseContext.getCurrentId());
queryWrapper.set(AddressBook::getIsDefault,0);
//sql:update address_book set is_default = 0 where id = ?
addressBookService.update(queryWrapper);

addressBook.setIsDefault(1);
//sql:update address_book set is_default = 1 where id = ?
addressBookService.updateById(addressBook);
return R.success(addressBook);
}
2.2.3 根据id查询地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 根据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("没有找到该对象");
}
}
2.2.4 查询默认地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 查询默认地址
* @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);
}
}
2.2.5 查询指定用户的全部地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 查询指定用户的全部地址
* @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));
}
2.2.6 修改地址

​ 点击修改符号,发现回显信息已经写好了;

img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 修改收货地址
* @param addressBook
* @return
*/
@PutMapping
public R<String> update(@RequestBody AddressBook addressBook){

if (addressBook == null){
return R.error("请求异常");
}
addressBookService.updateById(addressBook);

return R.success("修改成功");
}
2.2.7 删除地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 根据地址id删除用户地址
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam("ids") Long id){

if (id == null){
return R.error("请求异常");
}
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getId,id).eq(AddressBook::getUserId,BaseContext.getCurrentId());
addressBookService.remove(queryWrapper);
//addressBookService.removeById(id); 感觉直接使用这个removeById不太严谨.....
return R.success("删除地址成功");
}

三、菜品展示

1、处理业务逻辑

1. 页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
1. 页面发送ajax请求,获取第一个分类下的菜品或套餐

注意: 首页加载完成之后,还发送了一条ajax请求用于加载购物车数据,此次请求的地址暂时修改下,从静态json文件获取数据,等后续开发购物车功能时在修改过来。

1
2
3
4
5
6
7
8
9
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
//'url': '/shoppingCart/list', //原来的请求地址
'url': '/front/cartData.json',
'method': 'get',
params: {...data}
})
}

2、编码处理

​ 在前面的DishController和SetmealController中,已实现分类方法list,所以只需对原有的list方法进行改进即可。移动端相比页面端展示多了一个口味数据展示,因此需要将dish转换为DishDto,借用DishDto中的flavors参数得到口味数据,进而展示到移动端。

2.1 DishController的list方法修改

​ 原DishController的list方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());

//添加条件,查询status状态为1的菜品(启售)
queryWrapper.eq(Dish::getStatus,1);

//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

List<Dish> list = dishService.list(queryWrapper);

return R.success(list);
}

​ 改进后的DishController的list方法:

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
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());

//添加条件,查询status状态为1的菜品(启售)
queryWrapper.eq(Dish::getStatus,1);

//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

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> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DishFlavor::getDishId,dishId);
//sql:select * from dish_flavor where dish_id = ?
List<DishFlavor> dishFlavorList = dishFlavorService.list(wrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());

return R.success(dishDtoList);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.itreggie.reggie.dto;

import com.itreggie.reggie.entity.Dish;
import com.itreggie.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDto extends Dish {
//菜品对应的口味数据
private List<DishFlavor> flavors = new ArrayList<>();

private String categoryName;

private Integer copies;
}

2.2 SetmealController中的list方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal){
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);
}

注意:此处list数据传递的方式为键值对,因此对象不需要加针对json数据的@requestbody

3、点击套餐图片查看具体菜品

​ 根据浏览器的请求数据,可以找到对应的axios请求以及前端展示所需的具体数据类型:

img img

3.1 编码处理

​ 在SetmealController中实现dish方法:

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
/**
* 移动端点击套餐图片查看套餐具体内容
* 这里返回的是dto 对象,因为前端需要copies这个属性
* 前端主要要展示的信息是:套餐中菜品的基本信息,图片,菜品描述,以及菜品的份数
* @param SetmealId
* @return
*/
//这里前端是使用路径来传值的,要注意,不然你前端的请求都接收不到,就有点尴尬哈
@GetMapping("/dish/{id}")
public R<List<DishDto>> dish(@PathVariable("id") Long SetmealId){
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
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);
}

四、购物车功能开发

1、数据模型

​ 购物车对应的数据表为shopping_cart表

img

2、处理业务逻辑

1. 点击加入购物车或者“加号”按钮,页面发送ajax请求,请求服务端将菜品或套餐添加到购物车
1. 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
1. 点击清空购物车按钮,页面发送ajax请求,请求服务器执行清空购物车操作

开发购物车功能时需要把main.js中的请求地址改回来:

1
2
3
4
5
6
7
8
9
10
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
'url': '/shoppingCart/list', //原来的请求地址
//'url': '/front/cartData.json',
'method': 'get',
params: {...data}
})
}

3、编码处理

3.1 创建所需的类和接口

​ 实体类ShoppingCart,接口 ShoppingCartMapper,业务层接口 ShoppingCartService及其实现类ShoppingCartServiceImpl。

3.2 控制层 ShoppingCartController代码开发

3.2.1 添加购物车
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
package com.itreggie.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itreggie.reggie.common.BaseContext;
import com.itreggie.reggie.common.R;
import com.itreggie.reggie.entity.ShoppingCart;
import com.itreggie.reggie.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.List;

@RestController
@RequestMapping("/shoppingCart")
@Slf4j
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;

/**
* 添加购物车
*
* @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<>();
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);
cartServiceOne = shoppingCart;
}

return R.success(cartServiceOne);
}
}

3.2.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
41
/**
* 删减购物车数据
* @param shoppingCart
* @return
*/
@PostMapping("/sub")
public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart) {
log.info("购物车数据。。。");

//设置用户id,指定当前是哪个用户的购物车数据
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);

//查询当前菜品/套餐是否在购物车中
Long dishId = shoppingCart.getDishId();

LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
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();
if (number > 1){
cartServiceOne.setNumber(number - 1);
shoppingCartService.updateById(cartServiceOne);
}else {
shoppingCartService.remove(queryWrapper);
}
}
return R.success(cartServiceOne);
}
3.2.3 查看购物车
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 查看购物车
*
* @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);
}
3.2.4 清空购物车
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 清空购物车
*
* @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("清空购物车成功");
}

五、用户下单

1、数据模型

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

订单表orders表

img

订单明细表order_detail表

img

2、处理业务逻辑

1. 在购物车中点击结算按钮,页面跳转至订单确认页面
1. 订单确认页面发送ajax请求,请求服务端获取当前登录用户的默认地址(如果未设置会跳转至新增地址页面)
1. 订单确认页面发送ajax请求,请求服务端获取当前登录用户的购物车数据
1. 点击支付按钮,发送ajax请求,请求服务端完成下单操作

3、编码处理

3.1 创建所需的类和接口

​ 实体类orders、OrderDetail,Mapper接口,业务层OrderService、OrderDetailService以及其实现类。

3.2 业务实现层OrderServiceImpl 开发

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package com.itreggie.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itreggie.reggie.common.BaseContext;
import com.itreggie.reggie.common.CustomException;
import com.itreggie.reggie.entity.*;
import com.itreggie.reggie.mapper.OrderMapper;
import com.itreggie.reggie.service.*;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
@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);
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);
}
}

3.3 控制层OrderController开发

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
package com.itreggie.reggie.controller;

import com.itreggie.reggie.common.R;
import com.itreggie.reggie.entity.Orders;
import com.itreggie.reggie.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 订单
*/
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;

/**
* 用户下单
* @param orders
* @return
*/
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
log.info("订单数据:{}",orders);

orderService.submit(orders);
return R.success("下单成功");
}


}

六、用户查看订单

1、编码处理

​ 在orderController中添加userPage方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 用户订单分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/userPage")
public R<Page> page(int page, int pageSize){

//分页构造器对象
Page<Orders> pageInfo = new Page<>(page,pageSize);
//构造条件查询对象
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();

//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Orders::getOrderTime);
orderService.page(pageInfo,queryWrapper);

return R.success(pageInfo);
}

2、功能完善

​ 通过order.html这个页面(第40行)我们可以发现:前端还需要下面这些数据;所以我们后端要传给它

1
2
3
4
5
6
<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>

2.1 分析前端代码

​ item是从order.orderDetails里面 获取到的,但是orders实体类里面并没有orderDetails这个属性,而且数据库中这个order表里面也没有这个字段,所以使用的是dto来封装数据给前端。

2.2 创建OrderDto实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.itreggie.reggie.dto;


import com.itreggie.reggie.entity.OrderDetail;
import com.itreggie.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;

}

2.3 OrderController代码完善

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查询订单明细,得到一个订单明细的集合
//这里抽离出来是为了避免在stream中遍历的时候直接使用构造条件来查询导致eq叠加,从而导致后面查询的数据都是null
public List<OrderDetail> getOrderDetailListByOrderId(Long orderId){
LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(OrderDetail::getOrderId, orderId);
List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);
return orderDetailList;
}

/**
* 用户端展示自己的订单分页查询
* @param page
* @param pageSize
* @return
* 遇到的坑:原来分页对象中的records集合存储的对象是分页泛型中的对象,里面有分页泛型对象的数据
* 开始的时候我以为前端只传过来了分页数据,其他所有的数据都要从本地线程存储的用户id开始查询,
* 结果就出现了一个用户id查询到 n个订单对象,然后又使用 n个订单对象又去查询 m 个订单明细对象,
* 结果就出现了评论区老哥出现的bug(嵌套显示数据....)
* 正确方法:直接从分页对象中获取订单id就行,问题大大简化了......
*/
@GetMapping("/userPage")
public R<Page> page(int page, int pageSize){
//分页构造器对象
Page<Orders> pageInfo = new Page<>(page,pageSize);
Page<OrderDto> pageDto = new Page<>(page,pageSize);
//构造条件查询对象
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Orders::getUserId,BaseContext.getCurrentId());
//这里是直接把当前用户分页的全部结果查询出来,要添加用户id作为查询条件,否则会出现用户可以查询到其他用户的订单情况
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Orders::getOrderTime);
orderService.page(pageInfo,queryWrapper);

//通过OrderId查询对应的OrderDetail
LambdaQueryWrapper<OrderDetail> queryWrapper2 = new LambdaQueryWrapper<>();

//对OrderDto进行需要的属性赋值
List<Orders> records = pageInfo.getRecords();
List<OrderDto> orderDtoList = records.stream().map((item) ->{
OrderDto orderDto = new OrderDto();
//此时的orderDto对象里面orderDetails属性还是空 下面准备为它赋值
Long orderId = item.getId();//获取订单id
List<OrderDetail> orderDetailList = this.getOrderDetailListByOrderId(orderId);
BeanUtils.copyProperties(item,orderDto);
//对orderDto进行OrderDetails属性的赋值
orderDto.setOrderDetails(orderDetailList);
return orderDto;
}).collect(Collectors.toList());

//使用dto的分页有点难度.....需要重点掌握
BeanUtils.copyProperties(pageInfo,pageDto,"records");
pageDto.setRecords(orderDtoList);
return R.success(pageDto);
}

七、再来一单功能

由于没有写后台的确认订单功能,所以这里通过数据库orders表修改订单状态来完成测试

img

​ 先把数据库中的订单表中的status改一些为4:这样在前端才能点击这个再来一单的按钮:

img

​ 在front/page/order.html中可以看到这一段代码:

1
2
3
<div class="btn" v-if="order.status === 4">
<div class="btnAgain" @click="addOrderAgain(order)">再来一单</div>
</div>

​ 通过addOrderAgain这个方法:前端使用post请求,请求地址order/again:

img

1、编码处理

1.1 清空购物车功能完善

​ 将ShoppingCartController中清空购物车clean方法写入service层,便于后续调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    /**
* 清空购物车
*
* @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);*/

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

​ 将注释的代码封装到ShoppingCartService的clean方法中:

1
2
3
4
5
6
7
8
9
package com.itreggie.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itreggie.reggie.entity.ShoppingCart;

public interface ShoppingCartService extends IService<ShoppingCart> {
public void clean();
}

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
package com.itreggie.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itreggie.reggie.common.BaseContext;
import com.itreggie.reggie.common.R;
import com.itreggie.reggie.entity.ShoppingCart;
import com.itreggie.reggie.mapper.ShoppingCartMapper;
import com.itreggie.reggie.service.ShoppingCartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartService {
@Autowired
private ShoppingCartService shoppingCartService;

@Override
public void clean() {
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());

shoppingCartService.remove(queryWrapper);
}
}

1.2 OrderController代码实现

    - 前端点击再来一单会直接跳转至购物车,为避免数据有问题,在跳转之前需将购物车数据清除
    - 通过orderId获取订单明细
    - 将当前用户购物车表中的数据清除后将订单明细的数据放进购物车表中

ps:电商项目不能这么做

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
//客户端点击再来一单
@PostMapping("/again")
public R<String> 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);
//获取该订单对应的所有的订单明细表
List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);

//通过用户id把原来的购物车给清空,这里的clean方法是视频中讲过的,建议抽取到service中,那么这里就可以直接调用了
shoppingCartService.clean();

//获取用户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);

return R.success("操作成功");
}

不建议把业务代码写在controller,不然以后想复用的时候就会很麻烦

八、移动端用户登出

1、编码处理

​ 在UserController中实现loginOut方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 退出功能
* ①在controller中创建对应的处理方法来接受前端的请求,请求方式为post;
* ②清理session中的用户id
* ③返回结果(前端页面会进行跳转到登录页面)
* @return
*/
@PostMapping("/loginout")
public R<String> logout(HttpServletRequest request){
//清理session中的用户id
request.getSession().removeAttribute("user");
return R.success("退出成功");
}