成都建设项目环境影响登记网站,杭州免费网站建设,南京建设网站维护,typecho to wordpress文章目录新增套餐需求分析数据模型准备工作前端页面分析代码开发根据分类查询菜品功能实现功能测试保存套餐功能实现功能测试思维导图总结套餐分页查询需求分析前端页面分析代码开发基本信息查询问题分析功能完善功能测试思维导图总结删除套餐需求分析前端页面分析代码开发功能…
文章目录新增套餐需求分析数据模型准备工作前端页面分析代码开发根据分类查询菜品功能实现功能测试保存套餐功能实现功能测试思维导图总结套餐分页查询需求分析前端页面分析代码开发基本信息查询问题分析功能完善功能测试思维导图总结删除套餐需求分析前端页面分析代码开发功能测试思维导图总结短信发送短信服务介绍阿里云短信服务介绍阿里云短信服务准备注册账号开通短信服务设置短信签名设置短信模板设置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 ListSetmealDish 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 BaseMapperSetmealDish {
}4). 业务层接口 SetmealDishService
所属包 com.itheima.reggie.service
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.SetmealDish;public interface SetmealDishService extends IServiceSetmealDish {
}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 ServiceImplSetmealDishMapper,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请求参数?categoryId1397844263642378242
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 RListDish list(Dish dish){//构造查询条件LambdaQueryWrapperDish 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);ListDish list dishService.list(queryWrapper);return R.success(list);
}功能测试
代码编写完毕我们重新启动服务器进行测试可以通过debug断点跟踪的形式查看页面传递的参数封装情况及响应给页面的数据信息。 保存套餐
功能实现
在进行套餐信息保存时前端提交的数据不仅包含套餐的基本信息还包含套餐关联的菜品列表数据 setmealDishes。所以这个时候我们使用Setmeal就不能完成参数的封装了我们需要在Setmeal的基本属性的基础上再扩充一个属性 setmealDishes 来接收页面传递的套餐关联的菜品列表而我们在准备工作中导入进来的SetmealDto能够满足这个需求。
1). SetmealController中定义方法save新增套餐
在该Controller的方法中,我们不仅需要保存套餐的基本信息还需要保存套餐关联的菜品数据所以我们需要再该方法中调用业务层方法,完成两块数据的保存。
页面传递的数据是json格式需要在方法形参前面加上RequestBody注解, 完成参数封装。
PostMapping
public RString 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);ListSetmealDish 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请求参数?page1pageSize10namexxx
代码开发
基本信息查询
上述我们已经分析列表分页查询功能的请求信息接下来我们就在SetmealController中创建套餐分页查询方法。
该方法的逻辑如下
1). 构建分页条件对象
2). 构建查询条件对象如果传递了套餐名称根据套餐名称模糊查询 并对结果按修改时间降序排序
3). 执行分页查询
4). 组装数据并返回
代码实现 :
/*** 套餐分页查询* param page* param pageSize* param name* return*/
GetMapping(/page)
public RPage page(int page,int pageSize,String name){//分页构造器对象PageSetmeal pageInfo new Page(page,pageSize);LambdaQueryWrapperSetmeal 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 ListSetmealDish setmealDishes; //套餐关联菜品列表private String categoryName;//套餐分类名称
}完善后代码:
/**
* 套餐分页查询
* param page
* param pageSize
* param name
* return
*/
GetMapping(/page)
public RPage page(int page,int pageSize,String name){//分页构造器对象PageSetmeal pageInfo new Page(page,pageSize);PageSetmealDto dtoPage new Page();LambdaQueryWrapperSetmeal 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);ListSetmeal records pageInfo.getRecords();ListSetmealDto 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请求参数?ids1423640210125656065,1423338765002256385
代码开发
删除套餐的流程及请求信息我们分析完毕之后就来完成服务端的逻辑开发。在服务端的逻辑中, 删除套餐时, 我们不仅要删除套餐, 还要删除套餐与菜品的关联关系。
1). 在SetmealController中创建delete方法
我们可以先测试在delete方法中接收页面提交的参数具体逻辑后续再完善
/*** 删除套餐* param ids* return*/
DeleteMapping
public RString delete(RequestParam ListLong ids){log.info(ids:{},ids);return R.success(套餐数据删除成功);
}编写完代码我们重启服务之后访问套餐列表页面勾选复选框然后点击批量删除,我们可以看到服务端可以接收到集合参数ids并且在控制台也可以输出对应的数据 。 2). SetmealService接口定义方法removeWithDish
/*** 删除套餐同时需要删除套餐和菜品的关联数据* param ids*/
public void removeWithDish(ListLong ids);3). SetmealServiceImpl中实现方法removeWithDish
该业务层方法具体的逻辑为:
A. 查询该批次套餐中是否存在售卖中的套餐, 如果存在, 不允许删除
B. 删除套餐数据
C. 删除套餐关联的菜品数据
代码实现为:
/**
* 删除套餐同时需要删除套餐和菜品的关联数据
* param ids
*/
Transactional
public void removeWithDish(ListLong ids) {//select count(*) from setmeal where id in (1,2,3) and status 1//查询套餐状态确定是否可用删除LambdaQueryWrapperSetmeal 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)LambdaQueryWrapperSetmealDish 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 RString delete(RequestParam ListLong 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?spm5176.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?spm5176.12212571.help.dexternal.57a91cbewHHjKq 我们根据官方文档的提示引入对应的依赖然后再引入对应的java代码就可以发送消息了。 SDK : SDK 就是 Software Development Kit 的缩写翻译过来——软件开发工具包辅助开发某一类软件的相关文档、范例和工具的集合都可以叫做SDK。在我们与第三方接口相互时 一般都会提供对应的SDK来简化我们的开发。 具体实现
1). pom.xml
dependencygroupIdcom.aliyun/groupIdartifactIdaliyun-java-sdk-core/artifactIdversion4.5.16/version
/dependency
dependencygroupIdcom.aliyun/groupIdartifactIdaliyun-java-sdk-dysmsapi/artifactIdversion2.1.0/version
/dependency2). 将官方提供的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 BaseMapperUser{
}3). 业务层接口 UserService
所属包: com.itheima.reggie.service
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.User;public interface UserService extends IServiceUser {
}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 ServiceImplUserMapper,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 RString 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 RUser 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)){//如果能够比对成功说明登录成功LambdaQueryWrapperUser 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 RString login(RequestBody MapString,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条件构造器LambdaQueryWrapperUser 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也要做出如下修改
然后就可以正常运行了
思维导图总结
文章转载自: http://www.morning.mqgqf.cn.gov.cn.mqgqf.cn http://www.morning.ymdhq.cn.gov.cn.ymdhq.cn http://www.morning.nxnrt.cn.gov.cn.nxnrt.cn http://www.morning.rkfxc.cn.gov.cn.rkfxc.cn http://www.morning.ddtdy.cn.gov.cn.ddtdy.cn http://www.morning.zqdhr.cn.gov.cn.zqdhr.cn http://www.morning.kzhxy.cn.gov.cn.kzhxy.cn http://www.morning.hrrmb.cn.gov.cn.hrrmb.cn http://www.morning.smj79.cn.gov.cn.smj79.cn http://www.morning.nllst.cn.gov.cn.nllst.cn http://www.morning.csxlm.cn.gov.cn.csxlm.cn http://www.morning.rhfbl.cn.gov.cn.rhfbl.cn http://www.morning.qctsd.cn.gov.cn.qctsd.cn http://www.morning.gkjyg.cn.gov.cn.gkjyg.cn http://www.morning.xdjsx.cn.gov.cn.xdjsx.cn http://www.morning.kybyf.cn.gov.cn.kybyf.cn http://www.morning.sjpbh.cn.gov.cn.sjpbh.cn http://www.morning.khxwp.cn.gov.cn.khxwp.cn http://www.morning.pjtnk.cn.gov.cn.pjtnk.cn http://www.morning.rdymd.cn.gov.cn.rdymd.cn http://www.morning.wbqt.cn.gov.cn.wbqt.cn http://www.morning.kltmt.cn.gov.cn.kltmt.cn http://www.morning.hhqjf.cn.gov.cn.hhqjf.cn http://www.morning.huarma.com.gov.cn.huarma.com http://www.morning.pmghz.cn.gov.cn.pmghz.cn http://www.morning.tftw.cn.gov.cn.tftw.cn http://www.morning.xbptx.cn.gov.cn.xbptx.cn http://www.morning.yrhsg.cn.gov.cn.yrhsg.cn http://www.morning.pyxwn.cn.gov.cn.pyxwn.cn http://www.morning.mzhhr.cn.gov.cn.mzhhr.cn http://www.morning.yqyhr.cn.gov.cn.yqyhr.cn http://www.morning.rdtp.cn.gov.cn.rdtp.cn http://www.morning.tbjtp.cn.gov.cn.tbjtp.cn http://www.morning.mrfbp.cn.gov.cn.mrfbp.cn http://www.morning.kfrhh.cn.gov.cn.kfrhh.cn http://www.morning.fmkjx.cn.gov.cn.fmkjx.cn http://www.morning.drytb.cn.gov.cn.drytb.cn http://www.morning.gmztd.cn.gov.cn.gmztd.cn http://www.morning.spdyl.cn.gov.cn.spdyl.cn http://www.morning.ptmgq.cn.gov.cn.ptmgq.cn http://www.morning.ccphj.cn.gov.cn.ccphj.cn http://www.morning.tqrbl.cn.gov.cn.tqrbl.cn http://www.morning.ktskc.cn.gov.cn.ktskc.cn http://www.morning.zsgbt.cn.gov.cn.zsgbt.cn http://www.morning.fnbtn.cn.gov.cn.fnbtn.cn http://www.morning.fhsgw.cn.gov.cn.fhsgw.cn http://www.morning.qcrhb.cn.gov.cn.qcrhb.cn http://www.morning.hymmq.cn.gov.cn.hymmq.cn http://www.morning.wylpy.cn.gov.cn.wylpy.cn http://www.morning.llmhq.cn.gov.cn.llmhq.cn http://www.morning.nwljj.cn.gov.cn.nwljj.cn http://www.morning.yggwn.cn.gov.cn.yggwn.cn http://www.morning.rbxsk.cn.gov.cn.rbxsk.cn http://www.morning.gpxbc.cn.gov.cn.gpxbc.cn http://www.morning.nlygm.cn.gov.cn.nlygm.cn http://www.morning.fywqr.cn.gov.cn.fywqr.cn http://www.morning.kghss.cn.gov.cn.kghss.cn http://www.morning.tgbx.cn.gov.cn.tgbx.cn http://www.morning.nwjd.cn.gov.cn.nwjd.cn http://www.morning.jcbmm.cn.gov.cn.jcbmm.cn http://www.morning.ntgsg.cn.gov.cn.ntgsg.cn http://www.morning.jkpnm.cn.gov.cn.jkpnm.cn http://www.morning.ypbp.cn.gov.cn.ypbp.cn http://www.morning.wpmqq.cn.gov.cn.wpmqq.cn http://www.morning.ygbq.cn.gov.cn.ygbq.cn http://www.morning.lpppg.cn.gov.cn.lpppg.cn http://www.morning.brsgw.cn.gov.cn.brsgw.cn http://www.morning.lkbdy.cn.gov.cn.lkbdy.cn http://www.morning.hrkth.cn.gov.cn.hrkth.cn http://www.morning.gglhj.cn.gov.cn.gglhj.cn http://www.morning.tlnkz.cn.gov.cn.tlnkz.cn http://www.morning.sxjmz.cn.gov.cn.sxjmz.cn http://www.morning.pqcsx.cn.gov.cn.pqcsx.cn http://www.morning.wtyqs.cn.gov.cn.wtyqs.cn http://www.morning.gcqkb.cn.gov.cn.gcqkb.cn http://www.morning.bnkcl.cn.gov.cn.bnkcl.cn http://www.morning.rsxw.cn.gov.cn.rsxw.cn http://www.morning.jbpdk.cn.gov.cn.jbpdk.cn http://www.morning.lgwjh.cn.gov.cn.lgwjh.cn http://www.morning.zcwzl.cn.gov.cn.zcwzl.cn