一、新增套餐

1.1 需求分析

套餐就是菜品的集合。

后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。

image-20220808110904164

1.2 数据模型

新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:

说明 备注
setmeal 套餐表 存储套餐的基本信息
setmeal_dish 套餐菜品关系表 存储套餐关联的菜品的信息(一个套餐可以关联多个菜品)

两张表具体的表结构如下:

1). 套餐表setmeal

image-20220808110925552

在该表中,套餐名称name字段是不允许重复的,在建表时,已经创建了唯一索引。

2). 套餐菜品关系表setmeal_dish

image-20220808110952117

在该表中,菜品的名称name,菜品的原价price 实际上都是冗余字段,因为我们在这张表中存储了菜品的ID(dish_id),根据该ID我们就可以查询出name,price的数据信息,而这里我们又存储了name,price,这样的话,我们在后续的查询展示操作中,就不需要再去查询数据库获取菜品名称和原价了,这样可以简化我们的操作。

1.3 准备工作

1). 实体类 SetmealDish

所属包: com.itheima.reggie.entity

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
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
* 套餐菜品关系
*/
@Data
public class SetmealDish implements Serializable {
private static final long serialVersionUID = 1L;

private Long id;

//套餐id
private Long setmealId;

//菜品id
private Long dishId;

//菜品名称 (冗余字段)
private String name;

//菜品原价
private BigDecimal price;

//份数
private Integer copies;

//排序
private Integer sort;

@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). DTO SetmealDto

该数据传输对象DTO,主要用于封装页面在新增套餐时传递过来的json格式的数据,其中包含套餐的基本信息,还包含套餐关联的菜品集合。

所属包: com.itheima.reggie.dto

1
2
3
4
5
6
7
8
9
10
11
12
import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.entity.SetmealDish;
import lombok.Data;
import java.util.List;

@Data
public class SetmealDto extends Setmeal {

private List<SetmealDish> setmealDishes;//套餐关联的菜品集合

private String categoryName;//分类名称
}

3). Mapper接口 SetmealDishMapper

所属包: com.itheima.reggie.mapper

1
2
3
4
5
6
7
/**
* @auther xiaochen
* @create 2022-08-08 10:49
*/
@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}

4). 业务层接口 SetmealDishService

所属包: com.itheima.reggie.service

1
2
3
4
5
6
/**
* @auther xiaochen
* @create 2022-08-08 11:12
*/
public interface SetmealDishService extends IService<SetmealDish> {
}

5). 业务层实现类 SetmealDishServiceImpl

所属包: com.itheima.reggie.service.impl

1
2
3
4
5
6
7
8
/**
* @auther xiaochen
* @create 2022-08-08 11:12
*/
@Slf4j
@Service
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
}

6). 控制层 SetmealController

套餐管理的相关业务,我们都统一在 SetmealController 中进行统一处理操作。

所属包: com.itheima.reggie.service.impl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 套餐管理
*
* @auther xiaochen
* @create 2022-08-08 11:13
*/

@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishService setmealDishService;
}

1.4 前端页面分析

前端页面和服务端的交互过程:

  1. 点击新建套餐按钮,访问页面(backend/page/combo/add.html),页面加载发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中(==已实现==)

  2. 访问页面(backend/page/combo/add.html),页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中(==已实现==)

  3. 当点击添加菜品窗口左侧菜单的某一个分类, 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中

  4. 页面发送请求进行图片上传,请求服务端将图片保存到服务器(==已实现==)

  5. 页面发送请求进行图片下载,将上传的图片进行回显(==已实现==)

  6. 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端

经过上述的页面解析及流程分析,我们发送这里需要发送的请求有5个,分别是 :

A. 根据传递的参数,查询套餐分类列表

B. 根据传递的参数,查询菜品分类列表

C. 图片上传

D. 图片下载展示

E. 根据菜品分类ID,查询菜品列表

F. 保存套餐信息

而对于以上的前4个功能我们都已经实现, 所以我们接下来需要开发的功能主要是最后两项, 具体的请求信息如下:

1). 根据分类ID查询菜品列表

请求 说明
请求方式 GET
请求路径 /dish/list
请求参数 ?categoryId=1397844263642378242

2). 保存套餐信息

请求 说明
请求方式 POST
请求路径 /setmeal
请求参数 json格式数据

