当前位置: 首页 > news >正文

【SpringBoot项目实战+思维导图】瑞吉外卖⑤(新增套餐、套餐分页查询、删除套餐、短信发送、手机验证码登录)

文章目录

  • 新增套餐
    • 需求分析
    • 数据模型
    • 准备工作
    • 前端页面分析
    • 代码开发
      • 根据分类查询菜品
        • 功能实现
        • 功能测试
      • 保存套餐
        • 功能实现
        • 功能测试
    • 思维导图总结
  • 套餐分页查询
    • 需求分析
    • 前端页面分析
    • 代码开发
      • 基本信息查询
      • 问题分析
      • 功能完善
    • 功能测试
    • 思维导图总结
  • 删除套餐
    • 需求分析
    • 前端页面分析
    • 代码开发
    • 功能测试
    • 思维导图总结
  • 短信发送
    • 短信服务介绍
    • 阿里云短信服务介绍
    • 阿里云短信服务准备
      • 注册账号
      • 开通短信服务
      • 设置短信签名
      • 设置短信模板
      • 设置AccessKey
      • 配置权限
      • 禁用/删除AccessKey
    • 代码开发
  • 手机验证码登录
    • 需求分析
    • 数据模型
    • 前端页面分析
    • 代码开发
      • 准备工作
      • 功能实现
        • 修改LoginCheckFilter
        • 发送短信验证码
        • 验证码登录
    • 功能测试
    • 资料包代码纠错
    • 思维导图总结

新增套餐

需求分析

套餐就是菜品的集合。

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

在这里插入图片描述

数据模型

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

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

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

1). 套餐表setmeal

在这里插入图片描述

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

在这里插入图片描述

2). 套餐菜品关系表setmeal_dish

在这里插入图片描述

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

准备工作

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

1). 实体类 SetmealDish

所属包: com.itheima.reggie.entity

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;//套餐idprivate Long setmealId;//菜品idprivate 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

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

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.SetmealDish;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}

4). 业务层接口 SetmealDishService

所属包: com.itheima.reggie.service

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.SetmealDish;public interface SetmealDishService extends IService<SetmealDish> {
}

5). 业务层实现类 SetmealDishServiceImpl

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

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.SetmealDish;
import com.itheima.reggie.mapper.SetmealDishMapper;
import com.itheima.reggie.service.SetmealDishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;@Service
@Slf4j
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper,SetmealDish> implements SetmealDishService {
}

6). 控制层 SetmealController

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

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

import com.itheima.reggie.service.SetmealDishService;
import com.itheima.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;/*** 套餐管理*/
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {@Autowiredprivate SetmealService setmealService;@Autowiredprivate SetmealDishService setmealDishService;
}    

前端页面分析

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

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格式数据如下:

{"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}]
}

代码开发

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

根据分类查询菜品

功能实现

在当前的需求中,我们只需要根据页面传递的菜品分类的ID(categoryId)来查询菜品列表即可,我们可以直接定义一个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断点跟踪的形式查看页面传递的参数封装情况,及响应给页面的数据信息。

在这里插入图片描述

保存套餐

功能实现

在进行套餐信息保存时,前端提交的数据,不仅包含套餐的基本信息,还包含套餐关联的菜品列表数据 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("新增套餐成功");
}

2). SetmealService中定义方法saveWithDish

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

3). SetmealServiceImpl实现方法saveWithDish

具体逻辑:

A. 保存套餐基本信息

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

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

代码实现:

