paascloud学习系列四:用户注册

本文详细解析了注册过程中的验证码生成、分布式唯一ID生成(雪花算法)、注册逻辑以及邮件发送。首先,后台服务启动涉及多个模块如Eureka、Discovery、Provider等。接着,验证码的获取通过ValidateCodeProcessor生成并存储到Redis。注册时,雪花算法生成的ID用于用户ID,同时生成activeToken保存在Redis。最后,通过RocketMQ发送邮件消息到消息队列,等待后续处理。整个流程涵盖了前端交互、后端逻辑、数据库操作及消息队列的使用。

一、启动后台服务

  1. 启动paascloud-eureka
  2. 启动paascloud-discovery
  3. 启动paascloud-provider-uac
  4. 启动paascloud-provider-tpc
  5. 启动paascloud-provider-mdc
  6. 启动paascloud-gateway
    在这里插入图片描述

二、启动前台系统

1.启动paascloud-login-web系统

在这里插入图片描述
在这里插入图片描述

2.修复验证码不显示

正常启动后台系统之后(相关的域名改为localhost),前端请求网关地址7979能正常路由到/uac下,就可正常显示验证码
在这里插入图片描述

三、注册逻辑

1、获取验证码

路径:paascloud-login-web\src\views\auth\user-register\index.vue

1.页面请求

created () {
 this.getImage();
},
mounted () {
},
methods: {
  getImage () {
    this.deviceId = new Date().getTime();
    let that = this;
    that.$http({
      method: 'POST',
      url: '/uac/auth/code/image',
      headers: {
        'deviceId': that.deviceId
      }
    }).then((res) => {
      that.imageCode = 'data:image/jpg;base64,' + res.result;
    });
  },
  ......
  }

在create()中调用getImage(),请求获取验证码的方法(上一篇已经介绍了)。

2.页面后台处理

在这里插入图片描述
调用paascloud-security-core模块下ValidateCodeController

@PostMapping(SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/{type}")
public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws Exception {
	validateCodeProcessorHolder.findValidateCodeProcessor(type).create(new ServletWebRequest(request, response));
}
//String DEFAULT_VALIDATE_CODE_URL_PREFIX = "/auth/code";
findValidateCodeProcessor()
ValidateCodeProcessor findValidateCodeProcessor(String type) {
	//1)获取name值
	String name = type.toLowerCase() + ValidateCodeProcessor.class.getSimpleName();
	//2)validateCodeProcessors处理器集合
	ValidateCodeProcessor processor = validateCodeProcessors.get(name);
	if (processor == null) {
		throw new ValidateCodeException("验证码处理器" + name + "不存在");
	}
	return processor;
}
1)获取name值
ValidateCodeProcessor.class.getSimpleName();

在这里插入图片描述
在Class.java类中的getName()方法中,因为是ValidateCodeProcessor.class调用。所以this.name返回ValidateCodeProcessor字符串

  public String getName() {
      String name = this.name;
      if (name == null)
          this.name = name = getName0();
      return name;
  }

由此name = imageValidateCodeProcessor

2)validateCodeProcessors处理器集合

在ValidateCodeProcessorHolder.java中

private final Map<String, ValidateCodeProcessor> validateCodeProcessors;

@Autowired
public ValidateCodeProcessorHolder(Map<String, ValidateCodeProcessor> 	validateCodeProcessors) {
	this.validateCodeProcessors = validateCodeProcessors;
}

可见validateCodeProcessors 处理器集合是成员变量,且被注入。@Autowired放在构造器上,是使用构造器的方式进行注入。
在这里插入图片描述
在这里插入图片描述

以EmailCodeProcessor为例,继承了AbstractValidateCodeProcessor类,AbstractValidateCodeProcessor实现了ValidateCodeProcessor。由此
在注入Map<String, ValidateCodeProcessor> 时,会将ValidateCodeProcessor接口下所有被spring管理的子类进行注入,且放入map中。

@Component("emailValidateCodeProcessor")
public class EmailCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {}

@Component("imageValidateCodeProcessor")
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {}

@Component("smsValidateCodeProcessor")
@Slf4j
public class SmsCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {}

Map<String, ValidateCodeProcessor> validateCodeProcessors 中,Map集合中的String表示Component注册spring的名称,接口表示具体类。

a)ValidateCodeGenerator处理器集合

以ValidateCodeProcessor子类中的ImageCodeProcessor为例,构造函数:

public ImageCodeProcessor(Map<String, ValidateCodeGenerator> validateCodeGenerators, ValidateCodeRepository validateCodeRepository) {
	super(validateCodeGenerators, validateCodeRepository);
}

super()方法调用父类AbstractValidateCodeProcessor中的构造方法

/**
 * 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。
 */
private final Map<String, ValidateCodeGenerator> validateCodeGenerators;

private final ValidateCodeRepository validateCodeRepository;
	
@Autowired
public AbstractValidateCodeProcessor(Map<String, ValidateCodeGenerator> validateCodeGenerators,
									 ValidateCodeRepository validateCodeRepository) {
	this.validateCodeGenerators = validateCodeGenerators;
	this.validateCodeRepository = validateCodeRepository;
}

和ValidateCodeProcessors一样,Map<String, ValidateCodeGenerator> validateCodeGenerators注入的也是ValidateCodeGenerator接口的被spring管理的所有子类。
在这里插入图片描述

@Component("emailValidateCodeGenerator")
public class EmailCodeGenerator implements ValidateCodeGenerator {}

public class ImageCodeGenerator implements ValidateCodeGenerator {}