传递的json格式数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name":"营养超值工作餐",
"categoryId":"1399923597874081794",
"price":3800,
"code":"",
"image":"9cd7a80a-da54-4f46-bf33-af3576514cec.jpg",
"description":"营养超值工作餐",
"dishList":[],
"status":1,
"idType":"1399923597874081794",
"setmealDishes":[
{"copies":2,"dishId":"1423329009705463809","name":"米饭","price":200},
{"copies":1,"dishId":"1423328152549109762","name":"可乐","price":500},
{"copies":1,"dishId":"1397853890262118402","name":"鱼香肉丝","price":3800}
]
}

1.5 代码实现

1.根据分类查询菜品

在当前的需求中,我们只需要根据页面传递的菜品分类的ID(categoryId)来查询菜品列表即可,我们可以直接定义一个DishController的方法,声明一个Long类型的categoryId,这样做是没问题的。

但是考虑到该方法的拓展性,我们在这里定义方法时,通过Dish这个实体来接收参数。

在DishController中定义方法list,接收Dish类型的参数:

在查询时,需要根据菜品分类categoryId进行查询,并且还要限定菜品的状态为起售状态(status为1),然后对查询的结果进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//根据条件查询对应菜品数据
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){

//构造查询条件
LambdaQueryWrapper<Dish> lambdaQueryWrapper=new LambdaQueryWrapper<>();
//添加条件,查询状态为起售状态的菜品
lambdaQueryWrapper.eq(Dish::getStatus,1);
lambdaQueryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
//条件排序条件
lambdaQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

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

return R.success(list);
}

测试:

成功查询到数据

image-20220808112204822

2.保存套餐

在进行套餐信息保存时,前端提交的数据,不仅包含套餐的基本信息,还包含套餐关联的菜品列表数据 setmealDishes。

所以这个时候我们使用Setmeal就不能完成参数的封装了,我们需要在Setmeal的基本属性的基础上,再扩充一个属性 setmealDishes 来接收页面传递的套餐关联的菜品列表,所以要使用SetmealDto来完成这个需求。

1). SetmealController中定义方法save,新增套餐

在该Controller的方法中,我们不仅需要保存套餐的基本信息,还需要保存套餐关联的菜品数据,所以我们需要再该方法中调用业务层方法,完成两块数据的保存。

页面传递的数据是json格式,需要在方法形参前面加上@RequestBody注解, 完成参数封装。

1
2
3
4
5
6
7
8
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}",setmealDto);

setmealService.saveWithDish(setmealDto);

return R.success("新增套餐成功");
}

2). SetmealService中定义方法saveWithDish

1
2
3
4
5
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
public void saveWithDish(SetmealDto setmealDto);

3). SetmealServiceImpl实现方法saveWithDish

具体逻辑:

A. 保存套餐基本信息

B. 获取套餐关联的菜品集合,并为集合中的每一个元素赋值套餐ID(setmealId)

C. 批量保存套餐关联的菜品集合

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Autowired
private SetmealDishService setmealDishService;

/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal,执行insert操作
this.save(setmealDto);

List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item) -> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());

//保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
setmealDishService.saveBatch(setmealDishes);
}

4).测试

image-20220808140713987

套餐添加成功

image-20220808141006844

套餐和菜品关联表添加成功

image-20220808141029459

二、套餐分页查询

2.1 需求分析

系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

image-20220808142716349

在进行套餐数据的分页查询时,除了传递分页参数以外,还可以传递一个可选的条件(套餐名称)。

查询返回的字段中,包含套餐的基本信息之外,还有一个套餐的分类名称,在查询时,需要关联查询这个字段。

2.2 前端页面分析

前端页面和服务端的交互过程:

1). 访问页面(backend/page/combo/list.html),页面加载时,会自动发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据

2). 在列表渲染展示时,页面发送请求,请求服务端进行图片下载,用于页面图片展示(已实现)

具体的请求信息如下:

请求 说明
请求方式 GET
请求路径 /setmeal/page
请求参数 ?page=1&pageSize=10&name=xxx

2.3 代码实现

1. 基本信息查询

在SetmealController中创建套餐分页查询方法。

逻辑如下:

  1. 构建分页条件对象

  2. 构建查询条件对象,如果传递了套餐名称,根据套餐名称模糊查询, 并对结果按修改时间降序排序

  3. 执行分页查询

  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
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
//构造分页构造器
Page<Setmeal> pageInfo = new Page<>(page, pageSize);

