Commit c71c927d authored by 李健华's avatar 李健华

大数据时间上报下线

parent a7a25414
......@@ -360,57 +360,6 @@
<artifactId>qiniu-java-sdk</artifactId>
<version>7.2.27</version>
</dependency>
<!-- 大数据用户行为采集 http://confluence.quantgroup.cn/pages/viewpage.action?pageId=34818640 -->
<dependency>
<groupId>cn.qg.ec.data-stream-sdk</groupId>
<artifactId>data-stream-sdk</artifactId>
<version>1.1.12-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<!--<exclusion>-->
<!--<groupId>commons-io</groupId>-->
<!--<artifactId>commons-io</artifactId>-->
<!--</exclusion>-->
<exclusion>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
......
package cn.quantgroup.xyqb.aspect.captcha;
import cn.qg.ec.model.base.BusinessEventBaseInfo;
import cn.qg.ec.model.user.UserLoginRegEvent;
import cn.quantgroup.xyqb.entity.User;
import cn.quantgroup.xyqb.entity.enums.Device;
import cn.quantgroup.xyqb.entity.enums.KeyType;
import cn.quantgroup.xyqb.model.AuthBean;
import cn.quantgroup.xyqb.model.JsonResult;
import cn.quantgroup.xyqb.model.LoginRefuseResult;
import cn.quantgroup.xyqb.risk.entity.CountDevice;
import cn.quantgroup.xyqb.risk.entity.LoginInfo;
import cn.quantgroup.xyqb.risk.repository.LoginInfoRepository;
import cn.quantgroup.xyqb.risk.repository.WhiteListRepository;
import cn.quantgroup.xyqb.risk.uereventcollecting.UserEventCollectingUtil;
import cn.quantgroup.xyqb.service.user.IUserService;
import cn.quantgroup.xyqb.util.TenantUtil;
import cn.quantgroup.xyqb.util.encrypt.Md5Util;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
......@@ -63,7 +57,7 @@ public class LoginInterceptorAspect {
*/
private static final int[] RISK_STRATEGY = {1, 2};
/**
* 警示语
* 警示t
*/
private final String ALERT_WORDS = "检测到您的设备上账号登录异常,已被强制退出并暂时冻结您的账号。联系客服400-002-0061";
......@@ -129,10 +123,7 @@ public class LoginInterceptorAspect {
log.info("登录前风控策略开始deviceId:{},phone:{}", deviceId, phone);
Device device = Device.valueOfCode(deviceCode);
/**
* 尝试登录发一下
*/
sendEvent(UserLoginRegEvent.SubEventType.user_attempt_login, device, phone, deviceId, realIp, null, tenantId);
// 调用风控策略逻辑删除
// LoginRefuseResult result = verification(deviceId, phone, device, tenantId);
......@@ -294,7 +285,6 @@ public class LoginInterceptorAspect {
if (loginResult instanceof JsonResult) {
if (((JsonResult) loginResult).isSuccess()) {
info.setIsLogin(Boolean.TRUE);
sendEvent(UserLoginRegEvent.SubEventType.user_only_login, device, phone, deviceId, ip, loginResult, tenantId);
} else {
info.setIsLogin(Boolean.FALSE);
info.setLoginFailMsg(((JsonResult) loginResult)
......@@ -322,66 +312,6 @@ public class LoginInterceptorAspect {
return whiteListRepository.countByKeyEqualsAndTypeEqualsAndEnableIsTrue(key, type) > 0;
}
/**
* 发送登录事件
*
* @param type
* @param device
* @param phone
* @param deviceId
* @param ip
* @param loginResult
*/
private void sendEvent(UserLoginRegEvent.SubEventType type, Device device, String phone, String deviceId, String ip, Object loginResult, Integer tenantId) {
try {
if (null == phone) {
log.info("手机号没有直接估计是切面用在了不能用的地方直接放过");
return;
}
log.info("开始发送登录注册消息phone:{}", phone);
UserLoginRegEvent.UserLoginRegEventBuilder builder = UserLoginRegEvent.builder();
builder.maskPhoneNo(phone);
builder.hashPhoneNo(Md5Util.build(Md5Util.build(phone)));
builder.subEventType(type);
BusinessEventBaseInfo.BusinessEventBaseInfoBuilder baseInfoBuilder
= BusinessEventBaseInfo.builder();
if (null != loginResult) {
if (loginResult instanceof JsonResult) {
JsonResult jsonResult = ((JsonResult) loginResult);
if (jsonResult.isSuccess()) {
Object data = jsonResult.getData();
if (data instanceof AuthBean) {
String uuid = ((AuthBean) data).getUuid();
User user = iUserService.findByUuidWithCache(uuid);
baseInfoBuilder.userUuid(uuid);
baseInfoBuilder.channel(String.valueOf(user.getRegisteredFrom()));
} else {
return;
}
} else {
return;
}
}
}
baseInfoBuilder.deviceId(deviceId);
baseInfoBuilder.ip(ip);
baseInfoBuilder.deviceType(device.getCode());
builder.businessEventBaseInfo(baseInfoBuilder.build());
if (tenantId == null || tenantId.equals(TenantUtil.TENANT_DEFAULT)) {
UserEventCollectingUtil.addEvent(builder.build());
}
} catch (Exception e) {
log.error("发送用户登录事件异常phone:{}", phone);
}
}
/**
* 【登录设备反欺诈策略】
*
......
package cn.quantgroup.xyqb.event;
import java.io.UnsupportedEncodingException;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import com.amazonaws.services.kinesis.producer.UserRecordResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import cn.qg.ec.kinesis.EnvironmentConfig;
import cn.qg.ec.kinesis.KinesisProducerClient;
import cn.qg.ec.model.user.base.UserBaseDetailEvent;
import cn.quantgroup.tech.util.TechEnvironment;
import cn.quantgroup.xyqb.entity.User;
import cn.quantgroup.xyqb.exception.PushUserToLkbException;
import cn.quantgroup.xyqb.model.UserRegisterParam;
import cn.quantgroup.xyqb.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 注册成功之后,用户分群数据上报
* http://confluence.quantgroup.cn/pages/viewpage.action?pageId=34832018
* http://confluence.quantgroup.cn/pages/viewpage.action?pageId=41784208
* http://confluence.quantgroup.cn/pages/viewpage.action?pageId=34818640
* @author yutong
*/
@Slf4j
@Component
public class KinesisRegisteredEventListener implements ApplicationListener<RegisterEvent> {
@Override
public void onApplicationEvent(RegisterEvent event) {
UserRegisterParam userRegisterParam = event.getUserRegisterParam();
User user = userRegisterParam.getUser();
log.info("[KinesisRegistered] 用户分群数据上报准备, userId:{}, registeredFrom:{}", user.getId(), user.getRegisteredFrom());
sendRecord(user);
}
private void sendRecord(User user) {
// EnvironmentConfig.DEV 环境变量配置
KinesisProducerClient kinesisProducerClient = new KinesisProducerClient(
TechEnvironment.isPro() ? EnvironmentConfig.PROD : EnvironmentConfig.DEV);
// 用户登录事件发送
// UserBaseDetailEvent regEvent = UserBaseDetailEvent
// .builder()
// .channel(1L)
// .hashPhoneNo("*********")
// .businessEventBaseInfo(BusinessEventBaseInfo
// .builder()
// .channel("***")
// .deviceId("***")
// .ip("***")
// .userUuid("***")
// .build())
// .subEventType(UserBaseDetailEvent.SubEventType.REGISTER)
// .build();
UserBaseDetailEvent regEvent = UserBaseDetailEvent
.builder()
.userId(String.valueOf(user.getId()))
.userUuid(user.getUuid())
.subEventType(UserBaseDetailEvent.SubEventType.REGISTER)
.build();
regEvent.setRegisterChannel(user.getRegisteredFrom().intValue());
regEvent.setRegisterTime(user.getCreatedAt().getTime());
try {
ListenableFuture<UserRecordResult> listenableFuture = kinesisProducerClient.SendRecord(regEvent);
Futures.addCallback(listenableFuture, Callback);
log.info("[KinesisRegistered] 用户分群数据上报发送, regEvent:{}", JsonUtil.toJson(regEvent));
} catch (UnsupportedEncodingException | JsonProcessingException e) {
log.error("[KinesisRegistered]用户分群数据上报出错, e:{}", e);
throw new PushUserToLkbException("用户分群数据上报出错");
}
}
// 异步响应结果 如果发送失败,发送方需要进行重试发送
FutureCallback<UserRecordResult> Callback = new FutureCallback<UserRecordResult>() {
@Override
public void onFailure(Throwable t) {
/* Analyze and respond to the failure */
log.error(t.getMessage(), t);
log.error("[KinesisRegistered] 用户分群数据上报失败, Throwable:{}", ExceptionUtils.getStackTrace(t));
}
@Override
public void onSuccess(UserRecordResult result) {
log.info("[KinesisRegistered] 用户分群数据上报成功, result:{}", result.toString());
}
};
}
package cn.quantgroup.xyqb.event;
import cn.qg.ec.model.base.BusinessEventBaseInfo;
import cn.qg.ec.model.user.UserLoginRegEvent;
import cn.quantgroup.xyqb.entity.User;
import cn.quantgroup.xyqb.model.UserRegisterParam;
import cn.quantgroup.xyqb.risk.uereventcollecting.UserEventCollectingUtil;
import cn.quantgroup.xyqb.util.encrypt.Md5Util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* @author :dongjianhua
* @date :Created in 2020/12/23 18:07
* @description:风控注册事件
* @modified By:
* @version:
*/
@Slf4j
@Component
public class RiskRegisteredEventListener implements ApplicationListener<RegisterEvent> {
@Override
public void onApplicationEvent(RegisterEvent registerEvent) {
UserRegisterParam param =
registerEvent.getUserRegisterParam();
User user = param.getUser();
//拿不到user对象. 就是没注册了
if (user == null) {
return;
}
UserLoginRegEvent.UserLoginRegEventBuilder builder = UserLoginRegEvent.builder();
builder.maskPhoneNo(user.getPhoneNo());
builder.hashPhoneNo(Md5Util.build(Md5Util.build(user.getPhoneNo())));
builder.subEventType(UserLoginRegEvent.SubEventType.user_only_login);
BusinessEventBaseInfo.BusinessEventBaseInfoBuilder baseInfoBuilder
= BusinessEventBaseInfo.builder();
baseInfoBuilder.userUuid(user.getUuid());
baseInfoBuilder.channel(String.valueOf(user.getRegisteredFrom()));
builder.businessEventBaseInfo(baseInfoBuilder.build());
UserEventCollectingUtil.addEvent(builder.build());
}
}
package cn.quantgroup.xyqb.event;
import cn.qg.ec.model.base.BusinessEventBaseInfo;
import cn.qg.ec.model.user.UserBaseInfoEvent;
import cn.quantgroup.xyqb.entity.User;
import cn.quantgroup.xyqb.entity.UserDetail;
import cn.quantgroup.xyqb.risk.uereventcollecting.UserEventCollectingUtil;
import cn.quantgroup.xyqb.service.user.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* @author :dongjianhua
* @date :Created in 2020/12/22 16:21
* @description:用变更身份证号的时候发送给大数据
* @modified By:
* @version:
*/
@Slf4j
@Component
public class UserDetailUpdateEventListener implements ApplicationListener<UserDetailUpdateEvent> {
@Autowired
private IUserService userService;
@Override
public void onApplicationEvent(UserDetailUpdateEvent userDetailUpdateEvent) {
UserDetail userDetail = userDetailUpdateEvent.getUserDetail();
log.info("用户实名认证事件监听,userDetail:{}", userDetail);
if (null == userDetail) {
log.error("来了个假的?????");
return;
}
if(null == userDetail.getIdNo()){
log.warn("实名认证没有身份证号?userId:{}",userDetail.getUserId());
return;
}
UserBaseInfoEvent.UserBaseInfoEventBuilder builder = UserBaseInfoEvent.builder();
builder.maskIdNo(userDetail.getIdNo());
User user = userService.findById(userDetail.getUserId());
if (null == user) {
log.error("用户竟然不存在userDetail:{}", userDetail);
return;
}
BusinessEventBaseInfo.BusinessEventBaseInfoBuilder baseInfoBuilder = BusinessEventBaseInfo.builder();
baseInfoBuilder.userUuid(user.getUuid());
builder.businessEventBaseInfo(baseInfoBuilder.build());
builder.subEventType(UserBaseInfoEvent.SubEventType.user_certification);
UserEventCollectingUtil.addEvent(builder.build());
log.info("用户实名认证事件监听结束userId:{}",userDetail.getUserId());
}
}
package cn.quantgroup.xyqb.event;
import cn.qg.ec.model.base.BusinessEventBaseInfo;
import cn.qg.ec.model.user.UserBaseInfoEvent;
import cn.quantgroup.user.enums.EducationEnum;
import cn.quantgroup.user.enums.IncomeRangeEnum;
import cn.quantgroup.user.enums.MaritalStatus;
import cn.quantgroup.user.enums.OccupationEnum;
import cn.quantgroup.xyqb.entity.User;
import cn.quantgroup.xyqb.entity.UserExtInfo;
import cn.quantgroup.xyqb.risk.uereventcollecting.UserEventCollectingUtil;
import cn.quantgroup.xyqb.service.user.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* @author :dongjianhua
* @date :Created in 2020/12/22 14:25
* @description:监听
* @modified By:
* @version:
*/
@Slf4j
@Component
public class UserExtInfoSaveEventListener implements ApplicationListener<UserExtInfoSaveEvent> {
@Autowired
private IUserService userService;
@Override
public void onApplicationEvent(UserExtInfoSaveEvent userExtInfoSaveEvent) {
UserExtInfo userExtInfo = userExtInfoSaveEvent.getUserExtInfo();
if(null == userExtInfo){
log.info("没有扩展信息?那就是没有了");
return;
}
log.info("用户开始保存扩展信息userExtInfo:{}", userExtInfo);
EducationEnum educationEnum = userExtInfo.getEducationEnum();
IncomeRangeEnum incomeRangeEnum = userExtInfo.getIncomeRangeEnum();
MaritalStatus marryStatus = userExtInfo.getMarryStatus();
OccupationEnum occupationEnum = userExtInfo.getOccupationEnum();
User user = userService.findById(userExtInfo.getUserId());
if (null == user) {
log.error("没找到用户,应该不存在这种情况");
return;
}
if (null != educationEnum) {
send(UserBaseInfoEvent.SubEventType.user_education, user, String.valueOf(educationEnum.ordinal()));
}
if (null != incomeRangeEnum) {
send(UserBaseInfoEvent.SubEventType.user_income, user, String.valueOf(incomeRangeEnum.ordinal()));
}
if (null != marryStatus) {
send(UserBaseInfoEvent.SubEventType.user_marital_status, user, String.valueOf(marryStatus.ordinal()));
}
if (null != occupationEnum) {
send(UserBaseInfoEvent.SubEventType.user_occupation, user, String.valueOf(occupationEnum.ordinal()));
}
log.info("用户保存扩展信息结束userExtInfo:{}", userExtInfo);
}
private void send(UserBaseInfoEvent.SubEventType eventType, User user , String val) {
UserBaseInfoEvent.UserBaseInfoEventBuilder builder
= UserBaseInfoEvent.builder();
BusinessEventBaseInfo.BusinessEventBaseInfoBuilder baseInfoBuilder
= BusinessEventBaseInfo.builder();
baseInfoBuilder.userUuid(user.getUuid());
baseInfoBuilder.channel(String.valueOf(user.getRegisteredFrom()));
builder.subEventType(eventType);
builder.businessEventBaseInfo(baseInfoBuilder.build());
switch (eventType) {
case user_education:
builder.education(val);
break;
case user_income:
builder.income(val);
break;
case user_marital_status:
builder.maritalStatus(val);
break;
case user_occupation:
builder.occupation(val);
break;
default:
log.warn("没有这个类型");
return;
}
UserEventCollectingUtil.addEvent(builder.build());
}
}
package cn.quantgroup.xyqb.risk.uereventcollecting;
import cn.qg.ec.kinesis.EnvironmentConfig;
import cn.qg.ec.kinesis.KinesisProducerClient;
import cn.quantgroup.tech.util.TechEnvironment;
/**
* @author :dongjianhua
* @date :Created in 2020/12/21 16:15
* @description:client获取
* @modified By:
* @version:
*/
public class KinesisProducerClientFactory {
private KinesisProducerClientFactory() {
}
private static class KinesisProducerClientHolder {
private static final KinesisProducerClient client = new KinesisProducerClient(getConfig());
private static EnvironmentConfig getConfig() {
return TechEnvironment.isPro() ? EnvironmentConfig.PROD : EnvironmentConfig.DEV;
}
}
public static KinesisProducerClient getClient() {
return KinesisProducerClientHolder.client;
}
}
\ No newline at end of file
package cn.quantgroup.xyqb.risk.uereventcollecting;
import cn.qg.ec.kinesis.KinesisProducerClient;
import cn.qg.ec.model.base.EventInfo;
import com.amazonaws.services.kinesis.producer.UserRecordResult;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import java.io.UnsupportedEncodingException;
/**
* @author :dongjianhua
* @date :Created in 2020/12/21 16:01
* @description:用户事件采集工具类
* @modified By:
* @version: http://confluence.quantgroup.cn/pages/viewpage.action?pageId=34820403
*/
@Slf4j
public class UserEventCollectingUtil {
private static final FutureCallback<UserRecordResult> CALL_BACK = new MyCallBack();
public static void addEvent(EventInfo eventInfo) {
KinesisProducerClient client = KinesisProducerClientFactory.getClient();
try {
ListenableFuture<UserRecordResult> future = client.SendRecord(eventInfo);
Futures.addCallback(future, CALL_BACK);
} catch (UnsupportedEncodingException e) {
log.error("发送事件异常UnsupportedEncodingException:{}", e);
} catch (Exception e) {
log.error("发送事件未知异常e", e);
}
}
@Slf4j
public static class MyCallBack implements FutureCallback<UserRecordResult> {
@Override
public void onSuccess(@Nullable UserRecordResult userRecordResult) {
log.info("发送事件成功 SequenceNumber:{}", userRecordResult.getSequenceNumber());
}
@Override
public void onFailure(Throwable throwable) {
log.error("发送事件失败", throwable);
}
}
}
package cn.quantgroup.xyqb.security;
import cn.qg.ec.model.user.UserLoginRegEvent;
import cn.quantgroup.xyqb.service.event.KinesisService;
import cn.quantgroup.xyqb.urora.UroraAuthenticationManager;
import cn.quantgroup.xyqb.urora.UroraAuthenticationToken;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
......@@ -34,7 +30,6 @@ public class AuthorizationAspect extends HttpAspect {
@Around("pointCut()")
private Object around(ProceedingJoinPoint joinPoint) throws Throwable {
AssentHeader header = getAssentHeader();
// kinesisService.send(UserLoginRegEvent.SubEventType.user_attempt_login, header, null);
Authentication authenticate = authenticationAdapter.authenticate(header.getAuthType(), header.getAuthMessage());
if(!authenticate.isAuthenticated()){
throw new BadCredentialsException("not authorized");
......
package cn.quantgroup.xyqb.service.event;
import cn.qg.ec.model.base.BusinessEventBaseInfo;
import cn.qg.ec.model.user.UserLoginRegEvent;
import cn.quantgroup.xyqb.entity.User;
import cn.quantgroup.xyqb.security.AssentHeader;
import cn.quantgroup.xyqb.model.AuthBean;
import cn.quantgroup.xyqb.model.JsonResult;
import cn.quantgroup.xyqb.risk.uereventcollecting.UserEventCollectingUtil;
import cn.quantgroup.xyqb.service.user.IUserService;
import cn.quantgroup.xyqb.util.encrypt.Md5Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
/**
* Created by Administrator on 2021/6/30 0030.
*/
@Service
public class KinesisService {
private static final Logger logger = LoggerFactory.getLogger(KinesisService.class);
@Resource
private IUserService iUserService;
public void send(UserLoginRegEvent.SubEventType type, AssentHeader header, Object loginResult) {
String phoneNumber = header.getPhoneNumber();
if (StringUtils.isEmpty(phoneNumber)) {
return;
}
long nanoTime = System.nanoTime();
try {
logger.info(">>>> KNS-PRDR-B -- {}; .TP={}; .PN={}; .DCID={}; .CONT={}", nanoTime, type, header.getPhoneNumber(), header.getDeviceId(), header);
UserLoginRegEvent.UserLoginRegEventBuilder builder = UserLoginRegEvent.builder();
builder.maskPhoneNo(phoneNumber);
builder.hashPhoneNo(Md5Util.build(Md5Util.build(phoneNumber)));
builder.subEventType(type);
BusinessEventBaseInfo.BusinessEventBaseInfoBuilder baseInfoBuilder
= BusinessEventBaseInfo.builder();
if (null != loginResult) {
if (loginResult instanceof JsonResult) {
JsonResult jsonResult = ((JsonResult) loginResult);
if (jsonResult.isSuccess()) {
Object data = jsonResult.getData();
if (data instanceof AuthBean) {
String uuid = ((AuthBean) data).getUuid();
User user = iUserService.findByUuidWithCache(uuid);
baseInfoBuilder.userUuid(uuid);
baseInfoBuilder.channel(String.valueOf(user.getRegisteredFrom()));
} else {
return;
}
} else {
return;
}
}
}
baseInfoBuilder.deviceId(header.getDeviceId());
baseInfoBuilder.ip(header.getRemoteAddress());
baseInfoBuilder.deviceType(header.getDevice().getCode());
builder.businessEventBaseInfo(baseInfoBuilder.build());
UserEventCollectingUtil.addEvent(builder.build());
logger.info("++ KNS-PRDR -- {}; .CONT={}", nanoTime, builder.toString());
} catch (Exception e) {
logger.error("!! KNS-PRDR-E -- " + nanoTime + "; .CONT=" + e.getMessage(), e);
throw e;
} finally {
logger.info("<<<< KNS-PRDR-F -- {}", nanoTime);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment