Browse Source

接入音信通,接入通用发送验证码功能

master
liuxinxin 1 year ago
parent
commit
26d039c8a9
10 changed files with 553 additions and 0 deletions
  1. +5
    -0
      pmapi/pom.xml
  2. +35
    -0
      pmapi/src/main/java/com/ningdatech/pmapi/sms/constant/DatePattern.java
  3. +68
    -0
      pmapi/src/main/java/com/ningdatech/pmapi/sms/constant/VerificationCodeType.java
  4. +13
    -0
      pmapi/src/main/java/com/ningdatech/pmapi/sms/constant/YxtSmsTemplateConst.java
  5. +41
    -0
      pmapi/src/main/java/com/ningdatech/pmapi/sms/controller/VerificationCodeController.java
  6. +97
    -0
      pmapi/src/main/java/com/ningdatech/pmapi/sms/manage/SmsManage.java
  7. +27
    -0
      pmapi/src/main/java/com/ningdatech/pmapi/sms/model/dto/VerifyCodeCacheDTO.java
  8. +26
    -0
      pmapi/src/main/java/com/ningdatech/pmapi/sms/model/po/ReqVerificationCodePO.java
  9. +206
    -0
      pmapi/src/main/java/com/ningdatech/pmapi/sms/utils/DateUtil.java
  10. +35
    -0
      pmapi/src/main/java/com/ningdatech/pmapi/sms/utils/SmsRedisKeyUtils.java

+ 5
- 0
pmapi/pom.xml View File

@@ -250,6 +250,11 @@
<groupId>com.ningdatech</groupId>
<artifactId>nd-flowable-starter</artifactId>
</dependency>
<dependency>
<groupId>com.ningdatech</groupId>
<artifactId>nd-yxt-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!--浙政钉-->
<dependency>
<groupId>com.alibaba.xxpt</groupId>


+ 35
- 0
pmapi/src/main/java/com/ningdatech/pmapi/sms/constant/DatePattern.java View File

@@ -0,0 +1,35 @@
package com.ningdatech.pmapi.sms.constant;

/**
* <p>
* DatePattern
* </p>
*
* @author WendyYang
* @since 09:28 2022/5/10
*/
public interface DatePattern {

String TIME_ZONE = "GMT+8";

String T = "'T'";
String GMT = "XXX";

String YMD_HMS = "yyyy-MM-dd HH:mm:ss";

String YMD_HMS_1 = "yyyyMMddHHmmss";

String YMD_HMS_S = "yyyy-MM-dd HH:mm:ss.SSS";

String YMD = "yyyy-MM-dd";
String YMD_0 = "yyyy/MM/dd";
String YMD_1 = "yyyyMMdd";
String HMS = "HH:mm:ss";
String HMS_0 = "HH/mm/ss";
String HMS_1 = "HHmmss";

String YMD_HMS_GMT = YMD + T + HMS + GMT;

String EEE = "EEE";

}

+ 68
- 0
pmapi/src/main/java/com/ningdatech/pmapi/sms/constant/VerificationCodeType.java View File

@@ -0,0 +1,68 @@
package com.ningdatech.pmapi.sms.constant;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.stream.Stream;

/**
* @author liuxinxin
* @date 2023/2/16 下午4:50
*/

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "VerificationCodeType", description = "验证码类型")
public enum VerificationCodeType {

/**
* 用户注册
*/
LOGIN("用户登录", 1, 5, 10);

@ApiModelProperty(value = "描述")
private String desc;

@ApiModelProperty(value = "发送间隔(单位:分钟)")
private Integer sendInterval;

@ApiModelProperty(value = "过期时间(单位:分钟)")
private Integer expireTime;

/**
* 小于等于0则不限制次数,超出次数限制则锁定验证码发送
*/
@ApiModelProperty(value = "每日发送次数")
private Integer sendTimesByDay;


public static VerificationCodeType match(String val, VerificationCodeType def) {
return Stream.of(values()).filter(item -> item.name().equalsIgnoreCase(val)).findAny().orElse(def);
}

public static VerificationCodeType get(String val) {
return match(val, null);
}

public boolean eq(VerificationCodeType val) {
return val != null && this.name().equals(val.name());
}

@ApiModelProperty(value = "编码")
public String getCode() {
return this.name();
}

public static VerificationCodeType of(String key) {
for (VerificationCodeType statusEnum : VerificationCodeType.values()) {
if (statusEnum.name().equals(key)) {
return statusEnum;
}
}
throw new IllegalArgumentException(String.format("Illegal VerificationCodeType = %s", key));
}
}

+ 13
- 0
pmapi/src/main/java/com/ningdatech/pmapi/sms/constant/YxtSmsTemplateConst.java View File

@@ -0,0 +1,13 @@
package com.ningdatech.pmapi.sms.constant;

/**
* @author liuxinxin
* @date 2022/8/8 下午5:05
*/
public interface YxtSmsTemplateConst {

/**
* 短信登陆验证码
*/
String SMS_LOGIN_TEMPLATE = "验证码:%s(有效期为%s分钟),请勿泄露给他人,如非本人操作,请忽略此信息。";
}

+ 41
- 0
pmapi/src/main/java/com/ningdatech/pmapi/sms/controller/VerificationCodeController.java View File

@@ -0,0 +1,41 @@
package com.ningdatech.pmapi.sms.controller;

import com.ningdatech.pmapi.sms.constant.VerificationCodeType;
import com.ningdatech.pmapi.sms.manage.SmsManage;
import com.ningdatech.pmapi.sms.model.po.ReqVerificationCodePO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author liuxinxin
* @date 2023/2/16 下午4:40
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/verification")
@Api(tags = "验证码相关接口")
@RequiredArgsConstructor
public class VerificationCodeController {


private final SmsManage smsManage;

/**
* 通用的发送验证码功能
* 一个系统可能有很多地方需要发送验证码(注册、找回密码等)
* 每增加一个场景{@link VerificationCodeType}就需要增加一个枚举值
*/
@ApiOperation(value = "发送验证码", notes = "发送验证码")
@PostMapping(value = "/send")
public void send(@Validated @RequestBody ReqVerificationCodePO request) {
smsManage.sendVerificationCode(request);
}

}

+ 97
- 0
pmapi/src/main/java/com/ningdatech/pmapi/sms/manage/SmsManage.java View File

@@ -0,0 +1,97 @@
package com.ningdatech.pmapi.sms.manage;

import cn.hutool.core.util.PhoneUtil;
import cn.hutool.core.util.RandomUtil;
import com.ningdatech.basic.exception.BizException;
import com.ningdatech.cache.model.cache.CacheKey;
import com.ningdatech.cache.repository.CachePlusOps;
import com.ningdatech.pmapi.sms.constant.VerificationCodeType;
import com.ningdatech.pmapi.sms.constant.YxtSmsTemplateConst;
import com.ningdatech.pmapi.sms.model.dto.VerifyCodeCacheDTO;
import com.ningdatech.pmapi.sms.model.po.ReqVerificationCodePO;
import com.ningdatech.pmapi.sms.utils.DateUtil;
import com.ningdatech.pmapi.sms.utils.SmsRedisKeyUtils;
import com.ningdatech.yxt.client.YxtClient;
import com.ningdatech.yxt.constants.YxtSmsSignEnum;
import com.ningdatech.yxt.model.cmd.SendSmsCmd;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;