Page<SetmealDto> pageDtoInfo = new Page<>();
//构造条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//根据name进行模糊查询
queryWrapper.like(!StringUtils.isEmpty(name), Setmeal::getName, name);
//添加排序条件,根据sort进行排序
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
//进行分页查询
setmealService.page(pageInfo, queryWrapper);

//对象拷贝
BeanUtils.copyProperties(pageInfo, pageDtoInfo, "records");

List<Setmeal> records = pageInfo.getRecords();

List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();

BeanUtils.copyProperties(item, setmealDto);
Long categoryId = item.getCategoryId();
//根据id查分类对象
Category category = categoryService.getById(categoryId);
//分类不为空时,取出分类名称填入Dto
if (category != null) {
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());

pageDtoInfo.setRecords(list);

return R.success(pageDtoInfo);
}

2.4 功能测试

可以获取到套餐分类名称categoryName并可以在列表页面展示出来 。

image-20220808143354511

三、删除套餐

3.1 需求分析

在套餐管理列表页面,点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。

注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。

3.2 前端页面分析

前端页面和服务端的交互过程:

1). 点击删除, 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐

2). 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐

开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可,一次请求为根据ID删除,一次请求为根据ID批量删除。

观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。

具体的请求信息如下:

请求 说明
请求方式 DELETE
请求路径 /setmeal
请求参数 ?ids=1423640210125656065,1423338765002256385

3.3 代码实现

删除套餐的流程及请求信息,我们分析完毕之后,就来完成服务端的逻辑开发。在服务端的逻辑中, 删除套餐时, 我们不仅要删除套餐, 还要删除套餐与菜品的关联关系。

1). 在SetmealService接口中定义方法removeWithDish

1
2
3
4
5
6
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
*
* @param ids
*/
public void removeWithDish(List<Long> ids);

2). 在SetmealServiceImpl中实现方法removeWithDish

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
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
*
* @param ids
*/
@Transactional
public void removeWithDish(List<Long> ids) {

//SQL模拟:select count(*) from setmeal where id in (1,2,3) and status = 1
//首先查询套餐状态,确定是否能够删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();

queryWrapper.in(Setmeal::getId, ids);
queryWrapper.eq(Setmeal::getStatus, 1);
//根据所查询到的数据来判断
int count = this.count(queryWrapper);
if (count > 0) {
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除");
}

//如果可以删除,先删除套餐表中的数据---setmeal
this.removeByIds(ids);

//SQL模拟:delete from setmeal_dish where setmeal_id in (1,2,3)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId, ids);
//删除关系表中的数据----setmeal_dish
setmealDishService.remove(lambdaQueryWrapper);
}

3).在SetmealController中创建delete方法

1
2
3
4
5
6
7
8
9
10
11
/**
* 删除套餐
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids:{}",ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}

3.4 功能测试

首先删除一个起售的套餐

image-20220808151904295

测试:

image-20220808151336650

然后将状态再改为0

image-20220808151917298

然后删除

image-20220808151935701

四、短信发送

4.1 短信服务介绍

在项目中,如果我们要实现短信发送功能,我们无需自己实现,也无需和运营商直接对接,只需要调用第三方提供的短信服务即可。

目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员,并且按照提供的开发文档进行调用就可以发送短信。需要说明的是,这些短信服务一般都是收费服务。

常用短信服务:

  • 阿里云

  • 华为云

  • 腾讯云

  • 京东

  • 梦网

  • 乐信

本项目在选择短信服务的第三方服务提供商时,选择的是阿里云短信服务。

4.2 阿里云短信服务介绍

阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。

应用场景:

场景 案例
验证码 APP、网站注册账号,向手机下发验证码; 登录账户、异地登录时的安全提醒; 找回密码时的安全验证; 支付认证、身份校验、手机绑定等。
短信通知 向注册用户下发系统相关信息,包括: 升级或维护、服务开通、价格调整、 订单确认、物流动态、消费确认、 支付通知等普通通知短信。
推广短信 向注册用户和潜在客户发送通知和推广信息,包括促销活动通知、业务推广等商品与活动的推广信息。增加企业产品曝光率、提高产品的知名度。

image-20220808160529181

阿里云短信服务官方网站: https://www.aliyun.com/product/sms?spm=5176.19720258.J_8058803260.52.5c432c4a11Dcwf

4.3 代码实现

具体实现:

1). pom.xml

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>

2). 将官方提供的main方法封装为一个工具类

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
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;

/**
* 短信发送工具类
*/
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", "xxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxx");
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();
}
}

}

