提交20241207
This commit is contained in:
parent
8e24f48b52
commit
47b6f6a8a5
|
|
@ -7,7 +7,7 @@ import javax.validation.constraints.NotNull;
|
|||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 邮件发送 Request DTO
|
||||
* 邮箱发送 Request DTO
|
||||
*
|
||||
* @author wangjingqi
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package cn.hangtag.module.system.api.mail.dto.code;
|
||||
|
||||
import cn.hangtag.framework.common.validation.InEnum;
|
||||
import cn.hangtag.framework.common.validation.Mobile;
|
||||
import cn.hangtag.module.system.enums.mail.MailSceneEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 邮箱验证码的发送 Request DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class MailCodeSendReqDTO {
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Mobile
|
||||
@NotEmpty(message = "邮箱不能为空")
|
||||
private String mail;
|
||||
/**
|
||||
* 发送场景
|
||||
*/
|
||||
@NotNull(message = "发送场景不能为空")
|
||||
@InEnum(MailSceneEnum.class)
|
||||
private Integer scene;
|
||||
/**
|
||||
* 发送 IP
|
||||
*/
|
||||
@NotEmpty(message = "发送 IP 不能为空")
|
||||
private String createIp;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package cn.hangtag.module.system.api.mail.dto.code;
|
||||
|
||||
import cn.hangtag.framework.common.validation.InEnum;
|
||||
import cn.hangtag.framework.common.validation.Mobile;
|
||||
import cn.hangtag.module.system.enums.sms.SmsSceneEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 邮箱验证码的使用 Request DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class MailCodeUseReqDTO {
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Mobile
|
||||
@NotEmpty(message = "邮箱不能为空")
|
||||
private String mail;
|
||||
/**
|
||||
* 发送场景
|
||||
*/
|
||||
@NotNull(message = "发送场景不能为空")
|
||||
@InEnum(SmsSceneEnum.class)
|
||||
private Integer scene;
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@NotEmpty(message = "验证码")
|
||||
private String code;
|
||||
/**
|
||||
* 使用 IP
|
||||
*/
|
||||
@NotEmpty(message = "使用 IP 不能为空")
|
||||
private String usedIp;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package cn.hangtag.module.system.api.mail.dto.code;
|
||||
|
||||
import cn.hangtag.framework.common.validation.InEnum;
|
||||
import cn.hangtag.framework.common.validation.Mobile;
|
||||
import cn.hangtag.module.system.enums.sms.SmsSceneEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 邮箱验证码的校验 Request DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class MailCodeValidateReqDTO {
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Mobile
|
||||
@NotEmpty(message = "邮箱不能为空")
|
||||
private String mail;
|
||||
/**
|
||||
* 发送场景
|
||||
*/
|
||||
@NotNull(message = "发送场景不能为空")
|
||||
@InEnum(SmsSceneEnum.class)
|
||||
private Integer scene;
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@NotEmpty(message = "验证码")
|
||||
private String code;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package cn.hangtag.module.system.enums.mail;
|
||||
|
||||
import cn.hangtag.framework.common.core.IntArrayValuable;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 用户短信验证码发送场景的枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum MailSceneEnum implements IntArrayValuable {
|
||||
|
||||
MEMBER_LOGIN(1, "user-sms-login", "会员用户 - 邮箱登陆"),
|
||||
MEMBER_UPDATE_MOBILE(2, "user-update-mobile", "会员用户 - 修改手机"),
|
||||
MEMBER_UPDATE_PASSWORD(3, "user-update-password", "会员用户 - 修改密码"),
|
||||
MEMBER_RESET_PASSWORD(4, "user-reset-password", "会员用户 - 忘记密码"),
|
||||
|
||||
ADMIN_MEMBER_LOGIN(21, "admin-sms-login", "后台用户 - 邮箱登录");
|
||||
|
||||
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(MailSceneEnum::getScene).toArray();
|
||||
|
||||
/**
|
||||
* 验证场景的编号
|
||||
*/
|
||||
private final Integer scene;
|
||||
/**
|
||||
* 模版编码
|
||||
*/
|
||||
private final String templateCode;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
@Override
|
||||
public int[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static MailSceneEnum getCodeByScene(Integer scene) {
|
||||
return ArrayUtil.firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene),
|
||||
values());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package cn.hangtag.module.system.service.mail;
|
||||
|
||||
import cn.hangtag.framework.common.exception.ServiceException;
|
||||
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
|
||||
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
|
||||
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 短信验证码 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface MailCodeService {
|
||||
|
||||
/**
|
||||
* 创建邮件验证码,并进行发送
|
||||
*
|
||||
* @param reqDTO 发送请求
|
||||
*/
|
||||
void sendMailCode(@Valid MailCodeSendReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 验证邮件验证码,并进行使用
|
||||
* 如果正确,则将验证码标记成已使用
|
||||
* 如果错误,则抛出 {@link ServiceException} 异常
|
||||
*
|
||||
* @param reqDTO 使用请求
|
||||
*/
|
||||
void useMailCode(@Valid SmsCodeUseReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 检查验证码是否有效
|
||||
*
|
||||
* @param reqDTO 校验请求
|
||||
*/
|
||||
void validateMailCode(@Valid SmsCodeValidateReqDTO reqDTO);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package cn.hangtag.module.system.service.mail;
|
||||
|
||||
import cn.hangtag.module.system.api.mail.dto.code.MailCodeSendReqDTO;
|
||||
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
|
||||
import cn.hangtag.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
|
||||
import cn.hangtag.module.system.dal.dataobject.sms.SmsCodeDO;
|
||||
import cn.hangtag.module.system.dal.mysql.sms.SmsCodeMapper;
|
||||
import cn.hangtag.module.system.enums.sms.SmsSceneEnum;
|
||||
import cn.hangtag.module.system.framework.sms.config.SmsCodeProperties;
|
||||
import cn.hangtag.module.system.service.sms.SmsCodeService;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.hangtag.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.hangtag.framework.common.util.date.DateUtils.isToday;
|
||||
import static cn.hangtag.module.system.enums.ErrorCodeConstants.*;
|
||||
import static cn.hutool.core.util.RandomUtil.randomInt;
|
||||
|
||||
/**
|
||||
* 邮箱验证码 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class MailCodeServiceImpl implements MailCodeService {
|
||||
|
||||
@Resource
|
||||
private SmsCodeProperties smsCodeProperties;
|
||||
|
||||
@Resource
|
||||
private SmsCodeMapper smsCodeMapper;
|
||||
|
||||
@Resource
|
||||
private MailSendService mailSendService;
|
||||
|
||||
@Override
|
||||
public void sendMailCode(MailCodeSendReqDTO reqDTO) {
|
||||
SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene());
|
||||
Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene());
|
||||
// 创建验证码
|
||||
String code = createMailCode(reqDTO.getMail(), reqDTO.getScene(), reqDTO.getCreateIp());
|
||||
// 发送验证码
|
||||
mailSendService.sendSingleMail(reqDTO.getMail(), null, null,
|
||||
sceneEnum.getTemplateCode(), MapUtil.of("code", code));
|
||||
}
|
||||
|
||||
private String createMailCode(String mobile, Integer scene, String ip) {
|
||||
// 校验是否可以发送验证码,不用筛选场景
|
||||
SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null, null);
|
||||
if (lastSmsCode != null) {
|
||||
if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
|
||||
< smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁
|
||||
throw exception(SMS_CODE_SEND_TOO_FAST);
|
||||
}
|
||||
if (isToday(lastSmsCode.getCreateTime()) && // 必须是今天,才能计算超过当天的上限
|
||||
lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。
|
||||
throw exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
|
||||
}
|
||||
// TODO 芋艿:提升,每个 IP 每天可发送数量
|
||||
// TODO 芋艿:提升,每个 IP 每小时可发送数量
|
||||
}
|
||||
|
||||
// 创建验证码记录
|
||||
String code = String.format("%0" + smsCodeProperties.getEndCode().toString().length() + "d",
|
||||
randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1));
|
||||
SmsCodeDO newSmsCode = SmsCodeDO.builder().mobile(mobile).code(code).scene(scene)
|
||||
.todayIndex(lastSmsCode != null && isToday(lastSmsCode.getCreateTime()) ? lastSmsCode.getTodayIndex() + 1 : 1)
|
||||
.createIp(ip).used(false).build();
|
||||
smsCodeMapper.insert(newSmsCode);
|
||||
return code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void useMailCode(SmsCodeUseReqDTO reqDTO) {
|
||||
// 检测验证码是否有效
|
||||
SmsCodeDO lastSmsCode = validateMailCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene());
|
||||
// 使用验证码
|
||||
smsCodeMapper.updateById(SmsCodeDO.builder().id(lastSmsCode.getId())
|
||||
.used(true).usedTime(LocalDateTime.now()).usedIp(reqDTO.getUsedIp()).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateMailCode(SmsCodeValidateReqDTO reqDTO) {
|
||||
validateMailCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene());
|
||||
}
|
||||
|
||||
private SmsCodeDO validateMailCode0(String mobile, String code, Integer scene) {
|
||||
// 校验验证码
|
||||
SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, code, scene);
|
||||
// 若验证码不存在,抛出异常
|
||||
if (lastSmsCode == null) {
|
||||
throw exception(SMS_CODE_NOT_FOUND);
|
||||
}
|
||||
// 超过时间
|
||||
if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
|
||||
>= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期
|
||||
throw exception(SMS_CODE_EXPIRED);
|
||||
}
|
||||
// 判断验证码是否已被使用
|
||||
if (Boolean.TRUE.equals(lastSmsCode.getUsed())) {
|
||||
throw exception(SMS_CODE_USED);
|
||||
}
|
||||
return lastSmsCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -95,6 +95,9 @@ importers:
|
|||
diagram-js:
|
||||
specifier: ^12.8.0
|
||||
version: 12.8.1
|
||||
dom-to-image:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
driver.js:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
|
|
@ -2802,6 +2805,9 @@ packages:
|
|||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
dom-to-image@2.6.0:
|
||||
resolution: {integrity: sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==}
|
||||
|
||||
dom-walk@0.1.2:
|
||||
resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
|
||||
|
||||
|
|
@ -7410,7 +7416,7 @@ snapshots:
|
|||
'@types/web-bluetooth': 0.0.16
|
||||
'@vueuse/metadata': 9.13.0
|
||||
'@vueuse/shared': 9.13.0(vue@3.4.21(typescript@5.3.3))
|
||||
vue-demi: 0.14.7(vue@3.4.21(typescript@5.3.3))
|
||||
vue-demi: 0.14.10(vue@3.4.21(typescript@5.3.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
|
@ -7421,14 +7427,14 @@ snapshots:
|
|||
|
||||
'@vueuse/shared@10.9.0(vue@3.4.21(typescript@5.3.3))':
|
||||
dependencies:
|
||||
vue-demi: 0.14.7(vue@3.4.21(typescript@5.3.3))
|
||||
vue-demi: 0.14.10(vue@3.4.21(typescript@5.3.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@vueuse/shared@9.13.0(vue@3.4.21(typescript@5.3.3))':
|
||||
dependencies:
|
||||
vue-demi: 0.14.7(vue@3.4.21(typescript@5.3.3))
|
||||
vue-demi: 0.14.10(vue@3.4.21(typescript@5.3.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
|
@ -8217,6 +8223,8 @@ snapshots:
|
|||
domhandler: 5.0.3
|
||||
entities: 4.5.0
|
||||
|
||||
dom-to-image@2.6.0: {}
|
||||
|
||||
dom-walk@0.1.2: {}
|
||||
|
||||
dom7@3.0.0:
|
||||
|
|
|
|||
Loading…
Reference in New Issue