瑞吉外卖-day05

YangeIT大约 38 分钟瑞吉外卖套餐模块短信发送手机验证码

瑞吉外卖-day05

课程内容

  • 管理端
    • 新增套餐
    • 套餐分页查询
    • 删除套餐
  • 用户端(app 端)
    • 短信发送
    • 手机验证码登录

1. 新增套餐

1.1 需求分析

套餐就是菜品的集合。

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

image-20210805232711418

1.2 数据模型

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

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

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

1). 套餐表 setmeal

image-20210805233615067
image-20210805233615067

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

image-20210805234059563
image-20210805234059563

2). 套餐菜品关系表 setmeal_dish

image-20210805233807009
image-20210805233807009

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

1.3 准备工作

1). 实体类 SetmealDish

ps.直接从课程资料中导入即可,Setmeal 实体前面课程中已经导入过了。

所属包: com.itheima.reggie.entity

/**
 * 套餐菜品关系
 */
@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;
}

1.4 前端页面分析

服务端的基础准备工作我们准备完毕之后,在进行代码开发之前,需要梳理一下新增套餐时前端页面和服务端的交互过程

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

image-20210806002144537
image-20210806002144537

获取套餐分类列表的功能我们不用开发,之前已经开发完成了,之前查询时 type 传递的是 1,查询菜品分类; 本次查询时,传递的 type 为 2,查询套餐分类列表。

相关信息

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

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

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

F. 保存套餐信息

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

传递的 json 格式数据如下:

点击查看 json 数据
{
  "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.5.1 根据分类查询菜品

功能分析

在当前的需求中,我们只需要根据页面传递的菜品分类的 来查询菜品列表即可,我们可以直接定义一个 DishController 的方法,声明一个 Long 类型的 categoryId,这样做是没问题的。但是考虑到该方法的拓展性,我们在这里定义方法时,通过 Dish 这个实体来接收参数。

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

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

/**
* 根据条件查询对应的菜品数据
* @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());
    //添加条件,查询状态为1(起售状态)的菜品
    queryWrapper.eq(Dish::getStatus,1);
    //添加排序条件
    queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

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

    return R.success(list);
}
功能测试

代码编写完毕,我们重新启动服务器,进行测试,可以通过 debug 断点跟踪的形式查看页面传递的参数封装情况,及响应给页面的数据信息。

image-20210806012153982
image-20210806012153982

1.5.2 保存套餐 ❤️ 👈

功能分析

在进行套餐信息保存时,前端提交的数据,不仅包含套餐的基本信息,还包含套餐关联的菜品列表数据 setmealDishes。所以这个时候我们使用 Setmeal 就不能完成参数的封装了,我们需要在 Setmeal 的基本属性的基础上,再扩充一个属性 setmealDishes 来接收页面传递的套餐关联的菜品列表,而我们在准备工作中,导入进来的 SetmealDto 能够满足这个需求。

1️⃣ SetmealController 中定义方法 save,新增套餐

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

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

@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
    log.info("套餐信息:{}",setmealDto);

    setmealService.saveWithDish(setmealDto);

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

代码编写完毕,我们重新启动服务器,进行测试,可以通过 debug 断点跟踪的形式查看页面传递的参数封装情况,及套餐相关数据的保存情况。

录入表单数据:

image-20210806014328575
image-20210806014328575

debug 跟踪数据封装:

image-20210806014508310
image-20210806014508310

跟踪数据库保存的数据:

image-20210806014807017
image-20210806014807017

2. 套餐分页查询

2.1 需求分析

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

image-20210806073710653
image-20210806073710653

在进行套餐数据的分页查询时,除了传递分页参数以外,还可以传递一个可选的条件(套餐名称)。查询返回的字段中,包含套餐的基本信息之外,还有一个套餐的分类名称,在查询时,需要关联查询这个字段。

2.2 前端页面分析

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

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

image-20210806074846550

2.3 代码开发 ✏️ 👈

2.3.1 基本信息查询

上述我们已经分析列表分页查询功能的请求信息,接下来我们就在 SetmealController 中创建套餐分页查询方法。

逻辑如下

  1. 构建分页条件对象
  2. 构建查询条件对象,如果传递了套餐名称,根据套餐名称模糊查询, 并对结果按修改时间降序排序
  3. 执行分页查询
  4. 组装数据并返回

代码实现 :

/**
  * 套餐分页查询
  * @param page
  * @param pageSize
  * @param name
  * @return
  */
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
    //分页构造器对象
    Page<Setmeal> pageInfo = new Page<>(page,pageSize);

    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    //添加查询条件,根据name进行like模糊查询
    queryWrapper.like(name != null,Setmeal::getName,name);
    //添加排序条件,根据更新时间降序排列
    queryWrapper.orderByDesc(Setmeal::getUpdateTime);

    setmealService.page(pageInfo,queryWrapper);
    return R.success(pageInfo);
}