/**
* @author liuxinxin
* @date 2023/2/16 下午4:42
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SmsManage {

private final YxtClient yxtClient;
private final CachePlusOps cachePlusOps;

public void sendVerificationCode(ReqVerificationCodePO request) {
Assert.isTrue(PhoneUtil.isMobile(request.getMobile()), "手机号码格式不正确");
String verificationType = request.getVerificationType();
VerificationCodeType verificationCodeTypeEnum = VerificationCodeType.of(verificationType);

// 验证是否被锁定
String lockKey = SmsRedisKeyUtils.smsSendLockKey(verificationCodeTypeEnum, request.getMobile());
if (StringUtils.isNotBlank(cachePlusOps.get(lockKey))) {
throw BizException.wrap("今日" + verificationCodeTypeEnum.getDesc() + "的验证码发送次数过多,已被锁定");
}
// 验证发送间隔
String cacheKey = SmsRedisKeyUtils.smsCodeVerifyKey(verificationCodeTypeEnum, request.getMobile());
VerifyCodeCacheDTO preCache = (VerifyCodeCacheDTO) cachePlusOps.get(cacheKey);
if (preCache != null) {
if (LocalDateTime.now().minusMinutes(verificationCodeTypeEnum.getSendInterval())
.isBefore(preCache.getSendTime())) {
throw BizException.wrap(verificationCodeTypeEnum.getSendInterval() + "分钟之内已发送过验证码,请稍后重试");
}
}
String code = RandomUtil.randomNumbers(6);
VerifyCodeCacheDTO cache = VerifyCodeCacheDTO.builder()
.code(code)
.sendTime(LocalDateTime.now())
.mobile(request.getMobile())
.build();

// 创建短信内容
SendSmsCmd sendSmsCmd = new SendSmsCmd();
switch (verificationCodeTypeEnum) {
case LOGIN:
SendSmsCmd.SendSmsContext sendSmsContext = new SendSmsCmd.SendSmsContext();
sendSmsContext.setReceiveNumber(request.getMobile());
sendSmsContext.setContent(String.format(YxtSmsTemplateConst.SMS_LOGIN_TEMPLATE, code, verificationCodeTypeEnum.getExpireTime()));
sendSmsCmd.setContextList(Collections.singletonList(sendSmsContext));
sendSmsCmd.setSmsSignEnum(YxtSmsSignEnum.ZJS_ELECTRONIC_EXPERT_LIB);
break;
default:
throw new IllegalArgumentException("非法的短信发送类型");
}
// 发送 短信
yxtClient.submitSmsTask(sendSmsCmd);
log.info("send verificationCode mobile = {},code = {}", request.getMobile(), code);

cachePlusOps.set(new CacheKey(cacheKey, Duration.ofMinutes(verificationCodeTypeEnum.getExpireTime())), cache);
String limitKey = SmsRedisKeyUtils.smsSendLimitKey(verificationCodeTypeEnum, request.getMobile());
if (StringUtils.isNotBlank(cachePlusOps.get(limitKey))) {
long limitCount = cachePlusOps.incr(new CacheKey(limitKey, Duration.ofSeconds(DateUtil.restSecondsFromNowToNoon())));
// 超出单日发送次数之后直接锁定
if (limitCount >= verificationCodeTypeEnum.getSendTimesByDay().longValue()) {
cachePlusOps.set(new CacheKey(lockKey, Duration.ofSeconds(DateUtil.restSecondsFromNowToNoon())), request.getMobile());
}
} else {
cachePlusOps.set(new CacheKey(limitKey, Duration.ofSeconds(DateUtil.restSecondsFromNowToNoon())), 1);
}

}


}

+ 27
- 0
pmapi/src/main/java/com/ningdatech/pmapi/sms/model/dto/VerifyCodeCacheDTO.java View File

@@ -0,0 +1,27 @@
package com.ningdatech.pmapi.sms.model.dto;

import lombok.Builder;
import lombok.Data;
import lombok.experimental.Tolerate;

import java.time.LocalDateTime;

/**
* @author liuxinxin
* @date 2023/2/16 下午4:42
*/
@Data
@Builder
public class VerifyCodeCacheDTO {

@Tolerate
public VerifyCodeCacheDTO() {
}

private String mobile;

private LocalDateTime sendTime;

private String code;

}

+ 26
- 0
pmapi/src/main/java/com/ningdatech/pmapi/sms/model/po/ReqVerificationCodePO.java View File

@@ -0,0 +1,26 @@
package com.ningdatech.pmapi.sms.model.po;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

