SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】

文章目录

  • 外卖项目-第五天
    • 课程内容
    • 1. 新增套餐
      • 1.1 需求分析
      • 1.2 数据模型
      • 1.3 准备工作
      • 1.4 前端页面分析
      • 1.5 代码开发
        • 1.5.1 根据分类查询菜品
          • 1.5.1.1 功能实现
          • 1.5.1.2 功能测试
        • 1.5.2 保存套餐
          • 1.5.2.1 功能实现
          • 1.5.2.2 功能测试
    • 2. 套餐分页查询
      • 2.1 需求分析
      • 2.2 前端页面分析
      • 2.3 代码开发
        • 2.3.1 基本信息查询
        • 2.3.2 问题分析
        • 2.3.3 功能完善
      • 2.4 功能测试
    • 3. 删除套餐
      • 3.1 需求分析
      • 3.2 前端页面分析
      • 3.3 代码开发
      • 3.4 功能测试
    • 4、更多功能 (没有明确需求,按自己想法做的)
      • 4.1 套餐修改
      • 4.2 套餐批量启售/停售
  • ==开始移动端开发==
    • 5. 短信发送
      • 5.1 短信服务介绍
      • 5.2 阿里云短信服务介绍
      • 5.3 阿里云短信服务准备
        • 5.3.1 注册账号
        • 5.3.2 开通短信服务
        • 5.3.3 设置短信签名
        • 5.3.4 设置短信模板
        • 5.3.5 设置AccessKey
        • 5.3.6 配置权限
        • 5.3.7 禁用/删除AccessKey
      • 5.4 代码开发
    • 6. 手机验证码登录
      • 6.1 需求分析
      • 6.2 数据模型
      • 6.3 前端页面分析
      • 6.4 代码开发
        • 6.4.1 准备工作
        • 6.4.2 功能实现
          • 6.4.2.1 修改LoginCheckFilter
          • 6.4.2.2 发送短信验证码
          • 6.4.2.3 验证码登录 (Map接收无POJO对应的JSON)
      • 6.5 功能测试
    • 7、退出登陆
    • 总结:

外卖项目-第五天

课程内容

  • 新增套餐

  • 套餐分页查询

  • 删除套餐

  • 短信发送

  • 手机验证码登录

SSM实战-外卖项目-05- 新增套餐(涉及2张表DML,需要事务,设计DTO,分类菜品等多表查询)、分页查询(多表,DTO)、批量删除套餐(停售才可删除,多表delete,中间表’套餐-菜品’关联记录也要)、套餐修改(多表数据回显)、套餐批量启售/停售(业务需求没说,得提前协商好)

开始移动端开发:短信发送(阿里云一步步申请测试接口,可以的,就是审核有点慢,签名和模板都正式一点,否则审核不过)、封装短信发送工具类、多线程销毁验证码、 手机验证码登录

1. 新增套餐

1.1 需求分析

套餐就是菜品的集合。

后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第1张图片

1.2 数据模型

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

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

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

1). 套餐表setmeal

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第2张图片

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

在这里插入图片描述

2). 套餐菜品关系表setmeal_dish

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第3张图片

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

1.3 准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好,在做这一块儿的准备工作时,我们无需准备Setmeal的相关实体类、Mapper接口、Service接口及实现,因为之前在做分类管理的时候,我们已经引入了Setmeal的相关基础代码。 接下来,我们就来完成以下的几步准备工作:

1). 实体类 SetmealDish

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

所属包: cn.whu.reggie.entity

可以用MP代码生成器生成,但是得修改成驼峰命名

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格式的数据,其中包含套餐的基本信息,还包含套餐关联的菜品集合。直接从课程资料中导入即可。

所属包: cn.whu.reggie.dto

// 数据传输对象 肯定针对前端传过来的数据格式而封装
/*
    简单分析一下前端新增套餐时该提交哪些数据:
        1) 套餐基本信息 => extends Setmeal
        2) 套餐关联哪些菜品 => List (多对多关系 就是中间表多条记录维护的这种关系)
        3) 套餐分类 => categoryName (其实已经有categoryId了 这个字段完全辅助字段 可有可无 有会更方便 少根据id查一次)
*/
@Data
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;//套餐关联的菜品集合 (中间表多条记录维护)

    private String categoryName;//分类名称 (辅助字段,查询时少根据categoryId查一次)
}

3). Mapper接口 SetmealDishMapper

所属包: cn.whu.reggie.mapper

可以用MP代码生成器生成,但是得修改成驼峰命名
删除xml,加上@Mapper注解

@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}

4). 业务层接口 SetmealDishService

所属包: cn.whu.reggie.service

可以用MP代码生成器生成,但是得修改命名

public interface SetmealDishService extends IService<SetmealDish> {
}

5). 业务层实现类 SetmealDishServiceImpl

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

可以用MP代码生成器生成,但是得修改成驼峰命名

@Service
@Slf4j
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper,SetmealDish> implements SetmealDishService {
}

6). 控制层 SetmealController

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

所属包: cn.whu.reggie.service.impl

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

1.4 前端页面分析

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

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

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

2). 访问页面(backend/page/combo/add.html),页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中(已实现)
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第4张图片

本次查询分类列表,传递的type为1,表示需要查询的是菜品的分类。查询菜品分类的目的,是添加套餐关联的菜品时,我们需要根据菜品分类,来过滤查询菜品信息。查询菜品分类列表的代码已经实现, 具体展示效果如下:

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第5张图片