2.3.2 问题分析

基本分页查询代码编写完毕后,重启服务,测试列表查询,我们发现, 列表页面的数据可以展示出来, 但是套餐分类名称没有展示出来。

image-20210806082542473
image-20210806082542473

这是因为在服务端仅返回分类 ID(categoryId), 而页面展示需要的是 categoryName 属性。

2.3.3 功能完善 (方式1 纯MP实现,无sql语句 )

在查询套餐信息时, 只包含, 并不包含套餐的, 所以在这里查询到套餐的基本信息后, 还需要根据分类 ID(categoryId), 查询套餐分类名称(categoryName),并最终将(在第一小节已经导入)中。

完善后代码:

@Data
public class SetmealDto extends Setmeal {
    private List<SetmealDish> setmealDishes; //套餐关联菜品列表
    private String categoryName;//套餐分类名称
}

2.3.4 功能完善(方式2 Mybatis+sql 实现 )技术提高

思路和步骤

思路:先写sql语句进行多表查询,然后通过多表将数据映射到SetmealDto对象中,返回给前端

  1. MP 是对 mybatis 进行增强,因此使用 MP 也可以使用 Mybatisopen in new window

  2. Mybatis 的 Sql 语句可以用注解,也可以写在 mapper.xml 中

    • 第一步:创建一个 SetmealMapper.xml 文件,文件位置如下图
    • 第二步:在 SetmealMapper.xml 中 <mapper namespace="接口的全类名">
   <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.itheima.reggie.mapper.SetmealMapper">

</mapper>
  • 第三步:定义的<select id='接口中的方法'>标签,id 对应接口中的方法
  • 第四步:接口中方法的返回值类型和 <select id="方法名" resultType="类型"> 要一致(如果是集合,就填写泛型)
  1. 如果不清楚使用 Mybatis 代理模式开发,可以安装 MybatisX 插件进行辅助
    • 本插件能检测是否符合 Mybatis 代理开发的要求,现象如下:

      图片为菜品分页查询,功能很相似,也可以模仿菜品分页查询

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

    /**
     * 分页查询
     * @param page 当前页
     * @param pageSize  每页条数
     * @param name 检索词
     * @return
     */
    @GetMapping("/page")
    public R<Page<SetmealDto>> page(Integer page, Integer pageSize, String name){

        log.info("当前页:{},每页:{}条数 , 检索词:{}",page,pageSize,name);

        Page<SetmealDto> dishDtoPage = new Page<>(page, pageSize);
        Page<SetmealDto> dishDtoPage1 = setmealService.pageSetmealDto(dishDtoPage,name);


        return R.success(dishDtoPage1);
    }
}

2.4 功能测试

代码完善后,重启服务,测试列表查询,我们发现, 抓取浏览器的请求响应数据,我们可以获取到套餐分类名称 categoryName,也可以在列表页面展示出来 。

image-20210806083346578
image-20210806083346578

3.套餐禁用启用

3.1 需求分析

image-VeryCapture_20220926105037
image-VeryCapture_20220926105037

3.2 前端页面分析

1). 点击禁用, 页面发送 ajax 请求,根据套餐 id 禁用对应套餐

3.3 代码开发 ✏️ 👈


  /**
     * 如果是多个数据,用集合接收,必须使用,@RequestParam("ids")注解
     * @param status
     * @param ids
     * @return
     */
    @PostMapping("/status/{status}")
    public R<String> updateStatus(@PathVariable Integer status,@RequestParam("ids") List<Long> ids){

        log.info("updateStatus:ids:{},status:{}",ids,status);
        //1. 通过传过来的ids 获得所有的菜品
        //修改套餐状态只需要修改Setmeal表,因此不需要多表操作
        List<Setmeal> setmeals = setmealService.listByIds(ids);
        //2. 遍历菜品集合,依次设置状态值
        for (Setmeal setmeal : setmeals) {
            setmeal.setStatus(status);
        }
        //3. 修改状态
        boolean b = setmealService.updateBatchById(setmeals);

        return b?R.success("操作成功"):R.error("操作失败");
    }

4. 删除套餐

4.1 需求分析