/**
* @author liuxinxin
* @date 2023/2/16 下午4:40
*/
@Data
@ApiModel(value = "VerificationCodeRequest", description = "验证码发送验证")
public class ReqVerificationCodePO implements Serializable {

@ApiModelProperty(value = "手机号")
@NotBlank(message = "手机号不能为空")
private String mobile;

@ApiModelProperty(value = "短信类型", allowableValues = "LOGIN,RECOMMENDATION_PROOF_FILE_SUBMIT")
@NotBlank(message = "短信类型不能为空")
private String verificationType;

}

+ 206
- 0
pmapi/src/main/java/com/ningdatech/pmapi/sms/utils/DateUtil.java View File

@@ -0,0 +1,206 @@
package com.ningdatech.pmapi.sms.utils;


import com.ningdatech.pmapi.sms.constant.DatePattern;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;

/**
* @author qinxianyun
* JDK 8 新日期类 格式化与字符串转换 工具类
*/
public class DateUtil {

public static final DateTimeFormatter DTF_YMD_HMS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

public static final DateTimeFormatter DTF_YMD_HMS_COMPRESS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");

public static final DateTimeFormatter DTF_YMD_HM = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

public static final DateTimeFormatter DTF_YMD = DateTimeFormatter.ofPattern("yyyy-MM-dd");

public static final DateTimeFormatter DTF_YMD_SLASH = DateTimeFormatter.ofPattern("yyyy/MM/dd");

public static final DateTimeFormatter DTF_HMS = DateTimeFormatter.ofPattern("HH:mm:ss");

/**
* 最大时间,三位小数
*/
public static final LocalTime LOCAL_TIME_3D = LocalTime.of(23, 59, 59, 999_000_00);

/**
* LocalDateTime 转时间戳
*
* @param localDateTime /
* @return /
*/
public static Long getTimeStamp(LocalDateTime localDateTime) {
return localDateTime.atZone(ZoneId.systemDefault()).toEpochSecond();
}

/**
* 时间戳转LocalDateTime
*
* @param timeStamp /
* @return /
*/
public static LocalDateTime fromTimeStamp(Long timeStamp) {
return LocalDateTime.ofEpochSecond(timeStamp, 0, OffsetDateTime.now().getOffset());
}

/**
* LocalDateTime 转 Date
* Jdk8 后 不推荐使用 {@link Date} Date
*
* @param localDateTime /
* @return /
*/
public static Date toDate(LocalDateTime localDateTime) {
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}

/**
* LocalDate 转 Date
* Jdk8 后 不推荐使用 {@link Date} Date
*
* @param localDate /
* @return /
*/
public static Date toDate(LocalDate localDate) {
return toDate(localDate.atTime(LocalTime.now(ZoneId.systemDefault())));
}


/**
* Date转 LocalDateTime
* Jdk8 后 不推荐使用 {@link Date} Date
*
* @param date /
* @return /
*/
public static LocalDateTime toLocalDateTime(Date date) {
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}

/**
* 日期 格式化
*
* @param localDateTime /
* @param patten /
* @return /
*/
public static String localDateTimeFormat(LocalDateTime localDateTime, String patten) {
DateTimeFormatter df = DateTimeFormatter.ofPattern(patten);
return df.format(localDateTime);
}

/**
* 日期 格式化
*
* @param localDateTime /
* @param df /
* @return /
*/
public static String localDateTimeFormat(LocalDateTime localDateTime, DateTimeFormatter df) {
return df.format(localDateTime);
}

/**
* 日期格式化 yyyy-MM-dd HH:mm:ss
*
* @param localDateTime /
* @return /
*/
public static String localDateTimeFormatyMdHms(LocalDateTime localDateTime) {
return DTF_YMD_HMS.format(localDateTime);
}

/**
* 日期格式化 yyyy-MM-dd
*
* @param localDateTime /
* @return /
*/
public String localDateTimeFormatyMd(LocalDateTime localDateTime) {
return DTF_YMD.format(localDateTime);
}

/**
* 字符串转 LocalDateTime ,字符串格式 yyyy-MM-dd
*
* @param localDateTime /
* @return /
*/
public static LocalDateTime parseLocalDateTimeFormat(String localDateTime, String pattern) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern);
return LocalDateTime.from(dateTimeFormatter.parse(localDateTime));
}