/*** 新增套餐,同时需要保存套餐和菜品的关联关系* @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);
}

功能测试

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

录入表单数据:

在这里插入图片描述

debug跟踪数据封装:

在这里插入图片描述

跟踪数据库保存的数据:

在这里插入图片描述

思维导图总结

在这里插入图片描述

套餐分页查询

需求分析

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

在这里插入图片描述

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

前端页面分析

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

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

在这里插入图片描述

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

在这里插入图片描述

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

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

代码开发

基本信息查询

上述我们已经分析列表分页查询功能的请求信息,接下来我们就在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);
}

问题分析

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

在这里插入图片描述

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

功能完善

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

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

完善后代码:

/**
* 套餐分页查询
* @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);Page<SetmealDto> dtoPage = new Page<>();LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();//添加查询条件,根据name进行like模糊查询queryWrapper.like(name != null,Setmeal::getName,name);//添加排序条件,根据更新时间降序排列queryWrapper.orderByDesc(Setmeal::getUpdateTime);setmealService.page(pageInfo,queryWrapper);//对象拷贝BeanUtils.copyProperties(pageInfo,dtoPage,"records");List<Setmeal> records = pageInfo.getRecords();List<SetmealDto> list = records.stream().map((item) -> {SetmealDto setmealDto = new SetmealDto();//对象拷贝BeanUtils.copyProperties(item,setmealDto);//分类idLong categoryId = item.getCategoryId();//根据分类id查询分类对象Category category = categoryService.getById(categoryId);if(category != null){//分类名称String categoryName = category.getName();setmealDto.setCategoryName(categoryName);}return setmealDto;}).collect(Collectors.toList());dtoPage.setRecords(list);return R.success(dtoPage);
}

这里的基本思路就是:

分别构造Setmeal和SetmealDto的分页对象,然后使用条件构造器对Setmeal的分页对象中的数据进行过滤。接着将pageInfo除了records属性全部拷贝到dtoPage中。然后我们将pageInfo中的records列表拿出来进行流处理:将列表中的每一个元素的属性拷贝给setmealDto,然后将setmealDto的CategoryId拿出来,通过categoryService分类的服务层进行查询,得到setmealDto对应的分类名称。拿到名称之后,我们将名称赋值给CategoryName。接着我们进行流的收集,收集成一个SetmealDto的列表,我们将此列表装入SetmealDto分页对象中,最后返回此分页对象。

为什么要创建两个分页对象?
每一个分页对象都对应着分页内容(也就是泛型对应着的那张表),而我们是没有SetmealDto这张表的也就是说我们dtoPage分页对象中是个空的。我们只有借助pageInfo分页对象中的数据,再进行接下来的操作我们才能达到目的。

功能测试

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

在这里插入图片描述

思维导图总结

在这里插入图片描述

删除套餐

需求分析

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

在这里插入图片描述

前端页面分析

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

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

在这里插入图片描述

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

在这里插入图片描述

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

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

具体的请求信息如下:

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

代码开发

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

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. 删除套餐关联的菜品数据

代码实现为:

/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
@Transactional
public void removeWithDish(List<Long> ids) {//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("套餐正在售卖中,不能删除");}//如果可以删除,先删除套餐表中的数据---setmealthis.removeByIds(ids);//delete from setmeal_dish where setmeal_id in (1,2,3)LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);//删除关系表中的数据----setmeal_dishsetmealDishService.remove(lambdaQueryWrapper);
}

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

上面这段代码的基本逻辑就是:

先将拿到的id列表在数据库中进行查询,如果查询正在售卖的数量大于0,则抛出异常。如果不存在删除的套餐中有售卖的情况,则直接根据id列表进行删除。然后再根据套餐id将setmealDish中的套餐关联菜品进行删除。

代码注意点

queryWrapper.in(Setmeal::getId,ids);代表元素的id存在于列表ids中:
在这里插入图片描述

int count = this.count(queryWrapper);:根据 Wrapper 条件,查询总记录数

在这里插入图片描述

this.removeByIds(ids);:根据ID 批量删除

在这里插入图片描述

4). 完善SetmealController代码

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

功能测试

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

思维导图总结

在这里插入图片描述

短信发送

在这里插入图片描述

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

短信服务介绍

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

常用短信服务:

  • 阿里云

  • 华为云

  • 腾讯云

  • 京东

  • 梦网

  • 乐信

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

阿里云短信服务介绍

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

应用场景:

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

在这里插入图片描述

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

可以访问官网,熟悉一下短信服务:

在这里插入图片描述

阿里云短信服务准备

注册账号

阿里云官网:https://www.aliyun.com/

在这里插入图片描述

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

开通短信服务

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

在这里插入图片描述
在这里插入图片描述

设置短信签名

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

在这里插入图片描述

那么什么是短信签名呢?

短信签名是短信发送者的署名,表示发送方的身份。我们要调用阿里云短信服务发送短信,签名是比不可少的部分。
在这里插入图片描述
那么接下来,我们就需要来添加短信签名。

在这里插入图片描述

注意:

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

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

设置短信模板

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

在这里插入图片描述

那么什么是模板呢?

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

在这里插入图片描述

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

【xxxxx】您好,您的验证码为173822,5分钟之内有效,不要泄露给他人!

我们可以点击右上角的按钮,添加模板,然后填写模板的基本信息及设置的模板内容:

在这里插入图片描述

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

设置AccessKey

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

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

在这里插入图片描述

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

1). 继续使用AccessKey

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

2). 开始使用子用户AccessKey

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

接下来就来演示一下,如何创建子用户AccessKey。
在这里插入图片描述

配置权限

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

在这里插入图片描述

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

禁用/删除AccessKey

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

在这里插入图片描述

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

在这里插入图片描述

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

代码开发

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

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

在这里插入图片描述

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

在这里插入图片描述

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

具体实现:

1). pom.xml

<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方法封装为一个工具类

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

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

手机验证码登录

需求分析

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

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

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

3). 安全

在这里插入图片描述

登录流程:

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

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

数据模型

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

在这里插入图片描述

前端页面分析

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

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

代码开发

准备工作

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

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

所属包: com.itheima.reggie.entity

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

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

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

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

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 {@Autowiredprivate UserService userService;
}

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

所属包: com.itheima.reggie.utils

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

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

功能实现

修改LoginCheckFilter

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

在这里插入图片描述

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

如果移动端的用户进行了登录那么对应的session里面肯定是有东西的

增加如下逻辑:

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,方便后续登录时进行比对。

/*** 发送手机短信验证码* @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);//需要将生成的验证码保存到Sessionsession.setAttribute(phone,code);return R.success("手机验证码短信发送成功");}return R.error("短信发送失败");
}

备注:

这里发送短信我们只需要调用封装的工具类中的方法即可,我们这个功能流程跑通,在测试中我们不用真正的发送短信,只需要将验证码信息,通过日志输出,登录时,我们直接从控制台就可以看到生成的验证码(实际上也就是发送到我们手机上的验证码)

验证码登录

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

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

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

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

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

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

具体代码实现:

/*** 移动端用户登录* @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("登录失败");
}

个人觉得这里文档中的代码有误,如果是新用户的话这里session.setAttribute("user",user.getId());中的user.getId()是拿不到id的,所以我们要再查一遍。我的代码如下:

    /*** 用户登陆请求方法* @param map* @param request* @return*/@PostMapping("login")public R<String> login(@RequestBody Map<String,String> map,HttpServletRequest request){//获得用户的手机号码String phone = map.get("phone");//获得用户提供的验证码(String)String userCode = map.get("code");//获得系统提供的验证码(Integer)Integer systemCode = (Integer) request.getSession().getAttribute(phone);//将验证码进行比对if (userCode != null && userCode.equals(systemCode.toString())) {//比对验证码成功,说明用户登录成功//接下来查询用户是否是新用户//构建Lambda条件构造器LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();//构造id条件userLambdaQueryWrapper.eq(User::getPhone,phone);//调用User的Service接口查询该UserUser user = userService.getOne(userLambdaQueryWrapper);//判断是否是新用户if (user == null){//说明是新用户User newUser = new User();//设置电话号码newUser.setPhone(phone);//设置账号状态newUser.setStatus(1);//将该User存到数据库中userService.save(newUser);}//此处重查一遍数据库,确保新用户的id也能获取到user = userService.getOne(userLambdaQueryWrapper);//在session中存储用户idrequest.getSession().setAttribute("user",user.getId());//返回成功结果return R.success("登陆成功");}//返回失败结果return R.error("登陆失败");}

功能测试

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

资料包代码纠错

这里资料包里的前端代码似乎没有写完,我们点击获取验证码之后前端自己随机生成了验证码,没有经过后端。

我们这样修改:

在login.js文件中添加如下方法:

function sendMsgApi(data) {return $axios({'url': '/user/sendMsg','method': 'post',data})
}

然后login.html也要做出如下修改:
在这里插入图片描述

然后就可以正常运行了

思维导图总结

在这里插入图片描述

相关文章:

【SpringBoot项目实战+思维导图】瑞吉外卖⑤(新增套餐、套餐分页查询、删除套餐、短信发送、手机验证码登录)

文章目录新增套餐需求分析数据模型准备工作前端页面分析代码开发根据分类查询菜品功能实现功能测试保存套餐功能实现功能测试思维导图总结套餐分页查询需求分析前端页面分析代码开发基本信息查询问题分析功能完善功能测试思维导图总结删除套餐需求分析前端页面分析代码开发功能…...

OpenAI 发布GPT-4——全网抢先体验

OpenAI 发布GPT-4 最近 OpenAI 犹如开挂一般&#xff0c;上周才刚刚推出GPT-3.5-Turbo API&#xff0c;今天凌晨再次祭出GPT-4这个目前最先进的多模态预训练大模型。与上一代GPT3.5相比&#xff0c;GPT-4最大的飞跃是增加了识图能力&#xff0c;并且回答准确性也得到显著提高。…...

C++——多态

多态分为两类静态多态&#xff1a;函数重载和运算符重载属于静态多态&#xff0c;复用函数名动态多态&#xff1a;派生类和虚函数实现运行时多态静态多态和动态多态的区别&#xff1a;静态多态的函数地址早绑定——编译阶段确定函数地址动态多态的函数地址晚绑定——运行阶段确…...

javaSE系列之类与对象

javaSE系列之类与方法什么是类类的定义书写事项什么是实例化this引用this的注意事项对象的初始化构造方法封装的概念访问限定符封装扩展之包static成员static的特性static的初始化代码块注意事项内部类1.实例内部类&#x1f497; &#x1f497; 博客:小怡同学&#x1f497; &am…...

远程构建(命令、脚本构建)jenkins

在对应项目&#xff0c;开启远程构建开关添加API token系统设置调整用户权限获取crumbcurl调用构建 1、进入对应项目的设置页面&#xff1a;开启远程构建开关 2、 添加 API token&#xff1a;进入对应用户的设置页面 3、系统设置调整权限&#xff0c;如图 4、由于jenkins的安全…...

2023-03-15 ElasticSearch

ElasticSearch 1.Docker安装ElasticSearch 1.1. es及kibana下载 docker pull elasticsearch:7.4.2 docker pull kibana:7.4.2创建映射文件: mkdir -p /elasticsearch/configmkdir -p /elasticsearch/datamkdir -p /elasticsearch/plugins在config下执行 vim elasticsearch…...

指针和数组笔试题解析【下篇】

文章目录&#x1f441;️6.指针笔试题&#x1f440;6.1.试题&#xff08;1&#xff09;&#x1f440;6.2.试题&#xff08;2&#xff09;&#x1f440;6.3.试题&#xff08;3&#xff09;&#x1f440;6.4.试题&#xff08;4&#xff09;&#x1f440;6.5.试题&#xff08;5&am…...

DHCP原理简析及交互实践

环境&#xff1a; os&#xff1a;centos7 dnsmasq&#xff1a;version 2.76 一. dhcp工作原理 首先补充几个dhcp相关的基本概念&#xff1a; 1、动态主机配置协议DHCP&#xff08;Dynamic Host Configuration Protocol&#xff09;是一种网络管理协议&#xff0c;用于集中对用…...

用二极管、三极管和MOS管搭建逻辑门电路

文章目录1. 二极管&#xff08;1&#xff09;二极管与门&#xff08;2&#xff09;二极管或门2. 三极管&#xff08;1&#xff09;三极管非门&#xff08;2&#xff09;三极管与门&#xff08;3&#xff09;三极管或门&#xff08;4&#xff09;三极管与非门&#xff08;5&…...

SpringBoot:手写一个 SpringBoot Starter

声明&#xff1a;原文作者&#xff1a;yuan_404 文章目录1. 说明2 . 编写启动器3 . 新建项目测试自己写的启动器1. 说明 启动器模块是一个 空 jar 文件&#xff0c;仅提供辅助性依赖管理&#xff0c;这些依赖可能用于自动装配或者其他类库 命名归约&#xff1a; 官方命名&…...

【23】Verilog进阶 - 数位转换【实时处理 + 标志信号】

【初次尝试】VL32 非整数倍数据位宽转换24to128 1 理解题目含义 根据【模块端口】和【题目描述】本题的真实意思是比较清楚啦。但不可大意轻敌! (1)问题1:输出一直为0 猛然间发现计数值也为0,没有增加 去排查cnt的代码,很容易找到到问题,是cnt上电复位的逻辑写错了 …...

常见的HTTP状态码

一.2开头 200&#xff1a;响应成功&#xff1b; 204&#xff1a;响应成功&#xff0c;但是响应头没有数据&#xff1b; 206&#xff1a;部分响应成功&#xff0c;比如分片上传&#xff0c;断点续传&#xff1b; 二.3开头 301&#xff1a;永久重定向&#xff1b; 302&…...

D. Peculiar Movie Preferences(思维 + 一个坑)

Problem - D - Codeforces 米海打算去看电影。他只喜欢回文电影&#xff0c;所以他想跳过一些(可能是零)场景&#xff0c;让电影的其余部分变成回文。给你一个包含n个长度不超过3的非空字符串的列表&#xff0c;代表Mihai的电影场景。如果s的子序列非空&#xff0c;并且子序列中…...

真1分钟搞懂缓存穿透、缓存击穿、缓存雪崩

&#x1f497;推荐阅读文章&#x1f497; &#x1f338;JavaSE系列&#x1f338;&#x1f449;1️⃣《JavaSE系列教程》&#x1f33a;MySQL系列&#x1f33a;&#x1f449;2️⃣《MySQL系列教程》&#x1f340;JavaWeb系列&#x1f340;&#x1f449;3️⃣《JavaWeb系列教程》…...

蓝桥刷题总结1

数组三角形 题目描述 上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径&#xff0c;把路径上面的数加起来可以得到一个和&#xff0c;你的任务就是找到最大的和。 路径上的每一步只能从一个数走到下一层和它最近的左边的那个数或者右 边的那个…...

淘宝商品详情数据接口 关键字搜索接口 请求代码分享

item_get-获得淘宝商品详情item_get_app-获得淘宝app商品详情原数据item_search-按关键字搜索淘宝商品参数说明通用参数说明参数不要乱传&#xff0c;否则不管成功失败都会扣费url说明 https://api-gw.onebound.cn/平台/API类型/ 平台&#xff1a;淘宝&#xff0c;京东等&#…...

【数据结构】链表OJ(二)

Yan-英杰的博客 悟已往之不谏 知来者之可追 目录 一、反转链表 二、合并两个有序链表 三、链表分割 四、链表的回文结构 一、反转链表 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1] 示例 3&#xf…...