3). 当点击添加菜品窗口左侧菜单的某一个分类, 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第6张图片
dishList绑定在dishAddList变量里面,前端UI就可以拿到了

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

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第7张图片

6). 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第8张图片

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

A. 根据传递的参数,查询套餐分类列表 (已实现)

B. 根据传递的参数,查询菜品分类列表 (已实现)

C. 图片上传 (已实现)

D. 图片下载展示 (已实现)

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

F. 保存套餐信息

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

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

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

2). 保存套餐信息

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

传递的json格式数据如下:

{
    "name": "儿童套餐",
    "categoryId": "1413342269393674242",
    "price": 100,
    "code": "",
    "image": "9f7994ad-d02c-4259-bf04-3d26c44104d1.jpeg",
    "description": "好吃",
    "dishList": [],
    "status": 1,
    "idType": "1413342269393674242",
    "setmealDishes": [
        {
            "copies": 3,
            "dishId": "1644336736018776066",
            "name": "马铃薯",
            "price": 300
        },
        {
            "copies": 2,
            "dishId": "1644235293140062209",
            "name": "土豆",
            "price": 100
        }
    ]
}

1.5 代码开发

上面我们已经分析了接下来我们需要实现的两个功能,接下来我们就需要根据上述的分析,来完成具体的功能实现。

1.5.1 根据分类查询菜品

1.5.1.1 功能实现

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

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

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

/**
 * 根据条件查询对应的菜品数据 (多么通用的方法啊  自己写就只会写一个根据categoryId查询)
 * @param categoryId
 * @return
 */
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){ // 千万注意是Long不是Integer 封装不上一直报404,找了半天的错误,要死
    log.info("根据id查询所有商品: (舍弃这种写法 根据xx查询 完全可以通用一点啊 直接用实体类接受条件)");
    log.info("根据条件所有商品: dish = {}",dish);

    LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
    // 添加查询条件1:此处根据id查询
    lqw.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
    // 添加查询条件2:启售商品
    lqw.eq(Dish::getStatus,1);
    // 排序条件:(交互性更好)  sort升序,更新时间降序
    lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

    List<Dish> dishList = dishService.list(lqw);

    return R.success(dishList);
}

上面这么写的好处,稍作修改,就可以变成根据该表任何字段,甚至联合字段,进行条件查询,非常得通用!!

trick:(好点子都以trick标记 以后方便ctrl+F查询定位)
自己写就只会写成下面这个熊样,完全没有通用性,以及交互性,这就是差距啊
根据xx查询:想想能否写一个通用的条件查询呢?
查询list:想想排序条件提高交互性

日志多了,需要一边打断点,一边看日志,才方便,最新的日志,就是刚执行的代码的日志,eg:sql语句

@GetMapping("/list1")
public R<List<Dish>> list1(Integer categoryId){ // 千万注意是Long不是Integer 封装不上一直报404,找了半天的错误,要死
    log.info("根据id查询所有商品: categoryId = {}",categoryId);        

    LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
    lqw.eq(Dish::getCategoryId,dish.getCategoryId());  
    
    List<Dish> dishList = dishService.list(lqw);        
    return R.success(dishList);
}
1.5.1.2 功能测试

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第9张图片

1.5.2 保存套餐

1.5.2.1 功能实现

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

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

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

套餐表:setmeal 要保存
中间表:setmeal_dish 也要保存、
保存套餐涉及两张表很好理解,Setmeal实体肯定不够用,所以新建了SetmealDto专门数据传输实体类,里面加上List 也就是中间表的多条记录,然后中间表还可以加上套餐的一些需要展示的字段(冗余),虽然冗余,但是后期查询套餐时,就基本不用管dish菜品表了,直接查套餐表setmeal和中间表setmeal_dish即可。

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

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

    setmealService.saveWithDish(setmealDto);

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

2). SetmealService中定义方法saveWithDish

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

3). SetmealServiceImpl实现方法saveWithDish

具体逻辑:

A. 保存套餐基本信息

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

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

代码实现:

先注入:SetmealDishService

@Autowired
private SetmealDishService setmealDishService;

再分别插入两张表

操作两张表,不要忘记了事务

/**
 * 新增套餐,同时需要保存套餐和菜品的关联关系
 * @param setmealDto
 */
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
    // 打断点或者输出,看看setmealDto, setmealDishes有哪些缺失字段 有就在这里补充啊

    // 先保存套餐基本信息 
    // 没有确实字段
    this.save(setmealDto);//子类可以直接赋值给父类  //此行执行前id=null   此行执行后id=1645440490797514753

    // 再保存套餐关联的菜品信息,也就是保存中间表setmeal_dish多条记录
    List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
    //log.info(setmealDishes.toString());
    // Debug发现: sort和setmealId两个属性为null,但是sort有数据库默认值0  所以给setmealId赋下值就ok了
    // 啥值?当然是setmealDto.getId()啦 所以一定都先save(setmealDto) 才会有id,而且id会回显到setmealDto实体中
    setmealDishes.forEach(setmealDish->setmealDish.setSetmealId(setmealDto.getId()));
    setmealDishService.saveBatch(setmealDishes);
}
1.5.2.2 功能测试

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

录入表单数据:

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第10张图片

debug跟踪数据封装:

在这里插入图片描述
跟踪数据库保存的数据:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第11张图片

在这里插入图片描述

2. 套餐分页查询

2.1 需求分析

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第12张图片

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

2.2 前端页面分析

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

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第13张图片

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

在这里插入图片描述

而对于以上的流程中涉及到2个功能,文件下载功能我们已经实现,本小节我们主要实现列表分页查询功能, 具体的请求信息如下:

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

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 问题分析

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第14张图片

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