五、验证码登录

5.1 需求分析

为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。手机验证码登录有如下优点:

  1. 方便快捷,无需注册,直接登录

  2. 使用短信验证码作为登录凭证,无需记忆密码

  3. 安全

登录流程:

输入手机号 > 获取验证码 > 输入验证码 > 点击登录 > 登录成功

注意:通过手机验证码登录,手机号是区分不同用户的标识。

5.2 数据模型

通过手机验证码登录时,涉及的表为user表,即用户表。结构如下:

image-20220808160742214

5.3 前端页面分析

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

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

  2. 在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求。

如果服务端返回的登录成功,页面将会把当前登录用户的手机号存储在sessionStorage中,并跳转到移动的首页页面。

开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可,分别是获取短信验证码 和 登录请求,具体的请求信息如下:

1). 获取短信验证码

请求 说明
请求方式 POST
请求路径 /user/sendMsg
请求参数 {“phone”:”13100001111”}

2). 登录

请求 说明
请求方式 POST
请求路径 /user/login
请求参数 {“phone”:”13100001111”, “code”:”1111”}

5.4 代码开发

1. 准备工作

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

所属包: com.itheima.reggie.entity

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
import lombok.Data;
import java.io.Serializable;
/**
* 用户信息
*/
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;

private Long id;

//姓名
private String name;

//手机号
private String phone;

//性别 0 女 1 男
private String sex;

//身份证号
private String idNumber;

//头像
private String avatar;

//状态 0:禁用,1:正常
private Integer status;
}

2). Mapper接口 UserMapper

所属包: com.itheima.reggie.mapper

1
2
3
4
5
6
7
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User>{
}

3). 业务层接口 UserService

所属包: com.itheima.reggie.service

1
2
3
4
5
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.User;

public interface UserService extends IService<User> {
}

4). 业务层实现类 UserServiceImpl

所属包: com.itheima.reggie.service.impl

1
2
3
4
5
6
7
8
9
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.User;
import com.itheima.reggie.mapper.UserMapper;
import com.itheima.reggie.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService{
}

5). 控制层 UserController

所属包: com.itheima.reggie.controller

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.itheima.reggie.service.UserService;
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;

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

6). 工具类SMSUtils、ValidateCodeUtils(直接从课程资料中导入即可)

所属包: com.itheima.reggie.utils

image-20220808161055013

  • SMSUtils : 是我们上面改造的阿里云短信发送的工具类 ;

  • ValidateCodeUtils : 是验证码生成的工具类 ;

2.功能实现

修改LoginCheckFilter

前面我们已经完成了LoginCheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的两个请求(获取验证码和登录)需要在此过滤器处理时直接放行。

image-20220808163847435

对于移动的端的页面,也是用户登录之后,才可以访问的,那么这个时候就需要在 LoginCheckFilter 中进行判定,如果移动端用户已登录,我们获取到用户登录信息,存入ThreadLocal中(在后续的业务处理中,如果需要获取当前登录用户ID,直接从ThreadLocal中获取),然后放行。

增加如下逻辑:

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);

filterChain.doFilter(request,response);
return;
}
发送短信验证码

在UserController中创建方法,处理登录页面的请求,为指定手机号发送短信验证码,同时需要将手机号对应的验证码保存到Session,方便后续登录时进行比对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 发送手机短信验证码
* @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);

//调用阿里云提供的短信服务API完成发送短信
//SMSUtils.sendMessage("瑞吉外卖","",phone,code);

//需要将生成的验证码保存到Session
session.setAttribute(phone,code);
return R.success("手机验证码短信发送成功");
}
return R.error("短信发送失败");
}

3. 验证码登录

在UserController中增加登录的方法 login,该方法的具体逻辑为:

  1. 获取前端传递的手机号和验证码

  2. 从Session中获取到手机号对应的正确的验证码

  3. 进行验证码的比对 , 如果比对失败, 直接返回错误信息

  4. 如果比对成功, 需要根据手机号查询当前用户, 如果用户不存在, 则自动注册一个新用户

  5. 将登录用户的ID存储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
/**
* 移动端用户登录
* @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("登录失败");
}

5.5 功能测试

点击获取验证码

image-20220808164633394

后端接收到

image-20220808164647464

登陆成功

image-20220808164702196