瑞吉外卖实战项目全攻略——第五天
瑞吉外卖实战项目全攻略——第五天
该系列将记录一份完整的实战项目的完成过程,该篇属于第五天
案例来自B站黑马程序员Java项目实战《瑞吉外卖》,请结合课程资料阅读以下内容
该篇我们将完成以下内容:
- 新增套餐
- 套餐信息分页查询
- 批量停售/启售
- 删除套餐
- 修改套餐
- 短信发送
- 手机验证码登录
新增套餐
我们的功能开发通常分为三部分
需求分析
我们先打开F12,点开新增套餐,可以发现页面直接发送了两个请求
首先我们查看第一个请求:
这个请求是写在CategoryController中用于查看套餐分类的请求,我们在前面已经完成了,它是为了展示套餐分类下拉框操作的:
然后还有第二个请求:
它是针对我们的菜品里面的分类的获取:
但是当我们点击菜品中的响应菜品时,会跳出第三个请求,这个请求是我们需要完成的根据菜品分类id查找分类内的菜品:
另外还有一个未完成的操作当然是点击保存后将数据传递到数据库中:
然后我们查看一下我们这个操作需要的数据表Setmeal和SetmealDish
Setmeal是套餐表,用于存储套餐的相关信息:
SetmealDish是套餐与菜品的关联表,用于储存两者之间的关系:
最后注意我们在提交信息时的请求体中的数据是两个数据表的集合
所以我们需要采用DTO的实体类来完成接收操作以及相关的业务开发操作
代码实现
首先我们先来完成第一个操作,根据菜品分类id获得相应菜品:
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/dish")
public class DishController {
@Autowired
private DishServiceImpl dishService;
@Autowired
private CategoryServiceImpl categoryService;
/**
* 根据id查询菜品
* @param dish
* @return
*/
@GetMapping("/list")
public Result<List<Dish>> list(Dish dish){
// 提取CategoryID
Long id = dish.getCategoryId();
// 判断条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(id != null,Dish::getCategoryId,id);
queryWrapper.eq(Dish::getStatus,1);
queryWrapper.orderByAsc(Dish::getSort);
List<Dish> list = dishService.list(queryWrapper);
return Result.success(list);
}
}
然后我们来完成比较复杂的提交套餐保存的功能:
- 准备工作
// 我们需要提前准备好一些接口以及实现类(形式类似,这里不再赘述)
实体类SetmealDish
数据层SetmealDishMapper
业务层接口SetmealDishService
业务层SetmealDishServiceImpl
服务层SetmealController
- 定义DTO实体类
package com.qiuluo.reggie.dto;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import lombok.Data;
import java.util.List;
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName;
}
- 去业务层接口定义方法
package com.qiuluo.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.SetmealDto;
import java.util.List;
// 针对我们无法采用默认方法解决的功能,我们需要自己书写方法
public interface SetmealService extends IService<Setmeal> {
/**
* 带菜品关联一同保存
* @param setmealDto
*/
public void saveWithDish(SetmealDto setmealDto);
}
- 业务层实现方法
package com.qiuluo.reggie.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.CustomException;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.mapper.SetmealMapper;
import com.qiuluo.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {
@Autowired
private SetmealDishServiceImpl setmealDishService;
/**
* 带菜品一同保存
* @param setmealDto
*/
public void saveWithDish(SetmealDto setmealDto){
// 保存套餐数据
this.save(setmealDto);
//保存套餐的菜品数据(注意:传进的菜品关联信息里没有套餐的id,所以我们需要手动传入)
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes);
}
}
- 服务层完成功能
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
@Autowired
private SetmealServiceImpl setmealService;
@Autowired
private SetmealDishServiceImpl setmealDishService;
@Autowired
private CategoryServiceImpl categoryService;
@PostMapping
public Result<String> save(@RequestBody SetmealDto setmealDto){
setmealService.saveWithDish(setmealDto);
log.info("套餐新增成功");
return Result.success("新创套餐成功");
}
}
实际测试
我们需要测试两点
- 打开新创页面时,点击菜品可以看到菜品分类后的相关菜品
- 点击新增套餐,填写数据后提交,数据库中可以看到相关新添菜品
套餐信息分页查询
我们的功能开发通常分为三部分
需求分析
该功能与菜品管理的分页查询功能相似,我们不做赘述,简单步骤如下
我们直接点击套餐页面,F12查看相关代码即可:
因为返回数据中包含新的属性categoryName,我们需要采用DTO来完成该操作
代码实现
我们直接在SetmealController中完成该操作即可:
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
@Autowired
private SetmealServiceImpl setmealService;
@Autowired
private SetmealDishServiceImpl setmealDishService;
@Autowired
private CategoryServiceImpl categoryService;
@GetMapping("page")
public Result<Page> page(int page, int pageSize, String name){
// 构造分页器
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> setmealDtoPage = new Page<>();
// 构造条件
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name != null,Setmeal::getName,name);
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
// 查询
setmealService.page(pageInfo);
// 赋值
BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(item,setmealDto);
// 将CategoryName复制进去
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
setmealDtoPage.setRecords(list);
// 返回结果
return Result.success(setmealDtoPage);
}
}
实际测试
在打开套餐分类页面时,所有信息呈现即为功能开发成功
批量停售/启售
我们的功能开发通常分为三部分
需求分析
该功能视频中没有讲述,属于课后简单作业
我们的删除功能中需要删除已经停售的套餐业务,所以我们提前处理页面的停售启售操作
我们点击单个停售启售以及多个停售启售可以观察到url以及请求方法:
// 单个停售
请求 URL: http://localhost:8080/setmeal/status/0?ids=1415580119015145474
请求方法: POST
// 多个停售
请求 URL: http://localhost:8080/setmeal/status/0?ids=1415580119015145474,1583260610277715970
请求方法: POST
// 单个启售
请求 URL: http://localhost:8080/setmeal/status/1?ids=1415580119015145474
请求方法: POST
// 多个启售
请求 URL: http://localhost:8080/setmeal/status/1?ids=1415580119015145474,1583260610277715970
请求方法: POST
我们可以注意到我们的启售,停售操作其实可以简化为一个操作或两个操作完成,这里我们分为两个操作完成
代码实现
我们直接在SetmealController中完成该操作即可:
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
@Autowired
private SetmealServiceImpl setmealService;
@Autowired
private SetmealDishServiceImpl setmealDishService;
@Autowired
private CategoryServiceImpl categoryService;
// 其实还可以整合成一个方法,url写为/status,读取后面的值设置为status,根据status对ids进行操作即可
@PostMapping("/status/0")
public Result<String> closeStatus(@RequestParam List<Long> ids){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(Setmeal::getId,ids);
List<Setmeal> setmeals = setmealService.list(queryWrapper);
for (Setmeal setmeal:setmeals
) {
setmeal.setStatus(0);
setmealService.updateById(setmeal);
}
return Result.success("修改成功");
}
@PostMapping("/status/1")
public Result<String> openStatus(@RequestParam List<Long> ids){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(Setmeal::getId,ids);
List<Setmeal> setmeals = setmealService.list(queryWrapper);
for (Setmeal setmeal:setmeals
) {
setmeal.setStatus(1);
setmealService.updateById(setmeal);
}
return Result.success("修改成功");
}
}
实际测试
回到套餐管理页面,点击启售停售,操作成功即为功能开发成功
删除套餐
我们的功能开发通常分为三部分
需求分析
我们删除套餐的基本原则是当前套餐需要处于停售阶段才可以删除,所以我们在处理时需要先做判断
我们需要完成单个删除和多个删除的操作,我们首先对两个操作进行简单分析
我们同样F12查看url以及请求方式:
// 单个删除
请求 URL: http://localhost:8080/setmeal?ids=1583260610277715970
请求方法: DELETE
// 多个删除
请求 URL: http://localhost:8080/setmeal?ids=1583260610277715970,1583260610277715970
请求方法: DELETE
我们可以看到单个删除操作和多个删除操作可以直接归为一个方法中实现
代码实现
该操作比较复杂,我们分为几个步骤:
- 业务层接口定义方法
package com.qiuluo.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.SetmealDto;
import java.util.List;
public interface SetmealService extends IService<Setmeal> {
/**
* 带菜品关联一同删除
* @param ids
*/
public void removeWithDish(List<Long> ids);
}
- 业务层实现方法
package com.qiuluo.reggie.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.CustomException;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.mapper.SetmealMapper;
import com.qiuluo.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {
@Autowired
private SetmealDishServiceImpl setmealDishService;
/**
* 带菜品关联一同删除
* @param ids
*/
public void removeWithDish(List<Long> ids){
// 判断是否是停售状态,若不为停售不能删除,抛出业务异常
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("删除业务中有套餐处于启售状态,无法删除");
}
// 可以删除后执行删除操作
// 先删除套餐
this.removeByIds(ids);
// 再删除套餐关联信息
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(lambdaQueryWrapper);
}
}
- 服务层实现功能
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
@Autowired
private SetmealServiceImpl setmealService;
@DeleteMapping
public Result<String> delete(@RequestParam List<Long> ids){
setmealService.removeWithDish(ids);
return Result.success("删除成功");
}
}
实际测试
回到套餐管理页面,点击单个删除多个删除,删除成功即为功能开发成功
修改套餐
我们的功能开发通常分为三部分
需求分析
该功能视频没有提及,属于课后作业
首先我们打开F12点击修改操作,查看出错请求:
我们可以看到是GET请求,大概率是想要根据id获得当前套餐的相关信息并返回给页面
然后我们填写信息后点击提交,我们可以通过F12查看到相关url以及提交方式
该操作应该属于更新操作
代码实现
我们首先来完成页面回显操作:
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
@Autowired
private SetmealServiceImpl setmealService;
@Autowired
private SetmealDishServiceImpl setmealDishService;
@Autowired
private CategoryServiceImpl categoryService;
@GetMapping("/{id}")
public Result<SetmealDto> getById(@PathVariable Long id){
// 我们需要把setmealDto返回回去,定义一个新的setmealDto用于保存数据
SetmealDto setmealDto = new SetmealDto();
// 将普通数据传入
Setmeal setmeal = setmealService.getById(id);
BeanUtils.copyProperties(setmeal,setmealDto);
// 将菜品信息传递进去
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId,id);
List<SetmealDish> list = setmealDishService.list(queryWrapper);
setmealDto.setSetmealDishes(list);
// 返回setmealDto即可
return Result.success(setmealDto);
}
}
然后我们再来完成比较复杂的修改保存信息操作:
- 业务层接口定义
package com.qiuluo.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.SetmealDto;
import java.util.List;
public interface SetmealService extends IService<Setmeal> {
/**
* 修改操作
* @param setmealDto
*/
public void updateWithDish(SetmealDto setmealDto);
}
- 业务层定义
package com.qiuluo.reggie.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.CustomException;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.mapper.SetmealMapper;
import com.qiuluo.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {
@Autowired
private SetmealDishServiceImpl setmealDishService;
/**
* 修改操作
* @param setmealDto
*/
public void updateWithDish(SetmealDto setmealDto){
// 首先修改套餐上的信息
this.updateById(setmealDto);
// 修改内部菜品操作(同样先删除再添加)
// 删除操作
Long setmealId = setmealDto.getId();
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId,setmealId);
setmealDishService.remove(queryWrapper);
// 新填操作
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishService.saveBatch(setmealDishes);
}
}
- 服务层实现功能
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
@Autowired
private SetmealServiceImpl setmealService;
@PutMapping
public Result<String> update(@RequestBody SetmealDto setmealDto){
setmealService.updateById(setmealDto);
return Result.success("修改成功");
}
}
实际测试
我们需要注意两处测试内容:
- 点击修改功能后,打开修改页面,页面出现相关信息
- 点击修改完成功能后,页面信息内容成功修改即可
短信发送
短信发送章节比较特殊,我们在下面做简单了解
短信服务介绍
目前市场上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动,联通,电信)对接
我们只需要注册称为会员并按照提供的开发文档进行调用就可以发送短信
目前我们常见的提供短信服务的公司:
- 阿里云
- 华为云
- 腾讯云
- 京东
- 梦网
- 乐信
阿里云短信服务介绍
阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力
我们可以直接调用阿里云的API或者群发助手就可以发送验证码,通知类信息和营销类短信等,速度快,安全稳定
接下来我们来简单介绍阿里云短信申请的具体流程:
- 阿里云账号注册/登录
- 进入账号中心进行实名认证等信息设置
- 进入控制台来到短信服务页面
- 设置短信签名:短信是短信发送者的署名,表示发送方的身份(手续繁杂,我们仅作了解)
- 设置模板:模板就是发送短信的形式,其中${code}表示验证码占位符(手续繁杂,但系统自动赠送一条模板)
- 设置AccessKey:使用子用户表示有部分权限(在用户头像弹出的窗口中找到)
- 用户界面新创用户(选择API调用访问)
- 创建后会给出对应ID和密码,请保存(AccessKey ID 和 AccessKey Secret)
# 不方便展示
- 为我们的用户添加对应的SMS权限即可
到这里阿里云页面操作基本结束
代码开发部分内容展示
我们在前面也有提到:如果我们想用阿里云进行短信发送,查看官方相关文档即可
这里我们对代码开发步骤进行简单整合:
- 导入相关Maven坐标
<!--阿里云短信服务-->
<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>
- 调用API(资料中将API封装为工具类)
package com.qiuluo.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;
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
// 下面两个空需要我们手动填写账号AccessKey ID和密码AccessKey Secret
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
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();
}
}
}
此外,资料中还为我们提供了一个生成四位数随机验证码的工具类:
package com.qiuluo.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;
}
}
手机验证码登录
我们的功能开发通常分为三部分
资料修改
在正式开始讲解之前,我们将资料中的部分内容进行修改来满足我们下面调试的需求:
- 修改front/page/login.html页面(这里在发送请求时仅将手机号发送回后端且自动生成验证码,我们需要将手机号和验证码均发送)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,user-scalable=no,minimal-ui">
<title>菩提阁</title>
<link rel="icon" href="./../images/favico.ico">
<!--不同屏幕尺寸根字体设置-->
<script src="./../js/base.js"></script>
<!--element-ui的样式-->
<link rel="stylesheet" href="../../backend/plugins/element-ui/index.css" />
<!--引入vant样式-->
<link rel="stylesheet" href="../styles/vant.min.css"/>
<!-- 引入样式 -->
<link rel="stylesheet" href="../styles/index.css" />
<!--本页面内容的样式-->
<link rel="stylesheet" href="./../styles/login.css" />
</head>
<body>
<div id="login" v-loading="loading">
<div class="divHead">登录</div>
<div class="divContainer">
<el-input placeholder=" 请输入手机号码" v-model="form.phone" maxlength='20'/></el-input>
<div class="divSplit"></div>
<el-input placeholder=" 请输入验证码" v-model="form.code" maxlength='20'/></el-input>
<span @click='getCode'>获取验证码</span>
</div>
<div class="divMsg" v-if="msgFlag">手机号输入不正确,请重新输入</div>
<el-button type="primary" :class="{btnSubmit:1===1,btnNoPhone:!form.phone,btnPhone:form.phone}" @click="btnLogin">登录</el-button>
</div>
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="../../backend/plugins/vue/vue.js"></script>
<!-- 引入组件库 -->
<script src="../../backend/plugins/element-ui/index.js"></script>
<!-- 引入vant样式 -->
<script src="./../js/vant.min.js"></script>
<!-- 引入axios -->
<script src="../../backend/plugins/axios/axios.min.js"></script>
<script src="./../js/request.js"></script>
<script src="./../api/login.js"></script>
</body>
<script>
new Vue({
el:"#login",
data(){
return {
form:{
phone:'',
code:''
},
msgFlag:false,
loading:false
}
},
computed:{},
created(){},
mounted(){},
methods:{
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)
const res = sendMsgApi({phone:this.form.phone})
sessionStorage.setItem("code",res)
}else{
this.msgFlag = true
}
},
async btnLogin(){
if(this.form.phone && this.form.code){
this.loading = true
const res = await loginApi({phone:this.form.phone,code:this.form.code})
this.loading = false
if(res.code === 1){
sessionStorage.setItem("userPhone",this.form.phone)
window.requestAnimationFrame(()=>{
window.location.href= '/front/index.html'
})
}else{
this.$notify({ type:'warning', message:res.msg});
}
}else{
this.$notify({ type:'warning', message:'请输入手机号码'});
}
}
}
})
</script>
</html>
- 修改front/api/login.js(缺少一个方法,实现验证码发送到后端的方法)
function loginApi(data) {
return $axios({
'url': '/user/login',
'method': 'post',
data
})
}
function loginoutApi() {
return $axios({
'url': '/user/loginout',
'method': 'post',
})
}
function sendMsgApi(data) {
return $axios({
'url': '/user/sendMsg',
'method': 'post',
data
})
}
需求分析
我们的用户登录通常采用手机号登录,发送验证码,填写验证码后登录的流程操作
那么手机号便是我们区分用户的根本标识,我们的登录信息中也以手机号为标识进行数据返回
首先我们简单了解这次使用的数据表内容User:
我们点开手机端页面开始简单的信息整理(这里注意:我们需要点开F12设置为手机端才可访问页面,这是H5的特性):
我们查看该页面的两个请求
第一个请求是发送验证码请求:
第二个请求是登录请求:
至此我们分析暂时结束
代码实现
接下来我们来完成功能的具体实现步骤:
- 准备工作
# 我们老规矩先来准备一些常见的内容
实体类User
数据层UserMapper
业务层接口UserService
业务层UserServiceImpl
服务层UserController
工具类SMSUtils,ValidateCodeUtils
- 拦截器设置修改
/*
在开始手机验证码的设置前,我们先来设置拦截器
如果你前面和我一起查看需求分析的请求信息,你会发现你无法实现sendMsg和login方法,因为这两个方法被拦截下来了
所以我们需要先将这两个方法放入到不必拦截的String[]里面
另一方面,我们和后台设置一样,要设置用户登录后才能进入页面查看信息
所以我们可以重新判断手机端用户是否登录,若登录后再进行放行,否则拦截在登陆页面
*/
package com.qiuluo.reggie.filter;
import com.alibaba.fastjson.JSON;
import com.qiuluo.reggie.common.BaseContext;
import com.qiuluo.reggie.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 检查用户是否已经完成登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestURI = request.getRequestURI();
log.info("拦截到请求:{}",requestURI);
//定义不需要处理的请求路径("/user/login","/user/sendMsg"是手机端登录和短信发送请求)
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/login",
"/user/sendMsg"
};
boolean check = check(urls, requestURI);
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
//4-1、判断后台登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
log.info("登录中...");
log.info("线程id" + Thread.currentThread().getId());
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request,response);
return;
}
//4-2、判断移动端登录状态,如果已登录,则直接放行(和上述内容完全一样,修改Session中设置的名字即可)
if(request.getSession().getAttribute("user") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
log.info("登录中...");
log.info("线程id" + Thread.currentThread().getId());
Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(Result.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if(match){
return true;
}
}
return false;
}
}
- 短信发送实现
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.User;
import com.qiuluo.reggie.service.UserService;
import com.qiuluo.reggie.utils.SMSUtils;
import com.qiuluo.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.jws.soap.SOAPBinding;
import javax.servlet.http.HttpSession;
import java.util.Map;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
// 前端发送phone和code,我们直接采用Map类型接收,采用get方法获得值
@PostMapping("/sendMsg")
public Result<String> sendMsg(@RequestBody User user, HttpSession session){
// 保存手机号
String phone = user.getPhone();
// 判断手机号是否存在并设置内部逻辑
if (phone != null){
// 随机生成四位密码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
// 因为无法申请signName签名,我们直接在后台查看密码
log.info(code);
// 我们采用阿里云发送验证码
// SMSUtils.sendMessage("签名","模板",phone,code);
// 将数据放在session中待比对
session.setAttribute(phone,code);
return Result.success("验证码发送成功");
}
return Result.success("验证码发送失败");
}
}
- 登录功能实现
package com.qiuluo.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.User;
import com.qiuluo.reggie.service.UserService;
import com.qiuluo.reggie.utils.SMSUtils;
import com.qiuluo.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.jws.soap.SOAPBinding;
import javax.servlet.http.HttpSession;
import java.util.Map;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public Result<User> login(@RequestBody Map map, HttpSession session){
log.info(map.toString());
// 获得手机号
String phone = map.get("phone").toString();
// 获得验证码
String code = map.get("code").toString();
// 获得Session中的验证码
String codeInSession = session.getAttribute(phone).toString();
// 进行验证码比对
if (codeInSession != null && codeInSession.equals(code) ){
// 登陆成功
log.info("用户登陆成功");
// 判断是否为新用户,如果是自动注册
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里的user值为用户id,因为我们的过滤器需要以此判断用户是否登录
session.setAttribute("user",user.getId());
// 返回User信息,因为前端需要该数据来布置内部页面
return Result.success(user);
}
// 验证码比较失败则登陆失败
return Result.error("登陆失败");
}
}
实际测试
这部分的测试步骤比较麻烦,我们来逐步测试:
- 来到APP界面,输入手机号,点击发送验证码
- 来到后端查看验证码
- 将验证码输入并点击登录,登陆成功即可
结束语
该篇内容到这里就结束了,希望能为你带来帮助~
附录
该文章属于学习内容,具体参考B站黑马程序员的Java项目实战《瑞吉外卖》
这里附上视频链接:业务开发Day5-01-本章内容介绍_哔哩哔哩_bilibili