2.3.3 功能完善

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

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

完善后代码:

得先在SetmealController里 再注入一个CategoryService

@Autowired
private CategoryService categoryService;
@GetMapping("/page")
// 返回值类型可以不写Page的泛型 不用写Page  写了可读性高一点
public R<Page<SetmealDto>> page(int page,int pageSize,String name){
    log.info("套餐信息分页查询: 页码page={}, 页大小pageSize={}, 模糊条件={}",page,pageSize,name);
    // 分页构造器对象
    Page<Setmeal> pageInfo = new Page<>(page, pageSize);// 父类没办法直接赋值给子类 这里得先写Setmeal,然后再慢慢copy给子类
    Page<SetmealDto> setmealDtoPage = new Page<>();

    // 条件构造器
    LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
    // 模糊查询条件
    if(name!=null) name = name.trim();
    lqw.like(name!=null,Setmeal::getName,name);
    // 排序条件(查询最好都加上,提高用户体验) (没有sort字段 就只按照updateTime排序了)
    lqw.orderByDesc(Setmeal::getUpdateTime); // 注意是Desc 降序排列

    setmealService.page(pageInfo,lqw);

    // 封装SetmealDto
    // 普通属性直接复制
    BeanUtils.copyProperties(pageInfo, setmealDtoPage,"records");//records不能复制 不是想要的类型
    // setmealDishes慢慢转子类
    List<Setmeal> records = pageInfo.getRecords();
    List<SetmealDto> list = records.stream().map((setmeal)->{
        SetmealDto setmealDto = new SetmealDto();
        // 普通属性直接复制
        BeanUtils.copyProperties(setmeal, setmealDto);
        // 没有的属性,也即拓展的两个属性 自己加 查询的时候好像不需要setmealDishes 只需要CategoryName
        Long cid = setmeal.getCategoryId();
        Category category = categoryService.getById(cid);
        if(category!=null) {//判断category会好很多
            setmealDto.setCategoryName(category.getName());
        }
        return setmealDto;
    }).collect(Collectors.toList());
    setmealDtoPage.setRecords(list);

    return R.success(setmealDtoPage);
}

2.4 功能测试

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第15张图片

3. 删除套餐

3.1 需求分析

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第16张图片

3.2 前端页面分析

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

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第17张图片

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第18张图片

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

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

具体的请求信息如下:

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

3.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,并且在控制台也可以输出对应的数据 。

在这里插入图片描述

2). SetmealService接口定义方法removeWithDish

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

3). SetmealServiceImpl中实现方法removeWithDish

该业务层方法具体的逻辑为:

A. 查询该批次套餐中是否存在售卖中的套餐, 如果存在, 不允许删除

B. 删除套餐数据

C. 删除套餐关联的菜品数据

代码实现为:

/**
 * 根据ids批量删除套餐
 * 当然还得删除setmeal_dish中间表所有该套餐的 "套餐-菜品" 关联记录
 * 涉及两张表的delete: setmeal和setmeal_dish
 * @param ids
 */
@Transactional // 涉及多表DML 一定得加事务
public void removeWithDish(List<Long> ids) {
    // 感受: 没有外键 完全用java代码自己维护关联关系 也挺好的 先删setmeal表 再删中间表setmeal_dish 都不会有问题

    // 1. 先查询套餐启售状态,确定是否可以删除
    // List setmeals = this.listByIds(ids); // 我的方法 根据id查询所有商品 再循环判断status
    // 教程方法 直接 select count(*) from setmeal where id in (ids) and status = 1;
    LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
    lqw.in(Setmeal::getId,ids);
    lqw.eq(Setmeal::getStatus,1);
    int count = this.count(lqw);

    // 若不能删除,抛出一个业务异常★
    // 该批套餐不能有一个处于启售状态 (菜品有启售无所谓,又不会删除菜品)
    //for (Setmeal setmeal : setmeals) if(setmeal.getStatus()==1) throw new CustomException("有套餐正在启售中,不能删除");
    if(count>0) throw new CustomException("有套餐正在启售中,不能删除");

    // 2. 若可以删除,先删除中间表关联记录吧 (这样以后加了外键也不会有冲突)
    LambdaQueryWrapper<SetmealDish> sdLqw = new LambdaQueryWrapper<>();
    sdLqw.in(SetmealDish::getSetmealId,ids);//根据套餐id批量删除
    setmealDishService.remove(sdLqw);

    // 3. 再删除套餐本身
    this.removeByIds(ids);//根据主键批量删除

}

由于当前的业务方法中存在多次数据库操作,为了保证事务的完整性,需要在方法上加注解 @Transactional 来控制事务。

4). 完善SetmealController代码

@DeleteMapping
//@RequestParam是告诉MVC这是普通Collection类型,别看到引用类型就当作POJO去封装
// 用List是因为removeByIds()的参数就是Collection, 写数组类型还得自己转一次
public R<String> delete(@RequestParam List<Long> ids){
    log.info("根据id批量删除(单个id就是单删): ids={}",ids);

    setmealService.removeWithDish(ids);//删除套餐,还要删除中间表setmeal_dish的"套餐-菜品"关联记录

    return R.success("删除成功!");

}

3.4 功能测试

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

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第19张图片

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

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

在这里插入图片描述

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第20张图片
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第21张图片

4、更多功能 (没有明确需求,按自己想法做的)

一些功能教程没有做完 自己做着试试

