diff --git a/pom.xml b/pom.xml
index 4fc098f5..bd822674 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,6 +50,10 @@
5.6.72
8.3.8
+
+ 2.0.9
+ 3.1.500
+
localhost
http://${docker.registry.url}:2375
@@ -192,6 +196,18 @@
${okhttp.version}
+
+ com.aliyun
+ dysmsapi20170525
+ ${aliyun.sms.version}
+
+
+
+ com.tencentcloudapi
+ tencentcloud-sdk-java
+ ${tencent.sms.version}
+
+
de.codecentric
spring-boot-admin-starter-server
@@ -297,6 +313,13 @@
${ruoyi-vue-plus.version}
+
+
+ com.ruoyi
+ ruoyi-sms
+ ${ruoyi-vue-plus.version}
+
+
com.ruoyi
@@ -317,6 +340,7 @@
ruoyi-demo
ruoyi-extend
ruoyi-oss
+ ruoyi-sms
pom
diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml
index 6be268bf..d14047f3 100644
--- a/ruoyi-admin/src/main/resources/application-dev.yml
+++ b/ruoyi-admin/src/main/resources/application-dev.yml
@@ -178,3 +178,15 @@ mail:
timeout: 0
# Socket连接超时值,单位毫秒,缺省值不超时
connectionTimeout: 0
+
+--- # sms 短信
+sms:
+ enabled: false
+ # 阿里云 dysmsapi.aliyuncs.com
+ # 腾讯云 sms.tencentcloudapi.com
+ endpoint: "dysmsapi.aliyuncs.com"
+ accessKeyId: xxxxxxx
+ accessKeySecret: xxxxxx
+ signName: 测试
+ # 腾讯专用
+ sdkAppId:
diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml
index 4966a41e..6619e055 100644
--- a/ruoyi-admin/src/main/resources/application-prod.yml
+++ b/ruoyi-admin/src/main/resources/application-prod.yml
@@ -181,3 +181,15 @@ mail:
timeout: 0
# Socket连接超时值,单位毫秒,缺省值不超时
connectionTimeout: 0
+
+--- # sms 短信
+sms:
+ enabled: false
+ # 阿里云 dysmsapi.aliyuncs.com
+ # 腾讯云 sms.tencentcloudapi.com
+ endpoint: "dysmsapi.aliyuncs.com"
+ accessKeyId: xxxxxxx
+ accessKeySecret: xxxxxx
+ signName: 测试
+ # 腾讯专用
+ sdkAppId:
diff --git a/ruoyi-sms/pom.xml b/ruoyi-sms/pom.xml
new file mode 100644
index 00000000..8550e8c7
--- /dev/null
+++ b/ruoyi-sms/pom.xml
@@ -0,0 +1,40 @@
+
+
+
+ ruoyi-vue-plus
+ com.ruoyi
+ 4.1.0
+
+ 4.0.0
+
+ ruoyi-sms
+
+
+ SMS短信模块
+
+
+
+
+
+
+ com.ruoyi
+ ruoyi-common
+
+
+
+ com.aliyun
+ dysmsapi20170525
+ true
+
+
+
+ com.tencentcloudapi
+ tencentcloud-sdk-java
+ true
+
+
+
+
+
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/config/SmsConfig.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/config/SmsConfig.java
new file mode 100644
index 00000000..abc4bb97
--- /dev/null
+++ b/ruoyi-sms/src/main/java/com/ruoyi/sms/config/SmsConfig.java
@@ -0,0 +1,44 @@
+package com.ruoyi.sms.config;
+
+import com.ruoyi.sms.config.properties.SmsProperties;
+import com.ruoyi.sms.core.AliyunSmsTemplate;
+import com.ruoyi.sms.core.SmsTemplate;
+import com.ruoyi.sms.core.TencentSmsTemplate;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 短信配置类
+ *
+ * @author Lion Li
+ * @version 4.2.0
+ */
+@Configuration
+@ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
+public class SmsConfig {
+
+ @Configuration
+ @ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
+ static class AliyunSmsConfig {
+
+ @Bean
+ public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
+ return new AliyunSmsTemplate(smsProperties);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
+ static class TencentSmsConfig {
+
+ @Bean
+ public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
+ return new TencentSmsTemplate(smsProperties);
+ }
+
+ }
+
+}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
new file mode 100644
index 00000000..39359cdf
--- /dev/null
+++ b/ruoyi-sms/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
@@ -0,0 +1,47 @@
+package com.ruoyi.sms.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * SMS短信 配置属性
+ *
+ * @author Lion Li
+ * @version 4.2.0
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "sms")
+public class SmsProperties {
+
+ private Boolean enabled;
+
+ /**
+ * 配置节点
+ * 阿里云 dysmsapi.aliyuncs.com
+ * 腾讯云 sms.tencentcloudapi.com
+ */
+ private String endpoint;
+
+ /**
+ * key
+ */
+ private String accessKeyId;
+
+ /**
+ * 密匙
+ */
+ private String accessKeySecret;
+
+ /*
+ * 短信签名
+ */
+ private String signName;
+
+ /**
+ * 短信应用ID (腾讯专属)
+ */
+ private String sdkAppId;
+
+}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java
new file mode 100644
index 00000000..87c3f3bd
--- /dev/null
+++ b/ruoyi-sms/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java
@@ -0,0 +1,65 @@
+package com.ruoyi.sms.core;
+
+import com.aliyun.dysmsapi20170525.Client;
+import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
+import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
+import com.aliyun.teaopenapi.models.Config;
+import com.ruoyi.common.utils.JsonUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.sms.config.properties.SmsProperties;
+import com.ruoyi.sms.entity.SmsResult;
+import com.ruoyi.sms.exception.SmsException;
+import lombok.SneakyThrows;
+
+import java.util.Map;
+
+/**
+ * Aliyun 短信模板
+ *
+ * @author Lion Li
+ * @version 4.2.0
+ */
+public class AliyunSmsTemplate implements SmsTemplate {
+
+ private SmsProperties properties;
+
+ private Client client;
+
+ @SneakyThrows(Exception.class)
+ public AliyunSmsTemplate(SmsProperties smsProperties) {
+ this.properties = smsProperties;
+ Config config = new Config()
+ // 您的AccessKey ID
+ .setAccessKeyId(smsProperties.getAccessKeyId())
+ // 您的AccessKey Secret
+ .setAccessKeySecret(smsProperties.getAccessKeySecret())
+ // 访问的域名
+ .setEndpoint(smsProperties.getEndpoint());
+ this.client = new Client(config);
+ }
+
+ public SmsResult send(String phones, String templateId, Map param) {
+ if (StringUtils.isBlank(phones)) {
+ throw new SmsException("手机号不能为空");
+ }
+ if (StringUtils.isBlank(templateId)) {
+ throw new SmsException("模板ID不能为空");
+ }
+ SendSmsRequest req = new SendSmsRequest()
+ .setPhoneNumbers(phones)
+ .setSignName(properties.getSignName())
+ .setTemplateCode(templateId)
+ .setTemplateParam(JsonUtils.toJsonString(param));
+ try {
+ SendSmsResponse resp = client.sendSms(req);
+ return SmsResult.builder()
+ .isSuccess("OK".equals(resp.getBody().getCode()))
+ .message(resp.getBody().getMessage())
+ .response(resp)
+ .build();
+ } catch (Exception e) {
+ throw new SmsException(e.getMessage());
+ }
+ }
+
+}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/core/SmsTemplate.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/core/SmsTemplate.java
new file mode 100644
index 00000000..0aec3ddb
--- /dev/null
+++ b/ruoyi-sms/src/main/java/com/ruoyi/sms/core/SmsTemplate.java
@@ -0,0 +1,26 @@
+package com.ruoyi.sms.core;
+
+import com.ruoyi.sms.entity.SmsResult;
+
+import java.util.Map;
+
+/**
+ * 短信模板
+ *
+ * @author Lion Li
+ * @version 4.2.0
+ */
+public interface SmsTemplate {
+
+ /**
+ * 发送短信
+ *
+ * @param phones 电话号(多个逗号分割)
+ * @param templateId 模板id
+ * @param param 模板对应参数
+ * 阿里 需使用 模板变量名称对应内容 例如: code=1234
+ * 腾讯 需使用 模板变量顺序对应内容 例如: 1=1234, 1为模板内第一个参数
+ */
+ SmsResult send(String phones, String templateId, Map param);
+
+}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java
new file mode 100644
index 00000000..ce82c3c6
--- /dev/null
+++ b/ruoyi-sms/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java
@@ -0,0 +1,80 @@
+package com.ruoyi.sms.core;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.sms.config.properties.SmsProperties;
+import com.ruoyi.sms.entity.SmsResult;
+import com.ruoyi.sms.exception.SmsException;
+import com.tencentcloudapi.common.Credential;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import com.tencentcloudapi.sms.v20190711.SmsClient;
+import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
+import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;
+import com.tencentcloudapi.sms.v20190711.models.SendStatus;
+import lombok.SneakyThrows;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Tencent 短信模板
+ *
+ * @author Lion Li
+ * @version 4.2.0
+ */
+public class TencentSmsTemplate implements SmsTemplate {
+
+ private SmsProperties properties;
+
+ private SmsClient client;
+
+ @SneakyThrows(Exception.class)
+ public TencentSmsTemplate(SmsProperties smsProperties) {
+ this.properties = smsProperties;
+ Credential credential = new Credential(smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
+ HttpProfile httpProfile = new HttpProfile();
+ httpProfile.setEndpoint(smsProperties.getEndpoint());
+ ClientProfile clientProfile = new ClientProfile();
+ clientProfile.setHttpProfile(httpProfile);
+ this.client = new SmsClient(credential, "", clientProfile);
+ }
+
+ public SmsResult send(String phones, String templateId, Map param) {
+ if (StringUtils.isBlank(phones)) {
+ throw new SmsException("手机号不能为空");
+ }
+ if (StringUtils.isBlank(templateId)) {
+ throw new SmsException("模板ID不能为空");
+ }
+ SendSmsRequest req = new SendSmsRequest();
+ Set set = Arrays.stream(phones.split(",")).map(p -> "+86" + p).collect(Collectors.toSet());
+ req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
+ if (CollUtil.isNotEmpty(param)) {
+ req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));
+ }
+ req.setTemplateID(templateId);
+ req.setSign(properties.getSignName());
+ req.setSmsSdkAppid(properties.getSdkAppId());
+ try {
+ SendSmsResponse resp = client.SendSms(req);
+ SmsResult.SmsResultBuilder builder = SmsResult.builder()
+ .isSuccess(true)
+ .message("send success")
+ .response(resp);
+ for (SendStatus sendStatus : resp.getSendStatusSet()) {
+ if (!"Ok".equals(sendStatus.getCode())) {
+ builder.isSuccess(false).message(sendStatus.getMessage());
+ break;
+ }
+ }
+ return builder.build();
+ } catch (Exception e) {
+ throw new SmsException(e.getMessage());
+ }
+ }
+
+}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/entity/SmsResult.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/entity/SmsResult.java
new file mode 100644
index 00000000..3f13b277
--- /dev/null
+++ b/ruoyi-sms/src/main/java/com/ruoyi/sms/entity/SmsResult.java
@@ -0,0 +1,29 @@
+package com.ruoyi.sms.entity;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 上传返回体
+ *
+ * @author Lion Li
+ */
+@Data
+@Builder
+public class SmsResult {
+
+ /**
+ * 是否成功
+ */
+ private boolean isSuccess;
+
+ /**
+ * 响应消息
+ */
+ private String message;
+
+ /**
+ * 实际响应体
+ */
+ private Object response;
+}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/exception/SmsException.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/exception/SmsException.java
new file mode 100644
index 00000000..28632a37
--- /dev/null
+++ b/ruoyi-sms/src/main/java/com/ruoyi/sms/exception/SmsException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.sms.exception;
+
+/**
+ * Sms异常类
+ *
+ * @author Lion Li
+ */
+public class SmsException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public SmsException(String msg) {
+ super(msg);
+ }
+
+}