@Component("smsValidateCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {}

Map<String, ValidateCodeGenerator> validateCodeGenerators 中key就是各实现类中Component的name,value就是各个子实现类。

注意:这里的ImageCodeGenerator 类没有用@Component注解修饰,类中成员变量SecurityProperties 和Producer 也没使用@Autowired修饰,表示隐式注入。

public class ImageCodeGenerator implements ValidateCodeGenerator {
	private SecurityProperties securityProperties;
	private Producer captchaProducer;
}

等价于:

@Component("imageValidateCodeGenerator")
public class ImageCodeGenerator implements ValidateCodeGenerator {

	@Autowired
	private SecurityProperties securityProperties;
	@Autowired
	private Producer captchaProducer;
}

还是以ImageCodeGenerator 为例

public class ImageCodeGenerator implements ValidateCodeGenerator {

	private SecurityProperties securityProperties;
	private Producer captchaProducer;

	/**
	 * 生成图片验证码.
	 *
	 * @param request the request
	 *
	 * @return the image code
	 */
	@Override
	public ImageCode generate(ServletWebRequest request) {
		String kaptchaCode = captchaProducer.createText();
		BufferedImage image = captchaProducer.createImage(kaptchaCode);
		return new ImageCode(image, kaptchaCode, securityProperties.getCode().getImage().getExpireIn());
	}

	/**
	 * Sets security properties.
	 *
	 * @param securityProperties the security properties
	 */
	public void setSecurityProperties(SecurityProperties securityProperties) {
		this.securityProperties = securityProperties;
	}

	/**
	 * Sets captcha producer.
	 *
	 * @param captchaProducer the captcha producer
	 */
	public void setCaptchaProducer(Producer captchaProducer) {
		this.captchaProducer = captchaProducer;
	}


主要是对成员变量securityProperties和captchaProducer进行赋值。

b)SecurityProperties 安全配置
@ConfigurationProperties(prefix = "paascloud.security")
public class SecurityProperties {
	private BrowserProperties browser = new BrowserProperties();
	private ValidateCodeProperties code = new ValidateCodeProperties();
	private SocialProperties social = new SocialProperties();
	private OAuth2Properties oauth2 = new OAuth2Properties();
......
}
paascloud:
  security:
    code:
      image:
        expireIn: 180
        url: /auth/register,/auth/submitResetPwdEmail,/auth/code/sms
      sms:
        expireIn: 600
        url: /uac/auth/submitResetPwdPhone
        mobileMaxSendCount: 1
        ipMaxSendCount: 2
        totalMaxSendCount: 3

SecurityProperties 主要对成员类 ValidateCodeProperties进行赋值,在 @ConfigurationProperties修饰下,相对应的配置在yml中paascloud.security下。将paascloud.security.code中的值赋值到ValidateCodeProperties 中

c)ValidateCodeProperties配置
@Data
public class ValidateCodeProperties {

	/**
	 * 图片验证码配置
	 */
	private ImageCodeProperties image = new ImageCodeProperties();
	/**
	 * 短信验证码配置
	 */
	private SmsCodeProperties sms = new SmsCodeProperties();
	/**
	 * 邮箱验证码配置
	 */
	private EmailCodeProperties email = new EmailCodeProperties();

}

ValidateCodeProperties 有三个子成员类,其中ImageCodeProperties继承SmsCodeProperties

@Data
public class SmsCodeProperties {
	/**
	 * 验证码长度
	 */
	private int length = 6;
	/**
	 * 过期时间
	 */
	private int expireIn = 60;
	/**
	 * 要拦截的url,多个url用逗号隔开,ant pattern
	 */
	private String url;
	/**
	 * 每天每个手机号最大送送短信数量
	 */
	private int mobileMaxSendCount;
	/**
	 * 每天每个IP最大送送短信数量
	 */
	private int ipMaxSendCount;
	/**
	 * 每天最大送送短信数量
	 */
	private int totalMaxSendCount;
}

所以paascloud.security.code.sms封装到SmsCodeProperties中,paascloud.security.code.image封装到ImageCodeProperties 中,由于没有配置paascloud.security.code.email 所以EmailCodeProperties 对应默认参数。

综上,validateCodeProcessors表示三个加载器的集合,三个加载器分别为

  • emailValidateCodeProcessor
  • imageValidateCodeProcessor
  • smsValidateCodeProcessor

在这里插入图片描述
在这里插入图片描述
validateCodeProcessorHolder.findValidateCodeProcessor(type)方法返回的就是ImageCodeProcessor对象,但是是ValidateCodeProcessor的引用

create()

实际是调用AbstractValidateCodeProcessor的create方法。

@Override
public void create(ServletWebRequest request) throws Exception {
	C validateCode = generate(request);
	save(request, validateCode);
	send(request, validateCode);
}
1)generate()
private C generate(ServletWebRequest request) {
	String type = getValidateCodeType().toString().toLowerCase();
	String generatorName = type + ValidateCodeGenerator.class.getSimpleName();
	ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(generatorName);
	if (validateCodeGenerator == null) {
		throw new ValidateCodeException("验证码生成器" + generatorName + "不存在");
	}
	return (C) validateCodeGenerator.generate(request);
}

通过类型调用不同的ValidateCodeGenerator 验证码生成器。这次是ImageCodeGenerator。
然后调用ImageCodeGenerator类中的generate方法,返回一张带有验证码的图片对象。

