适合新手小白入门Java后端开发的Springboot + Mybatis Plus 项目。

后面我逐渐省略了一些功能的记录,主要是因为和前面的业务万变不离其宗,如果要看详细的代码可以到我的github仓库下拉reggie代码

业务实现(后台系统)

新增套餐

数据模型

image-20230706103537857

表现层要素

image-20230706103038582

请求类型:POST

请求路径:/setmeal

image-20230706103143613

请求参数:json格式的数据,除了setmeal套餐的基本信息外,还有套餐内的菜品信息setmealDishes也封装成json数组的格式。(还有idType和dishList是什么,当请求参数json数据key大于dto对象的时候,可以正常封装吗?)

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
addSetmeal(prams)
.then((res) => {
if (res.code === 1) {
this.$message.success('套餐添加成功!')
if (!st) {
this.goBack()
} else {
this.$refs.ruleForm.resetFields()
this.dishList = []
this.dishTable = []
this.ruleForm = {
name: '',
categoryId: '',
price: '',
code: '',
image: '',
description: '',
dishList: [],
status: true,
id: '',
idType: '',
}
this.imageUrl = ''
}
} else {
this.$message.error(res.msg || '操作失败')
}
})

返回值类型:Result<String>类型

核心业务思路

仍然是操作setmeal数据表和setmeal_dish数据表,要在Service类上加事务管理注释@Transactional

步骤1:在Service类自定义新增套餐的业务层方法,并在类上加事务管理注释@Transactional

步骤2:调用Service层原生的save方法将SetmealDto对象传入并保存;

步骤3:接下来要对setmeal_dish数据表进行添加多条数据,可以从dto对象中get到setmealDishes这个属性列表,但是不能直接将setmealDishes列表saveBatch,要对其赋值setmeal_id值;

步骤4:通过dto对象get到id值(执行了save操作,在setmeal表已经通过雪花算法为dto对象生成了id属性值),将其赋给setmealDishes列表的setmealDish.setmealId套餐id值;

步骤5:调用Service层的saveBatch将setmealDishes列表添加到setmeal_dish数据表中。

套餐信息分页查询

表现层要素

image-20230706111444821

请求类型:GET

请求路径:/setmeal/page

请求参数:普通参数page、pageSize和可选参数name

返回值类型:Result<Page>

核心业务思路

跟前面的菜品信息分页查询一模一样。

删除套餐

表现层要素

image-20230706113845252

请求类型:DELETE

请求路径:/setmeal

请求参数:普通参数ids,因为批量删除的id可能有多个,所以这里的方法形参是List<Long> ids,要加@RequestParam注解

1
2
3
4
5
6
7
8
deleteSetmeal(type === '批量' ? this.checkList.join(',') : id).then(res => {
if (res.code === 1) {
this.$message.success('删除成功!')
this.handleQuery()
} else {
this.$message.error(res.msg || '操作失败')
}
})

返回值类型:Result<String>

核心业务思路

首先操作setmeal表删除套餐信息,再操作setmeal_dish表删除setmeal_id字段 = ids的所有数据。

批量起售停售

image-20230706163258312

1
2
3
4
5
6
7
8
@PostMapping("/status/{status}")
public Result<String> changeStatus(@PathVariable Integer status, @RequestParam List<Long> ids){
LambdaUpdateWrapper<Setmeal> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
lambdaUpdateWrapper.in(Setmeal::getId, ids);
lambdaUpdateWrapper.set(Setmeal::getStatus, status);
setmealService.update(lambdaUpdateWrapper);
return Result.success("状态修改成功");
}

订单明细分页查询

跟前面的分页查询相比就是可选参数变多了,还有就是关注一下“时间”作为可选参数如何处理。

image-20230706203744254

可选参数:

image-20230706203801378

image-20230706203823723

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/page")
public Result<Page> pageSelect(int page, int pageSize, String number,String beginTime, String endTime){
Page<Orders> pageInfo = new Page<>(page ,pageSize);
LambdaQueryWrapper<Orders> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.like(StringUtils.hasText(number), Orders::getNumber, number);
lambdaQueryWrapper.gt(StringUtils.hasText(beginTime), Orders::getOrderTime, beginTime)
.lt(StringUtils.hasText(endTime), Orders::getOrderTime, endTime);
lambdaQueryWrapper.orderByDesc(Orders::getOrderTime);

orderService.page(pageInfo, lambdaQueryWrapper);

return Result.success(pageInfo);
}

业务实现(移动端系统)

手机验证码登录

