瑞吉点餐项目

Lombok提供了一个注解@Slf4j可以直接使用log变量log.info输出日志,方便代码调试

静态资源不一定要放在static,template文件夹里面,可以通过配置类的方式来设置静态资源的映射,告诉框架,指定的目录就是静态资源。

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    //设置静态资源映射
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射...");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:front/");
    }
}


 如果前端表单传送过来的是json格式的数据,后端对应的Controller接收参数需要加上注解@RequestBody,通过对象封装,实体类的属性要和前端参数名一致。


queryWrapper.eq(Employee::getUsername,employee.getUsername());

 Employee::getUsername的意思就相当于:

1.实例化一个Employee对象

Employee employee = new Employee()

 2.调用对象employee的get方法,这里调用的是getUsername

employee.getUsername();


@Controller与@RestController的区别

@Controller注解Controller,Controller中的方法能够返回jsp页面,视图解析器可以解析return的jsp,html页面,并且跳转到相应页面若要返回数据到页面,需要用model,或者加上@ResponseBody注解。

@ResponseBody能够把方法返回的结果封装成json数据返回

@RestController注解Controller,相当于@ResponseBody+@Controller合在一起的作用,Controller中的方法无法返回jsp页面,视图解析器无法解析,返回的内容就是返回到页面的数据。


在SpringBoot项目中如果用过滤器的方式拦截页面

首先定义好过滤器,加上注解@WebFilter

/**
 * 检查用户是否已经完成登录
 */
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        log.info("拦截到请求:{}",request.getRequestURI());
        filterChain.doFilter(request,response);
    }
}

要让这个过滤器生效,需要在启动类上加一个注解@ServletComponentScan,才能扫描到@WebFilter

@Slf4j
@ServletComponentScan
@SpringBootApplication
public class ReggieTakeOutApplication {

    public static void main(String[] args) {
        SpringApplication.run(ReggieTakeOutApplication.class, args);
        log.info("项目启动成功...");
    }

}

路径匹配器,支持通配符

//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

String requestURI = request.getRequestURI();//backend/index.html

String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
};

boolean match = PATH_MATCHER.match(url,requestURI);

通过输出流的方式向页面响应JSON数据

response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));

字段索引类型unique,表示这个字段不允许重复 

如果插入了相同的字段就会抛异常

 处理这个异常有两种处理方式:

1.在Controller方法中加入try、catch进行异常捕获

try{
    employeeService.save(employee);
}catch(Exception ex){
    return R.error("新增员工失败");
}

2.使用异常处理器进行全局异常捕获(建议使用)

/**
 * 全局异常处理
 */
//@ControllerAdvice用来指定拦截哪些Controller
@ControllerAdvice(annotations = {RestController.class, Controller.class})
//@ResponseBody能够把方法返回的结果封装成json数据返回
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 异常处理方法
     * @param ex
     * @return
     */
    //@ExceptionHandler用来指定方法用来处理哪些异常
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
        return R.error("用户名已存在");
    }
}

注意:

@ControllerAdvice用来指定拦截哪些Controller

@ResponseBody能够把方法返回的结果封装成json数据返回

@ExceptionHandler用来指定方法处理哪些异常(这里处理的是数据库抛出来的异常)

再优化一下这个异常处理器

/**
 * 全局异常处理
 */
//@ControllerAdvice用来指定拦截哪些Controller
@ControllerAdvice(annotations = {RestController.class, Controller.class})
//最终的方法数据需要返回json数据,@ResponseBody能够把结果封装成json数据返回
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 异常处理方法
     * @param ex
     * @return
     */
    //@ExceptionHandler用来指定方法用来处理哪些异常
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");//通过空格隔开
            String msg = split[2] + "已存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }
}

 MybatisPlus分页插件