@Override
public ImageCode generate(ServletWebRequest request) {
	String kaptchaCode = captchaProducer.createText();
	BufferedImage image = captchaProducer.createImage(kaptchaCode);
	return new ImageCode(image, kaptchaCode, securityProperties.getCode().getImage().getExpireIn());
}
2)save()

也是调用AbstractValidateCodeProcessor中的save()方法

private final ValidateCodeRepository validateCodeRepository;

private void save(ServletWebRequest request, C validateCode) {
	ValidateCode code = new ValidateCode(validateCode.getCode(), validateCode.getExpireTime());
	validateCodeRepository.save(request, code, getValidateCodeType());
}

使用validateCodeRepository进行保存

@Component
public class RedisValidateCodeRepository implements ValidateCodeRepository {
	private final RedisTemplate<String, Object> redisTemplate;
	
	@Autowired
	public RedisValidateCodeRepository(RedisTemplate<String, Object> redisTemplate) {
		this.redisTemplate = redisTemplate;
	}
		@Override
	public void save(ServletWebRequest request, ValidateCode code, ValidateCodeType type) {
		String key = buildKey(request, type);
		redisTemplate.opsForValue().set(key, code, 3, TimeUnit.MINUTES);
	}

	//生成redis中的key
	private String buildKey(ServletWebRequest request, ValidateCodeType type) {
			String deviceId = request.getHeader("deviceId");
			if (StringUtils.isBlank(deviceId)) {
				throw new ValidateCodeException("请在请求头中携带deviceId参数");
			}
			return "code:" + type.toString().toLowerCase() + ":" + deviceId;
		}
	}

注入的ValidateCodeRepository 实际上是RedisValidateCodeRepository 这个实现类。此时调用的就是RedisValidateCodeRepository 中的save方法,将验证码进行保存。
在这里插入图片描述
在这里插入图片描述

3)send()

调用ImageCodeProcessor类中的send方法将验证码写回

@Override
protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
	ByteArrayOutputStream bos = new ByteArrayOutputStream();
	ImageIO.write(imageCode.getImage(), "JPEG", bos);

	SecurityResult result = SecurityResult.ok(bos.toByteArray());

	String json = objectMapper.writeValueAsString(result);
	HttpServletResponse response = request.getResponse();
	response.setCharacterEncoding("UTF-8");
	response.getWriter().write(json);
}

2、注册

1.页面请求

<button class="register-btn" @click="submit">注册</button>

submit () {
 let validateResult = this.formValidate();
 // 验证成功
 if (validateResult.status) {
   this.register(() => {
     this.$store.dispatch('new_notice', {
       autoClose: true,
       content: '注册成功, 请登录邮箱激活用户'
     });
     this.loadPage('Login');
   });
 } else {
   this.$store.dispatch('new_notice', {
     autoClose: true,
     content: validateResult.msg
   });
 }
},
register (resolve) {
 this.$http({
   method: 'POST',
   headers: {
     'Content-Type': 'application/x-www-form-urlencoded',
     'deviceId': this.deviceId
   },
   url: '/uac/auth/register',
   params: this.registerForm
 }).then((res) => {
   this.getImage();
   if (res) {
     resolve(res);
   }
 }).catch((err) => {
   console.log(err);
 });
},

2.页面后台处理

Controller处理类所在位置

//AuthRestController.java
@PostMapping(value = "/register")
@ApiOperation(httpMethod = "POST", value = "注册用户")
public Wrapper registerUser(UserRegisterDto user) {
	uacUserService.register(user);
	return WrapMapper.ok();
}

UacUserServiceImpl.java/register()

@Override
public void register(UserRegisterDto registerDto) {
	// 1.校验前端页面封装的注册信息
	validateRegisterInfo(registerDto);
	String mobileNo = registerDto.getMobileNo();
	String email = registerDto.getEmail();
	Date row = new Date();
	
	//2.雪花算法生成分布式唯一ID
	String salt = String.valueOf(generateId());
	long id = generateId();
	
	UacUser uacUser = new UacUser();
	uacUser.setLoginName(registerDto.getLoginName());
	uacUser.setSalt(salt);
	uacUser.setLoginPwd(Md5Util.encrypt(registerDto.getLoginPwd()));
	uacUser.setMobileNo(mobileNo);
	//设置状态为不可用 DISABLE
	uacUser.setStatus(UacUserStatusEnum.DISABLE.getKey());
	//设置状态为不可用 DISABLE
	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());

	//3.生成activeToken并保存redis
	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());

	//4.发送激活邮件
	MqMessageData mqMessageData = emailProducer.sendEmailMq(to, UacEmailTemplateEnum.ACTIVE_USER, AliyunMqTopicConstants.MqTagEnum.ACTIVE_USER, param);
	userManager.register(mqMessageData, uacUser);
}
1.校验前端页面封装的注册信息(validateRegisterInfo)
// 1.校验前端页面封装的注册信息
	validateRegisterInfo(registerDto);

validateRegisterInfo方法主要校验前端传入的参数,并根据用户名、手机号、Email查询是否已经存在。

UacUser uacUser = new UacUser();
uacUser.setLoginName(registerDto.getLoginName());
int count = uacUserMapper.selectCount(uacUser);
if (count > 0) {
	throw new UacBizException(ErrorCodeEnum.UAC10011012);
}

uacUser = new UacUser();
uacUser.setMobileNo(registerDto.getMobileNo());
count = uacUserMapper.selectCount(uacUser);
if (count > 0) {
	throw new UacBizException(ErrorCodeEnum.UAC10011013);
}