流程分析

  1. 点击获取验证码,页面向服务端发送第一次请求,请求服务器随机生成要求的验证码并发送到表单输出的手机号中;
  2. 点击登录,页面向服务端发送第二次请求,携带表单输入的验证码和Session中储存的验证码进行比对,确定是否成功登录。

核心业务思路

  • 发送验证码请求

导入手机验证的依赖坐标:

1
2
3
4
5
6
7
8
9
10
11
<!-- 阿里云短信服务 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.2.1</version>
</dependency>

编写一个工具类(根据阿里云短信服务的帮助文档改的),用于发送手机验证码:

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
/**
* 短信发送工具类
*/
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", "", ""); // accessKeyId, secret
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
/**
* 随机生成验证码工具类
*/
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;
}
}

修改拦截器,对客户端登录相关的请求进行放行:

1
2
3
4
5
6
7
8
9
10
11
// 定义不需要处理的请求路径
String[] urls = {
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/",
"/common/**",
"/user/sendMsg", // 移动端发送短信
"/user/login" // 移动端登录
};

表现层方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@PostMapping("/sendMsg")
public Result<String> sendMsg(@RequestBody User user, HttpSession session){
// 获取手机号
String phone = user.getPhone();

if(StringUtils.hasText(phone)){
// 生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("code -> {}", code);
// 调用阿里云提供的短信服务API完成短信发送
SMSUtils.sendMessage("瑞吉外卖", "您的验证码为:${code},请勿泄露于他人!", phone, code);
// 需要将生成的验证码保存到Session中
session.setAttribute(phone, code);

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

return Result.error("手机短信验证码发送失败");
}
  • 登录请求

image-20230706171041816

请求参数是json格式的code和phone,phone是User实体类的属性,但是验证码code不是。有两种解决方案:① 定义增强类UserDto; ② 用Map的key-value接收参数。

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
@PostMapping("/login")
public Result<User> login(@RequestBody Map userMap, HttpSession session){
log.info(userMap.toString());

// 获取手机号
String phone = userMap.get("phone").toString();
// 获取验证码
String code = userMap.get("code").toString();

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

// 进行验证码的比对(页面提交的验证码和Session中保存的验证码做比对)
if(codeInSession != null && codeInSession.equals(code)){
// 如果能够比对成功,说明登录成功
// 判断当前手机号对应的用户是否为新用户
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getPhone, phone);
User user = userService.getOne(lambdaQueryWrapper);

if(user == null){
// 如果是新用户则自动注册
user = new User();
user.setPhone(phone);
userService.save(user);
}
session.setAttribute("user", user.getId());
return Result.success(user);
}

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

地址簿管理

address_book数据模型

image-20230706171952511

表现层代码

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
/**
* 地址簿管理
*/
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {

@Autowired
private AddressBookService addressBookService;

/**
* 新增
*/
@PostMapping
public Result<AddressBook> save(@RequestBody AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook:{}", addressBook);
addressBookService.save(addressBook);
return Result.success(addressBook);
}

/**
* 设置默认地址
*/
@PutMapping("/default")
public Result<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
log.info("addressBook:{}", addressBook);
LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
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 Result.success(addressBook);
}

/**
* 根据id查询地址
*/
@GetMapping("/{id}")
public Result get(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
if (addressBook != null) {
return Result.success(addressBook);
} else {
return Result.error("没有找到该对象");
}
}

/**
* 查询默认地址
*/
@GetMapping("/default")
public Result<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 Result.error("没有找到该对象");
} else {
return Result.success(addressBook);
}
}

/**
* 查询指定用户的全部地址
*/
@GetMapping("/list")
public Result<List<AddressBook>> list() {
AddressBook addressBook = new AddressBook();
addressBook.setUserId(BaseContext.getCurrentId());

//条件构造器
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 Result.success(addressBookService.list(queryWrapper));
}
}

核心业务思路

  • 设置默认地址

    一个用户的默认收货地址只能有一个,如何保证在设置默认地址时,每个用户的默认地址只有一个呢?那就是每次操作address_book数据表时,先将该用户的所有is_default字段都设置为0,再将选中的设置为1。

  • 查询指定用户的全部地址

    image-20230706173632478

​ 这里不需要请求参数,只需要用户userId,这个可以从Session中获取。

购物车

需求分析

  • 移动端用户可以将菜品/套餐添加到购物车
  • 对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车(前端实现)
  • 对于套餐来说,可以直接点击当前套餐加入购物车
  • 在购物车中可以修改菜品/套餐的数量,也可以清空购物车

表现层代码

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
110
111
112
113
114
115
116
117
@RestController
@RequestMapping("/shoppingCart")
@Slf4j
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;