/**
* 字符串转 LocalDateTime ,字符串格式 yyyy-MM-dd
*
* @param localDateTime /
* @return /
*/
public static LocalDateTime parseLocalDateTimeFormat(String localDateTime, DateTimeFormatter dateTimeFormatter) {
return LocalDateTime.from(dateTimeFormatter.parse(localDateTime));
}

/**
* 字符串转 LocalDateTime ,字符串格式 yyyy-MM-dd HH:mm:ss
*
* @param localDateTime /
* @return /
*/
public static LocalDateTime parseLocalDateTimeFormatyMdHms(String localDateTime) {
return LocalDateTime.from(DTF_YMD_HMS.parse(localDateTime));
}

public static String weekday(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern(DatePattern.EEE));
}

public static String weekdayWithChou(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern(DatePattern.EEE)).replace("星期", "周");
}

public static boolean between(LocalDate target, LocalDate start, LocalDate end) {
return !(target.isBefore(start) || target.isAfter(end));
}

public static boolean between(LocalDateTime target, LocalDateTime start, LocalDateTime end) {
return !(target.isBefore(start) || target.isAfter(end));
}

public static boolean between(LocalTime target, LocalTime start, LocalTime end) {
return !(target.isBefore(start) || target.isAfter(end));
}

public static boolean between(Date target, Date start, Date end) {
return !(target.before(start) || target.after(end));
}

public static LocalDateTime min(LocalDateTime time1, LocalDateTime time2) {
return time1.isAfter(time2) ? time2 : time1;
}

public static LocalDateTime max(LocalDateTime time1, LocalDateTime time2) {
return time1.isBefore(time2) ? time2 : time1;
}

public static long restSecondsFromNowToNoon() {
return ChronoUnit.SECONDS.between(LocalDateTime.now(), LocalDate.now().atTime(LocalTime.MAX));
}

public static boolean intersect(LocalDateTime startG1, LocalDateTime endG1, LocalDateTime startG2, LocalDateTime endG2) {
if (!startG1.isBefore(startG2) && !startG1.isAfter(endG2)) {
return Boolean.TRUE;
}
return !endG1.isBefore(startG2) && !endG1.isAfter(endG2);
}


}

+ 35
- 0
pmapi/src/main/java/com/ningdatech/pmapi/sms/utils/SmsRedisKeyUtils.java View File

@@ -0,0 +1,35 @@
package com.ningdatech.pmapi.sms.utils;

import cn.hutool.core.text.StrPool;
import com.ningdatech.pmapi.sms.constant.VerificationCodeType;

/**
* <p>
* SmsRedisKeyUtils
* </p>
*
* @author WendyYang
* @since 11:21 2022/7/25
*/
public class SmsRedisKeyUtils {

private SmsRedisKeyUtils() {
}

private static final String SMS_CODE_VERIFY_PREFIX = "sms:verify:";
private static final String SMS_SEND_LIMIT = "sms:limit:";
private static final String SMS_SEND_LOCK = "sms:lock:";

public static String smsCodeVerifyKey(VerificationCodeType type, String mobile) {
return SMS_CODE_VERIFY_PREFIX + StrPool.COLON + type.name() + StrPool.COLON + mobile;
}

public static String smsSendLimitKey(VerificationCodeType type, String mobile) {
return SMS_SEND_LIMIT + StrPool.COLON + type.name() + StrPool.COLON + mobile;
}

public static String smsSendLockKey(VerificationCodeType type, String mobile) {
return SMS_SEND_LOCK + StrPool.COLON + type.name() + StrPool.COLON + mobile;
}

}

Loading…
Cancel
Save