uacUser = new UacUser();
uacUser.setEmail(registerDto.getEmail());
count = uacUserMapper.selectCount(uacUser);
if (count > 0) {
	throw new UacBizException(ErrorCodeEnum.UAC10011019);
}
2.雪花算法生成分布式唯一ID(generateId)
	//2.雪花算法生成分布式唯一ID
	String salt = String.valueOf(generateId());
	long id = generateId();

generateId()调用抽象类BaseService.java中的

protected long generateId() {
	return UniqueIdGenerator.getInstance(IncrementIdGenerator.getServiceId()).nextId();
}
1)IncrementIdGenerator.getServiceId()

IncrementIdGenerator.java 所属paascloud-common-zk模块下

private static Long serviceId = null;
public static Long getServiceId() {
	return serviceId;
}

在这里插入图片描述
在paascloud-common-core模块下的ZookeeperInitRunner.java中

@Component
@Order
@Slf4j
public class ZookeeperInitRunner implements CommandLineRunner {
	@Resource
	private PaascloudProperties paascloudProperties;
	@Value("${spring.application.name}")
	private String applicationName;
	
	@Override
public void run(String... args) throws Exception {
	String hostAddress = InetAddress.getLocalHost().getHostAddress();
	log.info("###ZookeeperInitRunner,init. HostAddress={}, applicationName={}", hostAddress, applicationName);
	RegistryCenterFactory.startup(paascloudProperties, hostAddress, applicationName);
	log.info("###ZookeeperInitRunner,finish<<<<<<<<<<<<<");
}
}
a)ZookeeperInitRunner.run()

因为ZookeeperInitRunner 实现CommandLineRunner 接口,并且实现了run方法。paascloud-provider-uac的父pom是 paascloud-provide 。paascloud-provid的pom文件中依赖paascloud-common-core。所以随着paascloud-provider-uac模块启动,spring容器加载ZookeeperInitRunner 并运行run方法。
在这里插入图片描述
在run方法中调用RegistryCenterFactory.startup();

b)RegistryCenterFactory.startup();
//paascloudProperties:yml文件中GlobalConstant.ROOT_PREFIX(paascloud)路径下的配置
//host:此时就是10.10.110.120
//app:yml文件中spring.application.name的配置
public static void startup(PaascloudProperties paascloudProperties, String host, String app) {
	CoordinatorRegistryCenter coordinatorRegistryCenter = createCoordinatorRegistryCenter(paascloudProperties.getZk());
	RegisterDto dto = new RegisterDto(app, host, coordinatorRegistryCenter);
	Long serviceId = new IncrementIdGenerator(dto).nextId();
	IncrementIdGenerator.setServiceId(serviceId);
	registerMq(paascloudProperties, host, app);
}

paascloudProperties.getZk() 在获取的是paascloud路径下的配置。
在application-dev.yml文件中配置。返回ZookeeperProperties对象。
在这里插入图片描述

c)createCoordinatorRegistryCenter()
public static CoordinatorRegistryCenter createCoordinatorRegistryCenter(ZookeeperProperties zookeeperProperties) {
	//将zk的连接地址进行hash处理
	Hasher hasher = Hashing.md5().newHasher().putString(zookeeperProperties.getZkAddressList(), Charsets.UTF_8);
	HashCode hashCode = hasher.hash();
	//在注册中心中查找已经hash的连接地址
	//ConcurrentHashMap<HashCode, CoordinatorRegistryCenter> REG_CENTER_REGISTRY其实就是一个Map
	CoordinatorRegistryCenter result = REG_CENTER_REGISTRY.get(hashCode);
	//如果没有就新建一个ZookeeperRegistryCenter
	if (null != result) {
		return result;
	}
	result = new ZookeeperRegistryCenter(zookeeperProperties);
	//调用init()方法注册zk
	result.init();
	//加入map
	REG_CENTER_REGISTRY.put(hashCode, result);
	return result;
}
d)new IncrementIdGenerator(dto).nextId()

new IncrementIdGenerator(dto)其实就是通过构造方法,给registerDto赋值,在nextId()中使用。

public Long nextId() {
	//获取构造函数赋值之后的内容
	String app = this.registerDto.getApp();//paascloud-provider-uac
	String host = this.registerDto.getHost();//10.10.110.120
	CoordinatorRegistryCenter regCenter = this.registerDto.getCoordinatorRegistryCenter();
	
	//ZK_REGISTRY_ID_ROOT_PATH = "/paascloud/registry/id"
	//SLASH = "/"
	//path = "/paascloud/registry/id/paascloud-provider-uac/10.11.320.120"
	String path = GlobalConstant.ZK_REGISTRY_ID_ROOT_PATH + GlobalConstant.Symbol.SLASH + app + GlobalConstant.Symbol.SLASH + host;
	if (regCenter.isExisted(path)) {
		// 如果已经有该节点,表示已经为当前的host上部署的该app分配的编号(应对某个服务重启之后编号不变的问题),直接获取该id,而无需生成
		return Long.valueOf(regCenter.getDirectly(GlobalConstant.ZK_REGISTRY_ID_ROOT_PATH + GlobalConstant.Symbol.SLASH + app + GlobalConstant.Symbol.SLASH + host));
	} else {
		// 节点不存在,那么需要生成id,利用zk节点的版本号每写一次就自增的机制来实现
		//GlobalConstant.ZK_REGISTRY_SEQ = "/paascloud/seq"
		//创建zk自增节点
		regCenter.increment(GlobalConstant.ZK_REGISTRY_SEQ, new RetryNTimes(2000, 3));
		// 创建节点id
		Integer id = regCenter.getAtomicValue(GlobalConstant.ZK_REGISTRY_SEQ, new RetryNTimes(2000, 3)).postValue();
		// 将数据写入节点
		regCenter.persist(path);
		regCenter.persist(path, String.valueOf(id));
		return Long.valueOf(id);
	}
}

