From 156ff82411cf0d274ce61b77054cabdc4ed00cbb Mon Sep 17 00:00:00 2001 From: gaoqr <13665037151@163.com> Date: Wed, 28 Aug 2024 09:28:52 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E7=B3=BB=E7=BB=9F=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=94=B9=E9=80=A0=EF=BC=9A=E9=A6=96=E9=A1=B5=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=9C=BA=E6=9E=84=E3=80=81=E6=96=B0=E7=94=A8=E6=88=B7=E7=94=A8?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7=E7=99=BB=E5=BD=95=EF=BC=88=E6=9C=BA?= =?UTF-8?q?=E6=9E=84=E6=96=B0=E5=A2=9E=E7=BB=84=E7=BB=87=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E7=A7=BB=E9=99=A4=E8=B4=A6=E5=8F=B7=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E3=80=81=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E5=AF=86=E7=A0=81=EF=BC=89=E3=80=81=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E9=AA=8C=E8=AF=81=E7=A0=81=E7=9A=84=E9=83=A8=E5=88=86?= =?UTF-8?q?=EF=BC=88=E9=A6=96=E9=A1=B5=E5=BF=98=E8=AE=B0=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E3=80=81=E4=BF=AE=E6=94=B9=E5=AF=86=E4=BF=9D=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=EF=BC=89=EF=BC=9B2=E3=80=81=E7=BB=84=E7=BB=87=E6=9D=83?= =?UTF-8?q?=E9=99=90=E8=8F=9C=E5=8D=95=E6=A0=91=E5=A2=9E=E5=8A=A0visiable?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E8=BF=87=E6=BB=A4=EF=BC=9B3=E3=80=81?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=92=89=E9=92=89=E3=80=81=E9=98=BF=E9=87=8C?= =?UTF-8?q?=E4=BA=91=E7=9F=AD=E4=BF=A1=E9=85=8D=E7=BD=AE=E5=8A=A0=E5=AF=86?= =?UTF-8?q?=E3=80=81=E5=AF=B9=E6=8E=A5=E7=B3=BB=E7=BB=9F=E5=8F=91=E9=80=81?= =?UTF-8?q?=E7=9F=AD=E4=BF=A1=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/ChenfengSmsAutoConfiguration.java | 7 +- .../framework/sms/core/client/SmsClient.java | 8 -- .../sms/core/client/SmsClientFactory.java | 18 +-- .../sms/core/client/dto/SmsSendRespDTO.java | 9 ++ .../core/client/impl/AbstractSmsClient.java | 21 +-- .../client/impl/SmsClientFactoryImpl.java | 67 ++++----- .../client/impl/aliyun/AliyunSmsClient.java | 25 +++- .../impl/debug/DebugDingTalkSmsClient.java | 14 +- .../client/impl/tencent/TencentSmsClient.java | 13 +- .../sms/core/property/SmsProperties.java | 76 +++++++++++ .../impl/aliyun/AliyunSmsClientTest.java | 4 +- .../impl/tencent/TencentSmsClientTest.java | 6 +- ...henfengDataSourceEncryptConfiguration.java | 24 ++++ .../ChenfengDataSourceEncryptInitEvent.java | 41 ++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...ChenfengRedisEncryptAutoConfiguration.java | 77 +++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../src/main/resources/application.yaml | 4 +- .../api/sms/dto/code/SmsCodeSendReqDTO.java | 5 +- .../api/sms/dto/code/SmsCodeUseReqDTO.java | 3 +- .../sms/dto/code/SmsCodeValidateReqDTO.java | 2 + .../system/enums/ErrorCodeConstants.java | 9 +- .../system/enums/logger/LoginLogTypeEnum.java | 1 + .../system/enums/logger/LoginResultEnum.java | 3 +- .../module/system/enums/sms/SmsSceneEnum.java | 13 +- .../system/enums/sms/SmsTemplateTypeEnum.java | 13 ++ .../system/SystemServerApplication.java | 2 + .../controller/admin/auth/AuthController.java | 25 +++- .../admin/auth/vo/AuthLoginReqVO.java | 19 ++- .../auth/vo/AuthPermissionInfoRespVO.java | 3 + .../admin/organ/vo/organ/OrganSaveReqVO.java | 11 +- .../admin/permission/MenuController.java | 4 +- .../permission/vo/menu/MenuListReqVO.java | 4 +- .../controller/admin/user/UserController.java | 10 ++ .../admin/user/UserProfileController.java | 7 + .../vo/profile/UserMobileUpdateReqVO.java | 26 ++++ .../vo/profile/UserProfileUpdateReqVO.java | 2 + .../user/vo/user/UserResetPasswordReqVO.java | 28 ++++ .../admin/user/vo/user/UserSaveReqVO.java | 9 +- .../system/convert/auth/AuthConvert.java | 5 +- .../system/convert/organ/OrganConvert.java | 5 +- .../dal/mysql/permission/MenuMapper.java | 3 +- .../dal/mysql/user/AdminUserMapper.java | 27 +++- .../system/dal/redis/RedisKeyConstants.java | 10 ++ .../framework/sms/SmsCodeProperties.java | 11 ++ .../system/mq/message/sms/SmsSendMessage.java | 5 + .../system/mq/producer/sms/SmsProducer.java | 16 ++- .../system/service/auth/AdminAuthService.java | 6 +- .../service/auth/AdminAuthServiceImpl.java | 40 ++++-- .../oauth2/OAuth2GrantServiceImpl.java | 17 +-- .../system/service/sms/SmsChannelService.java | 7 + .../service/sms/SmsChannelServiceImpl.java | 51 ++++--- .../service/sms/SmsCodeServiceImpl.java | 128 ++++++++++++++---- .../system/service/sms/SmsLogService.java | 3 +- .../system/service/sms/SmsLogServiceImpl.java | 6 +- .../service/sms/SmsSendServiceImpl.java | 52 ++++--- .../service/sms/SmsTemplateServiceImpl.java | 2 +- .../sms/handler/SmsSendAfterSendHandler.java | 25 ++++ .../impl/SmsSendAfterSendHandlerImpl.java | 46 +++++++ .../system/service/user/AdminUserService.java | 27 +++- .../service/user/AdminUserServiceImpl.java | 121 +++++++++++++++-- .../src/main/resources/application.yaml | 15 +- .../auth/AdminAuthServiceImplTest.java | 14 +- .../service/sms/SmsChannelServiceTest.java | 11 +- .../service/sms/SmsLogServiceImplTest.java | 3 +- .../service/sms/SmsSendServiceImplTest.java | 14 +- .../user/AdminUserServiceImplTest.java | 4 +- 67 files changed, 973 insertions(+), 316 deletions(-) create mode 100644 cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/property/SmsProperties.java create mode 100644 cf-framework/cf-spring-boot-starter-mybatis/src/main/java/com/cf/imes/framework/datasource/config/ChenfengDataSourceEncryptConfiguration.java create mode 100644 cf-framework/cf-spring-boot-starter-mybatis/src/main/java/com/cf/imes/framework/mybatis/core/event/ChenfengDataSourceEncryptInitEvent.java create mode 100644 cf-framework/cf-spring-boot-starter-redis/src/main/java/com/cf/imes/framework/redis/config/ChenfengRedisEncryptAutoConfiguration.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/profile/UserMobileUpdateReqVO.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/user/UserResetPasswordReqVO.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/handler/SmsSendAfterSendHandler.java create mode 100644 cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/handler/impl/SmsSendAfterSendHandlerImpl.java diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/config/ChenfengSmsAutoConfiguration.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/config/ChenfengSmsAutoConfiguration.java index cd1ad245f..350085d23 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/config/ChenfengSmsAutoConfiguration.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/config/ChenfengSmsAutoConfiguration.java @@ -2,7 +2,9 @@ package com.cf.imes.framework.sms.config; import com.cf.imes.framework.sms.core.client.SmsClientFactory; import com.cf.imes.framework.sms.core.client.impl.SmsClientFactoryImpl; +import com.cf.imes.framework.sms.core.property.SmsProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; /** @@ -11,11 +13,12 @@ import org.springframework.context.annotation.Bean; * @author 晨丰科技 */ @AutoConfiguration +@EnableConfigurationProperties(SmsProperties.class) public class ChenfengSmsAutoConfiguration { @Bean - public SmsClientFactory smsClientFactory() { - return new SmsClientFactoryImpl(); + public SmsClientFactory smsClientFactory(SmsProperties smsProperties) { + return new SmsClientFactoryImpl(smsProperties); } } diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/SmsClient.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/SmsClient.java index ae9d8ba0c..0ee4eeba4 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/SmsClient.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/SmsClient.java @@ -14,14 +14,6 @@ import java.util.List; * @since 2021/1/25 14:14 */ public interface SmsClient { - - /** - * 获得渠道编号 - * - * @return 渠道编号 - */ - Long getId(); - /** * 发送消息 * diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/SmsClientFactory.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/SmsClientFactory.java index b6d6b04e9..519827e02 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/SmsClientFactory.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/SmsClientFactory.java @@ -1,6 +1,6 @@ package com.cf.imes.framework.sms.core.client; -import com.cf.imes.framework.sms.core.property.SmsChannelProperties; +import com.cf.imes.framework.sms.core.property.SmsProperties; /** * 短信客户端的工厂接口 @@ -9,15 +9,6 @@ import com.cf.imes.framework.sms.core.property.SmsChannelProperties; * @since 2021/1/28 14:01 */ public interface SmsClientFactory { - - /** - * 获得短信 Client - * - * @param channelId 渠道编号 - * @return 短信 Client - */ - SmsClient getSmsClient(Long channelId); - /** * 获得短信 Client * @@ -27,10 +18,11 @@ public interface SmsClientFactory { SmsClient getSmsClient(String channelCode); /** - * 创建短信 Client + * 获得短信 Client * - * @param properties 配置对象 + * @param smsProperties 短信配置 + * @return 短信 Client */ - void createOrUpdateSmsClient(SmsChannelProperties properties); + SmsClient getSmsClient(SmsProperties smsProperties); } diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/dto/SmsSendRespDTO.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/dto/SmsSendRespDTO.java index 6891cbdb1..293d1557c 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/dto/SmsSendRespDTO.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/dto/SmsSendRespDTO.java @@ -40,4 +40,13 @@ public class SmsSendRespDTO { */ private String apiMsg; + /** + * 短信发送的目标手机号 + */ + private String mobile; + + /** + * 渠道编码 + */ + private String channelCode; } diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/AbstractSmsClient.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/AbstractSmsClient.java index df1ff5bcb..9f61ddb99 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/AbstractSmsClient.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/AbstractSmsClient.java @@ -1,7 +1,7 @@ package com.cf.imes.framework.sms.core.client.impl; import com.cf.imes.framework.sms.core.client.SmsClient; -import com.cf.imes.framework.sms.core.property.SmsChannelProperties; +import com.cf.imes.framework.sms.core.property.SmsProperties; import lombok.extern.slf4j.Slf4j; /** @@ -16,9 +16,9 @@ public abstract class AbstractSmsClient implements SmsClient { /** * 短信渠道配置 */ - protected volatile SmsChannelProperties properties; + protected volatile SmsProperties properties; - public AbstractSmsClient(SmsChannelProperties properties) { + public AbstractSmsClient(SmsProperties properties) { this.properties = properties; } @@ -35,7 +35,15 @@ public abstract class AbstractSmsClient implements SmsClient { */ protected abstract void doInit(); - public final void refresh(SmsChannelProperties properties) { + /** + * 配置是否发生改变 + * + * @param properties + * @return + */ + protected abstract boolean propChanged(SmsProperties properties); + + public final void refresh(SmsProperties properties) { // 判断是否更新 if (properties.equals(this.properties)) { return; @@ -46,9 +54,4 @@ public abstract class AbstractSmsClient implements SmsClient { this.init(); } - @Override - public Long getId() { - return properties.getId(); - } - } diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/SmsClientFactoryImpl.java index a5779e0da..668198df1 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/SmsClientFactoryImpl.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -1,5 +1,6 @@ package com.cf.imes.framework.sms.core.client.impl; +import cn.hutool.core.util.ObjectUtil; import com.cf.imes.framework.sms.core.client.SmsClient; import com.cf.imes.framework.sms.core.client.SmsClientFactory; import com.cf.imes.framework.sms.core.client.impl.aliyun.AliyunSmsClient; @@ -7,11 +8,12 @@ import com.cf.imes.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; import com.cf.imes.framework.sms.core.client.impl.tencent.TencentSmsClient; import com.cf.imes.framework.sms.core.enums.SmsChannelEnum; import com.cf.imes.framework.sms.core.property.SmsChannelProperties; +import com.cf.imes.framework.sms.core.property.SmsProperties; import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.springframework.validation.annotation.Validated; -import java.util.Arrays; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -22,14 +24,9 @@ import java.util.concurrent.ConcurrentMap; */ @Validated @Slf4j +@Service public class SmsClientFactoryImpl implements SmsClientFactory { - /** - * 短信客户端 Map - * key:渠道编号,使用 {@link SmsChannelProperties#getId()} - */ - private final ConcurrentMap channelIdClients = new ConcurrentHashMap<>(); - /** * 短信客户端 Map * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} @@ -39,21 +36,24 @@ public class SmsClientFactoryImpl implements SmsClientFactory { */ private final ConcurrentMap channelCodeClients = new ConcurrentHashMap<>(); - public SmsClientFactoryImpl() { - // 初始化 channelCodeClients 集合 - Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { - // 创建一个空的 SmsChannelProperties 对象 - SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) - .setApiKey("default default").setApiSecret("default"); - // 创建 Sms 客户端 - AbstractSmsClient smsClient = createSmsClient(properties); - channelCodeClients.put(channel.getCode(), smsClient); - }); + public SmsClientFactoryImpl(SmsProperties smsProperties) { + // 从配置初始化对应的client客户端 + AbstractSmsClient client = createSmsClient(smsProperties); + client.init(); + channelCodeClients.put(smsProperties.getChannel(), client); } @Override - public SmsClient getSmsClient(Long channelId) { - return channelIdClients.get(channelId); + public SmsClient getSmsClient(SmsProperties smsProperties) { + String channel = smsProperties.getChannel(); + AbstractSmsClient client = channelCodeClients.get(channel); + // 1、新的短信类型;2、配置改变 + if (ObjectUtil.isNull(client) || client.propChanged(smsProperties)) { + client = this.createSmsClient(smsProperties); + client.init(); + channelCodeClients.put(channel, client); + } + return client; } @Override @@ -61,30 +61,19 @@ public class SmsClientFactoryImpl implements SmsClientFactory { return channelCodeClients.get(channelCode); } - @Override - public void createOrUpdateSmsClient(SmsChannelProperties properties) { - AbstractSmsClient client = channelIdClients.get(properties.getId()); - if (client == null) { - client = this.createSmsClient(properties); - client.init(); - channelIdClients.put(client.getId(), client); - } else { - client.refresh(properties); - } - } - - private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { - SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode()); - Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum)); + private AbstractSmsClient createSmsClient(SmsProperties smsProperties) { + String channel = smsProperties.getChannel(); + SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(channel); + Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channel)); // 创建客户端 switch (channelEnum) { - case ALIYUN: return new AliyunSmsClient(properties); - case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); - case TENCENT: return new TencentSmsClient(properties); + case ALIYUN: return new AliyunSmsClient(smsProperties); + case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(smsProperties); + case TENCENT: return new TencentSmsClient(smsProperties); } // 创建失败,错误日志 + 抛出异常 - log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); - throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); + log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", smsProperties); + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", smsProperties)); } } diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java index 2a4c063f1..f5194faa0 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java @@ -1,6 +1,8 @@ package com.cf.imes.framework.sms.core.client.impl.aliyun; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import com.aliyuncs.auth.Credential; import com.cf.imes.framework.common.core.KeyValue; import com.cf.imes.framework.common.util.collection.MapUtils; import com.cf.imes.framework.common.util.json.JsonUtils; @@ -9,7 +11,6 @@ import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO; import com.cf.imes.framework.sms.core.client.dto.SmsTemplateRespDTO; import com.cf.imes.framework.sms.core.client.impl.AbstractSmsClient; import com.cf.imes.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import com.cf.imes.framework.sms.core.property.SmsChannelProperties; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; @@ -18,6 +19,7 @@ import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; import com.aliyuncs.profile.DefaultProfile; import com.aliyuncs.profile.IClientProfile; +import com.cf.imes.framework.sms.core.property.SmsProperties; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; @@ -56,7 +58,7 @@ public class AliyunSmsClient extends AbstractSmsClient { */ private volatile IAcsClient client; - public AliyunSmsClient(SmsChannelProperties properties) { + public AliyunSmsClient(SmsProperties properties) { super(properties); Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); @@ -68,6 +70,23 @@ public class AliyunSmsClient extends AbstractSmsClient { client = new DefaultAcsClient(profile); } + @Override + protected boolean propChanged(SmsProperties properties) { + DefaultAcsClient defaultAcsClient = (DefaultAcsClient) client; + if (ObjectUtil.isNotNull(defaultAcsClient)) { + Credential credential = defaultAcsClient.getProfile().getCredential(); + String accessKeyId = credential.getAccessKeyId(); + if (ObjectUtil.notEqual(properties.getApiKey(), accessKeyId)) { + return true; + } + String accessSecret = credential.getAccessSecret(); + if (ObjectUtil.notEqual(properties.getApiSecret(), accessSecret)) { + return true; + } + } + return false; + } + @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { @@ -81,7 +100,7 @@ public class AliyunSmsClient extends AbstractSmsClient { // 执行请求 SendSmsResponse response = client.getAcsResponse(request); return new SmsSendRespDTO().setSuccess(Objects.equals(response.getCode(), API_CODE_SUCCESS)).setSerialNo(response.getBizId()) - .setApiRequestId(response.getRequestId()).setApiCode(response.getCode()).setApiMsg(response.getMessage()); + .setApiRequestId(response.getRequestId()).setApiCode(response.getCode()).setApiMsg(response.getMessage()).setChannelCode(properties.getChannel()); } @Override diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java index 0144e54bc..e329351dd 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java @@ -3,6 +3,7 @@ package com.cf.imes.framework.sms.core.client.impl.debug; import cn.hutool.core.codec.Base64; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.crypto.digest.HmacAlgorithm; @@ -15,7 +16,7 @@ import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO; import com.cf.imes.framework.sms.core.client.dto.SmsTemplateRespDTO; import com.cf.imes.framework.sms.core.client.impl.AbstractSmsClient; import com.cf.imes.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import com.cf.imes.framework.sms.core.property.SmsChannelProperties; +import com.cf.imes.framework.sms.core.property.SmsProperties; import java.util.HashMap; import java.util.List; @@ -31,7 +32,7 @@ import java.util.Objects; */ public class DebugDingTalkSmsClient extends AbstractSmsClient { - public DebugDingTalkSmsClient(SmsChannelProperties properties) { + public DebugDingTalkSmsClient(SmsProperties properties) { super(properties); Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); @@ -41,6 +42,11 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient { protected void doInit() { } + @Override + protected boolean propChanged(SmsProperties properties) { + return false; + } + @Override public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List> templateParams) throws Throwable { @@ -57,7 +63,7 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient { Map responseObj = JsonUtils.parseObject(responseText, Map.class); String errorCode = MapUtil.getStr(responseObj, "errcode"); return new SmsSendRespDTO().setSuccess(Objects.equals(errorCode, "0")).setSerialNo(StrUtil.uuid()) - .setApiCode(errorCode).setApiMsg(MapUtil.getStr(responseObj, "errorMsg")); + .setApiCode(errorCode).setApiMsg(MapUtil.getStr(responseObj, "errorMsg")).setMobile(mobile).setChannelCode(properties.getChannel()); } /** @@ -75,7 +81,7 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient { // 生成 sign String secret = properties.getApiSecret(); String stringToSign = timestamp + "\n" + secret; - byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign); + byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, CharSequenceUtil.bytes(secret)).digest(stringToSign); String sign = Base64.encode(signData); // 构建最终 URL return String.format("https://oapi.dingtalk.com/%s?access_token=%s×tamp=%d&sign=%s", diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/tencent/TencentSmsClient.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/tencent/TencentSmsClient.java index 98030b49d..03301d7aa 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/tencent/TencentSmsClient.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/client/impl/tencent/TencentSmsClient.java @@ -10,7 +10,7 @@ import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO; import com.cf.imes.framework.sms.core.client.dto.SmsTemplateRespDTO; import com.cf.imes.framework.sms.core.client.impl.AbstractSmsClient; import com.cf.imes.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import com.cf.imes.framework.sms.core.property.SmsChannelProperties; +import com.cf.imes.framework.sms.core.property.SmsProperties; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; @@ -56,7 +56,7 @@ public class TencentSmsClient extends AbstractSmsClient { private SmsClient client; - public TencentSmsClient(SmsChannelProperties properties) { + public TencentSmsClient(SmsProperties properties) { super(properties); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); validateSdkAppId(properties); @@ -69,6 +69,11 @@ public class TencentSmsClient extends AbstractSmsClient { client = new SmsClient(credential, ENDPOINT); } + @Override + protected boolean propChanged(SmsProperties properties) { + return false; + } + /** * 参数校验腾讯云的 SDK AppId * @@ -78,7 +83,7 @@ public class TencentSmsClient extends AbstractSmsClient { * * @param properties 配置 */ - private static void validateSdkAppId(SmsChannelProperties properties) { + private static void validateSdkAppId(SmsProperties properties) { String combineKey = properties.getApiKey(); Assert.notEmpty(combineKey, "apiKey 不能为空"); String[] keys = combineKey.trim().split(" "); @@ -108,7 +113,7 @@ public class TencentSmsClient extends AbstractSmsClient { SendSmsResponse response = client.SendSms(request); SendStatus status = response.getSendStatusSet()[0]; return new SmsSendRespDTO().setSuccess(Objects.equals(status.getCode(), API_CODE_SUCCESS)).setSerialNo(status.getSerialNo()) - .setApiRequestId(response.getRequestId()).setApiCode(status.getCode()).setApiMsg(status.getMessage()); + .setApiRequestId(response.getRequestId()).setApiCode(status.getCode()).setApiMsg(status.getMessage()).setChannelCode(properties.getChannel()); } @Override diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/property/SmsProperties.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/property/SmsProperties.java new file mode 100644 index 000000000..0413fe98f --- /dev/null +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/main/java/com/cf/imes/framework/sms/core/property/SmsProperties.java @@ -0,0 +1,76 @@ +package com.cf.imes.framework.sms.core.property; + +import com.cf.imes.framework.common.util.encrypt.AesUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +/** + * @author Gqr + * @since 2024/8/23 10:51 + */ +@ConfigurationProperties(prefix = "chenfeng.sms") +@Validated +public class SmsProperties { + + @Value("${chenfeng.encrypt.publicKey:}") + private String publicKey; + + /** + * 短信渠道 + */ + @NotEmpty(message = "短信渠道不能为空") + private String channel; + /** + * 短信签名 + */ + private String signature; + /** + * 短信 API 的账号 + */ + @NotEmpty(message = "短信apiKey不能为空") + private String apiKey; + /** + * 短信 API 的密钥 + */ + @NotEmpty(message = "短信apiSecret不能为空") + private String apiSecret; + + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + public String getSignature() { + return signature; + } + + public SmsProperties setSignature(String signature) { + this.signature = AesUtils.decrypt(signature, publicKey); + return this; + } + + public String getApiKey() { + return apiKey; + } + + + public SmsProperties setApiKey(String apiKey) { + this.apiKey = AesUtils.decrypt(apiKey, publicKey); + return this; + } + + public String getApiSecret() { + return apiSecret; + } + + public SmsProperties setApiSecret(String apiSecret) { + this.apiSecret = AesUtils.decrypt(apiSecret, publicKey); + return this; + } +} diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/test/java/com/cf/imes/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/test/java/com/cf/imes/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java index 5952ee2d8..1fce4400b 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/test/java/com/cf/imes/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/test/java/com/cf/imes/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java @@ -7,7 +7,7 @@ import com.cf.imes.framework.sms.core.client.dto.SmsReceiveRespDTO; import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO; import com.cf.imes.framework.sms.core.client.dto.SmsTemplateRespDTO; import com.cf.imes.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import com.cf.imes.framework.sms.core.property.SmsChannelProperties; +import com.cf.imes.framework.sms.core.property.SmsProperties; import com.cf.imes.framework.test.core.ut.BaseMockitoUnitTest; import com.aliyuncs.IAcsClient; import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; @@ -37,7 +37,7 @@ import static org.mockito.Mockito.when; */ public class AliyunSmsClientTest extends BaseMockitoUnitTest { - private final SmsChannelProperties properties = new SmsChannelProperties() + private final SmsProperties properties = new SmsProperties() .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 .setSignature("晨丰科技"); diff --git a/cf-framework/cf-spring-boot-starter-biz-sms/src/test/java/com/cf/imes/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java b/cf-framework/cf-spring-boot-starter-biz-sms/src/test/java/com/cf/imes/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java index e5ee12e69..2833228c0 100644 --- a/cf-framework/cf-spring-boot-starter-biz-sms/src/test/java/com/cf/imes/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java +++ b/cf-framework/cf-spring-boot-starter-biz-sms/src/test/java/com/cf/imes/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java @@ -9,7 +9,7 @@ import com.cf.imes.framework.sms.core.client.dto.SmsReceiveRespDTO; import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO; import com.cf.imes.framework.sms.core.client.dto.SmsTemplateRespDTO; import com.cf.imes.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import com.cf.imes.framework.sms.core.property.SmsChannelProperties; +import com.cf.imes.framework.sms.core.property.SmsProperties; import com.cf.imes.framework.test.core.ut.BaseMockitoUnitTest; import com.google.common.collect.Lists; import com.tencentcloudapi.sms.v20210111.SmsClient; @@ -39,7 +39,7 @@ import static org.mockito.Mockito.when; */ public class TencentSmsClientTest extends BaseMockitoUnitTest { - private final SmsChannelProperties properties = new SmsChannelProperties() + private final SmsProperties properties = new SmsProperties() .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 .setSignature("晨丰科技"); @@ -64,7 +64,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { @Test public void testRefresh() { // 准备参数 - SmsChannelProperties p = new SmsChannelProperties() + SmsProperties p = new SmsProperties() .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 .setSignature("晨丰科技"); diff --git a/cf-framework/cf-spring-boot-starter-mybatis/src/main/java/com/cf/imes/framework/datasource/config/ChenfengDataSourceEncryptConfiguration.java b/cf-framework/cf-spring-boot-starter-mybatis/src/main/java/com/cf/imes/framework/datasource/config/ChenfengDataSourceEncryptConfiguration.java new file mode 100644 index 000000000..5fa1cd56c --- /dev/null +++ b/cf-framework/cf-spring-boot-starter-mybatis/src/main/java/com/cf/imes/framework/datasource/config/ChenfengDataSourceEncryptConfiguration.java @@ -0,0 +1,24 @@ +package com.cf.imes.framework.datasource.config; + +import com.baomidou.dynamic.datasource.event.DataSourceInitEvent; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.cf.imes.framework.mybatis.core.event.ChenfengDataSourceEncryptInitEvent; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; + + +/** + * @author Gqr + * @since 2024/8/26 18:20 + */ +@AutoConfiguration +@AutoConfigureBefore(DynamicDataSourceAutoConfiguration.class) +public class ChenfengDataSourceEncryptConfiguration { + @Bean + @ConditionalOnProperty(name = "chenfeng.encrypt.enable", havingValue = "true") + public DataSourceInitEvent getDataSourceInitEvent() { + return new ChenfengDataSourceEncryptInitEvent(); + } +} diff --git a/cf-framework/cf-spring-boot-starter-mybatis/src/main/java/com/cf/imes/framework/mybatis/core/event/ChenfengDataSourceEncryptInitEvent.java b/cf-framework/cf-spring-boot-starter-mybatis/src/main/java/com/cf/imes/framework/mybatis/core/event/ChenfengDataSourceEncryptInitEvent.java new file mode 100644 index 000000000..d21405aeb --- /dev/null +++ b/cf-framework/cf-spring-boot-starter-mybatis/src/main/java/com/cf/imes/framework/mybatis/core/event/ChenfengDataSourceEncryptInitEvent.java @@ -0,0 +1,41 @@ +package com.cf.imes.framework.mybatis.core.event; + +import com.baomidou.dynamic.datasource.creator.DataSourceProperty; +import com.baomidou.dynamic.datasource.event.DataSourceInitEvent; +import com.cf.imes.framework.common.util.encrypt.AesUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; + +import javax.sql.DataSource; + +/** + * @author Gqr + * @since 2024/8/27 9:09 + */ +public class ChenfengDataSourceEncryptInitEvent implements DataSourceInitEvent { + @Value("${chenfeng.encrypt.publicKey:}") + private String publicKey; + + /** + * 数据源创建前 + * + * @param dataSourceProperty 数据源基础信息 + */ + @Override + public void beforeCreate(DataSourceProperty dataSourceProperty) { + if (StringUtils.isNotEmpty(publicKey)) { + dataSourceProperty.setUsername(AesUtils.decrypt(dataSourceProperty.getUsername(), publicKey)); + dataSourceProperty.setPassword(AesUtils.decrypt(dataSourceProperty.getPassword(), publicKey)); + } + } + + /** + * 数据源创建后 + * + * @param dataSource 连接池 + */ + @Override + public void afterCreate(DataSource dataSource) { + + } +} diff --git a/cf-framework/cf-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/cf-framework/cf-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index d501f7943..f8c1535cf 100644 --- a/cf-framework/cf-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/cf-framework/cf-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,3 @@ +com.cf.imes.framework.datasource.config.ChenfengDataSourceEncryptConfiguration com.cf.imes.framework.datasource.config.ChenfengDataSourceAutoConfiguration com.cf.imes.framework.mybatis.config.ChenfengMybatisAutoConfiguration diff --git a/cf-framework/cf-spring-boot-starter-redis/src/main/java/com/cf/imes/framework/redis/config/ChenfengRedisEncryptAutoConfiguration.java b/cf-framework/cf-spring-boot-starter-redis/src/main/java/com/cf/imes/framework/redis/config/ChenfengRedisEncryptAutoConfiguration.java new file mode 100644 index 000000000..f1cd7435b --- /dev/null +++ b/cf-framework/cf-spring-boot-starter-redis/src/main/java/com/cf/imes/framework/redis/config/ChenfengRedisEncryptAutoConfiguration.java @@ -0,0 +1,77 @@ +package com.cf.imes.framework.redis.config; + +import cn.hutool.core.bean.BeanUtil; +import com.cf.imes.framework.common.util.encrypt.AesUtils; +import org.redisson.config.ClusterServersConfig; +import org.redisson.config.Config; +import org.redisson.config.MasterSlaveServersConfig; +import org.redisson.config.ReplicatedServersConfig; +import org.redisson.config.SentinelServersConfig; +import org.redisson.config.SingleServerConfig; +import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.util.StringUtils; + +/** + * @author Gqr + * @since 2024/8/27 9:46 + */ +@AutoConfiguration +public class ChenfengRedisEncryptAutoConfiguration { + + @Value("${chenfeng.encrypt.publicKey:}") + private String publicKey; + + @Bean + @ConditionalOnProperty(name = "chenfeng.encrypt.enable", havingValue = "true") + public RedissonAutoConfigurationCustomizer redissonAutoConfigurationCustomizer() { + return configuration -> { + Config redissonConfig = new Config(); + BeanUtil.copyProperties(configuration, redissonConfig); + + String password; + SingleServerConfig singleServerConfig = redissonConfig.useSingleServer(); + if (singleServerConfig != null) { + + password = singleServerConfig.getPassword(); + singleServerConfig.setPassword( + StringUtils.isEmpty(password) ? null : AesUtils.decrypt(password, publicKey)); + return; + } + + MasterSlaveServersConfig masterSlaveServersConfig = redissonConfig.useMasterSlaveServers(); + if (masterSlaveServersConfig != null) { + password = masterSlaveServersConfig.getPassword(); + masterSlaveServersConfig.setPassword( + StringUtils.isEmpty(password) ? null : AesUtils.decrypt(password, publicKey)); + return; + } + + SentinelServersConfig sentinelServersConfig = redissonConfig.useSentinelServers(); + if (sentinelServersConfig != null) { + password = sentinelServersConfig.getPassword(); + sentinelServersConfig.setPassword( + StringUtils.isEmpty(password) ? null : AesUtils.decrypt(password, publicKey)); + return; + } + + ClusterServersConfig clusterServersConfig = redissonConfig.useClusterServers(); + if (clusterServersConfig != null) { + password = clusterServersConfig.getPassword(); + clusterServersConfig.setPassword( + StringUtils.isEmpty(password) ? null : AesUtils.decrypt(password, publicKey)); + return; + } + + ReplicatedServersConfig replicatedServersConfig = redissonConfig.useReplicatedServers(); + if (replicatedServersConfig != null) { + password = replicatedServersConfig.getPassword(); + replicatedServersConfig.setPassword( + StringUtils.isEmpty(password) ? null : AesUtils.decrypt(password, publicKey)); + } + }; + } +} diff --git a/cf-framework/cf-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/cf-framework/cf-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 339785994..d24fc60b0 100644 --- a/cf-framework/cf-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/cf-framework/cf-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,3 @@ com.cf.imes.framework.redis.config.ChenfengRedisAutoConfiguration +com.cf.imes.framework.redis.config.ChenfengRedisEncryptAutoConfiguration com.cf.imes.framework.redis.config.ChenfengCacheAutoConfiguration diff --git a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/application.yaml b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/application.yaml index 3b2c8da47..56cd5da62 100644 --- a/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/application.yaml +++ b/cf-module-prod-executor/cf-module-prod-executor-biz/src/main/resources/application.yaml @@ -160,5 +160,7 @@ chenfeng: send-maximum-quantity-per-day: 10 begin-code: 9999 # 这里配置 9999 的原因是,测试方便。 end-code: 9999 # 这里配置 9999 的原因是,测试方便。 - + encrypt: + enable: false + publicKey: cfimes debug: false diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java index acb888fc7..333dbf00d 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java @@ -7,7 +7,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; @Schema(description = "RPC 服务 - 短信验证码的发送 Request DTO") @Data @@ -19,12 +18,10 @@ public class SmsCodeSendReqDTO { private String mobile; @Schema(description = "发送场景", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "发送场景不能为空") @InEnum(SmsSceneEnum.class) - private Integer scene; + @Schema(description = "发送 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.20.30.40") - @NotEmpty(message = "发送 IP 不能为空") private String createIp; } diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java index 480044cba..2d31f8834 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java @@ -4,6 +4,7 @@ import com.cf.imes.framework.common.validation.InEnum; import com.cf.imes.framework.common.validation.Mobile; import com.cf.imes.module.system.enums.sms.SmsSceneEnum; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; import lombok.Data; import javax.validation.constraints.NotEmpty; @@ -11,6 +12,7 @@ import javax.validation.constraints.NotNull; @Schema(description = "RPC 服务 - 短信验证码的使用 Request DTO") @Data +@Builder public class SmsCodeUseReqDTO { @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") @@ -28,7 +30,6 @@ public class SmsCodeUseReqDTO { private String code; @Schema(description = "发送 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.20.30.40") - @NotEmpty(message = "使用 IP 不能为空") private String usedIp; } diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java index 542d67d04..4a312008e 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java @@ -4,6 +4,7 @@ import com.cf.imes.framework.common.validation.InEnum; import com.cf.imes.framework.common.validation.Mobile; import com.cf.imes.module.system.enums.sms.SmsSceneEnum; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; import lombok.Data; import javax.validation.constraints.NotEmpty; @@ -11,6 +12,7 @@ import javax.validation.constraints.NotNull; @Schema(description = "RPC 服务 - 短信验证码的校验 Request DTO") @Data +@Builder public class SmsCodeValidateReqDTO { @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java index 059482edb..a6f1e8dec 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/ErrorCodeConstants.java @@ -16,6 +16,7 @@ public interface ErrorCodeConstants { ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1_002_000_005, "未绑定账号,需要进行绑定"); ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1_002_000_006, "Token 已经过期"); ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1_002_000_007, "手机号不存在"); + ErrorCode AUTH_LOGIN_FIRST_LOGIN = new ErrorCode(1_002_000_008, "请通过验证码登录"); // ========== 菜单模块 1-002-001-000 ========== ErrorCode MENU_NAME_DUPLICATE = new ErrorCode(1_002_001_000, "已经存在该名字的菜单"); @@ -45,6 +46,7 @@ public interface ErrorCodeConstants { ErrorCode USER_IS_DISABLE = new ErrorCode(1_002_003_006, "名字为【{}】的用户已被禁用"); ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过组织最大组织配额({})!"); ErrorCode USER_ME_ERROR = new ErrorCode(1_002_003_009, "不可操作用户自身"); + ErrorCode USERNAME_MULTIPLE_ERROR = new ErrorCode(1_002_003_010,"存在名字为【{}】的重复用户,请联系系统管理员"); ErrorCode THE_PASSWORD_LENGTH_MUST_BE_AT_LEAST_6_CHARACTERS = new ErrorCode(1_002_003_010, "密码长度必须至少为6个字符"); @@ -105,14 +107,15 @@ public interface ErrorCodeConstants { ErrorCode SMS_SEND_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_013_002, "短信模板不存在"); // ========== 短信验证码 1-002-014-000 ========== - ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在"); - ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期"); - ErrorCode SMS_CODE_USED = new ErrorCode(1_002_014_002, "验证码已使用"); + ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在,请点击获取"); + ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期,请重新获取"); + ErrorCode SMS_CODE_USED = new ErrorCode(1_002_014_002, "验证码已使用,请重新获取"); ErrorCode SMS_CODE_NOT_CORRECT = new ErrorCode(1_002_014_003, "验证码不正确"); ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1_002_014_004, "超过每日短信发送数量"); ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频率"); ErrorCode SMS_CODE_IS_EXISTS = new ErrorCode(1_002_014_006, "手机号已被使用"); ErrorCode SMS_CODE_IS_UNUSED = new ErrorCode(1_002_014_007, "验证码未被使用"); + ErrorCode SMS_CODE_IS_VALID = new ErrorCode(1_002_014_008, "验证码在有效期内,5分钟内请勿重复发送!"); // ========== 组织信息 1-002-015-000 ========== ErrorCode ORGAN_NOT_EXISTS = new ErrorCode(1_002_015_000, "组织不存在"); diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/logger/LoginLogTypeEnum.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/logger/LoginLogTypeEnum.java index 192df5947..b79a4d8d4 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/logger/LoginLogTypeEnum.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/logger/LoginLogTypeEnum.java @@ -14,6 +14,7 @@ public enum LoginLogTypeEnum { LOGIN_SOCIAL(101), // 使用社交登录 LOGIN_MOBILE(103), // 使用手机登陆 LOGIN_SMS(104), // 使用短信登陆 + LOGIN_FORGET_PWD(105), // 登录忘记密码 LOGOUT_SELF(200), // 自己主动登出 LOGOUT_DELETE(202), // 强制退出 diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/logger/LoginResultEnum.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/logger/LoginResultEnum.java index dd341fb4e..faba49989 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/logger/LoginResultEnum.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/logger/LoginResultEnum.java @@ -15,8 +15,7 @@ public enum LoginResultEnum { USER_DISABLED(20), // 用户被禁用 CAPTCHA_NOT_FOUND(30), // 图片验证码不存在 CAPTCHA_CODE_ERROR(31), // 图片验证码不正确 - - ; + FIRST_LOGIN(32); // 首次登录 /** * 结果 diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/sms/SmsSceneEnum.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/sms/SmsSceneEnum.java index 11f624976..70406edcf 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/sms/SmsSceneEnum.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/sms/SmsSceneEnum.java @@ -16,12 +16,13 @@ import java.util.Arrays; @AllArgsConstructor public enum SmsSceneEnum 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", "后台用户 - 手机号登录"); + USER_UPDATE_MOBILE(1, "user-update-mobile", "修改手机"), + USER_RESET_PASSWORD(2, "user-reset-password", "忘记密码"), + USER_UPDATE_PASSWORD(3, "user-update-password", "修改密码"), + USER_LOGIN_CAPTCHAVERIFICATION(4, "user-login-captchaverification", "登录验证码"), + ADMIN_LOGIN(20, "admin-sms-login", "后台用户 - 手机号登录"), + ADMIN_MEMBER_LOGIN(21, "admin-sms-login", "后台用户 - 手机号登录"), + MEMBER_LOGIN(22, "user-sms-login", "会员用户 - 手机号登陆"); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SmsSceneEnum::getScene).toArray(); diff --git a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/sms/SmsTemplateTypeEnum.java b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/sms/SmsTemplateTypeEnum.java index 64171fee2..ff052f877 100644 --- a/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/sms/SmsTemplateTypeEnum.java +++ b/cf-module-system/cf-module-system-api/src/main/java/com/cf/imes/module/system/enums/sms/SmsTemplateTypeEnum.java @@ -22,4 +22,17 @@ public enum SmsTemplateTypeEnum { */ private final int type; + + public static SmsTemplateTypeEnum valueOf(Integer type) { + if (type == null) { + return null; + } + for (SmsTemplateTypeEnum value : values()) { + if (type == value.getType()) { + return value; + } + } + return null; + } + } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/SystemServerApplication.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/SystemServerApplication.java index be0d9596a..189dad7fd 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/SystemServerApplication.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/SystemServerApplication.java @@ -2,12 +2,14 @@ package com.cf.imes.module.system; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.context.config.annotation.RefreshScope; /** * 项目的启动类 * @author 晨丰科技 */ @SpringBootApplication +@RefreshScope public class SystemServerApplication { public static void main(String[] args) { diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/AuthController.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/AuthController.java index 5f443f64f..1f92d4585 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/AuthController.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/AuthController.java @@ -1,13 +1,13 @@ package com.cf.imes.module.system.controller.admin.auth; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.StrUtil; import com.cf.imes.framework.common.enums.CommonStatusEnum; import com.cf.imes.framework.common.enums.UserTypeEnum; import com.cf.imes.framework.common.pojo.CommonResult; import com.cf.imes.framework.operatelog.core.annotations.OperateLog; import com.cf.imes.framework.security.config.SecurityProperties; import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils; +import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO; @@ -20,6 +20,7 @@ import com.cf.imes.module.system.service.auth.AdminAuthService; import com.cf.imes.module.system.service.permission.MenuService; import com.cf.imes.module.system.service.permission.PermissionService; import com.cf.imes.module.system.service.permission.RoleService; +import com.cf.imes.module.system.service.sms.SmsCodeService; import com.cf.imes.module.system.service.social.SocialClientService; import com.cf.imes.module.system.service.user.AdminUserService; import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO; @@ -30,6 +31,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -43,6 +45,7 @@ import java.util.Set; import static com.cf.imes.framework.common.pojo.CommonResult.success; import static com.cf.imes.framework.common.util.collection.CollectionUtils.convertSet; +import static com.cf.imes.framework.common.util.servlet.ServletUtils.getClientIP; import static com.cf.imes.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 认证") @@ -68,11 +71,14 @@ public class AuthController { @Resource private SecurityProperties securityProperties; + @Resource + private SmsCodeService smsCodeService; + @PostMapping("/login") @PermitAll @Operation(summary = "使用账号密码登录") @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 - public CommonResult login(@RequestBody @Valid AuthLoginReqVO reqVO) { + public CommonResult login(@Valid @RequestBody AuthLoginReqVO reqVO) { return success(authService.login(reqVO)); } @@ -83,7 +89,7 @@ public class AuthController { public CommonResult logout(HttpServletRequest request) { String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); - if (StrUtil.isNotBlank(token)) { + if (StringUtils.isNotBlank(token)) { authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); } return success(true); @@ -112,7 +118,6 @@ public class AuthController { if (CollUtil.isEmpty(roleIds)) { return success(AuthConvert.INSTANCE.convert(user, Collections.emptyList(), Collections.emptyList())); } - //List roles = roleService.getRoleList(roleIds); List roles = roleService.getRoleList1(roleIds); roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色 @@ -144,6 +149,18 @@ public class AuthController { return success(true); } + @PutMapping("/sms-code") + @Operation(summary = "发送短信验证码") + @PermitAll + @OperateLog(enable = false) + public CommonResult sendSmsCode(@Valid @RequestBody SmsCodeSendReqDTO reqDTO) { + // 获取请求ip + reqDTO.setCreateIp(getClientIP()); + // 发送验证码 + smsCodeService.sendSmsCode(reqDTO); + return success(true); + } + // ========== 社交登录相关 ========== @GetMapping("/social-auth-redirect") diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/vo/AuthLoginReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/vo/AuthLoginReqVO.java index e91319c0e..928614140 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/vo/AuthLoginReqVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/vo/AuthLoginReqVO.java @@ -1,6 +1,6 @@ package com.cf.imes.module.system.controller.admin.auth.vo; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ObjectUtil; import com.cf.imes.framework.common.validation.InEnum; import com.cf.imes.module.system.enums.social.SocialTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; @@ -8,10 +8,10 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; @@ -29,12 +29,10 @@ public class AuthLoginReqVO { private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") - @NotEmpty(message = "密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; @Schema(description = "组织名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "晨丰科技") - @NotBlank(message = "组织名称不能空") private String organName; // ========== 图片验证码相关 ========== @@ -44,6 +42,10 @@ public class AuthLoginReqVO { @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) private String captchaVerification; + @Schema(description = "短信验证码", example = "123456") + @Length(min = 6, max = 6, message = "短信验证码长度为 6 位") + private String smsCaptchaVerification; + // ========== 绑定社交登录时,需要传递如下参数 ========== @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @@ -63,12 +65,17 @@ public class AuthLoginReqVO { @AssertTrue(message = "授权码不能为空") public boolean isSocialCodeValid() { - return socialType == null || StrUtil.isNotEmpty(socialCode); + return socialType == null || StringUtils.isNotEmpty(socialCode); } @AssertTrue(message = "授权 state 不能为空") public boolean isSocialState() { - return socialType == null || StrUtil.isNotEmpty(socialState); + return socialType == null || StringUtils.isNotEmpty(socialState); } + @AssertTrue(message = "请输入密码后登陆") + public boolean isPasswordValid() { + // 要么是密码登录、要么是验证码登录 + return ObjectUtil.isNotNull(password) || ObjectUtil.isNotNull(captchaVerification); + } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java index 8efa123a6..8c30da745 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java @@ -46,6 +46,9 @@ public class AuthPermissionInfoRespVO { @Schema(description = "组织id") private Long organId; + + @Schema(description = "是否需要设置密码") + private boolean needSetPwd; } @Schema(description = "管理后台 - 登录用户的菜单信息 Response VO") diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/organ/vo/organ/OrganSaveReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/organ/vo/organ/OrganSaveReqVO.java index da0db8e16..57cea191f 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/organ/vo/organ/OrganSaveReqVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/organ/vo/organ/OrganSaveReqVO.java @@ -1,13 +1,10 @@ package com.cf.imes.module.system.controller.admin.organ.vo.organ; -import cn.hutool.core.util.ObjectUtil; import com.cf.imes.framework.common.validation.Mobile; -import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.Length; -import javax.validation.constraints.AssertTrue; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; @@ -30,6 +27,7 @@ public class OrganSaveReqVO { @Schema(description = "联系手机", example = "15601691300") @Mobile + @NotNull(message = "联系人手机不能为空") private String contactMobile; @Schema(description = "组织状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @@ -62,13 +60,6 @@ public class OrganSaveReqVO { @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; - @AssertTrue(message = "用户账号、密码不能为空") - @JsonIgnore - public boolean isUsernameValid() { - return id != null // 修改时,不需要传递 - || (ObjectUtil.isAllNotEmpty(username, password)); // 新增时,必须都传递 username、password - } - @Schema(description = "是否大型组织") private Boolean large; diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/MenuController.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/MenuController.java index 90e567eb9..8ed922c89 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/MenuController.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/MenuController.java @@ -5,12 +5,10 @@ import com.cf.imes.framework.common.enums.CommonStatusEnum; import com.cf.imes.framework.common.pojo.CommonResult; import com.cf.imes.framework.common.util.object.BeanUtils; import com.cf.imes.framework.security.core.LoginUser; -import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuRespVO; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuSaveVO; import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuSimpleRespVO; -import com.cf.imes.module.system.convert.auth.AuthConvert; import com.cf.imes.module.system.dal.dataobject.permission.MenuDO; import com.cf.imes.module.system.dal.dataobject.permission.RoleDO; import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; @@ -111,7 +109,7 @@ public class MenuController { "在多组织的场景下,会只返回组织所在套餐有的菜单") public CommonResult> getSimpleMenuList() { List list = menuService.getMenuListByOrgan( - new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus())); + new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()).setVisiable(CommonStatusEnum.ENABLE.getStatus())); list.sort(Comparator.comparing(MenuDO::getSort)); return success(BeanUtils.toBean(list, MenuSimpleRespVO.class)); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java index ba7db8d8e..7e9300d38 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java @@ -10,7 +10,9 @@ public class MenuListReqVO { @Schema(description = "菜单名称,模糊匹配", example = "晨丰") private String name; - @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + @Schema(description = "菜单状态,参见 CommonStatusEnum 枚举类", example = "1") private Integer status; + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer visiable; } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/UserController.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/UserController.java index 1bd4daa91..3167c2d1e 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/UserController.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/UserController.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; +import javax.annotation.security.PermitAll; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.IOException; @@ -100,6 +101,15 @@ public class UserController { return success(true); } + @PutMapping("/reset-password") + @Operation(summary = "忘记密码重置") + @PermitAll + @OperateLog(enable = false) + public CommonResult resetUserPassword(@Valid @RequestBody UserResetPasswordReqVO reqVO) { + userService.resetUserPassword(reqVO); + return success(true); + } + @PutMapping("/update-status") @Operation(summary = "修改用户状态") // @PreAuthorize("@ss.hasPermission('system:user:update')") diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/UserProfileController.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/UserProfileController.java index 59a18aae7..8a218610b 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/UserProfileController.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/UserProfileController.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import com.cf.imes.framework.common.enums.UserTypeEnum; import com.cf.imes.framework.common.pojo.CommonResult; import com.cf.imes.framework.datapermission.core.annotation.DataPermission; +import com.cf.imes.module.system.controller.admin.user.vo.profile.UserMobileUpdateReqVO; import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileRespVO; import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; @@ -97,4 +98,10 @@ public class UserProfileController { return success(avatar); } + @PostMapping("/security/phone") + @Operation(summary = "修改密保手机") + public CommonResult updateUserSecurityPhone(@Valid @RequestBody UserMobileUpdateReqVO reqVO) { + userService.updateUserSecurityPhone(getLoginUserId(), reqVO); + return success(true); + } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/profile/UserMobileUpdateReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/profile/UserMobileUpdateReqVO.java new file mode 100644 index 000000000..8d25db462 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/profile/UserMobileUpdateReqVO.java @@ -0,0 +1,26 @@ +package com.cf.imes.module.system.controller.admin.user.vo.profile; + +import com.cf.imes.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; + + +@Schema(description = "管理后台 - 用户密保手机更新 Request VO") +@Data +public class UserMobileUpdateReqVO { + + + @Schema(description = "手机号码", example = "15601691300") + @Length(min = 11, max = 11, message = "手机号长度必须 11 位") + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + + @Schema(description = "验证码", example = "123456") + @Length(min = 6, max = 6, message = "验证码长度必须 6 位") + @NotEmpty(message = "短信验证码不能为空") + private String smsCaptchaVerification; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java index 7f7488ac5..35f1c83bb 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java @@ -1,5 +1,6 @@ package com.cf.imes.module.system.controller.admin.user.vo.profile; +import com.cf.imes.framework.common.validation.Mobile; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.hibernate.validator.constraints.Length; @@ -23,6 +24,7 @@ public class UserProfileUpdateReqVO { @Schema(description = "手机号码", example = "15601691300") @Length(min = 11, max = 11, message = "手机号长度必须 11 位") + @Mobile private String mobile; @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/user/UserResetPasswordReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/user/UserResetPasswordReqVO.java new file mode 100644 index 000000000..d8447f844 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/user/UserResetPasswordReqVO.java @@ -0,0 +1,28 @@ +package com.cf.imes.module.system.controller.admin.user.vo.user; + +import com.cf.imes.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 用户重置密码 Request VO") +@Data +public class UserResetPasswordReqVO { + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + @Schema(description = "短信验证码", example = "123456") + @Length(min = 6, max = 6, message = "短信验证码长度为 6 位") + @NotEmpty(message = "短信验证码不能为空") + private String smsCaptchaVerification; + + @Schema(description = "手机号", example = "15601691300") + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/user/UserSaveReqVO.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/user/UserSaveReqVO.java index a88ef6c82..7bf06d769 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/user/UserSaveReqVO.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/controller/admin/user/vo/user/UserSaveReqVO.java @@ -18,7 +18,6 @@ public class UserSaveReqVO { private Long id; @Schema(description = "a", requiredMode = Schema.RequiredMode.REQUIRED, example = "chenfeng") - @NotBlank(message = "用户名不能为空") @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户名由 数字、字母 组成") @Size(min = 4, max = 30, message = "用户名长度为 4-30 个字符") private String username; @@ -43,6 +42,7 @@ public class UserSaveReqVO { @Schema(description = "手机号码", example = "15601691300") @Mobile + @NotBlank(message = "手机号码不能为空") private String mobile; @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") @@ -57,13 +57,6 @@ public class UserSaveReqVO { @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; - @AssertTrue(message = "密码不能为空") - @JsonIgnore - public boolean isPasswordValid() { - return id != null // 修改时,不需要传递 - || (ObjectUtil.isAllNotEmpty(password)); // 新增时,必须都传递 password - } - @Schema(description = "帐号状态(0正常 1停用)") @NotNull(message = "账号状态不能空") private Integer status; diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/convert/auth/AuthConvert.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/convert/auth/AuthConvert.java index faaa9cdb8..0aa4d49e6 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/convert/auth/AuthConvert.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/convert/auth/AuthConvert.java @@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil; import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import com.cf.imes.module.system.api.social.dto.SocialUserBindReqDTO; -import com.cf.imes.module.system.controller.admin.auth.vo.*; import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO; @@ -15,6 +14,7 @@ import com.cf.imes.module.system.dal.dataobject.permission.MenuDO; import com.cf.imes.module.system.dal.dataobject.permission.RoleDO; import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; import com.cf.imes.module.system.enums.permission.MenuTypeEnum; +import org.apache.commons.lang3.StringUtils; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import org.slf4j.LoggerFactory; @@ -33,7 +33,8 @@ public interface AuthConvert { default AuthPermissionInfoRespVO convert(AdminUserDO user, List roleList, List menuList) { return AuthPermissionInfoRespVO.builder() - .user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).organId(user.getOrganId()).build()) + .user(AuthPermissionInfoRespVO.UserVO.builder() + .id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).organId(user.getOrganId()).needSetPwd(StringUtils.isEmpty(user.getPassword())).build()) .roles(convertSet(roleList, RoleDO::getCode)) // 权限标识信息 .permissions(convertSet(menuList, MenuDO::getPermission)) diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/convert/organ/OrganConvert.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/convert/organ/OrganConvert.java index d02b65f76..ed438f0a8 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/convert/organ/OrganConvert.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/convert/organ/OrganConvert.java @@ -17,9 +17,8 @@ public interface OrganConvert { default UserSaveReqVO convert02(OrganSaveReqVO bean) { UserSaveReqVO reqVO = new UserSaveReqVO(); - reqVO.setUsername(bean.getUsername()); - reqVO.setPassword(bean.getPassword()); - reqVO.setNickname(bean.getContactName()).setMobile(bean.getContactMobile()); + reqVO.setMobile(bean.getContactMobile()); + reqVO.setNickname(bean.getContactName()); return reqVO; } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuMapper.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuMapper.java index 6971ae3d4..7e3d39cff 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuMapper.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/permission/MenuMapper.java @@ -22,7 +22,8 @@ public interface MenuMapper extends BaseMapperX { default List selectList(MenuListReqVO reqVO) { return selectList(new LambdaQueryWrapperX() .likeIfPresent(MenuDO::getName, reqVO.getName()) - .eqIfPresent(MenuDO::getStatus, reqVO.getStatus())); + .eqIfPresent(MenuDO::getStatus, reqVO.getStatus()) + .eqIfPresent(MenuDO::getVisible, reqVO.getVisiable())); } default List selectListByPermission(String permission) { diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/user/AdminUserMapper.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/user/AdminUserMapper.java index 074e8c1b1..234e4a442 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/user/AdminUserMapper.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/mysql/user/AdminUserMapper.java @@ -1,8 +1,9 @@ package com.cf.imes.module.system.dal.mysql.user; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.cf.imes.framework.common.enums.CommonStatusEnum; +import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil; import com.cf.imes.framework.common.pojo.PageResult; import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX; import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX; @@ -11,7 +12,6 @@ import com.cf.imes.framework.security.core.LoginUser; import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils; import com.cf.imes.module.executor.enums.OrderDeletedEnum; import com.cf.imes.module.system.controller.admin.user.vo.user.UserPageReqVO; -import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO; import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -22,7 +22,8 @@ import java.time.LocalTime; import java.util.Collection; import java.util.List; import java.util.Objects; -import java.util.function.Consumer; + +import static com.cf.imes.module.system.enums.ErrorCodeConstants.USERNAME_MULTIPLE_ERROR; @Mapper public interface AdminUserMapper extends BaseMapperX { @@ -35,6 +36,26 @@ public interface AdminUserMapper extends BaseMapperX { ); } + /** + * 用户名查询唯一用户,存在多个用户抛出业务异常 + * + * @param userName + * @return + */ + @OrganIgnore + default AdminUserDO selectByUsernameUnique(String userName) { + AdminUserDO adminUserDO = null; + List adminUserDOS = selectList(new LambdaQueryWrapperX() + .eq(AdminUserDO::getUsername, userName)); + if (CollectionUtil.isNotEmpty(adminUserDOS)) { + if (adminUserDOS.size() > 1) { + ServiceExceptionUtil.exception(USERNAME_MULTIPLE_ERROR, userName); + } + adminUserDO = adminUserDOS.get(0); + } + return adminUserDO; + } + default AdminUserDO selectByEmail(String email) { return selectOne(AdminUserDO::getEmail, email); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/RedisKeyConstants.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/RedisKeyConstants.java index 52d97e7a6..fb6ff8302 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/RedisKeyConstants.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/dal/redis/RedisKeyConstants.java @@ -98,4 +98,14 @@ public interface RedisKeyConstants { * VALUE 数据格式:String 模版信息 */ String SMS_TEMPLATE = "sms_template"; + + /** + * 短信验证码的缓存 + */ + String SMS_CAPTCHA_VERIFICATION = "sms_captcha_verification:%s"; + + /** + * 短信验证码次数上限的缓存 + */ + String SMS_CAPTCHA_VERIFICATION_LIMIT = "sms_captcha_verification_limit:%s"; } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/sms/SmsCodeProperties.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/sms/SmsCodeProperties.java index 32c61170c..7f7284de6 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/sms/SmsCodeProperties.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/framework/sms/SmsCodeProperties.java @@ -38,4 +38,15 @@ public class SmsCodeProperties { @NotNull(message = "验证码最大值不能为空") private Integer endCode; + /** + * 窗口时间 + */ + @NotNull(message = "窗口时间不能为空") + private Duration sendWindowSizeMin; + + /** + * 窗口最大请求次数 + */ + @NotNull(message = "窗口最大请求次数不能为空") + private Integer sendMaxSendTimesPerWindow; } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/mq/message/sms/SmsSendMessage.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/mq/message/sms/SmsSendMessage.java index 92a07b800..502c9bc91 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/mq/message/sms/SmsSendMessage.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/mq/message/sms/SmsSendMessage.java @@ -39,4 +39,9 @@ public class SmsSendMessage { */ private List> templateParams; + /** + * 短信消息模板类型:system_sms_template.type + * 不同的消息模板类型有不同的消息发送后处理器 + */ + private Integer templateType; } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/mq/producer/sms/SmsProducer.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/mq/producer/sms/SmsProducer.java index 1061fbf46..88981e9a9 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/mq/producer/sms/SmsProducer.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/mq/producer/sms/SmsProducer.java @@ -1,10 +1,12 @@ package com.cf.imes.module.system.mq.producer.sms; import com.cf.imes.framework.common.core.KeyValue; +import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO; import com.cf.imes.module.system.mq.message.sms.SmsSendMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; import javax.annotation.Resource; import java.util.List; @@ -25,16 +27,16 @@ public class SmsProducer { /** * 发送 {@link SmsSendMessage} 消息 * - * @param logId 短信日志编号 - * @param mobile 手机号 - * @param channelId 渠道编号 - * @param apiTemplateId 短信模板编号 + * @param logId 短信日志编号 + * @param mobile 手机号 + * @param template 模板信息 * @param templateParams 短信模板参数 */ - public void sendSmsSendMessage(Long logId, String mobile, - Long channelId, String apiTemplateId, List> templateParams) { + public void sendSmsSendMessage(Long logId, String mobile, SmsTemplateDO template, List> templateParams) { SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile); - message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams); + message.setChannelId(template.getChannelId()).setApiTemplateId(template.getApiTemplateId()).setTemplateType(template.getType()).setTemplateParams(templateParams); + // event异步发送短信,保证子线程内部request不为空 + RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); applicationContext.publishEvent(message); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/auth/AdminAuthService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/auth/AdminAuthService.java index ab6ae3bb9..3c93d9ddb 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/auth/AdminAuthService.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/auth/AdminAuthService.java @@ -1,6 +1,5 @@ package com.cf.imes.module.system.service.auth; -import com.cf.imes.module.system.controller.admin.auth.vo.*; import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO; @@ -22,11 +21,10 @@ public interface AdminAuthService { /** * 验证账号 + 密码。如果通过,则返回用户 * - * @param username 账号 - * @param password 密码 + * @param reqVO * @return 用户 */ - AdminUserDO authenticate(String username, String password, String organName); + AdminUserDO authenticate(AuthLoginReqVO reqVO); /** * 账号登录 diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/auth/AdminAuthServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/auth/AdminAuthServiceImpl.java index 328c51fc5..34e0a7e41 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/auth/AdminAuthServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/auth/AdminAuthServiceImpl.java @@ -9,6 +9,7 @@ import com.cf.imes.framework.common.util.servlet.ServletUtils; import com.cf.imes.framework.common.util.validation.ValidationUtils; import com.cf.imes.module.system.api.logger.dto.LoginLogCreateReqDTO; import com.cf.imes.module.system.api.sms.SmsCodeApi; +import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import com.cf.imes.module.system.api.social.dto.SocialUserBindReqDTO; import com.cf.imes.module.system.api.social.dto.SocialUserRespDTO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO; @@ -27,6 +28,7 @@ import com.cf.imes.module.system.enums.sms.SmsSceneEnum; import com.cf.imes.module.system.service.logger.LoginLogService; import com.cf.imes.module.system.service.member.MemberService; import com.cf.imes.module.system.service.oauth2.OAuth2TokenService; +import com.cf.imes.module.system.service.sms.SmsCodeService; import com.cf.imes.module.system.service.social.SocialUserService; import com.cf.imes.module.system.service.organ.OrganService; import com.cf.imes.module.system.service.user.AdminUserService; @@ -47,7 +49,6 @@ import java.util.Objects; import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.cf.imes.framework.common.util.servlet.ServletUtils.getClientIP; import static com.cf.imes.module.system.enums.ErrorCodeConstants.ORGAN_DATA_CODE_NOT_EXISTS; -import static com.cf.imes.module.system.enums.ErrorCodeConstants.ORGAN_NOT_EXISTS; /** * Auth Service 实现类 @@ -77,6 +78,9 @@ public class AdminAuthServiceImpl implements AdminAuthService { @Resource private OrganService organService; + @Resource + private SmsCodeService smsCodeService; + /** * 验证码的开关,默认为 true */ @@ -84,19 +88,23 @@ public class AdminAuthServiceImpl implements AdminAuthService { private Boolean captchaEnable; @Override - public AdminUserDO authenticate(String username, String password, String organName) { + public AdminUserDO authenticate(AuthLoginReqVO reqVO) { + String username = reqVO.getUsername(); + String password = reqVO.getPassword(); + String smsCaptchaVerification = reqVO.getSmsCaptchaVerification(); final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; - OrganizationDO organ = organService.getOrganByName(organName); - if(Objects.isNull(organ)) { - throw ServiceExceptionUtil.exception(ORGAN_NOT_EXISTS); - } - // 校验账号是否存在 - AdminUserDO user = userService.getUserByUsernameAndOrganId(username, organ.getId()); + // 查询账号 + AdminUserDO user = userService.getUserUniqueByUserName(username); + // 校验用户是否存在 if (user == null) { createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_BAD_CREDENTIALS); } - if (!userService.isPasswordMatch(password, user.getPassword())) { + // 校验机构有效性 + organService.validOrgan(user.getOrganId()); + // 校验密码 + String userPassword = user.getPassword(); + if (StringUtils.isNotEmpty(userPassword) && !userService.isPasswordMatch(password, userPassword)) { createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_BAD_CREDENTIALS); } @@ -105,6 +113,11 @@ public class AdminAuthServiceImpl implements AdminAuthService { createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_USER_DISABLED); } + // 校验没有密码并且没有传验证码,告诉前端需要验证码登录 + if (StringUtils.isEmpty(userPassword) && StringUtils.isEmpty(smsCaptchaVerification)) { + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.FIRST_LOGIN); + throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_FIRST_LOGIN); + } return user; } @@ -114,7 +127,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { validateCaptcha(reqVO); // 使用账号密码,进行登录 - AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword(), reqVO.getOrganName()); + AdminUserDO user = authenticate(reqVO); // 如果 socialType 非空,说明需要绑定社交用户 if (reqVO.getSocialType() != null) { @@ -122,14 +135,17 @@ public class AdminAuthServiceImpl implements AdminAuthService { reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); } Long organId = user.getOrganId(); - // 校验机构有效性 - organService.validOrgan(organId); OrganizationDO organ = organService.getOrgan(organId); String dataSourceCode = organ.getDataSourceCode(); // 校验机构数据源 if (StringUtils.isBlank(dataSourceCode)) { throw exception(ORGAN_DATA_CODE_NOT_EXISTS); } + // 验证并消耗验证码 + String smsCaptchaVerification = reqVO.getSmsCaptchaVerification(); + if (StringUtils.isNotEmpty(smsCaptchaVerification)) { + smsCodeService.useSmsCode(SmsCodeUseReqDTO.builder().mobile(user.getUsername()).code(smsCaptchaVerification).scene(SmsSceneEnum.USER_LOGIN_CAPTCHAVERIFICATION.getScene()).build()); + } // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME,organ.getLarge(), dataSourceCode, user.getOrganId(), user.getNickname()); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/oauth2/OAuth2GrantServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/oauth2/OAuth2GrantServiceImpl.java index 5154f2c6e..0bec5e061 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/oauth2/OAuth2GrantServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/oauth2/OAuth2GrantServiceImpl.java @@ -1,14 +1,12 @@ package com.cf.imes.module.system.service.oauth2; import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; import com.cf.imes.framework.common.enums.UserTypeEnum; -import com.cf.imes.framework.common.exception.ServerException; -import com.cf.imes.framework.organ.core.context.OrganContextHolder; +import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO; import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2CodeDO; -import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO; import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; import com.cf.imes.module.system.enums.ErrorCodeConstants; import com.cf.imes.module.system.service.auth.AdminAuthService; @@ -17,7 +15,6 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; -import java.util.Objects; import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -64,16 +61,16 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { OAuth2CodeDO codeDO = oauth2CodeService.consumeAuthorizationCode(code); Assert.notNull(codeDO, "授权码不能为空"); // 防御性编程 // 校验 clientId 是否匹配 - if (!StrUtil.equals(clientId, codeDO.getClientId())) { + if (!CharSequenceUtil.equals(clientId, codeDO.getClientId())) { throw exception(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH); } // 校验 redirectUri 是否匹配 - if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) { + if (!CharSequenceUtil.equals(redirectUri, codeDO.getRedirectUri())) { throw exception(ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH); } // 校验 state 是否匹配 - state = StrUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串 - if (!StrUtil.equals(state, codeDO.getState())) { + state = CharSequenceUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串 + if (!CharSequenceUtil.equals(state, codeDO.getState())) { throw exception(ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH); } @@ -91,7 +88,7 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { @Override public OAuth2AccessTokenDO grantPassword(String username, String password, String clientId, List scopes) { // 使用账号 + 密码进行登录 - AdminUserDO user = adminAuthService.authenticate(username, password, null); + AdminUserDO user = adminAuthService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build()); Assert.notNull(user, "用户不能为空!"); // 防御性编程 /*Long organId = OrganContextHolder.getOrganId(); OrganizationDO organ = organService.getOrgan(organId); diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsChannelService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsChannelService.java index 57ea2ae89..d6dc0d99b 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsChannelService.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsChannelService.java @@ -78,4 +78,11 @@ public interface SmsChannelService { */ SmsClient getSmsClient(String code); + /** + * 根据配置获得短信客户端 + * + * @param + * @return 短信客户端 + */ + SmsClient getSmsClient(); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsChannelServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsChannelServiceImpl.java index a7464a033..7c97daf3f 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsChannelServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsChannelServiceImpl.java @@ -6,7 +6,7 @@ import com.cf.imes.framework.common.pojo.PageResult; import com.cf.imes.framework.common.util.object.BeanUtils; import com.cf.imes.framework.sms.core.client.SmsClient; import com.cf.imes.framework.sms.core.client.SmsClientFactory; -import com.cf.imes.framework.sms.core.property.SmsChannelProperties; +import com.cf.imes.framework.sms.core.property.SmsProperties; import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO; @@ -22,7 +22,6 @@ import javax.annotation.Resource; import java.time.Duration; import java.util.List; -import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.cf.imes.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; /** @@ -34,22 +33,28 @@ import static com.cf.imes.framework.common.util.cache.CacheUtils.buildAsyncReloa @Slf4j public class SmsChannelServiceImpl implements SmsChannelService { + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SmsChannelMapper smsChannelMapper; + + @Resource + private SmsTemplateService smsTemplateService; + + @Resource + private SmsProperties smsProperties; + /** * {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory */ @Getter private final LoadingCache idClientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L), - new CacheLoader() { + new CacheLoader<>() { @Override public SmsClient load(Long id) { - // 查询,然后尝试刷新 - SmsChannelDO channel = smsChannelMapper.selectById(id); - if (channel != null) { - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); - smsClientFactory.createOrUpdateSmsClient(properties); - } - return smsClientFactory.getSmsClient(id); + return smsClientFactory.getSmsClient(smsProperties); } }); @@ -59,30 +64,15 @@ public class SmsChannelServiceImpl implements SmsChannelService { */ @Getter private final LoadingCache codeClientCache = buildAsyncReloadingCache(Duration.ofSeconds(60L), - new CacheLoader() { + new CacheLoader<>() { @Override public SmsClient load(String code) { - // 查询,然后尝试刷新 - SmsChannelDO channel = smsChannelMapper.selectByCode(code); - if (channel != null) { - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); - smsClientFactory.createOrUpdateSmsClient(properties); - } - return smsClientFactory.getSmsClient(code); + return smsClientFactory.getSmsClient(smsProperties); } }); - @Resource - private SmsClientFactory smsClientFactory; - - @Resource - private SmsChannelMapper smsChannelMapper; - - @Resource - private SmsTemplateService smsTemplateService; - @Override public Long createSmsChannel(SmsChannelSaveReqVO createReqVO) { SmsChannelDO channel = BeanUtils.toBean(createReqVO, SmsChannelDO.class); @@ -160,7 +150,12 @@ public class SmsChannelServiceImpl implements SmsChannelService { @Override public SmsClient getSmsClient(String code) { - return codeClientCache.getUnchecked(code); + return smsClientFactory.getSmsClient(smsProperties); + } + + @Override + public SmsClient getSmsClient() { + return smsClientFactory.getSmsClient(smsProperties); } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsCodeServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsCodeServiceImpl.java index dbcc31a34..7c93141e2 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsCodeServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsCodeServiceImpl.java @@ -3,24 +3,29 @@ package com.cf.imes.module.system.service.sms; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO; import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import com.cf.imes.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; import com.cf.imes.module.system.dal.dataobject.sms.SmsCodeDO; +import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; import com.cf.imes.module.system.dal.mysql.sms.SmsCodeMapper; +import com.cf.imes.module.system.dal.redis.RedisKeyConstants; import com.cf.imes.module.system.enums.sms.SmsSceneEnum; import com.cf.imes.module.system.framework.sms.SmsCodeProperties; import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil; import com.cf.imes.module.system.enums.ErrorCodeConstants; +import com.cf.imes.module.system.service.user.AdminUserService; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; +import java.time.Duration; import java.time.LocalDateTime; +import java.util.HashMap; import static cn.hutool.core.util.RandomUtil.randomInt; -import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception; -import static com.cf.imes.framework.common.util.date.DateUtils.isToday; /** * 短信验证码 Service 实现类 @@ -40,49 +45,118 @@ public class SmsCodeServiceImpl implements SmsCodeService { @Resource private SmsSendService smsSendService; + @Resource + private RedisTemplate redisTemplate; + + @Resource + private AdminUserService userService; + @Override public void sendSmsCode(SmsCodeSendReqDTO reqDTO) { + // 非修改手机号的场景时校验手机号是否存在 + AdminUserDO user = userService.getUserByUsernameAndOrganId(reqDTO.getMobile(), null); + if (ObjectUtil.isNull(user) && ObjectUtil.notEqual(SmsSceneEnum.USER_UPDATE_MOBILE.getScene(), reqDTO.getScene())) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS); + } SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene()); Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene()); // 创建验证码 - String code = createSmsCode(reqDTO.getMobile(), reqDTO.getScene(), reqDTO.getCreateIp()); + String code = createSmsCode(reqDTO.getMobile(), reqDTO.getCreateIp()); // 发送验证码 + HashMap map = MapUtil.of("code", code); + map.put("product","晨丰科技"); smsSendService.sendSingleSms(reqDTO.getMobile(), null, null, - sceneEnum.getTemplateCode(), MapUtil.of("code", code)); + sceneEnum.getTemplateCode(), map); } - private String createSmsCode(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()) { // 发送过于频繁 + /** + * 生成验证码 + * + * @param mobile 手机号 + * @param ip 请求来源ip + * @return + */ + private String createSmsCode(String mobile, String ip) { + String redisMobileKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, mobile); + String redisMobileCountKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION_LIMIT, mobile); + String redisIpCountKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION_LIMIT, ip); + boolean hasCode = redisTemplate.hasKey(redisMobileKey); + if (!hasCode) { + // 验证码不存在 + Duration sendWindowSizeMin = smsCodeProperties.getSendWindowSizeMin(); + Integer sendMaxSendTimesPerWindow = smsCodeProperties.getSendMaxSendTimesPerWindow(); + // 限制手机号请求次数:10分钟5次 + boolean mobileCountRateLimit = slidingWindowRateLimit(redisMobileCountKey, sendWindowSizeMin.toMinutesPart(), sendMaxSendTimesPerWindow); + if (!mobileCountRateLimit) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_SEND_TOO_FAST); } - if (isToday(lastSmsCode.getCreateTime()) && // 必须是今天,才能计算超过当天的上限 - lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。 - throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY); + // 限制ip请求次数 + boolean ipCountRateLimit = slidingWindowRateLimit(redisIpCountKey, sendWindowSizeMin.toMinutesPart(), sendMaxSendTimesPerWindow); + if (!ipCountRateLimit) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_SEND_TOO_FAST); } - // TODO 晨丰:提升,每个 IP 每天可发送数量 - // TODO 晨丰:提升,每个 IP 每小时可发送数量 + } else { + // 验证码存在提示请勿重复发送 + throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_IS_VALID); } - // 创建验证码记录 - String code = String.valueOf(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; + // 生成6位随机验证码 + StringBuffer code = new StringBuffer(6); + for (int i = 0; i < 6; i++) { + code.append(randomInt(0, 9)); + } + return code.toString(); + } + + /** + * 滑动窗口限制发送次数 + * + * @param key + * @param winSizeMin 窗口时间大小 + * @param maxSendTimesPerWin 窗口内最大请求次数 + * @return false没有达到发送频繁告警要求 + */ + public boolean slidingWindowRateLimit(String key, int winSizeMin, int maxSendTimesPerWin) { + // 获取当前时间戳 + long currentTime = System.currentTimeMillis(); + + // 清除过期的时间片 + redisTemplate.opsForZSet().removeRangeByScore(key, 0L, currentTime - (winSizeMin * 60 * 1000)); + + // 检查过去 winSizeMin 分钟内的请求次数 + long requestCount = redisTemplate.opsForZSet().zCard(key); + if (requestCount >= maxSendTimesPerWin) { + return false; + } + + // 窗口足够添加当前请求的时间戳 + redisTemplate.opsForZSet().add(key, currentTime, currentTime); + + return true; } @Override public void useSmsCode(SmsCodeUseReqDTO reqDTO) { - // 检测验证码是否有效 - SmsCodeDO lastSmsCode = validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene()); - // 使用验证码 - smsCodeMapper.updateById(SmsCodeDO.builder().id(lastSmsCode.getId()) - .used(true).usedTime(LocalDateTime.now()).usedIp(reqDTO.getUsedIp()).build()); + // 非修改手机号的场景时校验手机号是否存在 + AdminUserDO user = userService.getUserByUsernameAndOrganId(reqDTO.getMobile(), null); + if (ObjectUtil.isNull(user) && ObjectUtil.notEqual(SmsSceneEnum.USER_UPDATE_MOBILE.getScene(), reqDTO.getScene())) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS); + } + String smsCodeKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, reqDTO.getMobile()); + Object keyObj = redisTemplate.opsForValue().get(smsCodeKey); + if (ObjectUtil.isNotNull(keyObj)) { + // 校验码存在 + if (ObjectUtil.notEqual(String.valueOf(keyObj), reqDTO.getCode())) { + // 验证码不正确 + throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_NOT_CORRECT); + } else { + // 消耗验证码 + redisTemplate.delete(smsCodeKey); + } + } else { + // 提示验证码不存在 + throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_NOT_FOUND); + } } @Override diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsLogService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsLogService.java index c76849e3d..288e30f82 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsLogService.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsLogService.java @@ -40,10 +40,11 @@ public interface SmsLogService { * @param apiSendMsg 短信 API 发送失败的提示 * @param apiRequestId 短信 API 发送返回的唯一请求 ID * @param apiSerialNo 短信 API 发送返回的序号 + * @param channelCode 短信 API 渠道编码 */ void updateSmsSendResult(Long id, Boolean success, String apiSendCode, String apiSendMsg, - String apiRequestId, String apiSerialNo); + String apiRequestId, String apiSerialNo, String channelCode); /** * 更新日志的接收结果 diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsLogServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsLogServiceImpl.java index 650f92490..d03f445d3 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsLogServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsLogServiceImpl.java @@ -40,8 +40,6 @@ public class SmsLogServiceImpl implements SmsLogService { logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType()); logBuilder.templateContent(templateContent).templateParams(templateParams) .apiTemplateId(template.getApiTemplateId()); - // 设置渠道相关字段 - logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode()); // 设置接收相关字段 logBuilder.receiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); @@ -54,12 +52,12 @@ public class SmsLogServiceImpl implements SmsLogService { @Override public void updateSmsSendResult(Long id, Boolean success, String apiSendCode, String apiSendMsg, - String apiRequestId, String apiSerialNo) { + String apiRequestId, String apiSerialNo, String channelCode) { SmsSendStatusEnum sendStatus = success ? SmsSendStatusEnum.SUCCESS : SmsSendStatusEnum.FAILURE; smsLogMapper.updateById(SmsLogDO.builder().id(id) .sendStatus(sendStatus.getStatus()).sendTime(LocalDateTime.now()) .apiSendCode(apiSendCode).apiSendMsg(apiSendMsg) - .apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).build()); + .apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).channelCode(channelCode).build()); } @Override diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsSendServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsSendServiceImpl.java index e44a5687d..de56919f6 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsSendServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsSendServiceImpl.java @@ -3,7 +3,6 @@ package com.cf.imes.module.system.service.sms; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.StrUtil; import com.cf.imes.framework.common.core.KeyValue; import com.cf.imes.framework.common.enums.CommonStatusEnum; import com.cf.imes.framework.common.enums.UserTypeEnum; @@ -12,16 +11,17 @@ import com.cf.imes.framework.datapermission.core.annotation.DataPermission; import com.cf.imes.framework.sms.core.client.SmsClient; import com.cf.imes.framework.sms.core.client.dto.SmsReceiveRespDTO; import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO; -import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO; import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO; import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; import com.cf.imes.module.system.mq.message.sms.SmsSendMessage; import com.cf.imes.module.system.mq.producer.sms.SmsProducer; import com.cf.imes.module.system.service.member.MemberService; +import com.cf.imes.module.system.service.sms.handler.SmsSendAfterSendHandler; import com.cf.imes.module.system.service.user.AdminUserService; import com.cf.imes.module.system.enums.ErrorCodeConstants; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -29,8 +29,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception; - /** * 短信发送 Service 发送的实现 * @@ -54,11 +52,14 @@ public class SmsSendServiceImpl implements SmsSendService { @Resource private SmsProducer smsProducer; + @Resource + private List smsSendAfterSendHandlers; + @Override @DataPermission(enable = false) // 发送短信时,无需考虑数据权限 public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map templateParams) { // 如果 mobile 为空,则加载用户编号对应的手机号 - if (StrUtil.isEmpty(mobile)) { + if (StringUtils.isEmpty(mobile)) { AdminUserDO user = adminUserService.getUser(userId); if (user != null) { mobile = user.getMobile(); @@ -71,7 +72,7 @@ public class SmsSendServiceImpl implements SmsSendService { @Override public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map templateParams) { // 如果 mobile 为空,则加载用户编号对应的手机号 - if (StrUtil.isEmpty(mobile)) { + if (StringUtils.isEmpty(mobile)) { mobile = memberService.getMemberUserMobile(userId); } // 执行发送 @@ -83,8 +84,6 @@ public class SmsSendServiceImpl implements SmsSendService { String templateCode, Map templateParams) { // 校验短信模板是否合法 SmsTemplateDO template = validateSmsTemplate(templateCode); - // 校验短信渠道是否合法 - SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId()); // 校验手机号码是否存在 mobile = validateMobile(mobile); @@ -92,30 +91,17 @@ public class SmsSendServiceImpl implements SmsSendService { List> newTemplateParams = buildTemplateParams(template, templateParams); // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 - Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()) - && CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus()); + Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams); Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams); // 发送 MQ 消息,异步执行发送短信 if (isSend) { - smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(), - template.getApiTemplateId(), newTemplateParams); + smsProducer.sendSmsSendMessage(sendLogId, mobile, template, newTemplateParams); } return sendLogId; } - @VisibleForTesting - SmsChannelDO validateSmsChannel(Long channelId) { - // 获得短信模板。考虑到效率,从缓存中获取 - SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); - // 短信模板不存在 - if (channelDO == null) { - throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS); - } - return channelDO; - } - @VisibleForTesting SmsTemplateDO validateSmsTemplate(String templateCode) { // 获得短信模板。考虑到效率,从缓存中获取 @@ -149,7 +135,7 @@ public class SmsSendServiceImpl implements SmsSendService { @VisibleForTesting public String validateMobile(String mobile) { - if (StrUtil.isEmpty(mobile)) { + if (StringUtils.isEmpty(mobile)) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_SEND_MOBILE_NOT_EXISTS); } return mobile; @@ -158,19 +144,29 @@ public class SmsSendServiceImpl implements SmsSendService { @Override public void doSendSms(SmsSendMessage message) { // 获得渠道对应的 SmsClient 客户端 - SmsClient smsClient = smsChannelService.getSmsClient(message.getChannelId()); + SmsClient smsClient = smsChannelService.getSmsClient(); Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId()); + + List> templateParams = message.getTemplateParams(); + String channelCode = null; // 发送短信 try { SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(), - message.getApiTemplateId(), message.getTemplateParams()); + message.getApiTemplateId(), templateParams); + channelCode = sendResponse.getChannelCode(); + // 发送成功后操作 + for (SmsSendAfterSendHandler afterSendHandler : smsSendAfterSendHandlers) { + if (afterSendHandler.checkTemplateType(message.getTemplateType())) { + afterSendHandler.afterSend(sendResponse, templateParams); + } + } smsLogService.updateSmsSendResult(message.getLogId(), sendResponse.getSuccess(), sendResponse.getApiCode(), sendResponse.getApiMsg(), - sendResponse.getApiRequestId(), sendResponse.getSerialNo()); + sendResponse.getApiRequestId(), sendResponse.getSerialNo(), channelCode); } catch (Throwable ex) { log.error("[doSendSms][发送短信异常,日志编号({})]", message.getLogId(), ex); smsLogService.updateSmsSendResult(message.getLogId(), false, - "EXCEPTION", ExceptionUtil.getRootCauseMessage(ex), null, null); + "EXCEPTION", ExceptionUtil.getRootCauseMessage(ex), null, null, channelCode); } } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsTemplateServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsTemplateServiceImpl.java index ab9995331..0989f9875 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsTemplateServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/SmsTemplateServiceImpl.java @@ -165,7 +165,7 @@ public class SmsTemplateServiceImpl implements SmsTemplateService { @VisibleForTesting void validateApiTemplate(Long channelId, String apiTemplateId) { // 获得短信模板 - SmsClient smsClient = smsChannelService.getSmsClient(channelId); + SmsClient smsClient = smsChannelService.getSmsClient(); Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", channelId)); SmsTemplateRespDTO template; try { diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/handler/SmsSendAfterSendHandler.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/handler/SmsSendAfterSendHandler.java new file mode 100644 index 000000000..897635319 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/handler/SmsSendAfterSendHandler.java @@ -0,0 +1,25 @@ +package com.cf.imes.module.system.service.sms.handler; + +import com.cf.imes.framework.common.core.KeyValue; +import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO; + +import java.util.List; + +/** + * 短信发送后处理器接口 + * + * @author Gqr + * @since 2024/8/12 18:42 + */ +public interface SmsSendAfterSendHandler { + /** + * 校验消息模板类型 + * + * @param templateType + * @return + */ + boolean checkTemplateType(Integer templateType); + + + void afterSend(SmsSendRespDTO sendResponse, List> params); +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/handler/impl/SmsSendAfterSendHandlerImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/handler/impl/SmsSendAfterSendHandlerImpl.java new file mode 100644 index 000000000..37b488055 --- /dev/null +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/sms/handler/impl/SmsSendAfterSendHandlerImpl.java @@ -0,0 +1,46 @@ +package com.cf.imes.module.system.service.sms.handler.impl; + +import cn.hutool.core.util.ObjectUtil; +import com.cf.imes.framework.common.core.KeyValue; +import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO; +import com.cf.imes.module.system.dal.redis.RedisKeyConstants; +import com.cf.imes.module.system.enums.sms.SmsTemplateTypeEnum; +import com.cf.imes.module.system.framework.sms.SmsCodeProperties; +import com.cf.imes.module.system.service.sms.handler.SmsSendAfterSendHandler; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * @author Gqr + * @since 2024/8/12 18:49 + */ +@Service +public class SmsSendAfterSendHandlerImpl implements SmsSendAfterSendHandler { + + @Resource + private RedisTemplate redisTemplate; + + @Resource + private SmsCodeProperties smsCodeProperties; + + @Override + public boolean checkTemplateType(Integer templateType) { + SmsTemplateTypeEnum smsTemplateTypeEnum = SmsTemplateTypeEnum.valueOf(templateType); + return ObjectUtil.isNotNull(smsTemplateTypeEnum); + } + + @Override + public void afterSend(SmsSendRespDTO sendResponse, List> params) { + // 发送成功把验证码存入redis + if (sendResponse.getSuccess()) { + for (KeyValue keyValue : params) { + if ("code".equals(keyValue.getKey())) { + redisTemplate.opsForValue().set(String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, sendResponse.getMobile()), keyValue.getValue(), smsCodeProperties.getExpireTimes()); + } + } + } + } +} diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/user/AdminUserService.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/user/AdminUserService.java index 44cfb5c61..5e524384b 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/user/AdminUserService.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/user/AdminUserService.java @@ -4,11 +4,13 @@ import cn.hutool.core.collection.CollUtil; import com.cf.imes.framework.common.pojo.PageResult; import com.cf.imes.framework.common.util.collection.CollectionUtils; import com.cf.imes.module.system.api.user.dto.OrganAdminUserRespDTO; +import com.cf.imes.module.system.controller.admin.user.vo.profile.UserMobileUpdateReqVO; import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; import com.cf.imes.module.system.controller.admin.user.vo.user.UserImportExcelVO; import com.cf.imes.module.system.controller.admin.user.vo.user.UserImportRespVO; import com.cf.imes.module.system.controller.admin.user.vo.user.UserPageReqVO; +import com.cf.imes.module.system.controller.admin.user.vo.user.UserResetPasswordReqVO; import com.cf.imes.module.system.controller.admin.user.vo.user.UserSaveReqVO; import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; @@ -71,7 +73,7 @@ public interface AdminUserService { * @param id 用户 id * @param avatarFile 头像文件 */ - String updateUserAvatar(Long id, InputStream avatarFile) throws Exception; + String updateUserAvatar(Long id, InputStream avatarFile); /** * 修改密码 @@ -81,6 +83,13 @@ public interface AdminUserService { */ void updateUserPassword(Long id, String password); + /** + * 重置密码 + * + * @param reqVO + */ + void resetUserPassword(UserResetPasswordReqVO reqVO); + /** * 修改状态 * @@ -113,6 +122,14 @@ public interface AdminUserService { */ AdminUserDO getUserByUsernameAndOrganId(String username, Long organId); + /** + * 用户名查询唯一用户 + * + * @param userName + * @return + */ + AdminUserDO getUserUniqueByUserName(String userName); + /** * 通过手机号获取用户 * @@ -242,4 +259,12 @@ public interface AdminUserService { * 用户活跃数统计 */ Map userActTotal(); + + /** + * 修改密保手机 + * + * @param id 用户编号 + * @param reqVO 用户个人信息 + */ + void updateUserSecurityPhone(Long id, @Valid UserMobileUpdateReqVO reqVO); } diff --git a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/user/AdminUserServiceImpl.java b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/user/AdminUserServiceImpl.java index 42e949da7..e49ea3429 100644 --- a/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/user/AdminUserServiceImpl.java +++ b/cf-module-system/cf-module-system-biz/src/main/java/com/cf/imes/module/system/service/user/AdminUserServiceImpl.java @@ -4,13 +4,17 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.ObjectUtil; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.cf.imes.framework.common.enums.CommonStatusEnum; +import com.cf.imes.framework.common.enums.UserTypeEnum; import com.cf.imes.framework.common.exception.ServiceException; import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil; import com.cf.imes.framework.common.pojo.PageResult; import com.cf.imes.framework.common.util.collection.CollectionUtils; +import com.cf.imes.framework.common.util.monitor.TracerUtils; import com.cf.imes.framework.common.util.object.BeanUtils; import com.cf.imes.framework.common.util.pinyin.PinYinUtils; +import com.cf.imes.framework.common.util.servlet.ServletUtils; import com.cf.imes.framework.datapermission.core.util.DataPermissionUtils; import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX; import com.cf.imes.framework.mybatis.core.query.MPJLambdaWrapperX; @@ -18,13 +22,17 @@ import com.cf.imes.framework.organ.core.aop.OrganIgnore; import com.cf.imes.framework.organ.core.context.OrganContextHolder; import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils; import com.cf.imes.module.infra.api.file.FileApi; +import com.cf.imes.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import com.cf.imes.module.system.api.user.dto.OrganAdminUserRespDTO; import com.cf.imes.module.system.constants.permission.InternalRoleConstants; +import com.cf.imes.module.system.controller.admin.user.vo.profile.UserMobileUpdateReqVO; import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; import com.cf.imes.module.system.controller.admin.user.vo.user.UserImportExcelVO; import com.cf.imes.module.system.controller.admin.user.vo.user.UserImportRespVO; import com.cf.imes.module.system.controller.admin.user.vo.user.UserPageReqVO; +import com.cf.imes.module.system.controller.admin.user.vo.user.UserResetPasswordReqVO; import com.cf.imes.module.system.controller.admin.user.vo.user.UserSaveReqVO; import com.cf.imes.module.system.dal.dataobject.dept.DeptDO; import com.cf.imes.module.system.dal.dataobject.dept.UserPostDO; @@ -32,11 +40,16 @@ import com.cf.imes.module.system.dal.dataobject.permission.UserRoleDO; import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; import com.cf.imes.module.system.dal.mysql.dept.UserPostMapper; import com.cf.imes.module.system.dal.mysql.user.AdminUserMapper; +import com.cf.imes.module.system.enums.logger.LoginLogTypeEnum; +import com.cf.imes.module.system.enums.logger.LoginResultEnum; +import com.cf.imes.module.system.enums.sms.SmsSceneEnum; import com.cf.imes.module.system.service.dept.DeptService; import com.cf.imes.module.system.service.dept.PostService; +import com.cf.imes.module.system.service.logger.LoginLogService; import com.cf.imes.module.system.service.permission.PermissionService; import com.cf.imes.module.system.service.organ.OrganService; import com.cf.imes.module.system.enums.ErrorCodeConstants; +import com.cf.imes.module.system.service.sms.SmsCodeService; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -92,6 +105,12 @@ public class AdminUserServiceImpl implements AdminUserService { @Resource private FileApi fileApi; + @Resource + private SmsCodeService smsCodeService; + + @Resource + private LoginLogService loginLogService; + // 定义密码校验的正则表达式 private static final String LENGTH_PATTERN = ".{6,}"; @@ -110,15 +129,14 @@ public class AdminUserServiceImpl implements AdminUserService { throw ServiceExceptionUtil.exception(ErrorCodeConstants.USER_COUNT_MAX, organ.getAccountCount()); } }); + // 改造:手机号直接当做用户名 + createReqVO.setUsername(createReqVO.getMobile()); // 校验正确性 validateUserForCreateOrUpdate(null, createReqVO.getUsername(), - createReqVO.getMobile(), createReqVO.getEmail(), createReqVO.getDeptId(), createReqVO.getPostIds(),createReqVO.getPassword(), organId); + createReqVO.getEmail(), createReqVO.getDeptId(), createReqVO.getPostIds(), null, organId); // 插入用户 AdminUserDO user = BeanUtils.toBean(createReqVO, AdminUserDO.class); user.setOrganId(createReqVO.getOrganId()); - //user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 - user.setPassword(encodePassword(createReqVO.getPassword())); // 加密密码 - String pinyinFull = PinYinUtils.convertToPinyin(user.getNickname()); if (StringUtils.isNotBlank(pinyinFull)) { pinyinFull = pinyinFull.replace(" ", ""); @@ -138,12 +156,11 @@ public class AdminUserServiceImpl implements AdminUserService { @Transactional(rollbackFor = Exception.class) public void updateUser(UserSaveReqVO updateReqVO) { Long organId = updateReqVO.getOrganId() == null ? OrganContextHolder.getOrganId(): updateReqVO.getOrganId(); - updateReqVO.setPassword(null); // 特殊:此处不更新密码 // 不允许自身操作 checkCurrentWhenOperate(updateReqVO.getId()); // 校验正确性 validateUserForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getUsername(), - updateReqVO.getMobile(), updateReqVO.getEmail(), updateReqVO.getDeptId(), updateReqVO.getPostIds(),null, organId); + updateReqVO.getEmail(), updateReqVO.getDeptId(), updateReqVO.getPostIds(), null, organId); // 更新用户 AdminUserDO updateObj = BeanUtils.toBean(updateReqVO, AdminUserDO.class); @@ -190,7 +207,7 @@ public class AdminUserServiceImpl implements AdminUserService { // 校验正确性 validateUserExists(id); validateEmailUnique(id, reqVO.getEmail(), organId); - validateMobileUnique(id, reqVO.getMobile(), organId); + validateMobileUnique(id, reqVO.getMobile()); // 执行更新 userMapper.updateById(BeanUtils.toBean(reqVO, AdminUserDO.class).setId(id)); } @@ -231,6 +248,46 @@ public class AdminUserServiceImpl implements AdminUserService { userMapper.updateById(updateObj); } + @Override + @Transactional(rollbackFor = Exception.class) + public void resetUserPassword(UserResetPasswordReqVO reqVO) { + String mobile = reqVO.getMobile(); + String password = reqVO.getPassword(); + // 校验用户名 + AdminUserDO adminUserDO = validateUserNameExists(mobile); + Long userId = adminUserDO.getId(); + String username = adminUserDO.getUsername(); + // 校验密码 + validatePassword(password); + // 忘记密码消耗验证码 + smsCodeService.useSmsCode(SmsCodeUseReqDTO.builder() + .mobile(mobile).code(reqVO.getSmsCaptchaVerification()).scene(SmsSceneEnum.USER_RESET_PASSWORD.getScene()) + .build()); + // 更新密码 + AdminUserDO updateObj = new AdminUserDO(); + updateObj.setId(userId); + updateObj.setPassword(encodePassword(password)); // 加密密码 + userMapper.updateById(updateObj); + + // 记录日志 + createLoginLog(userId, username, LoginLogTypeEnum.LOGIN_FORGET_PWD, LoginResultEnum.SUCCESS); + } + + private void createLoginLog(Long userId, String username, + LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { + // 插入登录日志 + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(logTypeEnum.getType()); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(UserTypeEnum.ADMIN.getValue()); + reqDTO.setUsername(username); + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(ServletUtils.getClientIP()); + reqDTO.setResult(loginResult.getResult()); + loginLogService.createLoginLog(reqDTO); + } + @Override public void updateUserStatus(Long id, Integer status) { // 不允许自身操作 @@ -269,10 +326,15 @@ public class AdminUserServiceImpl implements AdminUserService { public AdminUserDO getUserByUsernameAndOrganId(String username, Long organId) { return userMapper.selectOne(new LambdaQueryWrapperX() .eq(AdminUserDO::getUsername, username) - .eq(AdminUserDO::getOrganId, organId) + .eqIfPresent(AdminUserDO::getOrganId, organId) ); } + @Override + public AdminUserDO getUserUniqueByUserName(String userName) { + return userMapper.selectByUsernameUnique(userName); + } + @Override public AdminUserDO getUserByMobile(String mobile) { return userMapper.selectByMobile(mobile); @@ -363,7 +425,7 @@ public class AdminUserServiceImpl implements AdminUserService { return deptIds; } - private void validateUserForCreateOrUpdate(Long id, String username, String mobile, String email, + private void validateUserForCreateOrUpdate(Long id, String username, String email, Long deptId, Set postIds,String password ,Long organId) { // 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确 DataPermissionUtils.executeIgnore(() -> { @@ -375,8 +437,6 @@ public class AdminUserServiceImpl implements AdminUserService { validateUserExists(id); // 校验用户名唯一 validateUsernameUnique(id, username, organId); - // 校验手机号唯一 - validateMobileUnique(id, mobile, organId); // 校验邮箱唯一 validateEmailUnique(id, email, organId); // 校验部门处于开启状态 @@ -420,11 +480,21 @@ public class AdminUserServiceImpl implements AdminUserService { } @VisibleForTesting + private AdminUserDO validateUserNameExists(String userName) { + AdminUserDO user = userMapper.selectByUsernameUnique(userName); + if (user == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.USER_NOT_EXISTS); + } + return user; + } + + @VisibleForTesting + @OrganIgnore void validateUsernameUnique(Long id, String username, Long organId) { if (StringUtils.isBlank(username)) { return; } - AdminUserDO user = userMapper.selectOne(AdminUserDO::getUsername, username, AdminUserDO::getOrganId, organId); + AdminUserDO user = userMapper.selectOne(AdminUserDO::getUsername, username); if (user == null) { return; } @@ -456,11 +526,12 @@ public class AdminUserServiceImpl implements AdminUserService { } @VisibleForTesting - void validateMobileUnique(Long id, String mobile, Long organId) { + @OrganIgnore + void validateMobileUnique(Long id, String mobile) { if (StringUtils.isBlank(mobile)) { return; } - AdminUserDO user = userMapper.selectOne(AdminUserDO::getOrganId, organId, AdminUserDO::getMobile, mobile); + AdminUserDO user = userMapper.selectOne(AdminUserDO::getUsername, mobile); if (user == null) { return; } @@ -501,7 +572,7 @@ public class AdminUserServiceImpl implements AdminUserService { importUsers.forEach(importUser -> { // 校验,判断是否有不符合的原因 try { - validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(), + validateUserForCreateOrUpdate(null, null, importUser.getEmail(), importUser.getDeptId(), null,null, organId); } catch (ServiceException ex) { respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage()); @@ -510,6 +581,8 @@ public class AdminUserServiceImpl implements AdminUserService { // 判断如果不存在,在进行插入 AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername(), organId); if (existUser == null) { + // 改造:手机号直接当做用户名 + importUser.setUsername(importUser.getMobile()); userMapper.insert(BeanUtils.toBean(importUser, AdminUserDO.class) .setPassword(encodePassword(userInitPassword)).setPostIds(new HashSet<>())); // 设置默认密码及空岗位编号数组 respVO.getCreateUsernames().add(importUser.getUsername()); @@ -563,6 +636,24 @@ public class AdminUserServiceImpl implements AdminUserService { return organAdminUserRespDTOS; } + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUserSecurityPhone(Long id, UserMobileUpdateReqVO reqVO) { + String mobile = reqVO.getMobile(); + // 校验手机号唯一 + validateMobileUnique(id, mobile); + + // 校验和消耗验证码 + smsCodeService.useSmsCode(SmsCodeUseReqDTO.builder() + .mobile(reqVO.getMobile()).code(reqVO.getSmsCaptchaVerification()).scene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene()) + .build()); + + // 新手机号入库 -> setUsername + userMapper.update(new LambdaUpdateWrapper() + .set(AdminUserDO::getUsername, mobile) + .eq(AdminUserDO::getId, id)); + } + /** * 对密码进行加密 * diff --git a/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml b/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml index eddbc4904..476be12f3 100644 --- a/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml +++ b/cf-module-system/cf-module-system-biz/src/main/resources/application.yaml @@ -159,6 +159,8 @@ chenfeng: - /admin-api/system/data-source/* - /admin-api/system/auth/login - /admin-api/system/auth/logout + - /admin-api/system/auth/sms-code + - /admin-api/system/user/reset-password - /rpc-api/** #- /admin-api/system/** #- /rpc-api/system/organ/valid # 防止递归。避免调用 /rpc-api/system/organ/valid 接口时,又去触发 /rpc-api/system/organ/valid 去校验 @@ -193,10 +195,19 @@ chenfeng: use-data-code: imes_prod #动态数据源标识,后期添加的数据源需要修改此值 sms-code: # 短信验证码相关的配置项 - expire-times: 10m + expire-times: 5m send-frequency: 1m send-maximum-quantity-per-day: 10 + send-max-send-times-per-window: 5 #窗口最大请求次数 + send-window-size-min: 10m #窗口时间 begin-code: 9999 # 这里配置 9999 的原因是,测试方便。 end-code: 9999 # 这里配置 9999 的原因是,测试方便。 - + sms: + channel: DEBUG_DING_TALK + signature: AKhcjogF4mzWTn5G0I0sfQ== + apiKey: Bw9mvVZZ8cLBLoPFl9yY/AI1xOUCEALEtftakrgiv9BT/dOuKUhWsXuFr9UvxRlMSRporf8Wcd7C6IFB9ukUR0IWgKTelx2vv2Lhzdnz4ac= + apiSecret: Um956u5zFSl0tu8imFB/TOZyK7xysT5JMLY17mZIwXH+eh4BjZEmsjmFRfkcXin0A/GNhRzcUurJfm+N/S42uJZ9G9aCCIoPSjpnF9SXwt0= + encrypt: + enable: true + publicKey: cfimes debug: false diff --git a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/auth/AdminAuthServiceImplTest.java b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/auth/AdminAuthServiceImplTest.java index 3d8a0d8c5..99d651035 100644 --- a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/auth/AdminAuthServiceImplTest.java +++ b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/auth/AdminAuthServiceImplTest.java @@ -5,20 +5,14 @@ import com.cf.imes.framework.common.enums.CommonStatusEnum; import com.cf.imes.framework.common.enums.UserTypeEnum; import com.cf.imes.framework.test.core.ut.BaseDbUnitTest; import com.cf.imes.module.system.api.sms.SmsCodeApi; -import com.cf.imes.module.system.api.social.dto.SocialUserBindReqDTO; -import com.cf.imes.module.system.api.social.dto.SocialUserRespDTO; -import com.cf.imes.module.system.controller.admin.auth.vo.*; import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO; -import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO; import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsSendReqVO; -import com.cf.imes.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO; import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO; import com.cf.imes.module.system.enums.logger.LoginLogTypeEnum; import com.cf.imes.module.system.enums.logger.LoginResultEnum; import com.cf.imes.module.system.enums.sms.SmsSceneEnum; -import com.cf.imes.module.system.enums.social.SocialTypeEnum; import com.cf.imes.module.system.service.logger.LoginLogService; import com.cf.imes.module.system.service.member.MemberService; import com.cf.imes.module.system.service.oauth2.OAuth2TokenService; @@ -91,7 +85,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); // 调用 - AdminUserDO loginUser = authService.authenticate(username, password, null); + AdminUserDO loginUser = authService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build()); // 校验 assertPojoEquals(user, loginUser); } @@ -103,7 +97,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { String password = randomString(); // 调用, 并断言异常 - assertServiceException(() -> authService.authenticate(username, password, null), + assertServiceException(() -> authService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build()), AUTH_LOGIN_BAD_CREDENTIALS); verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) @@ -123,7 +117,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { when(userService.getUserByUsername(eq(username), null)).thenReturn(user); // 调用, 并断言异常 - assertServiceException(() -> authService.authenticate(username, password, null), + assertServiceException(() -> authService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build()), AUTH_LOGIN_BAD_CREDENTIALS); verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) @@ -145,7 +139,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); // 调用, 并断言异常 - assertServiceException(() -> authService.authenticate(username, password, null), + assertServiceException(() -> authService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build()), AUTH_LOGIN_USER_DISABLED); verify(loginLogService).createLoginLog( argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) diff --git a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsChannelServiceTest.java b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsChannelServiceTest.java index 2a6fdcb40..bfd612c8a 100644 --- a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsChannelServiceTest.java +++ b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsChannelServiceTest.java @@ -5,6 +5,7 @@ import com.cf.imes.framework.common.pojo.PageResult; import com.cf.imes.framework.common.util.object.BeanUtils; import com.cf.imes.framework.sms.core.client.SmsClient; import com.cf.imes.framework.sms.core.client.SmsClientFactory; +import com.cf.imes.framework.sms.core.enums.SmsChannelEnum; import com.cf.imes.framework.sms.core.property.SmsChannelProperties; import com.cf.imes.framework.test.core.ut.BaseDbUnitTest; import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; @@ -200,16 +201,12 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { Long id = channel.getId(); // mock 方法 SmsClient mockClient = mock(SmsClient.class); - when(smsClientFactory.getSmsClient(eq(id))).thenReturn(mockClient); + when(smsClientFactory.getSmsClient(eq(SmsChannelEnum.ALIYUN.getCode()))).thenReturn(mockClient); // 调用 SmsClient client = smsChannelService.getSmsClient(id); // 断言 assertSame(client, mockClient); - verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> { - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); - return properties.equals(arg); - })); } @Test @@ -227,10 +224,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest { SmsClient client = smsChannelService.getSmsClient(code); // 断言 assertSame(client, mockClient); - verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> { - SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class); - return properties.equals(arg); - })); } } diff --git a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsLogServiceImplTest.java b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsLogServiceImplTest.java index 33752d6e8..0576fd92e 100644 --- a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsLogServiceImplTest.java +++ b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsLogServiceImplTest.java @@ -130,10 +130,11 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest { String apiSendMsg = randomString(); String apiRequestId = randomString(); String apiSerialNo = randomString(); + String channelCode = randomString(); // 调用 smsLogService.updateSmsSendResult(id, success, - apiSendCode, apiSendMsg, apiRequestId, apiSerialNo); + apiSendCode, apiSendMsg, apiRequestId, apiSerialNo, channelCode); // 断言 dbSmsLog = smsLogMapper.selectById(id); assertEquals(success ? SmsSendStatusEnum.SUCCESS.getStatus() : SmsSendStatusEnum.FAILURE.getStatus(), diff --git a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsSendServiceImplTest.java b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsSendServiceImplTest.java index bf0f196f4..a21c8b6ef 100644 --- a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsSendServiceImplTest.java +++ b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/sms/SmsSendServiceImplTest.java @@ -85,8 +85,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 - verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), - eq(template.getChannelId()), eq(template.getApiTemplateId()), + verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), eq(template), eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); } @@ -124,8 +123,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 - verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), - eq(template.getChannelId()), eq(template.getApiTemplateId()), + verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(template), eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); } @@ -164,8 +162,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 - verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), - eq(template.getChannelId()), eq(template.getApiTemplateId()), + verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(template), eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); } @@ -204,8 +201,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 断言 assertEquals(smsLogId, resultSmsLogId); // 断言调用 - verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(), - anyLong(), any(), anyList()); + verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), any(), any(), anyList()); } @Test @@ -274,7 +270,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 断言 verify(smsLogService).updateSmsSendResult(eq(message.getLogId()), eq(sendResult.getSuccess()), eq(sendResult.getApiCode()), - eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getSerialNo())); + eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getSerialNo()), eq(sendResult.getChannelCode())); } @Test diff --git a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/user/AdminUserServiceImplTest.java b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/user/AdminUserServiceImplTest.java index 61a9c5866..e38a94fd5 100644 --- a/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/user/AdminUserServiceImplTest.java +++ b/cf-module-system/cf-module-system-biz/src/test/java/com/cf/imes/module/system/service/user/AdminUserServiceImplTest.java @@ -588,7 +588,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile))); // 调用,校验异常 - assertServiceException(() -> userService.validateMobileUnique(null, mobile, 1L), + assertServiceException(() -> userService.validateMobileUnique(null, mobile), USER_MOBILE_EXISTS); } @@ -601,7 +601,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile))); // 调用,校验异常 - assertServiceException(() -> userService.validateMobileUnique(id, mobile, 1L), + assertServiceException(() -> userService.validateMobileUnique(id, mobile), USER_MOBILE_EXISTS); }