4.1 套餐修改

  • 1)根据setmeal.id回显数据
    SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第22张图片
    SetmealController

    /**
     * 根据id查询套餐信息 包含中间表的关联数据信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<SetmealDto> getById(@PathVariable Long id){
        log.info("根据id查询套餐信息(修改回显需要): id={}",id);
    
        SetmealDto setmealDto = setmealService.getByIdWithSetmealDish(id);
    
        return R.success(setmealDto);
    }
    

    SetmealService 接口

    /**
     * 根据setmeal套餐id,回显记录 (setmeal表)
     * 还涉及套餐关联的“套餐-菜品”中间表信息(setmeal_dish表),需要查两张表
     * @param id
     * @return
     */
    SetmealDto getByIdWithSetmealDish(Long id);
    

    SetmealServiceImpl 实现类

    /**
     * 根据setmeal套餐id,回显记录 (setmeal表)
     * 还涉及套餐关联的“套餐-菜品”中间表信息(setmeal_dish表),需要查两张表
     * @param id
     * @return
     */
    @Override
    public SetmealDto getByIdWithSetmealDish(Long id) {
    
        // 1、根据id到套餐表setmeal查询套餐基本信息
        Setmeal setmeal = this.getById(id);
    
        // 2、根据套餐id到中间表setmeal_dish查询关联记录信息
        LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
        lqw.eq(SetmealDish::getSetmealId,id);
        List<SetmealDish> setmealDishes = setmealDishService.list(lqw);
    
        // 3、最终要返回封装好的SetmealDto, 其中包含setmeal基本信息和 setmealDishes 关联记录
        SetmealDto setmealDto = new SetmealDto();
        // 3.1、Setmeal基本信息直接复制 (父->子 只能复制)
        BeanUtils.copyProperties(setmeal,setmealDto);
        // 3.2、setmealDishes 得单独设置了
        // 直接从数据库里查到的记录 肯定不会有确实字段
        setmealDto.setSetmealDishes(setmealDishes);
    
        return setmealDto;
    }
    

测试如下:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第23张图片

  • 2)提交修改后的新数据,更新

点保存之后捕获请求:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第24张图片
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第25张图片

{
    "id": "1645698209181790209",
    "categoryId": "1413386191767674881",
    "name": "套餐1",
    "price": 100,
    "status": 1,
    "code": "",
    "description": "好吃",
    "image": "310e1f3d-95c2-4b4f-9d27-4954e57e3add.jpg",
    "createTime": "2023-04-11 16:00:03",
    "createUser": "1",
    "updateUser": "1",
    "isDeleted": 0,
    "setmealDishes": [
        {
            "copies": 2,
            "dishId": "1644357343485767681",
            "name": "腊肉",
            "price": 300
        },
        {
            "copies": 1,
            "dishId": "1644336736018776066",
            "name": "马铃薯",
            "price": 300
        }
    ],
    "categoryName": null,
    "idType": "1413386191767674881"
}

显然修改传过来的也是SetmealDto

@Data
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;//套餐关联的菜品集合 (中间表多条记录维护)

    private String categoryName;//分类名称 (辅助字段,查询时少根据categoryId查一次)
}

SetmealController

    /**
     * 根据id更新套餐信息
     * 但是还得更新中间表 该套餐关联的 "套餐-菜品" 记录信息
     * @param setmealDto
     * @return
     */
    @PutMapping
    public R<String> updateById(@RequestBody SetmealDto setmealDto){
        log.info("修改套餐信息:{}",setmealDto);

        setmealService.updateWithSetmealDish(setmealDto);

        return R.success("修改成功");
    }

SetmealService 接口

/**
 * 根据id更新套餐信息
 * 同时还得更新关联的中间表 “套餐-菜品” 记录
 * @param setmealDto
 */
void updateWithSetmealDish(SetmealDto setmealDto);

SetmealServiceImpl 实现类

/**
 * 根据id更新套餐信息
 * 同时还得更新关联的中间表 “套餐-菜品” 记录
 * @param setmealDto
 */
@Transactional // 多表DML 需要 事务
public void updateWithSetmealDish(SetmealDto setmealDto) {
    // 1、先更新套餐基本信息
    this.updateById(setmealDto); // 子类可以直接赋值给父类
    // 打断点发现setmeal没有缺失字段

    // 2、再更新套餐关联的中间表 "套餐-菜品" 一条条记录
    // 复杂逻辑简单化 先清空该套餐在中间表的所有记录
    LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
    lqw.eq(SetmealDish::getSetmealId,setmealDto.getId()); // 打断点 看sql语句 就能判断有没有问题 eq就能多删 不需要in
    setmealDishService.remove(lqw);
    // 再重新插入该套餐的新的所有记录
    List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
    // 打断点看看有没有确实记录,很可能有的:果然除了填充字段和id正常null外,sort和setmealId也为null,
    // sort有默认值0, setmealId必须手动赋值了
    setmealDishes.forEach(setmealDish->setmealDish.setSetmealId(setmealDto.getId()));

    setmealDishService.saveBatch(setmealDishes);
}

测试OK~

4.2 套餐批量启售/停售

单个停售:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第26张图片
批量停售:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第27张图片

/**
 * 根据ids批量修改状态 启售/停售
 * @param status
 * @param ids
 * @return
 */
@PostMapping("/status/{status}")
public R<String> updateByIds(@PathVariable int status,@RequestParam List<Long> ids) {

    Setmeal setmeal = new Setmeal();
    setmeal.setStatus(status);
    LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
    lqw.in(Setmeal::getId,ids);//根据ids批量修改状态 启售/停售
    setmealService.update(setmeal,lqw);

    return R.success("批量起售/停售成功!");
}