nextId()就是通过在zk上注册节点,生成服务注册的id,以便通过zk给不同服务不同节点生成分布式唯一id
最后IncrementIdGenerator.setServiceId(serviceId);将生成的 id赋值给IncrementIdGenerator类中的serviceId。

由此,uac模块在启动的时候就连接好了zk,并复制了serviceId,最后IncrementIdGenerator.getServiceId()就能获取了。
在这里插入图片描述

2)UniqueIdGenerator.getInstance().nextId()
a)UniqueIdGenerator.getInstance()

对UniqueIdGenerator进行加锁操作,防止出现相同id的情况,此时appHostId=1,调用new UniqueIdGenerator(appHostId)对成员变量进行赋值。

public static UniqueIdGenerator getInstance(long appHostId) {
	if (idGen == null) {
		synchronized (UniqueIdGenerator.class) {
			if (idGen == null) {
				idGen = new UniqueIdGenerator(appHostId);
			}
		}
	}
	return idGen;
}

private UniqueIdGenerator(long appHostId) {
	if (appHostId > MAX_APP_HOST_ID) {
		// zk分配的serviceId过大(基本小规模的公司不会出现这样的问题)
		throw new IllegalArgumentException(String.format("app host Id wrong: %d ", appHostId));
	}
	this.appHostId = appHostId;
}
b)nextId()

没有具体研究,应该可以简单理解为时间戳。

public Long nextId() {
		return this.genUniqueId();
}
/**
 * 生成唯一id的具体实现
 */
private synchronized long genUniqueId() {
	long current = System.currentTimeMillis();

	if (current < lastTimestamp) {
		// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过,出现问题返回-1
		return -1;
	}

	if (current == lastTimestamp) {
		// 如果当前生成id的时间还是上次的时间,那么对sequence序列号进行+1
		sequence = (sequence + 1) & MAX_SEQUENCE;

		if (sequence == MAX_SEQUENCE) {
			// 当前毫秒生成的序列数已经大于最大值,那么阻塞到下一个毫秒再获取新的时间戳
			current = this.nextMs(lastTimestamp);
		}
	} else {
		// 当前的时间戳已经是下一个毫秒
		sequence = 0L;
	}

	// 更新上次生成id的时间戳
	lastTimestamp = current;

	// 进行移位操作生成int64的唯一ID
	return ((current - START_TIME) << TIMESTAMP_LEFT_SHIFT)
			| (this.appHostId << APP_HOST_ID_SHIFT)
			| sequence;
}
3.生成activeToken并保存redis
//3.生成activeToken并保存redis
String activeToken = PubUtils.uuid() + super.generateId();
redisService.setKey(RedisKeyUtil.getActiveUserKey(activeToken), email, 1, TimeUnit.DAYS);

PubUtils.uuid()就是生成随机的randomUUID。super.generateId()就是雪花算法生成的分布式id。

1)getActiveUserKey()

getActiveUserKey() 返回的activeToken值为paascloud:activeUser:xxxx

public static String getActiveUserKey(String activeToken) {
	Preconditions.checkArgument(StringUtils.isNotEmpty(activeToken), "参数不能为空");
	//ACTIVE_USER = "paascloud:activeUser"
	return ACTIVE_USER + ":" + activeToken;
}
2)setKey()
@Override
public void setKey(String key, String value, long timeout, TimeUnit unit) {
	Preconditions.checkArgument(StringUtils.isNotEmpty(key), "Redis key is not null");
	Preconditions.checkArgument(unit != null, "TimeUnit is not null");
	ValueOperations<String, String> ops = rt.opsForValue();
	ops.set(key, value);
	rt.expire(key, timeout, unit);
	log.info("setKey. [OK] key={}, value={}, timeout={}, unit={}", key, value, timeout, unit);
}

setKey()中key就是生成的activeToken,value就是注册的email。过期时间为1天。

4.发送激活邮件

发送邮件是通过rocketMq消息队列去完成的

//4.发送激活邮件
//to 发送邮件的邮箱地址数组
//UacEmailTemplateEnum.ACTIVE_USER("ACTIVE_USER", "注册用户-等待激活用户", "email/activeUserTemplate.ftl"),
//AliyunMqTopicConstants.MqTagEnum.ACTIVE_USER("ACTIVE_USER", MqTopicEnum.SEND_EMAIL_TOPIC.getTopic(), "激活用户"),
//param就是发送邮件的其他参数信息
MqMessageData mqMessageData = emailProducer.sendEmailMq(to, UacEmailTemplateEnum.ACTIVE_USER, AliyunMqTopicConstants.MqTagEnum.ACTIVE_USER, param);