Linux系统搭建FTP服务器

安装vsftpdyum -y install vsftpd添加FTP用户方式1、添加只允许通过ftp访问的用户useradd -d /home/ftp ftp_user #-d指定用户登录时的启始目录方式2、允许用户登录操作系统usermod -d /home/ftp -s /bin/bash ftp_user #-s指定用户登入后所使用的shell设置用户登录密码passwd …...

MySQL数据同步到 Redis 缓存的几种方法

1 Mysql查完数据&#xff0c;再同步写入到Redis中缺点1&#xff1a;会对接口造成延迟&#xff0c;因为同步写入redis本身就有延迟&#xff0c;并且还要做重试&#xff0c;如果redis写入失败&#xff0c;还需要重试&#xff0c;那就更费时间了。缺点2&#xff1a;不解耦&#xf…...

2023年网络安全比赛--CMS网站渗透中职组(超详细)

一、竞赛时间 180分钟 共计3小时 二、竞赛阶段 1.使用渗透机对服务器信息收集,并将服务器中网站服务端口号作为flag提交; 2.使用渗透机对服务器信息收集,将网站的名称作为flag提交; 3.使用渗透机对服务器渗透,将可渗透页面的名称作为flag提交; 4.使用渗透机对服务器渗透,…...

【蓝桥杯集训·每日一题】AcWing 4309. 消灭老鼠