在套餐管理列表页面,点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。

image-20210806214443507
image-20210806214443507

4.2 前端页面分析

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

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

image-20210806215911878
image-20210806215911878

4.3 代码开发 ✏️ 👈

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

1). 在 SetmealController 中创建 delete 方法

我们可以先测试在 delete 方法中接收页面提交的参数,具体逻辑后续再完善:

/**
 * 删除套餐
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
    log.info("ids:{}",ids);
    return R.success("套餐数据删除成功");
}

编写完代码,我们重启服务之后,访问套餐列表页面,勾选复选框,然后点击"批量删除",我们可以看到服务端可以接收到集合参数 ids,并且在控制台也可以输出对应的数据 。

image-20210806221603303
image-20210806221603303

4.4 功能测试

代码完善后,重启服务,测试套餐的删除功能,主要测试以下几种情况。

1). 删除正在启用的套餐

image-20210806224121877
image-20210806224121877

2). 执行批量操作, 删除两条记录, 一个启售的, 一个停售的

由于当前我们并未实现启售/停售功能,所以我们需要手动修改数据库表结构的 status 状态,将其中的一条记录 status 修改为 0。

image-20210806224603405
image-20210806224603405

3). 删除已经停售的套餐信息,执行删除之后, 检查数据库表结构 setmeal , setmeal_dish 表中的数据

image-20210806224807108
image-20210806224807108

5.套餐修改【拓展功能】 🚀 🚀

5.1 需求分析

  1. 回显数据

    • 1.通过 id 查询套餐基本信息
    • 2.通过 id 查询出菜品信息
    • 3.拼接数据返回
  2. 修改数据

    • 1.修改基本信息
    • 2.删除套餐原包含的菜品信息
    • 3.添加套餐新包含的菜品信息
image-VeryCapture_20221017154322
image-VeryCapture_20221017154322

经过上述的分析:

  1. 套餐分类下拉框的展示(已完成)

  2. 图片的下载回显功能(已完成)

  3. 根据 ID 查询套餐及套餐菜品信息

    请求说明
    请求方式GET
    请求路径/setmeal/{id}
  4. 修改套餐及套餐菜品信息

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

5.2 代码开发

回显数据

  • 1.通过 id 查询套餐基本信息
  • 2.通过 id 查询出菜品信息
  • 3.拼接数据返回

类:com.itheima.reggie.controller.SetmealController

/**
    * 修改的回显数据
    * 步鄹
    * 1.通过id查询套餐基本信息
    * 2.通过id查询出菜品信息
    * 3.拼接数据返回
    *
    * @param setmealid
    * @return
    */
@GetMapping("/{setmealid}")
public R<SetmealDto> getById(@PathVariable Long setmealid){

    SetmealDto setmealDto=setmealService.getByIdWithDish(setmealid);

    return R.success(setmealDto);

}

保存数据

  • 1.修改基本信息
  • 2.删除套餐原包含的菜品信息
  • 3.添加套餐新包含的菜品信息

类:com.itheima.reggie.controller.SetmealController


Boolean updateWithDish(SetmealDto setmealDto);

5.3 测试

image-VeryCapture_20221017154322
image-VeryCapture_20221017154322

6. 短信发送 🍐 🚀

image-20210806225505074

在我们接下来要实现的移动端的业务开发中,第一块儿我们需要开发的功能就是移动端的登录功能,而移动端的登录功能,比较流行的方式就是基于短信验证码进行登录,那么这里涉及到了短信发送的知识,所以本章节,我们就来讲解,在项目开发中,我们如何发送短信。

6.1 短信服务介绍

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

常用短信服务:

  • 阿里云
  • 华为云
  • 腾讯云
  • 京东
  • 梦网
  • 乐信

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

6.2 阿里云短信服务介绍

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

应用场景:

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

阿里云短信服务官方网站open in new window

6.3 阿里云短信服务准备

5.3.1 注册账号

阿里云官网:https://www.aliyun.com/open in new window

image-20210807074911618

点击官网首页注册按钮,跳转到如下注册页面:

image-20210807074934251

当我们把账号注册完毕之后,我们就可以登录到阿里云系统控制台。

6.4 代码开发

使用阿里云短信服务发送短信,可以参照官方提供的文档即可。

官方文档open in new window

image-20210807193047220
image-20210807193047220

我们根据官方文档的提示,引入对应的依赖,然后再引入对应的 java 代码,就可以发送消息了。

image-20210807193829131
image-20210807193829131