userManager.register(mqMessageData, uacUser);
1)sendEmailMq()
public MqMessageData sendEmailMq(Set<String> emailSet, UacEmailTemplateEnum emailTemplateEnum, AliyunMqTopicConstants.MqTagEnum tagEnum, Map<String, Object> param) {
	log.info("pcSendEmailRequest - 发送邮件MQ. emailSet={}, param={}", emailSet, param);
	String msgBody;
	try {
		Preconditions.checkArgument(emailTemplateEnum != null, "邮箱模板信息不存在");
		//构建请求
		PcSendEmailRequest request = new PcSendEmailRequest();
		//通过传入的参数获取邮件文本内容
		//emailTemplateEnum:("ACTIVE_USER", "注册用户-等待激活用户", "email/activeUserTemplate.ftl")
		String templateLocation = emailTemplateEnum.getLocation();//email/activeUserTemplate.ftl
		String text = uacFreeMarkerService.getTemplate(param, templateLocation);

		request.setText(text);
		request.setTo(emailSet);
		request.setSubject(emailTemplateEnum.getSubject());//"注册用户-等待激活用户"
		//将请求转化成json
		msgBody = JSON.toJSONString(request);
	} catch (Exception e) {
		log.error("发送邮件验证码 smsMessage转换为json字符串失败", e);
		throw new UacBizException(ErrorCodeEnum.UAC10011021);
	}
	//tagEnum:ACTIVE_USER("ACTIVE_USER", MqTopicEnum.SEND_EMAIL_TOPIC.getTopic(), "激活用户"),
	//SEND_EMAIL_TOPIC("SEND_EMAIL_TOPIC", "发送邮件"),
	String topic = tagEnum.getTopic();//"SEND_EMAIL_TOPIC"
	String tag = tagEnum.getTag();//"ACTIVE_USER"
	
	String key = RedisKeyUtil.createMqKey(topic, tag, emailSet.toString(), msgBody);
	return new MqMessageData(msgBody, topic, tag, key);
}
a)createMqKey()
//topic:"SEND_EMAIL_TOPIC"
//tag:"ACTIVE_USER"
//refNo:发送的目的地邮箱地址
//body:邮件内容json
public static String createMqKey(String topic, String tag, String refNo, String body) {
	String temp = refNo;
	//...省略检查
	if (refNo.length() > REF_NO_MAX_LENGTH) {//REF_NO_MAX_LENGTH = 100
		//如果发送的邮件地址大于100个 才进入
		temp = StringUtils.substring(refNo, 0, REF_NO_MAX_LENGTH) + "...";
	}
	return topic + "_" + tag + "_" + temp + "-" + body.hashCode();
}

createMqKey()就是组合传入参数进行组装,返回一个存入redis中的key。

b)new MqMessageData()

就是将sendEmailMq()方法中的内容封装成一个MqMessageData对象。

2)register() 调用切面方法

在UserManager.java中register方法

@MqProducerStore
public void register(final MqMessageData mqMessageData, final UacUser uacUser) {
	log.info("注册用户. mqMessageData={}, user={}", mqMessageData, uacUser);
	//@MqProducerStore切面完成调用joinPoint.proceed()后,执行最后保存用户信息到pc_uac_user表中
	uacUserMapper.insertSelective(uacUser);
}

因为用@MqProducerStore修饰,在进入register()方法之前,首先进入切面MqProducerStoreAspect.java中的processMqProducerStoreJoinPoint方法

//MqProducerStoreAspect.java
@Pointcut("@annotation(com.paascloud.provider.annotation.MqProducerStore)")
public void mqProducerStoreAnnotationPointcut() {
}

@Value("${paascloud.aliyun.rocketMq.producerGroup}")
	private String producerGroup; //在uac-dev.yml中为PID_UAC

@Around(value = "mqProducerStoreAnnotationPointcut()")
public Object processMqProducerStoreJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
	log.info("processMqProducerStoreJoinPoint - 线程id={}", Thread.currentThread().getId());
	Object result;
	//args表示传入切面修饰方法的参数 在register()方法中[mqMessageData uacUser]
	Object[] args = joinPoint.getArgs();
	//通过getAnnotation()方法获取注解对象
	MqProducerStore annotation = getAnnotation(joinPoint);
	//annotation.sendType()返回默认 MqSendTypeEnum.WAIT_CONFIRM
	MqSendTypeEnum type = annotation.sendType();
	//annotation.orderType()返回MqOrderTypeEnum.ORDER		   	//MqOrderTypeEnum.orderType()返回默认的ORDER(1),表示有序
	int orderType = annotation.orderType().orderType();
	//annotation.delayLevel()返回DelayLevelEnum.ZERO  ZERO(0, "不延时"),
	DelayLevelEnum delayLevelEnum = annotation.delayLevel();
	if (args.length == 0) {
		throw new TpcBizException(ErrorCodeEnum.TPC10050005);
	}
	//获取传入register()方法中的MqMessageData 参数
	MqMessageData domain = null;
	for (Object object : args) {
		if (object instanceof MqMessageData) {
			domain = (MqMessageData) object;
			break;
		}
	}

	if (domain == null) {
		throw new TpcBizException(ErrorCodeEnum.TPC10050005);
	}

	domain.setOrderType(orderType);//orderType = 1
	domain.setProducerGroup(producerGroup);//producerGroup = PID_UAC
	//此时type = MqSendTypeEnum.WAIT_CONFIRM
	if (type == MqSendTypeEnum.WAIT_CONFIRM) { 
	//此时delayLevelEnum  = DelayLevelEnum.ZERO
		if (delayLevelEnum != DelayLevelEnum.ZERO) {
			domain.setDelayLevel(delayLevelEnum.delayLevel());
		}
		//(1)执行saveWaitConfirmMessage
		mqMessageService.saveWaitConfirmMessage(domain);
	}
	//(2)joinPoint.proceed() 返回注解方法,执行业务
	result = joinPoint.proceed();
	if (type == MqSendTypeEnum.SAVE_AND_SEND) {
		mqMessageService.saveAndSendMessage(domain);
	} else if (type == MqSendTypeEnum.DIRECT_SEND) {
		mqMessageService.directSendMessage(domain);
	} else {
		//(3) type = WAIT_CONFIRM 执行confirmAndSendMessage
		final MqMessageData finalDomain = domain;
		taskExecutor.execute(() -> mqMessageService.confirmAndSendMessage(finalDomain.getMessageKey()));
	}
	return result;
}
a)执行saveWaitConfirmMessage()