文章目录一、题目1、原题链接2、题目描述二、解题报告1、思路分析2、时间复杂度3、代码详解三、知识风暴最大公约数一、题目 1、原题链接 4309. 消灭老鼠 2、题目描述 约翰的农场可以看作一个二维平面。 农场中有 n 个老鼠&#xff0c;在毁坏着农田。 第 i 个老鼠的位置坐标为…...

FPGA实现CSI-2 解码MIPI视频 2line 720P分辨率 OV5647采集 提供工程源码和技术支持

目录1、前言2、Xilinx官方主推的MIPI解码方案3、纯Vhdl方案解码MIPI4、vivado工程介绍5、上板调试验证6、福利&#xff1a;工程代码的获取1、前言 FPGA图像采集领域目前协议最复杂、技术难度最高的应该就是MIPI协议了&#xff0c;MIPI解码难度之高&#xff0c;令无数英雄竞折腰…...

JS面试题收集(持续更新好中...)

1.JavaScript 中的垃圾回收机制 定义&#xff1a;指一块被分配的内存既不能使用&#xff0c;又不能回收&#xff0c;直到浏览器进程结束。 JavaScript在创建对象时会为它们分配内存&#xff0c;不再使用时会自动释放内存&#xff0c;这个过程称为垃圾收集。 四种常见的内存泄…...