测试OK~


开始移动端开发


5. 短信发送

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第28张图片

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

5.1 短信服务介绍

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

常用短信服务:

  • 阿里云

  • 华为云

  • 腾讯云

  • 京东

  • 梦网

  • 乐信

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

5.2 阿里云短信服务介绍

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

应用场景:

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第29张图片

阿里云短信服务官方网站: https://www.aliyun.com/product/sms

可以访问官网,熟悉一下短信服务:
在这里插入图片描述

5.3 阿里云短信服务准备

5.3.1 注册账号

阿里云官网:https://www.aliyun.com/
有账号直接登录,无账号,注册一个

5.3.2 开通短信服务

注册成功后,点击登录按钮进行登录。登录后进入控制台, 在左上角的菜单栏中搜索短信服务。第一次使用,需要点击,并开通短信服务。

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第30张图片
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第31张图片
在这里插入图片描述

新用户可以个人免费领取短信服务
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第32张图片
自己摸索一下,跟着提示来申请一些没有的东西即可

5.3.3 设置短信签名

开通短信服务之后,进入短信服务管理页面,选择国内消息菜单,我们需要在这里添加短信签名。

那么什么是短信签名呢?

短信签名是短信发送者的署名,表示发送方的身份。我们要调用阿里云短信服务发送短信,签名是比不可少的部分。

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第33张图片

那么接下来,我们就需要来添加短信签名。

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第34张图片

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第35张图片

注意:

目前,阿里云短信服务申请签名主要针对企业开发,个人申请时有一定难度的,在审核时,会审核资质,需要上传营业执照 ;

所以,我们课程中,主要是演示一下短信验证码如何发送,大家只需要学习这块儿的开发流程、实现方式即可,无需真正的发送短信。如果以后在企业中做项目,需要发送短信,我们会以公司的资质去申请对应的签名。


但是其实申请个人免费的短信服务,也是可以的,学习测试使用嘛,耐心等待即可

5.3.4 设置短信模板

切换到【模板管理】标签页:

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第36张图片

那么什么是模板呢?

短信模板包含短信发送内容、场景、变量信息。模板的详情如下:

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第37张图片

最终我们,给用户发送的短信中,具体的短信内容,就是上面配置的这个模板内容,将${code}占位符替换成对应的验证码数据即可。如下:

【xxxxx】您好,您的验证码为173822,请勿泄露于他人!

送的模版本身没有签名
还无法匹配自定义签名
所以还是自定义一个模版吧

所以还是自定义一个模版吧
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第38张图片

等待签名审核通过,我们可以点击左上角的按钮,添加模板,然后填写模板的基本信息及设置的模板内容:

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第39张图片

添加的短信模板,也是需要进行审核的只有审核通过,才可以正常使用。

有了签名之后可以申请自己的模版啦,啥都用自己申请的,送的真不好用。

等待审核通过,就申请成功

在这里插入图片描述
然后点击详情,就有该模板需要的code和签名信息了
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第40张图片

5.3.5 设置AccessKey

AccessKey 是访问阿里云 API 的密钥,具有账户的完全权限,我们要想在后面通过API调用阿里云短信服务的接口发送短信,那么就必须要设置AccessKey。

我们点击右上角的用户头像,选择"AccessKey管理",这时就可以进入到AccessKey的管理界面。

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第41张图片
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第42张图片

进入到AccessKey的管理界面之后,提示两个选项 “继续使用AccessKey” 和 “开始使用子用户AccessKey”,两个区别如下:

1). 继续使用AccessKey

如果选择的是该选项,我们创建的是阿里云账号的AccessKey,是具有账户的完全权限,有了这个AccessKey以后,我们就可以通过API调用阿里云的服务,不仅是短信服务,其他服务(OSS,语音服务,内容安全服务,视频点播服务…等)也可以调用。 相对来说,并不安全,当前的AccessKey泄露,会影响到我当前账户的其他云服务。

2). 开始使用子用户AccessKey

可以创建一个子用户,这个子用户我们可以分配比较低的权限,比如仅分配短信发送的权限,不具备操作其他的服务的权限,即使这个AccessKey泄漏了,也不会影响其他的云服务, 相对安全。

接下来就来演示一下,如何创建子用户AccessKey。

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第43张图片
填写基本信息:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第44张图片

创建成功如下:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第45张图片

Secret要记住,后面好像查不到了,只得删除重新创建.(所以立刻复制备份)

5.3.6 配置权限

上述我们已经创建了子用户, 但是这个子用户,目前没有任何权限,接下来,我们需要为创建的这个用户来分配权限。

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第46张图片
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第47张图片
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第48张图片

经过上述的权限配置之后,那么新创建的这个 reggie 用户,仅有短信服务操作的权限,不具备别的权限,即使当前的AccessKey泄漏了,也只会影响短信服务,其他服务是不受影响的。

5.3.7 禁用/删除AccessKey

如果在使用的过程中 AccessKey 不小心泄漏了,我们可以在阿里云控制台中, 禁用或者删除该AccessKey。

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第49张图片

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第50张图片

然后再创建一个新的AccessKey, 保存好AccessKeyId和AccessKeySecret。

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第51张图片

注意: 创建好了AccessKey后,请及时保存AccessKeyId 和 AccessKeySecret ,弹窗关闭后将无法再次获取该信息,但您可以随时创建新的 AccessKey。

5.4 代码开发

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

官方文档: https://help.aliyun.com/product/44282.html?spm=5176.12212571.help.dexternal.57a91cbewHHjKq