/**
 * 配置MP的分页插件
 */
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}
/**
     * 员工信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize,String name){
        log.info("page = {},pageSize = {},name = {}",page,pageSize,name);
        //构造分页构造器
        Page pageInfo = new Page(page,pageSize);
        //构造条件构造器
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        //添加过滤条件
        queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
        //添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);
        //执行查询
        employeeService.page(pageInfo,queryWrapper);

        return R.success(pageInfo);
    }

 注意:前端将json格式的数据传给后端,后端Controller可以直接接收对应的参数。


@RequestBody的作用是将前端json格式的数据转为java对象。前端的json数据会自动匹配到后端接收对象的属性中了,当然属性名称要一样。


前端JS对Long型id处理的时候会丢失精度,只能保证前16位数字是精确的,后面的数字做四舍五入。

 

如何解决这个问题?

可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串

具体实现步骤:

1.提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

注意:

把Long型数据转换成了字符串

把日期格式也进行了更改

 2.在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换

/**
     * 扩展mvc框架的消息转换器
     * 项目启动的时候就会调用
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //converters包含了Springmvc默认的几个消息转换器
        //追加自己的消息转换器,将上面的消息转换器对象追加到mvc框架的转换器集合中,设置成优先使用
        converters.add(0,messageConverter);
    }

注意:

项目启动的时候就会调用这个消息转换器

追加自己的消息转换器,将上面的消息转换器对象追加到mvc框架的转换器集合中,设置成优先使用


公共字段自动填充自定义的元数据对象处理器MetaObjectHandler,现在自动填充的createUser和updateUser设置的用户id是固定值,需要改造成动态获取当前登录用户的id。 

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入操作,自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {

        log.info("公共字段自动填充insert...");
        log.info(metaObject.toString());
        metaObject.setValue("createTime",LocalDateTime.now());
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("createUser",new Long(1));//这里无法直接获取到当前用户request Session对象
        metaObject.setValue("updateUser",new Long(1));
    }

    /**
     * 更新操作,自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充update...");
        log.info(metaObject.toString());
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("updateUser",new Long(1));
    }
}

注意:在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以需要通过其他方式来获取登录用户id,可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。

ThreadLocal

在学习ThreadLocal之前,需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:

1.LoginCheckFilter的doFilter方法

2.EmployeeController的update方法

3.MyMetaObjectHandler的updateFill方法

可以在上面的三个方法中分别加入下面代码(获取当前线程id):

Long id = Thread.currentThread().getId();
log.info("线程id:{}",id);

执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的:

 什么是ThreadLocal?

ThreadLocal并不是Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

ThreadLocal常用方法:

  • public void set(T value) 设置当前线程的线程布局变量的值
  • public T get()返回当前线程所对应的线程局部变量的值

可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。

实现步骤: 

1.编写BaseContext工具类,基于ThreadLocal封装的工具类

/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    /**
     * 设置值
     * @param id
     */
    public  static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    /**
     * 获取值
     * @return
     */
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

2.在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id

//4.判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee")!=null){
     log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
     Long empId = (Long) request.getSession().getAttribute("employee");
     BaseContext.setCurrentId(empId);
     log.info("线程id为:{}",Thread.currentThread().getId());
     filterChain.doFilter(request,response);
     return;
}

3.在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入操作,自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {

        log.info("公共字段自动填充insert...");
        log.info(metaObject.toString());
        metaObject.setValue("createTime",LocalDateTime.now());
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("createUser",BaseContext.getCurrentId());//这里无法直接获取到当前用户request Session对象
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }

    /**
     * 更新操作,自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充update...");
        log.info(metaObject.toString());
        log.info("线程id为:{}",Thread.currentThread().getId());
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }
}

自定义业务异常类

/**
 * 自定义业务异常类
 */
public class CustomException extends RuntimeException{
    public CustomException(String message){
        super(message);
    }
}

需要的时候可以直接抛出这个异常

throw new CustomException("菜品正在售卖中,不能删除");

全局异常处理GlobalExceptionHandler处理这个异常

@ExceptionHandler(CustomException.class)//处理数据库抛出的异常
    public R<String> exceptionHandler(CustomException ex){
        log.error(ex.getMessage());
        return R.error(ex.getMessage());
    }


 文件上传

文件上传时,对页面的form表单有如下要求:

  • method="post"  采用post方式提交数据
  • enctype="multipart/form-data" 采用multipart格式上传文件
  • type="file" 使用input的file控件上传
<form method="post" action="/common/upload" enctype="multipart/form-data">
       <input name="myFile" type="file"/>
       <input type="submit" value="提交"/>
</form>

服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:

  • commons-fileupload
  • commons-io

Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件 

/**
 * 文件上传和下载
 */
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){//参数名一定要与FormData的name的值保持一致
        log.info(file.toString());
        return null;
    }
}

 注意:

MultipartFile file参数名一定要与FormData的name的值保持一致,不然上传不了为null

file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除

文件下载

通过浏览器进行文件下载,通常由两种表现形式:

  • 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文件上传</title>
  <!-- 引入样式 -->
  <link rel="stylesheet" href="../../plugins/element-ui/index.css" />
  <link rel="stylesheet" href="../../styles/common.css" />
  <link rel="stylesheet" href="../../styles/page.css" />
</head>
<body>
   <div class="addBrand-container" id="food-add-app">
    <div class="container">
        <el-upload class="avatar-uploader"
                action="/common/upload"
                :show-file-list="false"
                :on-success="handleAvatarSuccess"
                :before-upload="beforeUpload"
                ref="upload">
            <img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
        </el-upload>
    </div>
  </div>
    <!-- 开发环境版本,包含了有帮助的命令行警告 -->
    <script src="../../plugins/vue/vue.js"></script>
    <!-- 引入组件库 -->
    <script src="../../plugins/element-ui/index.js"></script>
    <!-- 引入axios -->
    <script src="../../plugins/axios/axios.min.js"></script>
    <script src="../../js/index.js"></script>
    <script>
      new Vue({
        el: '#food-add-app',
        data() {
          return {
            imageUrl: ''
          }
        },
        methods: {
          handleAvatarSuccess (response, file, fileList) {
              this.imageUrl = `/common/download?name=${response.data}`
          },
          beforeUpload (file) {
            if(file){
              const suffix = file.name.split('.')[1]
              const size = file.size / 1024 / 1024 < 2
              if(['png','jpeg','jpg'].indexOf(suffix) < 0){
                this.$message.error('上传图片只支持 png、jpeg、jpg 格式!')
                this.$refs.upload.clearFiles()
                return false
              }
              if(!size){
                this.$message.error('上传文件大小不能超过 2MB!')
                return false
              }
              return file
            }
          }
        }
      })
    </script>
</body>
</html>

 注意:图片上传成功后会自动发送下载请求,下载成功后图片才会显示成功

/**
 * 文件上传和下载
 */
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
    @Value("${reggie.path}")
    private String basePath;
    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){//参数名一定要与FormData的name的值保持一致
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
        log.info(file.toString());
        //原始文件名
        String originalFilename = file.getOriginalFilename();
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));//.jpg
        //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
        String fileName = UUID.randomUUID().toString() + suffix;
        //创建一个目录对象
        File dir = new File(basePath);
        //判断当前目录是否存在
        if(!dir.exists()){
            //目录不存在,需要创建
            dir.mkdirs();
        }
        try {
            //将临时文件转存到指定位置
            file.transferTo(new File(basePath + fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(fileName);
    }
    /**
     * 文件下载
     * @param name
     * @param response
     * @return
     */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        try {
            //输入流,通过输入流读取文件内容
            FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
            //输出流,通过输出流将文件写回浏览器,在浏览器展示图片
            ServletOutputStream outputStream = response.getOutputStream();
            response.setContentType("image/jpeg");
            int len = 0;
            byte[] bytes = new byte[1024];
            while((len = fileInputStream.read(bytes))!=-1){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
            //关闭资源
            outputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

@Value的用法 

1.@Value("${}")从配置文件读取值,也就是从application.yml文件中获取值

reggie:
  path: C:\Users\83825\Desktop\test\
@Value("${reggie.path}")
    private String basePath;

2.@Value("")常量注入

3.@Value("#{}")获取bean属性,系统属性,表达式


 前端传过来的Json数据有flavors属性,但实体类Dish没有该属性字段

前端Json数据属性与后端实体类属性不对应的情况,可以通过DTO解决

DTO,全程Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输

前端数据,封装页面提交的数据

@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;

    private Integer copies;
}

涉及多张表的操作需要加入事务控制@Transactional,保证数据的一致性        

@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
    @Autowired
    private DishFlavorService dishFlavorService;
    /**
     * 新增菜品,同时保存对应的口味数据
     * @param dishDto
     */
    @Override
    @Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到菜品表dish
        this.save(dishDto);
        Long dishId = dishDto.getId();//菜品id
        //菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors = flavors.stream().map((item)->{
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());
        //保存菜品口味数据到菜品口味表dish_flavor
        dishFlavorService.saveBatch(flavors);
    }
}

要让@Transactional需要在启动类上开启事务的支持@EnableTransactionManagement

@Slf4j
@ServletComponentScan
@SpringBootApplication
@EnableTransactionManagement
public class ReggieTakeOutApplication {

    public static void main(String[] args) {
        SpringApplication.run(ReggieTakeOutApplication.class, args);
        log.info("项目启动成功...");
    }

}

对象拷贝BeanUtils.copyProperties(),把一个对象的属性值拷贝到另一个对象对应的属性里面去,可以设置忽略的属性。

BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");

遇到问题:因循环引用导致启动时报错,项目启动失败。

报错原因:因循环引用导致启动时报错:SetmealServiceImpl引入了自己的SetmealService导致了循环调用。依赖循环引用是不鼓励的,默认情况下是禁止的。

public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
    @Autowired
    SetmealDishService setmealDishService;
    @Autowired
    SetmealService setmealService;
}

解决方法1:

不通过这种方式调用SetmealService,可以通过this的方式调用。

解决方法2:

更新应用程序以删除bean之间的依赖循环。作为最后的手段,可以通过设置spring.main来自动打破循环。

spring:
 main:
  allow-circular-references: true

RequestParam和PathVariable的区别


短信发送 

短信服务介绍

  • 目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,只需要注册成为会员,并且按照提供的开发文档进行调用就可以发送短信。
  • 需要说明的是,这些短信服务一般都是收费服务
  • 常用短信服务: 阿里云 华为云 腾讯云 京东 梦网 乐信 本项目在选择短信服务的第三方服务提供商时,选择的是阿里云短信服务

阿里云短信服务

代码开发

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

官方文档: https://help.aliyun.com/document_detail/112148.html

1.导入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>1.1.0</version>
</dependency>

 2.调用API

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.profile.DefaultProfile;
import com.google.gson.Gson;
import java.util.*;
import com.aliyuncs.dysmsapi.model.v20170525.*;

public class SendSms {

    public static void main(String[] args) {
        DefaultProfile profile = DefaultProfile.getProfile("cn-beijing", "<your-access-key-id>", "<your-access-key-secret>");
        /** use STS Token
        DefaultProfile profile = DefaultProfile.getProfile(
            "<your-region-id>",           // The region ID
            "<your-access-key-id>",       // The AccessKey ID of the RAM account
            "<your-access-key-secret>",   // The AccessKey Secret of the RAM account
            "<your-sts-token>");          // STS Token
        **/
        IAcsClient client = new DefaultAcsClient(profile);

        SendSmsRequest request = new SendSmsRequest();
        request.setPhoneNumbers("1368846****");//接收短信的手机号码
        request.setSignName("阿里云");//短信签名名称
        request.setTemplateCode("SMS_20933****");//短信模板CODE
        request.setTemplateParam("张三");//短信模板变量对应的实际值

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

    }
}

封装成SMSUtils

package com.itheima.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){
		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();
		}
	}
}

使用 

@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();
            //调用阿里云提供的短信服务API完成发送短信
            SMSUtils.sendMessage("瑞吉外卖","",phone,code);
            //需要将生成的验证码保存到Session
            session.setAttribute(phone,code);
            return R.success("手机验证码短信发送成功");
        }
        return R.success("短信发送失败");
    }

接收前端json数据,除了利用对象,Dto之外还有一种简单的方式就是利用键值对Map

@PostMapping("/login")
    public R<User> login(@RequestBody Map map){
        log.info(map.toString());
        return null;
    }

Vue生命周期中mounted和created的区别

created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。

mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。


原子数, 保证在多线程的情况下计算也没有问题,能够保证线程安全。


Mybatis-plus时间范围内的查询方式

@GetMapping("/page")
    public R<Page> page(int page, int pageSize, String number,String beginTime,String endTime){
        log.info("page={},pageSize={},number={},beginTime={},endTime={}",page,pageSize,number,beginTime,endTime);
        Page<Orders> pageInfo = new Page<>(page,pageSize);
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(number!=null,Orders::getNumber,number);
        queryWrapper.ge(beginTime!=null,Orders::getOrderTime,beginTime);
        queryWrapper.le(endTime!=null,Orders::getOrderTime,endTime);
        queryWrapper.orderByDesc(Orders::getOrderTime);
        ordersService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

在这里插入图片描述

在这里插入图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值