SDK : SDK 就是 Software Development Kit 的缩写,翻译过来——软件开发工具包,辅助开发某一类软件的相关文档、范例和工具的集合都可以叫做 SDK。在我们与第三方接口相互时, 一般都会提供对应的 SDK,来简化我们的开发。

1).导入依赖

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

注意:

由于我们个人目前无法申请阿里云短信服务,所以这里我们只需要把流程跑通,具体的短信发送可以实现。

7. 手机验证码登录 ✏️

7.1 需求分析

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

1). 方便快捷,无需注册,直接登录 2). 使用短信验证码作为登录凭证,无需记忆密码 3). 安全

登录流程

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

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

7.2 数据模型

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

image-20210807231948412
image-20210807231948412

7.3 前端页面分析

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

image-20210807232653592

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

image-20210807233018171

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

相关信息

1). 获取短信验证码

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

2). 登录

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

7.4 代码开发 ✏️ 👈

7.4.1 准备工作

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

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

所属包: com.itheima.reggie.entity

/**
 * 用户信息
 */
@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;
}

7.4.2 功能实现 ❤️ 👈

1️⃣ 修改 LoginCheckFilter

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

image-20210807235349089
image-20210807235349089
  String[] urls = new String[]{
                "/employee/login",  // 登陆页 直接放行
                "/employee/logout",//注销页 直接放行
                "/backend/**", //所有的pc端的 前端资源 放行
                "/front/**", //所有的移动端 前端资源 放行
                "/common/**",  //上传文件和下载文件 放心
                "/user/sendMsg",
                "/user/login"
        };






 
 

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

增加如下逻辑:

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

代码位置(完整代码在下方):

点击查看过滤器完整代码
package com.itheima.reggie.filter;

/**
 *
 * 1.创建LoginCheckFilter,表示拦截所有的请求
 * 2.在启动类ReggieApplication中配置一个注解@ServletComponentScan,用来识别Web的注解如:@WebFilter
 * 3. 在doFilter中书写逻辑:
 *  A. 获取本次请求的 URI
 *  B. 判断本次请求, 是否需要登录, 才可以访问
 *  C. 如果不需要,则直接放行
 *  D. 判断登录状态,如果已登录,则直接放行
 *  E. 如果未登录, 则返回未登录结果
 */
@Slf4j
@WebFilter("/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        Thread thread = Thread.currentThread();
        log.info("过滤器  当前线程id:{}",thread.getId());


        HttpServletRequest request= (HttpServletRequest) servletRequest;


// *  A. 获取本次请求的 URI
        String uri = request.getRequestURI();
// *  B. 判断本次请求, 是否需要登录, 才可以访问
//        定义一个白名单
        String[] urls = new String[]{
                "/employee/login",  // 登陆页 直接放行
                "/employee/logout",//注销页 直接放行
                "/backend/**", //所有的pc端的 前端资源 放行
                "/front/**", //所有的移动端 前端资源 放行
                "/common/**",  //上传文件和下载文件 放心
                "/user/sendMsg",
                "/user/login"
        };
        boolean check = check(urls, uri);
        log.info("当前的路径:{},是否需要拦截:{}",uri,check?"不需要":"需要");

        if (check){
            // *  C. 如果不需要,则直接放行
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }

//管理端
        HttpSession session = request.getSession();
        Long employeeid = (Long) session.getAttribute("employee");

        if (employeeid!=null){
//            存用户id
            BaseContext.setCurrentId(employeeid);
            // *  D. 判断登录状态,如果已登录,则直接放行
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }

        //用户端:判断登录状态,如果已登录,则直接放行
        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,servletResponse);
            return;
        }



// *  E. 如果未登录, 则返回未登录结果 R类型的数据
        servletResponse.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));

    }

    /**
     * 路径匹配,检查本次请求是否需要放行
     * @param urls 白名单
     * @param requestURI 请求的路径
     * @return
     */
    public boolean check(String[] urls,String requestURI){
//        路径匹配器,能够匹配路径--如果匹配上了,就返回true 否则返回false
        AntPathMatcher PATH_MATCHER = new AntPathMatcher();

        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if(match){
                return true;
            }
        }
        return false;
    }



}







































 
 























 
 
 
 
 
 
 
 
 

































7.5 功能测试

代码完成后,重启服务,测试短信验证码的发送及登录功能。

1). 测试错误验证码的情况

image-20210808001952043

2). 测试正确验证码的情况

image-20210808002356092
image-20210808002356092

检查 user 表,用户的数据也插入进来了:

image-20210808002501618
image-20210808002501618