paascloud--用户邮箱注册流程
用户注册
- github 开源项目–
paascloud-master
:https://github.com/paascloud/paascloud-master - 分布式解决方案–
基于可靠消息的最终一致性
:https://github.com/paascloud/paascloud-master/wiki/可靠消息
本篇文章目的是理解该项目可靠消息服务中心(TCP)发送消息、消费消息的流程,用户注册发送激活邮箱和激活后发送注册成功邮箱都是利用可靠消息服务来解决分布式事务,理解了该流程也就弄懂了该项目中其他业务流程。
发送激活邮箱过程
- 消息生产端:
UAC
- 可靠消息服务:
TPC
- 消息服务端:
OPC
用户注册后,向注册邮箱发送一封激活邮箱。
消息生产端(UAC)
大致流程为:
- 本地服务
UAC
先持久化预发送消息
(等待确认消息),表pc_mq_message_data
; - 调用远端可靠消息服务
TPC
持久化预发送消息
,可靠消息表pc_tpc_mq_message
; - 执行本地事务即
保存用户信息
; - 调用远端可靠消息服务
TPC
更新第2步中的等待确认
状态为发送中sending
; - 同时创建消费待确认列表,即持久化该
Topic
类型的消息被哪些消费者订阅监听的所有消费待确认列表,状态为未确认
,表pc_tpc_mq_confirm
; - 完成上面操作后,发送消息到
RocketMQ
。
controller层
- AuthRestController.java
@PostMapping(value = "/register")
@ApiOperation(httpMethod = "POST", value = "注册用户")
public Wrapper registerUser(UserRegisterDto user) {
uacUserService.register(user);
return WrapMapper.ok();
}
service层
- 用户ID生成:雪花算法生成分布式唯一 ID
- 用户密码加密:
SpringSecurity
BCryptPasswordEncoder
强哈希方法加密,每次加密的结果都不一样。
bcrypt
可以有效抵御彩虹表暴力破解,其原理就是在加盐的基础上多次 hash,关于密码参考:https://mp.weixin.qq.com/s/DkHlZs1HgZmGC9r7WaEDeQ
- Redis存储激活邮箱token:
key(active_token):email:过期时间1天
,即激活接口参数:activeUserToken
; - 生成邮件发送模板(freeMarker):
activeUserTemplate.ftl
- 根据上面模板和发送邮件参数生成实体:
MqMessageData(pc_mq_message_data)
各个子系统消息落地的消息表,比如用户服务系统主要就是邮件消息、短信消息等。
@Override
public void register(UserRegisterDto registerDto) {
// 校验注册信息
validateRegisterInfo(registerDto);
String mobileNo = registerDto.getMobileNo();
String email = registerDto.getEmail();
Date row = new Date();
String salt = String.valueOf(generateId());
// 封装注册信息
long id = generateId(); // id 雪花算法生成
UacUser uacUser = new UacUser();
uacUser.setLoginName(registerDto.getLoginName());
uacUser.setSalt(salt);
uacUser.setLoginPwd(Md5Util.encrypt(registerDto.getLoginPwd()));
uacUser.setMobileNo(mobileNo);
uacUser.setStatus(UacUserStatusEnum.DISABLE.getKey());
uacUser.setUserSource(UacUserSourceEnum.REGISTER.getKey());
uacUser.setCreatedTime(row);
uacUser.setUpdateTime(row);
uacUser.setEmail(email);
uacUser.setId(id);
uacUser.setCreatorId(id);
uacUser.setCreator(registerDto.getLoginName());
uacUser.setLastOperatorId(id);
uacUser.setUserName(registerDto.getLoginName());
uacUser.setLastOperator(registerDto.getLoginName());
// 发送激活邮件
String activeToken = PubUtils.uuid() + super.generateId();
redisService.setKey(RedisKeyUtil.getActiveUserKey(activeToken), email, 1, TimeUnit.DAYS);
Map<String, Object> param = Maps.newHashMap();
param.put("loginName", registerDto.getLoginName());
param.put("email", registerDto.getEmail());
param.put("activeUserUrl", activeUserUrl + activeToken);
param.put("dateTime", DateUtil.formatDateTime(new Date()));
Set<String> to = Sets.newHashSet();
to.add(registerDto.getEmail());
MqMessageData mqMessageData = emailProducer.sendEmailMq(to, UacEmailTemplateEnum.ACTIVE_USER, AliyunMqTopicConstants.MqTagEnum.ACTIVE_USER, param);
// 即下面的第6步
userManager.register(mqMessageData, uacUser);
}
userManager.register()
通过注解@MqProducerStore
发送消息服务。
执行该方法前,先进入切面编程
@MqProducerStore
public void register(final MqMessageData mqMessageData, final UacUser uacUser) {
log.info("注册用户. mqMessageData={}, user={}", mqMessageData, uacUser);
uacUserMapper.insertSelective(uacUser);
}
@Target({
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MqProducerStore {
// WAIT_CONFIRM:等待确认;SAVE_AND_SEND:直接发送;
MqSendTypeEnum sendType() default MqSendTypeEnum.WAIT_CONFIRM;
// ORDER(1):有序;DIS_ORDER(0):无序
MqOrderTypeEnum orderType() default MqOrderTypeEnum.ORDER;
// Rocketmq 默认延时级别
// ZERO(0, 不延时);ONE(1, 1秒)....EIGHTEEN(18, 2小时)
DelayLevelEnum delayLevel() default DelayLevelEnum.ZERO;
}
- 切面中,因为邮件激活发送消息类型为默认的:
等待确认
。- 此处本地服务
UAC
消息落地:保存待确认消息 MqMessageData 到mysql
,表pc_mq_message_data
; - 发送待确认消息到可靠消息系统(TPC):发送预发送状态的消息给消息中心
- 此处本地服务
// 切面
MqMessageData domain = null;
for (Object object : args) {
if (object instanceof MqMessageData) {
domain = (MqMessageData) object;
break;
}
}
domain.setOrderType(orderType);
domain.setProducerGroup(producerGroup);
// 1. 等待确认
if (type == MqSendTypeEnum.WAIT_CONFIRM) {
if (delayLevelEnum != DelayLevelEnum.ZERO) {
domain.setDelayLevel(delayLevelEnum.delayLevel());
}
// 1.1 发送待确认消息到可靠消息系统
// 本地服务消息落地,可靠消息服务中心也持久化预发送消息,但是不发送
mqMessageService.saveWaitConfirmMessage(domain);
}
result = joinPoint.proceed(); // 返回注解方法,执行业务
@Override
@Transactional(rollbackFor = Exception.class)
public void saveWaitConfirmMessage(final MqMessageData mqMessageData) {
// 1. 持久化到本地mysql
this.saveMqProducerMessage(mqMessageData);
// 2. 发送预发送状态的消息给消息中心
TpcMqMessageDto tpcMqMessageDto = mqMessageData.getTpcMqMessageDto();
// 3. 调用远端可靠消息服务(tpc),持久化等待确认消息
tpcMqMessageFeignApi.