官方快速入门

SDK参考->原版SDK->Java SDK
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第52张图片

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

先导入下面具体实现的依赖,然后用人家官网提供的代码测试一下

注意:
1、测试签名(模版)的只能发短信给申请测试签名时输入的手机号
2、提前给自己阿里云账户里充点钱,否则会报:余额不足
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第53张图片

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第54张图片

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.exceptions.ServerException;
import com.aliyuncs.profile.DefaultProfile;
import com.google.gson.Gson;

public class SendSmsTest {

    public static void main(String[] args) {
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "L..填自己创建用户时的ID ..u", "l..填自己创建用户时的secret..c");
        IAcsClient client = new DefaultAcsClient(profile);

        SendSmsRequest request = new SendSmsRequest();
        request.setPhoneNumbers("17..填自己申请测试签名时的手机号..8");//接收短信的手机号码
        request.setSignName("自己申请的测试签名");//短信签名名称  赠送的似乎没有签名
        request.setTemplateCode("SMS_..填自己的模版Code..");//短信模板CODE
        request.setTemplateParam("{\"code\":\"1998\"}");//短信模板变量对应的实际值  注意得是json格式
        // 得是这种json格式:  {"code":"1998"}

        try {
            SendSmsResponse response = client.getAcsResponse(request);
            System.out.println(new Gson().toJson(response));
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            System.out.println("ErrCode:" + e.getErrCode());
            System.out.println("ErrMsg:" + e.getErrMsg());
            System.out.println("RequestId:" + e.getRequestId());
        }

    }
}

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第55张图片
成功收到,下面将这个demo封装成工具类就OK啦

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

具体实现:

1). pom.xml

<dependency>
    <groupId>com.aliyungroupId>
    <artifactId>aliyun-java-sdk-coreartifactId>
    <version>4.5.16version>
dependency>
<dependency>
    <groupId>com.aliyungroupId>
    <artifactId>aliyun-java-sdk-dysmsapiartifactId>
    <version>2.1.0version>
dependency>

2). 将官方提供的main方法,稍作修改,封装为一个工具类

一个验证码的发送方法
一个验证码失效方法,也即隔指定时间在session中删除该验证码

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 lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpSession;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 短信发送工具类
 */
@Slf4j
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", "LTAI5tCe6mHKiLJJs7YK4VFu", "loz8jOqHorAl2FJQy5KBPdaCd4cE0c");
		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("短信发送成功:"+response.getMessage());
			System.out.println(response.getCode());
		}catch (ClientException e) {
			log.info("{} 的短信发送失败",phoneNumbers);
			e.printStackTrace();
		}
	}

	/**
	 * 设置session里字段值的失效时间
	 * @param t 失效时间 单位 秒
	 */
	public static void deadTime(Integer ts, String phone, HttpSession session){
		try {
			final Timer timer = new Timer();
			timer.schedule(new TimerTask() {
				@Override
				public void run() {
					session.removeAttribute(phone);
					log.info("{} 的手机验证码已经失效, 因为有效时间到了",phone);
					timer.cancel();
				}
			},ts*1000);//ts以秒为单位
		} catch (Exception e) {
			log.error("销毁session里的验证码时出错了~");
			e.printStackTrace();
		}
	}

}

然后也测试一下工具类:

@Test
public void testSMSUtils(){
    String phone = "自己的号码";
    String code = "2002"; // 要发送的验证码
    SMSUtils.sendMessage("自己的签名","自己的模版code",phone,code);
}

手机也能成功收到,OK啦~

好好注册,可以跑通

6. 手机验证码登录

6.1 需求分析

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

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

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

3). 安全

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第56张图片

登录流程:

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

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

6.2 数据模型

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第57张图片
没有密码字段,只有手机号,就OK了~

6.3 前端页面分析

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

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

http://localhost:8080/front/page/login.html

由于是H5开发的,会自动适应屏幕大小,电脑浏览器访问就要调一下页面
按 F12-》 先修改宽度 -》 再切换到手机模式

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第58张图片
提供的代码有问题,之前在前端就生成了验证码,需要修改为后端动态获取

之前的文件有问题,就没有这个js方法
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第59张图片
front/api/login.js里加上一个方法
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第60张图片

getCode(){
    this.form.code = ''
    const regex = /^(13[0-9]{9})|(15[0-9]{9})|(17[0-9]{9})|(18[0-9]{9})|(19[0-9]{9})$/;
    if (regex.test(this.form.phone)) {
        this.msgFlag = false
        //this.form.code = (Math.random()*1000000).toFixed(0)
        sendMsgApi({phone : this.form.phone});
    }else{
        this.msgFlag = true
    }
},
function sendMsgApi(data){
    return $axios({
        'url':'/user/sendMsg',
        'method':'post',
        data
    })
}

前端流程分析:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第61张图片

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第62张图片
前端代码还是有问题,将

const res = await loginApi({phone:this.form.phone})

改成:

const res = await loginApi(this.form) // 直接传整个表单就行了 前前缀phone:和{}也去掉

整个表单都要提交过去,不仅仅只是提交一个手机号 (表单里还有验证码呢)
(我怀疑提供资料的人这块就跳过了,没有真的做阿里云验证码获取. 或者人家做了,也不会给你他的短信接口的,所以故意把页面改成了这样)

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

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

其实浏览器端F12捕获,最为快捷,前提是人家的前端页面已经写好了:

1). 获取短信验证码

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

2). 登录