在MqMessageServiceImpl.java中

@Override
@Transactional(rollbackFor = Exception.class)
public void saveWaitConfirmMessage(final MqMessageData mqMessageData) {
	//保存消息到pc_mq_message_data表中
	this.saveMqProducerMessage(mqMessageData);
	// 发送预发送状态的消息给消息中心
	TpcMqMessageDto tpcMqMessageDto = mqMessageData.getTpcMqMessageDto();
	//远程调用,保存消息到pc_tpc_mq_message表中
	tpcMqMessageFeignApi.saveMessageWaitingConfirm(tpcMqMessageDto);
	log.info("<== saveWaitConfirmMessage - 存储预发送消息成功. messageKey={}", mqMessageData.getMessageKey());
}

saveMqProducerMessage方法通过mqMessageDataMapper将消息内容保存到pc_mq_message_data表中。

@Override
public void saveMqProducerMessage(MqMessageData mqMessageData) {
	// 校验消息数据
	this.checkMessage(mqMessageData);
	// 先保存消息
	mqMessageData.setMessageType(MqMessageTypeEnum.PRODUCER_MESSAGE.messageType());//PRODUCER_MESSAGE(10, "生产者"), type = 10
	mqMessageData.setId(UniqueIdGenerator.generateId());
	mqMessageDataMapper.insertSelective(mqMessageData);
}

远程调用tpcMqMessageFeignApi.saveMessageWaitingConfirm();

//调用paascloud-provider-tpc服务
@FeignClient(value = "paascloud-provider-tpc", configuration = OAuth2FeignAutoConfiguration.class, fallback = TpcMqMessageFeignApiHystrix.class)
public interface TpcMqMessageFeignApi {
@PostMapping(value = "/api/tpc/saveMessageWaitingConfirm")
Wrapper saveMessageWaitingConfirm(@RequestBody TpcMqMessageDto mqMessageDto);
}

在这里插入图片描述

//paascloud-provider-tpc模块下TpcMqMessageFeignClient.java
@Override
@ApiOperation(httpMethod = "POST", value = "预存储消息")
public Wrapper saveMessageWaitingConfirm(@RequestBody TpcMqMessageDto mqMessageDto) {
	logger.info("预存储消息. mqMessageDto={}", mqMessageDto);
	//保存到pc_tpc_mq_message表中
	tpcMqMessageService.saveMessageWaitingConfirm(mqMessageDto);
	return WrapMapper.ok();
}

将消息内容存储到pc_tpc_mq_message表中

综上,saveWaitConfirmMessage()方法将消息内容保存到了
pc_mq_message_datapc_tpc_mq_message表中
且用@Transactional修饰,表示两次保存是同一个事务。

b)joinPoint.proceed() 返回注解方法,执行业务

调用joinPoint.proceed()就执行@MqProducerStore修饰方法的方法体。

@MqProducerStore
public void register(final MqMessageData mqMessageData, final UacUser uacUser) {
	log.info("注册用户. mqMessageData={}, user={}", mqMessageData, uacUser);
	//@MqProducerStore切面完成调用joinPoint.proceed()后,执行最后保存用户信息到pc_uac_user表中
	uacUserMapper.insertSelective(uacUser);
}

将UacUser 中封装的注册人信息通过uacUserMapper保存到pc_uac_user表中
执行完register()方法后,依然返回切面方法中。

c)type = WAIT_CONFIRM 执行confirmAndSendMessage
final MqMessageData finalDomain = domain;
//finalDomain.getMessageKey()其实就是MqMessageData 的messageKey。也就是
//createMqKey()生成的 topic + "_" + tag + "_" + temp + "-" + body.hashCode();
taskExecutor.execute(() -> mqMessageService.confirmAndSendMessage(finalDomain.getMessageKey()));

//MqMessageServiceImpl.java中