2023携程面试题

Java I/O 面试前需要准备&#xff1a; 1. Java 八股文&#xff1a;了解常考的题型和回答思路&#xff1b; 2. 算法&#xff1a;刷 100-200 道题&#xff0c;记住刷题最重要的是要理解其思想&#xff0c;不要死记硬背&#xff0c;碰上原题很难&#xff0c;但 大多数的解题思…...

CANoe中使用CAPL函数接口调用Vflash文件

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…...

三天吃透计算机网络面试八股文

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/…...

shp数据添加wkt字段并导出成csv,leaflet绘制使用

准备的东西&#xff1a;软件2跟软件3具体怎么有这些软件需要自行百度postgresql postgis相关 1.shp数据 2.软件2 3.软件3 1.数据导入 首先你得有软件2的数据库&#xff0c;即postgresql数据库&#xff0c;然后通过postgis的插件进行连接并导入数据&#xff0c; 导入数据…...

Java——二叉树的最近公共祖先及二叉搜索树介绍

目录 二叉树的最近公共祖先 题目 思路一&#xff1a;如果给定的是一颗二叉搜索树&#xff0c; 思路二&#xff1a;假设是孩子双亲表示法 二叉搜索树 定义Node类 查找 删除 插入 二叉树的最近公共祖先 题目 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百…...

