REQ-2411: 发送短信, 限制发送重复内容
This commit is contained in:
parent
f3508208e2
commit
507aee4112
@ -2,6 +2,7 @@ package cn.axzo.msg.center.domain.entity;
|
|||||||
|
|
||||||
import cn.axzo.msg.center.domain.persistence.BaseOwnEntity;
|
import cn.axzo.msg.center.domain.persistence.BaseOwnEntity;
|
||||||
import cn.axzo.trade.datasecurity.core.annotation.CryptField;
|
import cn.axzo.trade.datasecurity.core.annotation.CryptField;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -129,4 +130,8 @@ public class MNSMessage extends BaseOwnEntity<MNSMessage> {
|
|||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return JSON.toJSONString(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package cn.axzo.msg.center.domain.entity;
|
|||||||
|
|
||||||
import cn.axzo.msg.center.domain.enums.YesNoEnum;
|
import cn.axzo.msg.center.domain.enums.YesNoEnum;
|
||||||
import cn.axzo.msg.center.domain.persistence.BaseOwnEntity;
|
import cn.axzo.msg.center.domain.persistence.BaseOwnEntity;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -79,6 +80,11 @@ public class MNSMessageTemplate extends BaseOwnEntity<MNSMessageTemplate> {
|
|||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return JSON.toJSONString(this);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasParam() {
|
public boolean hasParam() {
|
||||||
return YesNoEnum.YES.getCode().equals(hasParam);
|
return YesNoEnum.YES.getCode().equals(hasParam);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -184,8 +184,11 @@ public class AliYunSmsClientImpl implements AliYunSmsClient {
|
|||||||
String message = responseBody.getMessage();
|
String message = responseBody.getMessage();
|
||||||
boolean isRateLimited = message != null && message.contains("触发") && message.contains("流控");
|
boolean isRateLimited = message != null && message.contains("触发") && message.contains("流控");
|
||||||
log.warn("AliyunSmsService#checkResponse is fail, error message : {}", message);
|
log.warn("AliyunSmsService#checkResponse is fail, error message : {}", message);
|
||||||
if (!isRateLimited)
|
if (isRateLimited) {
|
||||||
|
throw new RateLimitException(ReturnCodeEnum.SYSTEM_ERROR, message);
|
||||||
|
} else {
|
||||||
throw new BizException(ReturnCodeEnum.SYSTEM_ERROR, message);
|
throw new BizException(ReturnCodeEnum.SYSTEM_ERROR, message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return responseBody;
|
return responseBody;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
package cn.axzo.msg.center.notices.integration.client.impl;
|
||||||
|
|
||||||
|
import cn.axzo.msg.center.notices.common.enums.ReturnCodeEnum;
|
||||||
|
import cn.axzo.msg.center.notices.common.exception.BizException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yanglin
|
||||||
|
*/
|
||||||
|
public class RateLimitException extends BizException {
|
||||||
|
|
||||||
|
public RateLimitException(ReturnCodeEnum returnCode, String message) {
|
||||||
|
super(returnCode, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package cn.axzo.msg.center.notices.manager.api.dto.request;
|
package cn.axzo.msg.center.notices.manager.api.dto.request;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -48,4 +49,14 @@ public class MnsRequestDto {
|
|||||||
*/
|
*/
|
||||||
private String expansion;
|
private String expansion;
|
||||||
|
|
||||||
|
private Object internalObj;
|
||||||
|
|
||||||
|
public <T> T getInternalObj(Class<T> clazz) {
|
||||||
|
return clazz.isInstance(internalObj) ? clazz.cast(internalObj) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return JSON.toJSONString(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,6 +64,7 @@ public class MNSNoticesApiImpl implements MNSNoticesApi {
|
|||||||
BeanUtils.copyProperties(request, mnsRequestDto);
|
BeanUtils.copyProperties(request, mnsRequestDto);
|
||||||
log.info("request value={},mnsRequestDto value={}", JSONUtil.toJsonStr(request),JSONUtil.toJsonStr(mnsRequestDto));
|
log.info("request value={},mnsRequestDto value={}", JSONUtil.toJsonStr(request),JSONUtil.toJsonStr(mnsRequestDto));
|
||||||
try {
|
try {
|
||||||
|
mnsRequestDto.setInternalObj(MnsType.BIZ);
|
||||||
messageService.sendMessage(mnsRequestDto);
|
messageService.sendMessage(mnsRequestDto);
|
||||||
return CommonResponse.success();
|
return CommonResponse.success();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@ -1,17 +1,37 @@
|
|||||||
package cn.axzo.msg.center.notices.service.impl;
|
package cn.axzo.msg.center.notices.service.impl;
|
||||||
|
|
||||||
|
import cn.axzo.msg.center.common.utils.PlaceholderResolver;
|
||||||
import cn.axzo.msg.center.dal.MNSMessageAppDao;
|
import cn.axzo.msg.center.dal.MNSMessageAppDao;
|
||||||
import cn.axzo.msg.center.domain.entity.*;
|
import cn.axzo.msg.center.domain.entity.MNSBatchMessageRequest;
|
||||||
|
import cn.axzo.msg.center.domain.entity.MNSChannelMessageTemplate;
|
||||||
|
import cn.axzo.msg.center.domain.entity.MNSMessage;
|
||||||
|
import cn.axzo.msg.center.domain.entity.MNSMessageApp;
|
||||||
|
import cn.axzo.msg.center.domain.entity.MNSMessageChannel;
|
||||||
|
import cn.axzo.msg.center.domain.entity.MNSMessageRedo;
|
||||||
|
import cn.axzo.msg.center.domain.entity.MNSMessageTemplate;
|
||||||
import cn.axzo.msg.center.domain.enums.YesNoEnum;
|
import cn.axzo.msg.center.domain.enums.YesNoEnum;
|
||||||
import cn.axzo.msg.center.notices.common.annotation.ApiRequestLog;
|
import cn.axzo.msg.center.notices.common.annotation.ApiRequestLog;
|
||||||
import cn.axzo.msg.center.notices.common.constans.CommonConstants;
|
import cn.axzo.msg.center.notices.common.constans.CommonConstants;
|
||||||
import cn.axzo.msg.center.notices.common.domain.BatchMessageSendContext;
|
import cn.axzo.msg.center.notices.common.domain.BatchMessageSendContext;
|
||||||
import cn.axzo.msg.center.notices.common.domain.MessageContext;
|
import cn.axzo.msg.center.notices.common.domain.MessageContext;
|
||||||
import cn.axzo.msg.center.notices.common.enums.*;
|
import cn.axzo.msg.center.notices.common.enums.AvailableStatusEnum;
|
||||||
|
import cn.axzo.msg.center.notices.common.enums.BatchMessageRequestStatusEnum;
|
||||||
|
import cn.axzo.msg.center.notices.common.enums.BatchSendTypeEnum;
|
||||||
|
import cn.axzo.msg.center.notices.common.enums.MessageStatusEnum;
|
||||||
|
import cn.axzo.msg.center.notices.common.enums.MessageTypeEnum;
|
||||||
|
import cn.axzo.msg.center.notices.common.enums.NotifyTypeEnum;
|
||||||
|
import cn.axzo.msg.center.notices.common.enums.RetryingFlagEnum;
|
||||||
|
import cn.axzo.msg.center.notices.common.enums.ReturnCodeEnum;
|
||||||
import cn.axzo.msg.center.notices.common.exception.BizException;
|
import cn.axzo.msg.center.notices.common.exception.BizException;
|
||||||
import cn.axzo.msg.center.notices.common.utils.DateUtils;
|
import cn.axzo.msg.center.notices.common.utils.DateUtils;
|
||||||
import cn.axzo.msg.center.notices.integration.client.DingDingClient;
|
import cn.axzo.msg.center.notices.integration.client.DingDingClient;
|
||||||
import cn.axzo.msg.center.notices.manager.api.*;
|
import cn.axzo.msg.center.notices.integration.client.impl.RateLimitException;
|
||||||
|
import cn.axzo.msg.center.notices.manager.api.BatchMessageRequestManger;
|
||||||
|
import cn.axzo.msg.center.notices.manager.api.MessageChannelRouter;
|
||||||
|
import cn.axzo.msg.center.notices.manager.api.MessageManager;
|
||||||
|
import cn.axzo.msg.center.notices.manager.api.MessageTemplateManager;
|
||||||
|
import cn.axzo.msg.center.notices.manager.api.RetryStrategy;
|
||||||
|
import cn.axzo.msg.center.notices.manager.api.SmsSendManager;
|
||||||
import cn.axzo.msg.center.notices.manager.api.dto.request.MessageSendRequestDto;
|
import cn.axzo.msg.center.notices.manager.api.dto.request.MessageSendRequestDto;
|
||||||
import cn.axzo.msg.center.notices.manager.api.dto.request.MnsRequestDto;
|
import cn.axzo.msg.center.notices.manager.api.dto.request.MnsRequestDto;
|
||||||
import cn.axzo.msg.center.notices.manager.api.dto.request.SendBatchMessageRequestDto;
|
import cn.axzo.msg.center.notices.manager.api.dto.request.SendBatchMessageRequestDto;
|
||||||
@ -30,7 +50,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.collections4.CollectionUtils;
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.apache.commons.collections4.MapUtils;
|
import org.apache.commons.collections4.MapUtils;
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||||
import org.springframework.context.EnvironmentAware;
|
import org.springframework.context.EnvironmentAware;
|
||||||
@ -38,8 +57,12 @@ import org.springframework.core.env.Environment;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.*;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@ -84,6 +107,9 @@ public class MessageServiceImpl implements MessageService, EnvironmentAware {
|
|||||||
@Resource(name = "userCellphoneProperties")
|
@Resource(name = "userCellphoneProperties")
|
||||||
private UserCellphoneProperties userCellphoneProperties;
|
private UserCellphoneProperties userCellphoneProperties;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private MnsLimiter mnsLimiter;
|
||||||
|
|
||||||
@Value("${prod.profile.string:master}")
|
@Value("${prod.profile.string:master}")
|
||||||
private String prodProfileString;
|
private String prodProfileString;
|
||||||
|
|
||||||
@ -109,6 +135,8 @@ public class MessageServiceImpl implements MessageService, EnvironmentAware {
|
|||||||
boolean isLegalPhone = PhoneUtil.isPhone(request.getPhoneNo());
|
boolean isLegalPhone = PhoneUtil.isPhone(request.getPhoneNo());
|
||||||
BizException.error(isLegalPhone, ReturnCodeEnum.INVALID_PARAMETER, "非法的手机号:" + request.getPhoneNo());
|
BizException.error(isLegalPhone, ReturnCodeEnum.INVALID_PARAMETER, "非法的手机号:" + request.getPhoneNo());
|
||||||
|
|
||||||
|
mnsLimiter.maybeLimitSend(request);
|
||||||
|
|
||||||
// 查询并校验
|
// 查询并校验
|
||||||
MNSMessageTemplate messageTemplate = messageTemplateManager.queryAndCheckInnerTemplate(request.getTemplateNo());
|
MNSMessageTemplate messageTemplate = messageTemplateManager.queryAndCheckInnerTemplate(request.getTemplateNo());
|
||||||
|
|
||||||
@ -147,12 +175,19 @@ public class MessageServiceImpl implements MessageService, EnvironmentAware {
|
|||||||
dto.setRequestChannelNo(message.getMessageOrderNo());
|
dto.setRequestChannelNo(message.getMessageOrderNo());
|
||||||
SendSmsCommonResponseDto response = smsSendManagerComposite.sendMessage(dto);
|
SendSmsCommonResponseDto response = smsSendManagerComposite.sendMessage(dto);
|
||||||
messageManager.updateToProcessing(response.getBizId(), response.getRequestId(), message.getId());
|
messageManager.updateToProcessing(response.getBizId(), response.getRequestId(), message.getId());
|
||||||
|
mnsLimiter.setSendSuccess(request);
|
||||||
|
} catch (RateLimitException e) {
|
||||||
|
messageManager.updateToFail(message.getId());
|
||||||
|
mnsLimiter.setSendFail(request, e);
|
||||||
|
// don't rethrow rate limit exception to let client retry
|
||||||
} catch (BizException e) {
|
} catch (BizException e) {
|
||||||
messageManager.updateToFail(message.getId());
|
messageManager.updateToFail(message.getId());
|
||||||
|
mnsLimiter.setSendFail(request, e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
dingDingClient.notifySingleMessage(request.getTemplateNo(), request.getRequestNo(), e.getMessage());
|
dingDingClient.notifySingleMessage(request.getTemplateNo(), request.getRequestNo(), e.getMessage());
|
||||||
|
mnsLimiter.setSendFail(request, e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,7 +241,15 @@ public class MessageServiceImpl implements MessageService, EnvironmentAware {
|
|||||||
message.setChannelName(CommonConstants.MOCK_CHANNEL);
|
message.setChannelName(CommonConstants.MOCK_CHANNEL);
|
||||||
message.updateById();
|
message.updateById();
|
||||||
|
|
||||||
String messageContent = parse(template.getTemplateContent(), request.getParams());
|
String messageContent;
|
||||||
|
try {
|
||||||
|
messageContent = PlaceholderResolver.tryResolve(template.getTemplateContent(), request.getParams());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("fail to resolve template content, fallback to old method. request={}, template={}, message={}",
|
||||||
|
request, template, message, e);
|
||||||
|
//fallback
|
||||||
|
messageContent = parse(template.getTemplateContent(), request.getParams());
|
||||||
|
}
|
||||||
dingDingClient.notifyMockMessage(request.getPhoneNo(), template.getTitle(), messageContent);
|
dingDingClient.notifyMockMessage(request.getPhoneNo(), template.getTitle(), messageContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,172 @@
|
|||||||
|
package cn.axzo.msg.center.notices.service.impl;
|
||||||
|
|
||||||
|
import cn.axzo.basics.common.exception.ServiceException;
|
||||||
|
import cn.axzo.msg.center.notices.common.enums.ReturnCodeEnum;
|
||||||
|
import cn.axzo.msg.center.notices.manager.api.dto.request.MnsRequestDto;
|
||||||
|
import com.google.common.hash.Hasher;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
|
import org.joda.time.DateTimeZone;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yanglin
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MnsLimiter {
|
||||||
|
|
||||||
|
private final MnsProperties props;
|
||||||
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于固定窗口, 限制重复内容的发送次数
|
||||||
|
*/
|
||||||
|
void maybeLimitSend(MnsRequestDto request) {
|
||||||
|
if (!props.shouldLimit(request.getTemplateNo())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// key中包含了固定窗口因子
|
||||||
|
String key = buildTimeAwareKey(request);
|
||||||
|
String countStr = stringRedisTemplate.opsForValue().get(key);
|
||||||
|
if (StringUtils.isBlank(countStr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long alreadySendCount = Long.parseLong(countStr);
|
||||||
|
|
||||||
|
MnsType msnType = request.getInternalObj(MnsType.class);
|
||||||
|
if (msnType == null) msnType = MnsType.BIZ;
|
||||||
|
|
||||||
|
long limitCount;
|
||||||
|
String error;
|
||||||
|
if (msnType == MnsType.BIZ) {
|
||||||
|
limitCount = props.getBizLimitPerDay();
|
||||||
|
error = String.format("业务短信: 每天发送给同一个手机号的重复内容不能超过 %s 条。 请勿重试发送!!", limitCount);
|
||||||
|
} else {
|
||||||
|
limitCount = props.getVerifyCodeLimitPerWindow();
|
||||||
|
error = String.format("验证码短信: 每 %s 分钟发送给同一个手机号的重复内容不能超过 %s 条。 请勿重试发送!!",
|
||||||
|
props.getVerifyCodeLimitWindowMinutes(), limitCount);
|
||||||
|
}
|
||||||
|
if (alreadySendCount + 1 > limitCount) {
|
||||||
|
log.warn("{}, request={}", error, request);
|
||||||
|
throw new ServiceException(ReturnCodeEnum.FAIL.getCode(), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSendSuccess(MnsRequestDto request) {
|
||||||
|
if (!props.shouldLimit(request.getTemplateNo())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// key中包含了固定窗口因子
|
||||||
|
String key = buildTimeAwareKey(request);
|
||||||
|
stringRedisTemplate.opsForValue().increment(key);
|
||||||
|
// 这里设置过期时间只是为了删除对应的key, 和限制逻辑本身没有太大的关系
|
||||||
|
long expireMinutes = getTimeAwareExpireTimeMinutes(request);
|
||||||
|
// 延迟删除
|
||||||
|
stringRedisTemplate.expire(key, expireMinutes + 1, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSendFail(MnsRequestDto request, Exception e) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取key过期时间
|
||||||
|
*/
|
||||||
|
private long getTimeAwareExpireTimeMinutes(MnsRequestDto request) {
|
||||||
|
MnsType msnType = request.getInternalObj(MnsType.class);
|
||||||
|
if (msnType == null) {
|
||||||
|
msnType = MnsType.BIZ;
|
||||||
|
}
|
||||||
|
return msnType == MnsType.BIZ
|
||||||
|
? TimeUnit.DAYS.toMinutes(1)
|
||||||
|
: props.getVerifyCodeLimitWindowMinutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建包含时间元素的缓存key, key中包含了固定窗口因子. 固定窗口: 限制发送重复的内容
|
||||||
|
*/
|
||||||
|
private String buildTimeAwareKey(MnsRequestDto request) {
|
||||||
|
MnsType msnType = request.getInternalObj(MnsType.class);
|
||||||
|
if (msnType == null) {
|
||||||
|
msnType = MnsType.BIZ;
|
||||||
|
}
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
buf.append("msg-center:mns_limit");
|
||||||
|
Consumer<String> appender = value -> {
|
||||||
|
if (StringUtils.isNotBlank(value))
|
||||||
|
buf.append(":").append(value);
|
||||||
|
};
|
||||||
|
// 通过templateNo和params就可以判断出发送的内容是不是一样的
|
||||||
|
appender.accept(msnType.name());
|
||||||
|
appender.accept(request.getTemplateNo());
|
||||||
|
appender.accept(request.getPhoneNo());
|
||||||
|
appender.accept(buildTimeAwareSegment(request));
|
||||||
|
appender.accept(hashParams(request.getParams()));
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildTimeAwareSegment(MnsRequestDto request) {
|
||||||
|
MnsType mnsType = request.getInternalObj(MnsType.class);
|
||||||
|
if (mnsType == null) {
|
||||||
|
mnsType = MnsType.BIZ;
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
|
||||||
|
int windowMinutes = props.getVerifyCodeLimitWindowMinutes();
|
||||||
|
return mnsType == MnsType.BIZ
|
||||||
|
? sdf.format(now)
|
||||||
|
: String.format("%s-%d-%d",
|
||||||
|
sdf.format(now), windowMinutes, getVerifySegmentIndex(now, windowMinutes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getVerifySegmentIndex(Date now, int windowMinutes) {
|
||||||
|
DateTime dateTime = new DateTime(now, DateTimeZone.forID("Asia/Shanghai"));
|
||||||
|
// Calculate the total number of intervals in a day
|
||||||
|
int intervalsPerDay = 24 * 60 / windowMinutes;
|
||||||
|
// Get the current time in minutes
|
||||||
|
int currentMinutes = dateTime.getHourOfDay() * 60 + dateTime.getMinuteOfHour();
|
||||||
|
// Find the index of the interval that the current time falls into
|
||||||
|
int intervalIndex = currentMinutes / windowMinutes;
|
||||||
|
// Ensure the index is within the range of the array
|
||||||
|
if (intervalIndex >= intervalsPerDay) {
|
||||||
|
intervalIndex = intervalsPerDay - 1;
|
||||||
|
}
|
||||||
|
return intervalIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
|
private static String hashParams(Map<String, Object> params) {
|
||||||
|
if (params == null || params.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Hasher hasher = Hashing.murmur3_128().newHasher();
|
||||||
|
Consumer<Object> appender = value -> {
|
||||||
|
if (value != null) {
|
||||||
|
hasher.putString(String.valueOf(value), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (Map.Entry<String, Object> e : new TreeMap<>(params).entrySet()) {
|
||||||
|
if (e.getKey() == null || e.getValue() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
appender.accept(e.getKey());
|
||||||
|
appender.accept(e.getValue());
|
||||||
|
}
|
||||||
|
return hasher.hash().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package cn.axzo.msg.center.notices.service.impl;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yanglin
|
||||||
|
*/
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
@RefreshScope
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "user.cellphone")
|
||||||
|
public class MnsProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启重复发送限制
|
||||||
|
*/
|
||||||
|
private boolean sendLimitOn = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不验证重复发送的内部模板编码
|
||||||
|
*/
|
||||||
|
private String limitWhitelist;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务上每天发送重复内容的次数限制
|
||||||
|
*/
|
||||||
|
private long bizLimitPerDay = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码15分钟内发送的内容都是一样的
|
||||||
|
*/
|
||||||
|
private int verifyCodeLimitWindowMinutes = 15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码每15分钟可以重复发送的次数
|
||||||
|
*/
|
||||||
|
private long verifyCodeLimitPerWindow = 20;
|
||||||
|
|
||||||
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||||
|
public boolean shouldLimit(String templateNo) {
|
||||||
|
return sendLimitOn && !isInLimitWhitelist(templateNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInLimitWhitelist(String templateNo) {
|
||||||
|
return StringUtils.isNotBlank(limitWhitelist)
|
||||||
|
&& limitWhitelist.contains(templateNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package cn.axzo.msg.center.notices.service.impl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author yanglin
|
||||||
|
*/
|
||||||
|
public enum MnsType {
|
||||||
|
/**
|
||||||
|
* 业务短信
|
||||||
|
*/
|
||||||
|
BIZ,
|
||||||
|
/**
|
||||||
|
* 验证码
|
||||||
|
*/
|
||||||
|
VERIFY_CODE
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import cn.axzo.msg.center.notices.common.properties.SmsProperties;
|
|||||||
import cn.axzo.msg.center.notices.manager.api.dto.request.MnsRequestDto;
|
import cn.axzo.msg.center.notices.manager.api.dto.request.MnsRequestDto;
|
||||||
import cn.axzo.msg.center.notices.service.api.MessageService;
|
import cn.axzo.msg.center.notices.service.api.MessageService;
|
||||||
import cn.axzo.msg.center.notices.service.gateway.SmsGateway;
|
import cn.axzo.msg.center.notices.service.gateway.SmsGateway;
|
||||||
|
import cn.axzo.msg.center.notices.service.impl.MnsType;
|
||||||
import cn.axzo.msg.center.notices.service.request.CaptchaParam;
|
import cn.axzo.msg.center.notices.service.request.CaptchaParam;
|
||||||
import cn.axzo.msg.center.notices.service.request.SendCodeV2Req;
|
import cn.axzo.msg.center.notices.service.request.SendCodeV2Req;
|
||||||
import cn.axzo.msg.center.notices.service.response.SmsCodeInfoRes;
|
import cn.axzo.msg.center.notices.service.response.SmsCodeInfoRes;
|
||||||
@ -31,7 +32,13 @@ import org.springframework.cloud.context.config.annotation.RefreshScope;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.*;
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author zhangPeng
|
* @author zhangPeng
|
||||||
@ -68,7 +75,7 @@ public class SmsManager extends BaseManager implements SmsGateway {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public int sendSmsCode(SendCodeV2Req req) {
|
public int sendSmsCode(SendCodeV2Req req) {
|
||||||
sendMnsCode(req.getPhone(), req.getParam(), req.getAppCode(),req.getTemplateNo());
|
sendMnsCode(req.getPhone(), req.getParam(), req.getAppCode(),req.getTemplateNo(), req.getCode());
|
||||||
log.info("发送验证码 手机号{} -- 参数 {} -- 应用程序code {} -- 模板编号templateCode {}", req.getPhone(), req.getParam(), req.getAppCode(),req.getTemplateNo());
|
log.info("发送验证码 手机号{} -- 参数 {} -- 应用程序code {} -- 模板编号templateCode {}", req.getPhone(), req.getParam(), req.getAppCode(),req.getTemplateNo());
|
||||||
return req.getCode();
|
return req.getCode();
|
||||||
}
|
}
|
||||||
@ -176,7 +183,7 @@ public class SmsManager extends BaseManager implements SmsGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMnsCode(String phoneNumber, Map<String, Object> param, String appCode,String templateNo) {
|
public void sendMnsCode(String phoneNumber, Map<String, Object> param, String appCode, String templateNo, Integer code) {
|
||||||
if (!isSendMnsCode) {
|
if (!isSendMnsCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -186,6 +193,10 @@ public class SmsManager extends BaseManager implements SmsGateway {
|
|||||||
request.setTemplateNo(templateNo);
|
request.setTemplateNo(templateNo);
|
||||||
request.setParams(param);
|
request.setParams(param);
|
||||||
request.setRequestNo(UUID.randomUUID().toString());
|
request.setRequestNo(UUID.randomUUID().toString());
|
||||||
|
request.setInternalObj(MnsType.VERIFY_CODE);
|
||||||
|
HashMap<String, Object> params = new HashMap<>();
|
||||||
|
params.put("code", code);
|
||||||
|
request.setParams(params);
|
||||||
messageService.sendMessage(request);
|
messageService.sendMessage(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user