02、数据同步:思路分析及常量准备
改造项目,实现搜索服务、商品静态页的数据同步。
1)思路分析
发送方:商品微服务
-
什么时候发?
当商品服务对商品进行新增和上下架的时候,需要发送一条消息,通知其它服务。
-
发送什么内容?
对商品的增删改时其它服务可能需要新的商品数据,但是如果消息内容中包含全部商品信息,数据量太大,而且并不是每个服务都需要全部的信息。因此我们只发送商品id,其它服务可以根据id查询自己需要的信息。
接收方:搜索微服务、静态页微服务
- 接收消息后如何处理?
- 搜索微服务:
- 上架:添加新的数据到索引库
- 下架:删除索引库数据
- 静态页微服务:
- 上架:创建新的静态页
- 下架:删除原来的静态页
- 搜索微服务:
2)常量准备
在ly-common
中编写一个常量类,记录将来会用到的Exchange名称、Queue名称、routing_key名称
package com.leyou.commom.constants;
/**
* @author yy
*/
public class MqConstants {
public static final class Exchange {
/**
* 商品服务交换机名称
*/
public static final String ITEM_EXCHANGE_NAME = "ly.item.exchange";
public static final class RoutingKey {
/**
* 商品上架的routing-key
*/
public static final String ITEM_UP_KEY = "item.up";
/**
* 商品下架的routing-key
*/
public static final String ITEM_DOWN_KEY = "item.down";
public static final class Queue{
/**
* 搜索服务,商品上架的队列
*/
public static final String SEARCH_ITEM_UP = "search.item.up.queue";
/**
* 搜索服务,商品下架的队列
*/
public static final String SEARCH_ITEM_DOWN = "search.item.down.queue";
/**
* 商品详情服务,商品上架的队列
*/
public static final String PAGE_ITEM_UP = "page.item.up.queue";
/**
* 商品详情服务,商品下架的队列
*/
public static final String PAGE_ITEM_DOWN = "page.item.down.queue";
}
}
这些常量我们用一个图来展示用在什么地方:
03、数据同步:商品微服务发送消息
我们先在商品微服务ly-item
中实现发送消息。
1)引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2)配置文件
我们在application.yml中添加一些有关RabbitMQ的配置:
spring:
rabbitmq:
host: 127.0.0.1
username: leyou
password: leyou
virtual-host: /leyou
publisher-confirms: true
template:
retry:
enabled: true
initial-interval: 10000ms
max-interval: 80000ms
multiplier: 2
- template:有关
AmqpTemplate
的配置- retry:失败重试
- enabled:开启失败重试
- initial-interval:第一次重试的间隔时长
- max-interval:最长重试间隔,超过这个间隔将不再重试
- multiplier:下次重试间隔的倍数,此处是2即下次重试间隔是上次的2倍
- exchange:缺省的交换机名称,此处配置后,发送消息如果不指定交换机就会使用这个
- retry:失败重试
- publisher-confirms:生产者确认机制,确保消息会正确发送,如果发送失败会有错误回执,从而触发重试
3)Json消息序列化方式(选做)
需要注意的是,默认情况下,AMQP会使用JDK的序列化方式进行处理,传输数据比较大,效率太低。我们可以自定义消息转换器,使用JSON来处理:
package com.leyou.item.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置json消息转换
*/
@Configuration
public class RabbitConfig {
@Bean
public Jackson2JsonMessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
4)改造GoodsService
改造GoodsService中的商品上下架功能,发送消息,注意用静态导入方式,导入在ly-common中定义的常量:
/**
* 商品上下架
*/
@PutMapping("/saleable")
public ResponseEntity<Void> updateSaleable(TbSpu tbSpu){
try {
spuService.updateById(tbSpu);
String routingKey = tbSpu.getSaleable()? MqConstants.RoutingKey.ITEM_UP_KEY:MqConstants.RoutingKey.ITEM_DOWN_KEY;
amqpTemplate.convertAndSend(MqConstants.Exchange.ITEM_EXCHANGE_NAME,routingKey,tbSpu.getId());
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}catch (Exception e) {
e.printStackTrace();
throw new LyException(ResultCode.UPDATE_OPERATION_FAIL);
}
}
注意:此刻不能启动项目测试,因为rabbitMQ中还没有交换机和队列。
04、数据同步:搜索微服务接收消息
搜索服务接收到消息后要做的事情:
- 上架:添加新的数据到索引库
- 下架:删除索引库数据
我们需要两个不同队列,监听不同类型消息。
1)引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2)添加配置
spring:
rabbitmq:
host: 127.0.0.1
username: leyou
password: leyou
virtual-host: /leyou
这里只是接收消息而不发送,所以不用配置template相关内容。
3)Json消息序列化配置
不过,不要忘了消息转换器(如果生产者没有加消息转换器,那么消费者也不要加):
package com.leyou.item.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yy
*/
@Configuration
public class RabbitConfig {
@Bean
public Jackson2JsonMessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
4)编写监听器
代码:
package com.leyou.search.repository.search.mq;
import com.leyou.commom.constants.MqConstants;
import com.leyou.search.repository.search.SearchRepository;
import com.leyou.search.repository.search.service.impl.SearchServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.search.SearchService;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author yy
* 监听商品上下架
*/
@Component
@Slf4j
public class ItemListener {
@Autowired
private SearchServiceImpl searchServiceImpl;
/**
* 上架->导入索引
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value= MqConstants.Queue.SEARCH_ITEM_UP),
exchange = @Exchange(name = MqConstants.Exchange.ITEM_EXCHANGE_NAME,type = ExchangeTypes.TOPIC),
key = MqConstants.RoutingKey.ITEM_UP_KEY
)
)
public void createIndex(Long spuId){
try {
searchServiceImpl.createIndex(spuId);
log.info("【索引同步】索引创建成功,ID:"+spuId);
} catch (Exception e) {
e.printStackTrace();
log.error("【索引同步】索引创建失败,原因:"+e.getMessage());
}
}
/**
* 下架->删除索引
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value= MqConstants.Queue.SEARCH_ITEM_DOWN),
exchange = @Exchange(name = MqConstants.Exchange.ITEM_EXCHANGE_NAME,type = ExchangeTypes.TOPIC),
key = MqConstants.RoutingKey.ITEM_DOWN_KEY
)
)
public void deleteIndex(Long spuId){
try {
searchServiceImpl.deleteIndex(spuId);
log.info("【索引同步】索引删除成功,ID:"+spuId);
} catch (Exception e) {
e.printStackTrace();
log.error("【索引同步】索引删除失败,原因:"+e.getMessage());
}
}
}
5)编写创建和删除索引方法
这里因为要创建和删除索引,我们需要在SearchService中拓展两个方法,创建和删除索引:
public void createIndex(Long spuId) {
//1.根据spuId查询SpuDTO
SpuDTO spuDTO = itemClient.findSpuDTOById(spuId);
//2.把SpuDTO转换为Goods
Goods goods = buildGoods(spuDTO);
//3.把商品存入ES索引库
searchRepository.save(goods);
}
public void deleteIndex(Long spuId) {
searchRepository.deleteById(spuId);
}
05、数据同步:静态页服务接收消息
商品静态页服务接收到消息后的处理:
- 上架:创建新的静态页
- 下架:删除原来的静态页
与前面搜索服务类似,也需要两个队列来处理。
1)引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2)添加配置
spring:
rabbitmq:
host: 127.0.0.1
username: leyou
password: leyou
virtual-host: /leyou
这里只是接收消息而不发送,所以不用配置template相关内容。
3)Json消息序列化配置
不过,不要忘了消息转换器:
package com.leyou.search.repository.search.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yy
*/
@Configuration
public class RabbitConfig {
@Bean
public Jackson2JsonMessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
4)编写监听器
代码:
package com.leyou.mq;
import com.leyou.commom.constants.MqConstants;
import com.leyou.service.impl.PageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author yy
* 监听商品上下架
*/
@Component
@Slf4j
public class ItemListener {
@Autowired
private PageService pageService;
/**
* 上架->创建静态页
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value= MqConstants.Queue.PAGE_ITEM_UP),
exchange = @Exchange(name = MqConstants.Exchange.ITEM_EXCHANGE_NAME,type = ExchangeTypes.TOPIC),
key = MqConstants.RoutingKey.ITEM_UP_KEY
)
)
public void createStaticPage(Long spuId){
try {
pageService.createStaticPage(spuId);
log.info("【静态页同步】静态页创建成功,ID:"+spuId);
} catch (Exception e) {
e.printStackTrace();
log.error("【静态页同步】静态页创建失败,原因:"+e.getMessage());
}
}
/**
* 下架->删除静态页
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value= MqConstants.Queue.PAGE_ITEM_DOWN),
exchange = @Exchange(name = MqConstants.Exchange.ITEM_EXCHANGE_NAME,type = ExchangeTypes.TOPIC),
key = MqConstants.RoutingKey.ITEM_DOWN_KEY
)
)
public void deleteStaticPage(Long spuId){
try {
pageService.deleteStaticPage(spuId);
log.info("【静态页同步】静态页删除成功,ID:"+spuId);
} catch (Exception e) {
e.printStackTrace();
log.error("【静态页同步】静态页删除失败,原因:"+e.getMessage());
}
}
}
5)添加删除页面方法
创建商品详情静态页面的方法我们之前已经添加过了,只需要添加删除静态页的方法即可:
/**
* 用于为每个商品生产静态页面
*/
public void createStaticPage(Long spuId){
//1)创建Context上下文对象,设置动态数据(Map集合)
Context context = new Context();
//设置动态数据
context.setVariables(getDetailData(spuId));
//2)设计一个模板页面(item.html) 注意:模板引擎对象默认会到classpath:templates目录下读取该文件
String tempName = itemTemplate+".html";
//3)使用模板引擎对象写出静态页面,定义输出流(输出到nginx服务器的目录中)
/**
* 参数一:模板页面名称(封装静态数据)
* 参数二:Context上下文对象(封装动态数据)
* 参数三:输出位置
*/
//特别注意:一旦使用输出流,记住必须关闭流,否则文件被锁定,无法被删除或修改。
PrintWriter writer = null;
try {
writer = new PrintWriter(new File(itemDir,spuId+".html"));
templateEngine.process(tempName,context,writer);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if(writer!=null)writer.close();
}
}
public void deleteStaticPage(Long spuId) {
//1.读取文件
File file = new File(itemDir,spuId+".html");
//2.删除文件
if(file.exists()){
file.delete();
}
}
06、数据同步:测试数据同步
查看RabbitMQ控制台
重新启动项目,并且登录RabbitMQ管理界面:http://127.0.0.1:15672
可以看到,交换机已经创建出来了:
队列也已经创建完毕:
并且队列都已经绑定到交换机:
第一次搜索:美图
后台下架其中一件商品:
第二次搜索,发现少了一件商品
07、用户注册:注册功能分析
完成了商品的详情展示,下一步自然是购物了。不过购物之前要完成用户的注册和登录等业务,我们打开注册页面分析下需求:
综上要完成注册功能需要的技术点有:
加密技术
redis技术(**)
mq消息中间件
发短信
08、用户注册:Redis的回顾
1)NoSQL
Redis是目前非常流行的一款NoSql数据库。
什么是NoSql?
常见的NoSql产品:
2)Redis简介
Redis的网址:
官网:速度很慢,几乎进去不去啊。
中文网站:有部分翻译的官方文档,英文差的同学的福音
历史:
特性:
3)Redis安装
解压完成后,双击redis-server.exe
即可,然后使用redis-destop-manager
链接即可。
4)Redis与Memcache
Redis和Memcache是目前非常流行的两种NoSql数据库,读可以用于服务端缓存。两者有怎样的差异呢?
- 从实现来看:
- redis:单线程
- Memcache:多线程
- 从存储方式来看:
- redis:支持数据持久化和主从备份,数据更安全
- Memcache:数据存于内存,没有持久化功能
- 从功能来看:
- redis:除了基本的k-v 结构外,支持多种其它复杂结构、事务等高级功能
- Memcache:只支持基本k-v 结构
- 从可用性看:
- redis:支持主从备份、数据分片、哨兵监控
- memcache:没有分片功能,需要从客户端支持
可以看出,Redis相比Memcache功能更加强大,支持的数据结构也比较丰富,已经不仅仅是一个缓存服务。而Memcache的功能相对单一。
一些面试问题:Redis缓存击穿问题、缓存雪崩问题。
5)Redis命令:help
通过help
命令可以让我们查看到Redis的指令帮助信息:
在help
后面跟上空格
,然后按tab
键,会看到Redis对命令分组的组名:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C0etm6Eb-1660034586645)(assets/test.gif)]
主要包含:
- @generic:通用指令
- @string:字符串类型指令
- @list:队列结构指令
- @set:set结构指令
- @sorted_set:可排序的set结构指令
- @hash:hash结构指令
其中除了@generic以为的,对应了Redis中常用的5种数据类型:
- String:等同于java中的,
Map<String,String>
- list:等同于java中的
Map<String,List<String>>
- set:等同于java中的
Map<String,Set<String>>
- sort_set:可排序的set
- hash:等同于java中的:
Map<String,Map<String,String>>
可见,Redis中存储数据结构都是类似java的map类型。Redis不同数据类型,只是'map'
的值的类型不同。
6)Redis命令:通用指令
keys
获取符合规则的键名列表。
-
语法:keys pattern
示例:keys * (查询所有的键)
这里的pattern其实是正则表达式,所以语法基本是类似的
exists
判断一个键是否存在,如果存在返回整数1,否则返回0
-
语法:EXISTS key
-
示例:
del
DEL:删除key,可以删除一个或多个key,返回值是删除的key的个数。
-
语法:DEL key [key … ]
-
示例:
expire
-
语法:
EXPIRE key seconds
-
作用:设置key的过期时间,超过时间后,将会自动删除该key。
-
返回值:
- 如果成功设置过期时间,返回1。
- 如果key不存在或者不能设置过期时间,返回0
TTL
TTL:查看一个key的过期时间
- 语法:
TTL key
- 返回值:
- 返回剩余的过期时间
- -1:永不过期
- -2:已过期或不存在
- 示例:
persist
-
语法:
persist key
-
作用:
移除给定key的生存时间,将这个 key 从带生存时间 key 转换成一个不带生存时间、永不过期的 key 。
-
返回值:
- 当生存时间移除成功时,返回 1 .
- 如果 key 不存在或 key 没有设置生存时间,返回 0 .
-
示例:
7)Redis命令:字符串指令
字符串结构,其实是Redis中最基础的K-V结构。其键和值都是字符串。类似Java的Map<String,String>
常用指令:
语法 | 说明 |
---|---|
SET key value | 设置指定 key 的值 |
GET key | 获取指定 key 的值。 |
GETRANGE key start end | 返回 key 中字符串值的子字符 |
INCR key | 将 key 中储存的数字值增一。 |
INCRBY key increment | 将 key 所储存的值加上给定的增量值(increment) 。 |
DECR key | 将 key 中储存的数字值减一。 |
DECRBY key decrement | key 所储存的值减去给定的减量值(decrement) 。 |
APPEND key value | 如果 key 已经存在并且是一个字符串, APPEND 命令将 value 追加到 key 原来的值的末尾。 |
STRLEN key | 返回 key 所储存的字符串值的长度。 |
MGET key1 key2 … | 获取所有(一个或多个)给定 key 的值。 |
MSET key value key value … | 同时设置一个或多个 key-value 对。 |
8)Redis命令:hash结构命令
Redis的Hash结构类似于Java中的Map<String,Map<String,Stgring>>,键是字符串,值是另一个映射。结构如图:
这里我们称键为key,字段名为 hKey, 字段值为 hValue
常用指令:
HSET、HSETNX和HGET(添加、获取)
HSET
-
介绍:
- Redis Hset 命令用于为哈希表中的字段赋值 。
- 如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作。
- 如果字段已经存在于哈希表中,旧值将被覆盖。
-
返回值:
- 如果字段是哈希表中的一个新建字段,并且值设置成功,返回 1 。
- 如果哈希表中域字段已经存在且旧值已被新值覆盖,返回 0
-
示例:
HGET
- 介绍:
Hget 命令用于返回哈希表中指定字段的值。
- 返回值:返回给定字段的值。如果给定的字段或 key 不存在时,返回 nil
- 示例:
HGETALL
-
介绍:
-
返回值:
指定key 的所有字段的名及值。返回值里,紧跟每个字段名(field name)之后是字段的值(value),所以返回值的长度是哈希表大小的两倍
- 示例:
HKEYS
- 介绍
- 示例:
HVALS
-
注意:这个命令不是HVALUES,而是HVALS,是value 的缩写:val
-
示例:
HDEL(删除)
Hdel 命令用于删除哈希表 key 中的一个或多个指定字段,不存在的字段将被忽略。
-
语法:
HDEL key field1 [field2 … ]
-
返回值:
被成功删除字段的数量,不包括被忽略的字段
-
示例:
10)Redis的持久化:RDB
Redis有两种持久化方案:RDB和AOF
触发条件
RDB是Redis的默认持久化方案,当满足一定的条件时,Redis会自动将内存中的数据全部持久化到硬盘。
条件在redis.conf文件中配置,格式如下:
save (time) (count)
当满足在time(单位是秒)时间内,至少进行了count次修改后,触发条件,进行RDB快照。
例如,默认的配置如下:
基本原理
RDB的流程是这样的:
- Redis使用fork函数来复制一份当前进程(父进程)的副本(子进程)
- 父进程继续接收并处理请求,子进程开始把内存中的数据写入硬盘中的临时文件
- 子进程写完后,会使用临时文件代替旧的RDB文件
11)Redis的持久化:AOF
基本原理
AOF方式默认是关闭的,需要修改配置来开启:
appendonly yes # 把默认的no改成yes
AOF持久化的策略是,把每一条服务端接收到的写命令都记录下来,每隔一定时间后,写入硬盘的AOF文件中,当服务器重启后,重新执行这些命令,即可恢复数据。
AOF文件写入的频率是可以配置的:
AOF文件重写
当记录命令过多,必然会出现对同一个key的多次写操作,此时只需要记录最后一条即可,前面的记录都毫无意义了。因此,当满足一定条件时,Redis会对AOF文件进行重写,移除对同一个key的多次操作命令,保留最后一条。默认的触发条件:
主从
09、用户注册:SpringDataRedis的使用(*)
之前,我们使用Redis都是采用的Jedis客户端,不过既然我们使用了SpringBoot,为什么不使用Spring对Redis封装的套件呢?
1)Spring Data Redis
官网:http://projects.spring.io/spring-data-redis/
Spring Data Redis,是Spring Data 家族的一部分。 对Jedis客户端进行了封装,与spring进行了整合。可以非常方便的来实现redis的配置和操作。
2)RedisTemplate基本操作
与以往学习的套件类似,Spring Data 为 Redis 提供了一个工具类:RedisTemplate。里面封装了对于Redis的五种数据结构的各种操作,包括:
- redisTemplate.opsForValue() :操作字符串 <key, String>
- redisTemplate.opsForHash() :操作hash <key, Map<String, Object>>
- redisTemplate.opsForList():操作list <key, List>
- redisTemplate.opsForSet():操作set <key, Set>
- redisTemplate.opsForZSet():操作zset <key, Set>
例如我们对字符串操作比较熟悉的有:get、set等命令,这些方法都在 opsForValue()返回的对象中有:
其它一些通用命令,如del,可以通过redisTemplate.xx()来直接调用。
3)StringRedisTemplate
RedisTemplate在创建时,可以指定其泛型类型:
- K:代表key 的数据类型
- V: 代表value的数据类型
注意:这里的类型不是Redis中存储的数据类型,而是Java中的数据类型,RedisTemplate会自动将Java类型转为Redis支持的数据类型:字符串、字节、二二进制等等。
不过RedisTemplate默认会采用JDK自带的序列化(Serialize)来对对象进行转换。生成的数据十分庞大,因此一般我们都会指定key和value为String类型,这样就由我们自己把对象序列化为json字符串来存储即可。
因为大部分情况下,我们都会使用key和value都为String的RedisTemplate,因此Spring就默认提供了这样一个实现:
4)测试
我们新建一个测试项目,然后在项目中引入Redis启动器:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou</groupId>
<artifactId>redis</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
然后在配置文件中指定Redis地址:
spring:
redis:
host: 127.0.0.1
编写启动类
package cn.itcast;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
*
*/
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class,args);
}
}
然后就可以直接注入StringRedisTemplate
对象了:
package com.ithiema;
import com.itheima.RedisApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class RedisDemo {
@Autowired
private StringRedisTemplate redisTemplate;//key和value都是字符串
/**
* 存入数据
* 如果我们的操作的简单数据(字符串,一个JavaBean对象)(不是集合类型的)
*/
@Test
public void testSet1(){
redisTemplate.opsForValue().set("name","jack");
//redisTemplate.opsForValue().set("age","18",20, TimeUnit.SECONDS);//20s过期
}
/**
* 存入数据
* 如果我们的操作的复杂数据(List,Set,ZSet,Hash)(属于集合类型的)
*/
@Test
public void testSet2(){
//redisTemplate.boundValueOps("name").set("rose");
BoundValueOperations<String, String> boundValueOps = redisTemplate.boundValueOps("name");
boundValueOps.set("lucy");
boundValueOps.expire(20,TimeUnit.SECONDS);
}
@Test
public void testGet(){
String name = redisTemplate.opsForValue().get("name");
System.out.println(name);
}
}
10、用户注册:加密技术
加密从大的范畴来说分两类:
- 密钥加密:提前生成密钥,安全级别高,比较复杂,一般用于文件类的加密。比如git。
- 加盐加密:直接指定加盐规则即可,灵活方便,适用于密码加密。从加盐的方式上又分两类,动态加盐加密和固定加盐加密,动态加盐加密更安全。
加盐加密:
- 固定加盐加密:加盐规则固定,反复加密结果是一样的,比如md5就可以实现固定加盐加密。
- 动态加盐加密:加盐规则是动态的,每次加密结构都不一样。比如spring的加密方式。多次加密的效果如下:导入工具包:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
package com.ithiema;
import org.junit.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordDemo {
/**
* 加密
* $2a$10$7DRujhhUuuGjIYmci0iUd.5ls3geak.nGY8JCibOG4VIe60hmjN2m
* $2a$10$PWlo1m0BKJlJcKPF0gA5e.xDgfHXa9jtkGMOwyfdzQbTn5v5Pf0ei
* $2a$10$qMikrmatspF5vPjIBvDpL.PSirqHGdAmORbq1yRunmZ/UGau.V13e
*/
@Test
public void testEncoder(){
String password = "123456";
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(password);
System.out.println(encode);
}
@Test
public void testMatches(){
String password = "12345";
String encoder = "$2a$10$qMikrmatspF5vPjIBvDpL.PSirqHGdAmORbq1yRunmZ/UGau.V13e";
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
Boolean result= passwordEncoder.matches(password,encoder);
System.out.println(result);
}
}
11、用户注册:开通阿里短信服务
1) 创建阿里云用户并授权
这一步,我们之前在使用阿里OSS服务时已经做过了,这里只需要给用户授予SMS短信服务权限即可。
2) 进入阿里SMS服务主界面,通过“快速入门”来准备使用SMS服务相关的操作。
由此处进入阿里云SMS主界面
3) 申请短信签名
现在不给未上线的应用使用签名,这边选用测试api。
12、用户注册:阿里在线短信调试
我们可以借助阿里的在线调试短信功能来测试我们的短信签名和模板。
进入测试界面:
点击如图所示SendSms,
填写调试短信的信息:
填入各类参数后,点击 发起调用
按钮即可,然后该手机会受到这样一条短信:
获取SDK代码示例:
// This file is auto-generated, don't edit it. Thanks.
package com.aliyun.sample;
import com.aliyun.tea.*;
import com.aliyun.dysmsapi20170525.*;
import com.aliyun.dysmsapi20170525.models.*;
import com.aliyun.teaopenapi.*;
import com.aliyun.teaopenapi.models.*;
import com.aliyun.teautil.*;
import com.aliyun.teautil.models.*;
public class Sample {
/**
* 使用AK&SK初始化账号Client
* @param accessKeyId
* @param accessKeySecret
* @return Client
* @throws Exception
*/
public static com.aliyun.dysmsapi20170525.Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
Config config = new Config()
// 您的 AccessKey ID
.setAccessKeyId(accessKeyId)
// 您的 AccessKey Secret
.setAccessKeySecret(accessKeySecret);
// 访问的域名
config.endpoint = "dysmsapi.aliyuncs.com";
return new com.aliyun.dysmsapi20170525.Client(config);
}
public static void main(String[] args_) throws Exception {
java.util.List<String> args = java.util.Arrays.asList(args_);
com.aliyun.dysmsapi20170525.Client client = Sample.createClient("accessKeyId", "accessKeySecret");
SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setSignName("阿里云短信测试")
.setTemplateCode("SMS_154950909")
.setPhoneNumbers("1548785")
.setTemplateParam("{\"code\":\"1234\"}");
RuntimeOptions runtime = new RuntimeOptions();
try {
// 复制代码运行请自行打印 API 的返回值
client.sendSmsWithOptions(sendSmsRequest, runtime);
} catch (TeaException error) {
// 如有需要,请打印 error
com.aliyun.teautil.Common.assertAsString(error.message);
} catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error);
// 如有需要,请打印 error
com.aliyun.teautil.Common.assertAsString(error.message);
}
}
}
以上代码依赖的maven包:
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.1</version>
</dependency>
13、用户注册:搭建短信微服务
因为系统中不止注册一个地方需要短信发送,因此我们将短信发送抽取为微服务:ly-sms
,凡是需要的地方都可以使用。
另外,因为短信发送API调用时长的不确定性,为了提高程序的响应速度,短信发送我们都将采用异步发送方式,即:
- 短信服务监听MQ消息,收到消息后发送短信。
- 其它服务要发送短信时,通过MQ通知短信微服务。
1)创建module
2)依赖坐标
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyou</artifactId>
<groupId>com.leyou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ly-sms</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.leyou</groupId>
<artifactId>ly-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3)编写启动类
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LySmsApplication {
public static void main(String[] args) {
SpringApplication.run(LySmsApplication.class, args);
}
}
4)编写application.yml
server:
port: 8085
spring:
application:
name: sms-service
rabbitmq:
host: 127.0.0.1
username: leyou
password: leyou
virtual-host: /leyou
14、用户注册:封装短信工具
1) 获取SMS发短信的代码
具体代码如下:
// This file is auto-generated, don't edit it. Thanks.
package com.aliyun.sample;
import com.aliyun.tea.*;
import com.aliyun.dysmsapi20170525.*;
import com.aliyun.dysmsapi20170525.models.*;
import com.aliyun.teaopenapi.*;
import com.aliyun.teaopenapi.models.*;
public class Sample {
/**
* 使用AK&SK初始化账号Client
* @param accessKeyId
* @param accessKeySecret
* @return Client
* @throws Exception
*/
public static com.aliyun.dysmsapi20170525.Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
Config config = new Config()
// 您的AccessKey ID
.setAccessKeyId(accessKeyId)
// 您的AccessKey Secret
.setAccessKeySecret(accessKeySecret);
// 访问的域名
config.endpoint = "dysmsapi.aliyuncs.com";
return new com.aliyun.dysmsapi20170525.Client(config);
}
public static void main(String[] args_) throws Exception {
java.util.List<String> args = java.util.Arrays.asList(args_);
com.aliyun.dysmsapi20170525.Client client = Sample.createClient("accessKeyId", "accessKeySecret");
SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setSignName("阿里云短信测试")
.setTemplateCode("SMS_154950909")
.setPhoneNumbers("1548785")
.setTemplateParam("{\"code\":\"1234\"}");
client.sendSms(sendSmsRequest);
}
}
2)抽取短信相关的属性
我们首先把一些常量抽取到application.yml中:
ly:
sms:
accessKeyID: ****************# 你自己的accessKeyId
accessKeySecret: **************** # 你自己的AccessKeySecret
endpoint: dysmsapi.aliyuncs.com
signName: 阿里云短信测试 # 签名名称
verifyCodeTemplate: SMS_154950909 # 模板名称
code: code # 短信模板中验证码的占位符
然后提供解析配置文件的配置类:
package com.leyou.sms.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 读取配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "ly.sms")
public class SmsProperties {
/**
* 账号
*/
String accessKeyID;
/**
* 密钥
*/
String accessKeySecret;
/**
* 访问API
*/
String endpoint;
/**
* 短信签名
*/
String signName;
/**
* 短信模板
*/
String verifyCodeTemplate;
/**
* 短信模板中验证码的占位符
*/
String code;
}
3)阿里客户端交给spring管理
首先,把发请求需要的客户端注册到Spring容器:
package com.leyou.config;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.teaopenapi.models.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 初始化短信API的对象
*/
@Configuration
public class SmsConfig {
@Bean
public Client createClient(SmsProperties smsProps) throws Exception {
Config config = new Config()
// 您的AccessKey ID
.setAccessKeyId(smsProps.getAccessKeyID())
// 您的AccessKey Secret
.setAccessKeySecret(smsProps.getAccessKeySecret());
// 访问的域名
config.endpoint = smsProps.getEndpoint();
return new Client(config);
}
}
5)发送短信工具类
我们把阿里提供的demo进行简化和抽取,封装一个工具类:
package com.leyou.sms.utils;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.leyou.sms.config.SmsProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 短信发送工具类
*/
@Component
@Slf4j
public class SmsHelper {
@Autowired
private SmsProperties smsProps;
@Autowired
private Client client;
/**
* 发送短信验证码
*/
public void sendVerifyCode(String phone,String code){
try {
SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName(smsProps.getSignName())
.setTemplateCode(smsProps.getVerifyCodeTemplate())
.setTemplateParam("{\""+smsProps.getCode()+"\":\""+code+"\"}");
SendSmsResponse smsResponse = client.sendSms(sendSmsRequest);
String respCode = smsResponse.body.code;
String message = smsResponse.body.message;
if(respCode.equals("OK")){
log.info("【短信API】短信发送成功");
}else{
log.error("【短信API】短信发送失败,原因:"+message);
}
} catch (Exception e) {
e.printStackTrace();
log.error("【短信API】短信发送失败,原因:"+e.getMessage());
}
}
}
15、用户注册:MQ监听器异步发送短信
接下来,编写消息监听器,当接收到消息后,我们发送短信。
1)改造ly-common中的MQ常量
不要忘了,几个队列和交换机的名称,定义到ly-common
中:
package com.leyou.commom.constants;
/**
* @author yy
*/
public class MqConstants {
public static final class Exchange {
/**
* 商品服务交换机名称
*/
public static final String ITEM_EXCHANGE_NAME = "ly.item.exchange";
/**
* 消息服务交换机名称
*/
public static final String SMS_EXCHANGE_NAME = "ly.sms.exchange";
}
public static final class RoutingKey {
/**
* 商品上架的routing-key
*/
public static final String ITEM_UP_KEY = "item.up";
/**
* 商品下架的routing-key
*/
public static final String ITEM_DOWN_KEY = "item.down";
/**
* 消息服务的routing-key
*/
public static final String VERIFY_CODE_KEY = "sms.verify.code";
}
public static final class Queue{
/**
* 搜索服务,商品上架的队列
*/
public static final String SEARCH_ITEM_UP = "search.item.up.queue";
/**
* 搜索服务,商品下架的队列
*/
public static final String SEARCH_ITEM_DOWN = "search.item.down.queue";
/**
* 商品详情服务,商品上架的队列
*/
public static final String PAGE_ITEM_UP = "page.item.up.queue";
/**
* 商品详情服务,商品下架的队列
*/
public static final String PAGE_ITEM_DOWN = "page.item.down.queue";
/**
* 消息服务的队列
*/
public static final String SMS_VERIFY_CODE_QUEUE = "sms.verify.code.queue";
}
}
###2)编写监听器
在ly-sms
项目编写短信消息监听器:
这边因为测试的api code只支持6为数组,不支持中文,这边处理一下
package com.leyou.mq;
import com.leyou.common.constants.MQConstants;
import com.leyou.utils.SmsHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/*
监听短信
*/
@Component
@Slf4j
public class SmsListener {
@Autowired
private SmsHelper smsHelper;
/**
* 发送短信验证码
*/
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value= MQConstants.Queue.SMS_VERIFY_CODE_QUEUE),
exchange = @Exchange(name = MQConstants.Exchange.SMS_EXCHANGE_NAME,type = ExchangeTypes.TOPIC),
key = MQConstants.RoutingKey.VERIFY_CODE_KEY
)
)
public void sendVerifyCode(Map<String,String> msgMap){
String phone = (String) msgMap.get("phone");
Integer code = (Integer) msgMap.get("code");
smsHelper.sendVerifyCode(phone,code);
}
}
我们注意到,消息体是一个Map,里面有两个属性:
- phone:电话号码
- code:短信验证码
3)单元测试
import com.leyou.LySmsApplication;
import com.leyou.commom.constants.MqConstants;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.HashMap;
import java.util.Map;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LySmsApplication.class)
public class SmsDemo {
@Autowired
private AmqpTemplate amqpTemplate;
@Test
public void testSendMsg(){
Integer a = 123456;
Map<String,Object> msgMap = new HashMap<>();
msgMap.put("phone","****");
msgMap.put("code",a);
amqpTemplate.convertAndSend(
MqConstants.Exchange.SMS_EXCHANGE_NAME,
MqConstants.RoutingKey.VERIFY_CODE_KEY,
msgMap);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
查看RabbitMQ控制台,发现交换机已经创建:
16、用户注册:搭建用户中心
1) 创建ly-pojo-user对象模块
2) 创建ly-user工程并导入jar包
pom文件加入依赖坐标
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyou</artifactId>
<groupId>com.leyou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ly-user</artifactId>
<dependencies>
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!-- 实体类包 -->
<dependency>
<groupId>com.leyou</groupId>
<artifactId>ly-pojo-user</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--mq消息中间件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.leyou</groupId>
<artifactId>ly-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3) 提供启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.leyou.user.mapper")
public class LyUserApplication {
public static void main(String[] args) {
SpringApplication.run(LyUserApplication.class,args);
}
}
4) 提供配置文件
server:
port: 8086
spring:
application:
name: user-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/leyou?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
username: root
password: root
redis:
host: 127.0.0.1
rabbitmq:
host: 127.0.0.1
virtual-host: /leyou
username: leyou
password: leyou
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
mybatis-plus:
type-aliases-package: com.leyou.user.entity
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.leyou: debug
5) 添加路由网关
我们修改ly-gateway
配置,添加路由规则,对ly-user
进行路由:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GsXBYbSr-1660034586657)(assets/1577329581013.png)]
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=2
17、用户注册:后台代码准备
1)这边使用一开始的代码生成工具生成实体,service,impl,controller
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入" + tip + ":");
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
String hostUri = "jdbc:mysql://localhost:3306/leyou?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8";
String userName = "root";
String passWord = "root";
String author = "yy";
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/ly-user/src/main/java");
gc.setAuthor(author);
gc.setOpen(false);
// 设置名字
gc.setControllerName("%sController");
gc.setServiceName("%sService");
gc.setServiceImplName("%sServiceImpl");
gc.setMapperName("%sMapper");
gc.setXmlName("%sMapper");
// 设置 resultMap
gc.setBaseResultMap(true);
gc.setBaseColumnList(true);
// gc.setFileOverride(true);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl(hostUri);
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername(userName);
dsc.setPassword(passWord);
mpg.setDataSource(dsc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 包配置
PackageConfig pc = new PackageConfig();
// pc.setModuleName(scanner("模块名"));
pc.setParent("com.leyou.user");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);
// 如果模板引擎是 velocity
String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/ly-user/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 写于父类中的公共字段
// strategy.setSuperEntityColumns("id");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new VelocityTemplateEngine());
mpg.execute();
}
}
完成之后将实体移到上边所创建的地方去
18、用户注册:校验数据是否唯一
1)接口说明
接口路径
GET /check/{data}/{type}
参数说明
参数 | 说明 | 是否必须 | 数据类型 | 默认值 |
---|---|---|---|---|
data | 要校验的数据 | 是 | String | 无 |
type | 要校验的数据类型:1,用户名;2,手机 | 是 | Integer | 无 |
返回结果
返回布尔类型结果:
- true:可用
- false:不可用
状态码:
- 200:校验成功
- 400:参数有误
- 500:服务器内部异常
2)Controller
因为有了接口,我们可以不关心页面,所有需要的东西都一清二楚:
- 请求方式:GET
- 请求路径:/check/{param}/{type}
- 请求参数:param,type
- 返回结果:true或false
package com.leyou.user.controller;
import com.leyou.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
private UserService userService;
/**
* 校验用户名和手机号是否唯一
*/
@GetMapping("/check/{data}/{type}")
public ResponseEntity<Boolean> checkData(@PathVariable("data") String data,@PathVariable("type") Integer type){
Boolean isCanUse = userService.checkData(data,type);
return ResponseEntity.ok(isCanUse);
}
}
3)Service
package com.leyou.user.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.leyou.user.mapper.UserMapper;
import com.leyou.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class UserService {
@Autowired
private UserMapper userMapper;
public Boolean checkData(String data, Integer type) {
//1.封装条件
User user = new User();
switch (type){
case 1:
user.setUsername(data);
break;
case 2:
user.setPhone(data);
break;
}
QueryWrapper<User> queryWrapper = Wrappers.query(user);
//2.执行查询,返回数据
return baseMapper.selectCount(queryWrapper)==0;
}
}
4)测试
我们查看数据库中的数据:
然后启动微服务,在浏览器调用接口,测试用户名:
测试手机号码:
19、用户注册:发送短信验证码
短信微服务已经准备好,我们就可以继续编写用户中心接口了。
1)思路分析
这里的业务逻辑是这样的:
- 1)我们接收页面发送来的手机号码
- 2)生成一个随机验证码
- 3)将验证码保存在服务端
- 4)发送短信,将验证码发送到用户手机
那么问题来了:验证码保存在哪里呢?
验证码有一定有效期,一般是5分钟,我们可以利用Redis的过期机制来保存。
2)接口文档
功能说明
用户输入手机号,点击发送验证码,前端会把手机号发送到服务端。服务端生成随机验证码,长度为6位,纯数字。并且调用短信服务,发送验证码到用户手机。
步骤:
- 接收用户请求,手机号码
- 验证手机号格式
- 生成验证码
- 保存验证码到redis
- 发送RabbitMQ消息到ly-sms
接口路径
POST /code
参数说明
参数 | 说明 | 是否必须 | 数据类型 | 默认值 |
---|---|---|---|---|
phone | 用户的手机号码 | 是 | String | 无 |
返回结果
无
状态码:
- 204:请求已接收
- 400:参数有误
- 500:服务器内部异常
3)Controller
/**
* 发送手机验证码
*/
@PostMapping("/code")
public ResponseEntity<Void> sendVerifyCode(@RequestParam("phone") String phone){
userService.sendVerifyCode(phone);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
4)在ly-common常量类中提供注册时短信验证码在redis中的key的前缀
/*注册时短信验证码在redis中的key的前缀*/
public static final String REDIS_KEY_PRE = "REDIS_KEY_PRE";
5)Service
@Override
public void sendVerifyCode(String phone) {
//1.生成随机6位数字的验证码
String code = RandomStringUtils.randomNumeric(6);
//2.把验证码存入redis,设置5分钟过期
redisTemplate.opsForValue().set(LyConstants.REDIS_KEY_PRE+phone,code,5, TimeUnit.MINUTES);
//3.发消息给MQ
Map<String,Object> msMap = new HashMap<>();
msMap.put("phone",phone);
int i = Integer.parseInt(code);
msMap.put("code",i);
amqpTemplate.convertAndSend(
MqConstants.Exchange.SMS_EXCHANGE_NAME,
MqConstants.RoutingKey.VERIFY_CODE_KEY,
msMap
);
}
20、用户注册:编写用户注册业务
1)接口说明
功能说明
用户页面填写数据,发送表单到服务端,服务端实现用户注册功能。需要对用户密码进行加密存储,使用MD5加密,加密过程中使用随机码作为salt加盐。另外还需要对用户输入的短信验证码进行校验。
- 验证短信验证码
- 生成盐
- 对密码加密
- 写入数据库
接口路径
POST /register
参数说明
form表单格式
参数 | 说明 | 是否必须 | 数据类型 | 默认值 |
---|---|---|---|---|
username | 用户名,格式为4~30位字母、数字、下划线 | 是 | String | 无 |
password | 用户密码,格式为4~30位字母、数字、下划线 | 是 | String | 无 |
phone | 手机号码 | 是 | String | 无 |
code | 短信验证码 | 是 | String | 无 |
返回结果
无返回值。
状态码:
- 201:注册成功
- 400:参数有误,注册失败
- 500:服务器内部异常,注册失败
2)Controller
/**
* 用户注册
*/
@PostMapping("/register")
public ResponseEntity<Void> register(User user,@RequestParam("code") String code){
userService.register(user,code);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
3)在用户微服务中提供加密对象
public void register(User user, String code) {
//1.取出redis保存的验证码
String redisCode = redisTemplate.opsForValue().get(LyConstants.REDIS_KEY_PRE + user.getPhone());
//2.判断和用户输入的验证码是否一致
if(redisCode==null || !redisCode.equals(code)){
throw new LyException(ExceptionEnum.INVALID_VERIFY_CODE);
}
try {
//3.对密码加盐加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
//4.保存用户表数据
userMapper.insert(user);
} catch (Exception e) {
e.printStackTrace();
throw new LyException(ExceptionEnum.INSERT_OPERATION_FAIL);
}
}
4)Service
基本逻辑:
- 1)校验短信验证码
- 2)对密码加密
- 3)写入数据库
代码:
public void register(User user, String code) {
//1.从redis取出验证码
String redisCode = redisTemplate.opsForValue().get("VERIFY_" + user.getPhone());
//2.判断和用户输入的是否一致
if(redisCode==null || !redisCode.equals(code)){
throw new LyException(ExceptionEnum.INVALID_VERIFY_CODE);
}
//3.对密码加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
try {
//4.保存用户数据到数据库
userMapper.insert(user);
} catch (Exception e) {
e.printStackTrace();
throw new LyException(ExceptionEnum.INSERT_OPERATION_FAIL);
}
}