/**
* 向购物车中新增菜品或套餐
* @param shoppingCart
* @return
*/
@RequestMapping("/add")
public Result<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){

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

Long dishId = shoppingCart.getDishId();
// 菜品和套餐的区别是发送的请求参数一个是dishId一个是setmealId
// SQL: select * from shopping_cart where user_id = ? and [dish_id = ? 或者 setmeal_id = ?]
LambdaQueryWrapper<ShoppingCart> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ShoppingCart::getUserId, currentId);
if(dishId == null){
// 加入购物车的是套餐setmealId
lambdaQueryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}else{
// 加入购物车的是单菜品
lambdaQueryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
}

// shopping_cart中的一行数据
ShoppingCart shoppingCartOne = shoppingCartService.getOne(lambdaQueryWrapper);

// 还要检查加入购物车菜品或者套餐的数量 -- 如果是第一次添加则setNumber=1,并执行save操作;如果不是第一次添加则updateNumber++
if(shoppingCartOne == null){
// 第一次添加
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartService.save(shoppingCart);
shoppingCartOne = shoppingCart;
}else{
// 不是第一次添加
Integer number = shoppingCartOne.getNumber();
shoppingCartOne.setNumber(++number);
shoppingCartService.updateById(shoppingCartOne);
}
return Result.success(shoppingCartOne);
}

/**
* 向购物车中减少菜品或套餐
* @param shoppingCart
* @return
*/
@RequestMapping("/sub")
public Result<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart){
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);

// 实际请求参数只有dishId或者setmealId
LambdaQueryWrapper<ShoppingCart> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ShoppingCart::getUserId, userId);
if(shoppingCart.getDishId() != null){
// 菜品
lambdaQueryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
} else{
// 套餐
lambdaQueryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
ShoppingCart one = shoppingCartService.getOne(lambdaQueryWrapper);

if(one.getNumber() == 1){
shoppingCartService.remove(lambdaQueryWrapper);
} else if(one.getNumber() > 1){
one.setNumber(one.getNumber() - 1);
shoppingCartService.updateById(one);
}
return Result.success(one);
}



/**
* 根据登录用户的id查询购物车的列表清单
* @return
*/
@GetMapping("/list")
public Result<List<ShoppingCart>> list(){

Long currentId = BaseContext.getCurrentId();

LambdaQueryWrapper<ShoppingCart> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ShoppingCart::getUserId, currentId);
lambdaQueryWrapper.orderByAsc(ShoppingCart::getCreateTime);

List<ShoppingCart> list = shoppingCartService.list(lambdaQueryWrapper);
return Result.success(list);
}


/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
public Result<String> cleanShoppingCart(){
Long currentId = BaseContext.getCurrentId();

LambdaQueryWrapper<ShoppingCart> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ShoppingCart::getUserId, currentId);
shoppingCartService.remove(lambdaQueryWrapper);
return Result.success("清空购物车成功");
}

}

用户下单

流程分析

  1. 在订单确认页面中,发送ajax请求,请求服务端,获取当前登录用户的默认地址
  2. 在订单确认页面,发送ajax请求,请求服务端,获取当前登录用户的购物车数据
  3. 在订单确认页面点击去支付按钮,发送ajax请求,请求服务端,完成下单操作

数据模型

orders数据表:

image-20230706184553636

order_detail数据表:

image-20230706184642018

“下单”核心业务思路

步骤1:查询当前用户的购物车,判断购物车是否为空,如果为空抛出业务异常无法下单;

步骤2:向订单表orders插入(1条)数据;

步骤3:向订单明细表order_detail插入(多条)数据;

步骤4:清空购物车数据

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
/**
* 用户下单提交
* @param orders
*/
@Override
public void submit(Orders orders) {
// 获得当前用户的id
Long currentId = BaseContext.getCurrentId();

// 查询当前用户id的购物车数据
LambdaQueryWrapper<ShoppingCart> cartLambdaQueryWrapper = new LambdaQueryWrapper<>();
cartLambdaQueryWrapper.eq(ShoppingCart::getUserId, currentId);
List<ShoppingCart> cartList = shoppingCartService.list(cartLambdaQueryWrapper);

if (cartList == null || cartList.size() == 0){
throw new ServiceException("购物车为空,无法下单");
}

// 查询用户数据
User user = userService.getById(currentId);
// 查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);

long orderId = IdWorker.getId();//订单号

AtomicInteger amount = new AtomicInteger(0);

// 遍历购物车数据得到订单明细列表数据
List<OrderDetail> orderDetails = cartList.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(currentId);
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()));
// 向订单表orders插入(1条)数据
this.save(orders);

// 向订单明细表order_detail插入(多条)数据
orderDetailService.saveBatch(orderDetails);

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