Stable Diffusion加chilloutmixni真人图片生成模型,AI绘图杀疯了

上期图文教程,我们分享过AI绘图大模型Stable Diffusion以及中文版本文心AI绘画大模型的基础知识以及代码实现,截至到目前为止。Stable Diffusion模型已经更新到了V2.1版本,其文生图大模型也越来越火,其在2022年底,由AI绘制的图片被荣为国际大奖,让大家对AI绘画大模型也越…...

Matplotlib 绘图实用大全

本文只介绍最简单基本的画图方法 预设 要想画出来的图有些逼格&#xff0c;首先应该进行如下设置 plt.rcParams[font.sans-serif][SimHei] #画图时显示中文字体 plt.rcParams[axes.unicode_minus] False #防止因修改成中文字符&#xff0c;导致某些 unicode 字符不能…...

IP怎么屏蔽网站域名/国家培训网官网

Vue中的缩写&#xff1a;v-bind、v-on v-bind 缩写&#xff1a;:预期&#xff1a;any (with argument) | Object (without argument)参数&#xff1a;attrOrProp (optional)修饰符&#xff1a; .prop - 被用于绑定 DOM 属性。.camel - (2.1.0) 将 kebab-case 特性名转换为 came…...

苏州网站制作好的公司/厦门人才网官网登录

《1.1.1什么是计算机网络》由会员分享&#xff0c;可在线阅读&#xff0c;更多相关《1.1.1什么是计算机网络(9页珍藏版)》请在人人文库网上搜索。1、认识计算机网络,一、计算机网络的定义,计算机网络是一种地理上分散、具有独立功的各台计算机通过软、硬件设备互连&#xff0c;…...

网站制作的评价标准/百度经验登录入口

1、就业范围广 PMP证书在全球206个国家通用&#xff0c;是世界上许多优秀企业的入职门槛&#xff0c;企业项目管理团队95%以上成员持有&#xff0c;PMP证书的优势就是不受行业限制&#xff0c;不管是IT、建筑、制药、制造业&#xff0c;还是电信、金融、通信领域&#xff0c;P…...

ppt怎么做网站/计算机培训机构哪个最好

/* DML:数据库操作语言主要对表中的数据库进行 增删改****增:插入一条记录insert into 表名 (列名1,列名2..) values (值1,值2..)注意: 1.列名可以在表中选择一列或者几列2.后面的值 必须和前面的列 一一对应3.在SQL中除了int类型的数据,其他数据必须用或者""引起来我…...

密云富阳网站建设/百度官网下载安装

1.Jstack使用介绍该命令打印java线程的堆栈跟踪&#xff0c;可以得知哪些线程被阻塞或正等待&#xff0c;以便于查找如线程死锁的原因用法&#xff1a;jstack [ option ] pidjstack [ option ] executable corejstack [ option ] [server-id]remote-hostname-or-IP常用选项&…...

wordpress访问后台/seo研究中心培训机构

编辑&#xff1a;业余草来源&#xff1a;https://www.xttblog.com/?p4978世上无难事&#xff0c;只怕有心人&#xff01;今天我们来说说线程池&#xff01;线程池简介使用线程池&#xff0c;一般会使用JDK提供的几种封装类型&#xff0c;即&#xff1a;newFixedThreadPool、ne…...