@Override
public void confirmAndSendMessage(String messageKey) {
	// 发送确认消息给消息中心
	try {
		Wrapper wrapper = tpcMqMessageFeignApi.confirmAndSendMessage(messageKey);
		//.....
}

远程调用confirmAndSendMessage方法。

@FeignClient(value = "paascloud-provider-tpc", configuration = OAuth2FeignAutoConfiguration.class, fallback = TpcMqMessageFeignApiHystrix.class)
public interface TpcMqMessageFeignApi {

	@PostMapping(value = "/api/tpc/confirmAndSendMessage")
	Wrapper confirmAndSendMessage(@RequestParam("messageKey") String messageKey);
}

TpcMqMessageFeignClient.java

@Override
@ApiOperation(httpMethod = "POST", value = "确认并发送消息")
public Wrapper confirmAndSendMessage(@RequestParam("messageKey") String messageKey) {
	logger.info("确认并发送消息. messageKey={}", messageKey);
	tpcMqMessageService.confirmAndSendMessage(messageKey);
	return WrapMapper.ok();
}

调用confirmAndSendMessage()

@Override
public void confirmAndSendMessage(String messageKey) {
//通过messageKey查询pc_tpc_mq_message表中的数据
	final TpcMqMessage message = tpcMqMessageMapper.getByMessageKey(messageKey);
	if (message == null) {
		throw new TpcBizException(ErrorCodeEnum.TPC10050002);
	}

	TpcMqMessage update = new TpcMqMessage();
	//设置发送状态为Sending
	update.setMessageStatus(MqSendStatusEnum.SENDING.sendStatus());
	update.setId(message.getId());
	update.setUpdateTime(new Date());
	//第一步:
	//主要是修改pc_tpc_mq_message表中的MessageStatus为Sending
	tpcMqMessageMapper.updateByPrimaryKeySelective(update);
	//第二步:
	// 创建消费待确认列表
	this.createMqConfirmListByTopic(message.getMessageTopic(), message.getId(), message.getMessageKey());
	// 直接发送消息
	this.directSendMessage(message.getMessageBody(), message.getMessageTopic(),
			message.getMessageTag(), message.getMessageKey(),
			message.getProducerGroup(), message.getDelayLevel());
}

第一步:使用tpcMqMessageMapper将pc_tpc_mq_message表中已存入的数据进行修改,其中将MessageStatus设置Sending。

第二步:调用this.createMqConfirmListByTopic()创建消费待确认列表。将消息等待对象保存到pc_tpc_mq_confirm表中

public void createMqConfirmListByTopic(final String topic, final Long messageId, final String messageKey) {
	// topic :"SEND_EMAIL_TOPIC"
	//messageKey:reateMqKey()生成的 topic + "_" + tag + "_" + temp + "-" + body.hashCode();
	List<TpcMqConfirm> list = Lists.newArrayList();
	TpcMqConfirm tpcMqConfirm;
	//SELECT consumer_code from pc_tpc_mq_subscribe WHERE topic_code = #{topic}
	//通过topic查询pc_tpc_mq_subscribe表,查到所有订阅该topic的消费者组
	List<String> consumerGroupList = tpcMqConsumerService.listConsumerGroupByTopic(topic);
	if (PublicUtil.isEmpty(consumerGroupList)) {
		throw new TpcBizException(ErrorCodeEnum.TPC100500010, topic);
	}
	for (final String cid : consumerGroupList) {
		//遍历消费者组,得到所有的消费者,封装到TpcMqConfirm对象中
		tpcMqConfirm = new TpcMqConfirm(UniqueIdGenerator.generateId(), messageId, messageKey, cid);
		list.add(tpcMqConfirm);
	}
	//将所有的TpcMqConfirm对象批量保存到pc_tpc_mq_confirm表中
	tpcMqConfirmMapper.batchCreateMqConfirm(list);
}

第三步:使用this.directSendMessage()发送消息

@Override
public void directSendMessage(String body, String topic, String tag, String key, String pid, Integer delayLevel) {
	RocketMqProducer.sendSimpleMessage(body, topic, tag, key, pid, delayLevel);
}

RocketMqProducer.java

private static final int PRODUCER_RETRY_TIMES = 3;
public static SendResult sendSimpleMessage(String body, String topic, String tag, String key, String pid, Integer delayLevel) {
		//...
		return retrySendMessage(pid, message);
}

private static SendResult retrySendMessage(String pid, Message msg) {
		int iniCount = 1;
		SendResult result;
		while (true) {
			try {
				result = MqProducerBeanFactory.getBean(pid).send(msg);
				break;
			} catch (Exception e) {
				log.error("发送消息失败:", e);
				if (iniCount++ >= PRODUCER_RETRY_TIMES) {
					throw new TpcBizException(ErrorCodeEnum.TPC100500014, msg.getTopic(), msg.getKeys());
				}
			}
		}
		log.info("<== 发送MQ SendResult={}", result);
		return result;
	}

实际上就是通过MqProducerBeanFactory工厂getBean()获取消息生产类,最后send()发送
getBean()

private static final ConcurrentHashMap<String, DefaultMQProducer> DEFAULT_MQ_PRODUCER_MAP = new ConcurrentHashMap<>();
public static DefaultMQProducer getBean(String pid) {
//省略检查
	return DEFAULT_MQ_PRODUCER_MAP.get(pid);
}

DEFAULT_MQ_PRODUCER_MAP集合在buildProducerBean()中赋值。
buildProducerBean()在MqBeanInitRunner.java中调用。同样MqBeanInitRunner实现了CommandLineRunner,在tpc模块启动时,spring将其加入容器。不再赘述。
DEFAULT_MQ_PRODUCER_MAP集合主要取自zk中这里节点的内容。
在这里插入图片描述

调用完之后,调用updateOnLineStatusByPid方法,通过mdcMqProducerMapper把pc_tpc_mq_producer表中的状态更新为在线20.

tpcMqProducerService.updateOnLineStatusByPid(producerDto.getProducerGroup());

public void updateOffLineStatusByPid(final String producerGroup) {
	logger.info("更新生产者pid={}状态为离线", producerGroup);
	this.updateStatus(producerGroup, 20);
}

private void updateStatus(final String producerGroup, final int status) {
	TpcMqProducer tpcMqProducer = mdcMqProducerMapper.getByPid(producerGroup);
	if (tpcMqProducer.getStatus() != null && tpcMqProducer.getStatus() != status) {
		TpcMqProducer update = new TpcMqProducer();
		update.setStatus(status);
		update.setId(tpcMqProducer.getId());
		mdcMqProducerMapper.updateByPrimaryKeySelective(update);
	}

最后调用send()将消息发送。
至此,切面中的confirmAndSendMessage()调用完成

taskExecutor.execute(() -> mqMessageService.confirmAndSendMessage(finalDomain.getMessageKey()));

AuthRestController中/register 完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值