请求 说明
请求方式 POST
请求路径 /user/login
请求参数 {“phone”:“13100001111”, “code”:“1111”}
  • 点击获取验证码
    SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第63张图片
    SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第64张图片
  • 点击登陆
    SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第65张图片
    SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第66张图片

错误示例:
在这里插入图片描述

6.4 代码开发

6.4.1 准备工作

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

用代码生成器生成即可:

package cn.whu.reggie.utils;

import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;

public class CodeGenerator2 {
    public static void main(String[] args) {
        //1. 创建代码生成器对象,执行生成代码操作
        AutoGenerator autoGenerator = new AutoGenerator();

        // 中间三大配置 让他一步步生成我们想要的代码样式
        //设置全局配置
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setOutputDir(System.getProperty("user.dir") + "/src/main/java");    //设置代码生成位置 不是项目下的module 不需要写项目名了
        globalConfig.setOpen(false);    //设置生成完毕后是否打开生成代码所在的目录
        globalConfig.setAuthor("whu");    //设置作者
        globalConfig.setFileOverride(false);     //设置是否覆盖原始生成的文件   不能覆盖 万一按错了 就悲剧了
        autoGenerator.setGlobalConfig(globalConfig);

        //设置包名相关配置
        PackageConfig packageInfo = new PackageConfig();
        packageInfo.setParent("cn.whu.reggie");   //设置生成的包名,与代码所在位置不冲突,二者叠加组成完整路径
        autoGenerator.setPackageInfo(packageInfo);

        //策略设置
        StrategyConfig strategyConfig = new StrategyConfig();
        strategyConfig.setInclude("user");  //设置当前参与生成的表名,参数为可变参数 // 不写就是生成所有表
        strategyConfig.setRestControllerStyle(true);    //设置是否启用Rest风格

        strategyConfig.setEntityLombokModel(true);  //设置是否启用lombok 实体类就自动用lombok了
        autoGenerator.setStrategy(strategyConfig);


        //2. 数据源相关配置:读取数据库中的信息,根据数据库表结构生成代码
        DataSourceConfig dataSource = new DataSourceConfig();
        dataSource.setDriverName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        dataSource.setUsername("root");
        dataSource.setPassword("1234");
        autoGenerator.setDataSource(dataSource);

        //3. 执行生成操作
        autoGenerator.execute();
    }
}

1). 实体类 User

所属包: cn.whu.reggie.entity

修改一下idNumber的驼峰命名即可

@Data
@EqualsAndHashCode(callSuper = false)
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 性别
     */
    private String sex;

    /**
     * 身份证号
     */
    private String idNumber;

    /**
     * 头像
     */
    private String avatar;

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

2). Mapper接口 UserMapper

所属包: cn.whu.reggie.mapper

F2重命名: IUserMapper -> UserMapper

@Mapper
public interface UserMapper extends BaseMapper<User> {

}

3). 业务层接口 UserService

所属包: cn.whu.reggie.service

public interface UserService extends IService<User> {

}

4). 业务层实现类 UserServiceImpl

所属包: cn.whu.reggie.service.impl

无需修改

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

}

5). 控制层 UserController

所属包: cn.whu.reggie.controller

注入一下UserService

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

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

所属包: cn.whu.reggie.utils

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第67张图片

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

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

SMSUtils

package cn.whu.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 lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpSession;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 短信发送工具类
 */
@Slf4j
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", "LTAI5tCe6mHKiLJJs7YK4VFu", "loz8jOqHorAl2FJQy5KBPdaCd4cE0c");
		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("短信发送成功:"+response.getMessage());
			System.out.println(response.getCode());
		}catch (ClientException e) {
			log.info(phoneNumbers+"的短信发送失败");
			e.printStackTrace();
		}
	}

	/**
	 * 设置session里字段值的失效时间
	 * @param t 失效时间 单位 秒
	 */
	public static void deadTime(Integer ts, String phone, HttpSession session){
		try {
			final Timer timer = new Timer();
			timer.schedule(new TimerTask() {
				@Override
				public void run() {
					session.removeAttribute(phone);
					log.info("{} 的手机验证码已经失效");
					timer.cancel();
				}
			},ts);
		} catch (Exception e) {
			log.error("销毁session里的验证码时出错了~");
			e.printStackTrace();
		}
	}

}


ValidateCodeUtils

package cn.whu.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;
    }
}

6.4.2 功能实现

6.4.2.1 修改LoginCheckFilter

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第68张图片

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

增加如下逻辑:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第69张图片

cn.whu.reggie.filter.LoginCheckFilter 里加上如下代码

Long userId = (Long) request.getSession().getAttribute("userId");
//D-2. 判断移动端登录状态,如果已登录,则直接放行
if(userId!=null){
    log.info("用户已经登录,直接放行,用户id为{}",userId);
    // 用户id绑定到当前线程 (MyMetaObjectHandler里公共字段自动填充用)
    BaseContext.setCurrentId(userId);
    chain.doFilter(request,response);//已经登录上了 直接放行
    return;
}

注意后面移动端用户登录成功,其session中的key得取名叫做"userId"

6.4.2.2 发送短信验证码

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

/**
 * 发送手机短信验证码
 * @param user
 * @return
 */
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){ //参数里直接能注入session 好
    log.info("获取短信验证码: 手机号为:{}",user.getPhone());

    // 1. 获取手机号
    String phone = user.getPhone();
    if(phone == null||phone.trim().length()==0) return R.error("手机号不正确,短信发送失败");

    // 2. 生成随机4位验证码
    String code = String.valueOf(ValidateCodeUtils.generateValidateCode(4));
    log.info("code: {}",code);

    // 3. 调用阿里云提供的短信服务API完成短信发送
    SMSUtils.sendMessage("填自己的签名","填自己的模板code",phone,code);//测试阶段其实可以注释这一,不必真的发送,不过0.045也即是4.5分钱一条,也不贵就是了
    // 工具类里已经有异常处理了 也会打印反馈消息

    // 4. 将生成的验证码保存到Session
    session.setAttribute(phone,code); // 直接手机号phone作为key, 验证码code作为值

    // 5. 设置session失效时间 单位为秒
    SMSUtils.deadTime(5*60,phone,session);

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

}

然后可以将失效时间设置为10s,再写一个单独的controller获取code,看看过期前能否取到,过期后是否是null

/**
 * 获取session里的验证码
 * 测试短信验证码失效时间用
 * @return
 */
// 注意拦截器里给本方法 放行
@GetMapping("/sessionCode")
@ResponseBody
public String getSessionCode(HttpSession session) {
    String code = (String) session.getAttribute("13112345678");
    log.info("code: {}",code);
    return "code: "+code;
    // 测试没有问题,过期确实失效 新线程法真好用
}

测试:
1)先访问登陆页面请求发送验证码
http://localhost:8080/front/page/login.html
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第70张图片
2)10s内访问获取验证码的测试controller
http://localhost:8080/user/sessionCode
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第71张图片

稍等片刻,控制台打印日志。(注意测试时,超时时间设置为10s就ok了)
在这里插入图片描述
再访问测试controller
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第72张图片
符合预期,ok啦

6.4.2.3 验证码登录 (Map接收无POJO对应的JSON)

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

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

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

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

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

5). 将登录用户的ID存储Session中

具体代码实现:

前端传过来的是json格式,但是却没有对呀的pojo (phone+code),要么自己自定义UserDto
这里采用一种简单的方式: 直接@RequestBody Map map 接受json格式数据,直接封装为map

/**
 * 移动端用户登录
 * 手机号+短信验证码 登陆
 * @param map
 * @param session
 * @return
 */
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session){
    log.info("验证码登陆: map接收任意格式的json, map={}",map);

    //1). 获取前端传递的手机号和验证码
    String phone = (String) map.get("phone"); //.toString() 为空null报异常  不过你非法访问,报异常也很好
    String code = (String) map.get("code");
    log.info("手机号:{} 验证码:{}",phone,code);

    //2). 从Session中获取到手机号对应的正确的验证码
    String sessionCode = (String) session.getAttribute(phone); // 这么写即使获取null也不会报错 .toString会报错的

    //3). 进行验证码的比对 , 如果比对失败, 直接返回错误信息
    if(sessionCode==null) return R.error("验证码已失效或未生成验证码");
    if(StringUtils.isNotEmpty(code)&&!code.equals(sessionCode)) return R.error("验证码错误");

    //4). 如果比对成功, 需要根据手机号查询当前用户, 如果用户不存在, 则自动注册一个新用户
    LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
    lqw.eq(User::getPhone,phone);
    User user = userService.getOne(lqw);
    if(user==null){
        log.info("手机号 {} 暂无账号,自动注册",phone);
        user = new User();
        user.setPhone(phone); // 真的就设置一个值就OK了 其他值要么默认值,要么等用户登陆后自己修改设置
        user.setStatus(1);//数据库 Status 默认值竟然是0
        userService.save(user); // 插入后就会有id回显的(显然是save方法帮你设置的值)
    }

    //5). 将登录用户的ID存储Session中 //否则拦截器那关过不了哦
    session.setAttribute("userId",user.getId());

    // 登陆成功返回user对象 浏览器端也要展示用户名等信息嘛
    return R.success(user);
}

6.5 功能测试

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

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第73张图片

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

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第74张图片

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

在这里插入图片描述

前端保存user信息的代码也改了一下:
SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第75张图片

sessionStorage.setItem("user",JSON.stringify(res.data))

之前仅仅保存了一个手机号码

登陆成功可以到浏览器端查看

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第76张图片

7、退出登陆

SSM实战-外卖项目-05- 新增套餐(多表DML,事务,DTO)、分页查询(多表,DTO)、批量删除(先停售,多表delete(中间表))、修改(多表回显)、批量启售/停售。【短信发送、短信登陆】_第77张图片
UserController

@PostMapping("/loginout")
public R<String> loginout(HttpSession session){
    log.info("移动端退出登陆");
    // 从session中移除当前用户id
    session.removeAttribute("userId");
    return R.success("已退出登陆");
}

总结:

SSM实战-外卖项目-05-新增套餐(涉及2张表DML,需要事务,设计DTO,分类菜品等多表查询)、分页查询(多表,DTO)、批量删除套餐(停售才可删除,多表delete,中间表’套餐-菜品’关联记录也要)、套餐修改(多表数据回显)、套餐批量启售/停售(业务需求没说,得提前协商好)

套餐的保存和删除,涉及两张表的DML, 业务逻辑复杂,代码操作也复杂,但是提前写好需求文档,整理好思路,也很简单,多敲敲这种多表操作!

开始移动端开发:短信发送(阿里云一步步申请测试接口,可以的,就是审核有点慢,签名和模板都正式一点,否则审核不过)、封装短信发送工具类、多线程销毁验证码、 手机验证码登录

短信接口申请,不难,但是很烦,主要阿里那边审核一次要1~2个小时,审核不过又得重新提交申请,又得等,所以耐心一点,都能做到

你可能感兴趣的:(#,ssm实战,数据库,java,android)