diff --git a/README.md b/README.md
index 8f3d2baf..83b44dfe 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@
| 分布式任务调度 | Xxl-Job | [Xxl-Job官网](https://www.xuxueli.com/xxl-job/) | 高性能 高可靠 易扩展 |
| 分布式文件存储 | Minio | [Minio文档](https://docs.min.io/) | 本地存储 |
| 分布式云存储 | 七牛、阿里、腾讯 | [OSS使用文档](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4359146&doc_id=1469725) | 云存储 |
+| 短信模块 | 阿里、腾讯 | [短信使用文档](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=5578491&doc_id=1469725) | 短信发送 |
| 分布式监控(未完成) | Prometheus、Grafana | [Prometheus文档](https://prometheus.io/docs/introduction/overview/) | 全方位性能监控 |
| 服务监控 | SpringBoot-Admin | [SpringBoot-Admin文档](https://codecentric.github.io/spring-boot-admin/current/) | 全方位服务监控 |
| 数据库框架 | Mybatis-Plus | [Mybatis-Plus文档](https://baomidou.com/guide/) | 快速 CRUD 增加开发效率 |
diff --git a/config/dev/application-common.yml b/config/dev/application-common.yml
index 42900a6f..a6fde852 100644
--- a/config/dev/application-common.yml
+++ b/config/dev/application-common.yml
@@ -313,3 +313,14 @@ mail:
timeout: 0
# Socket连接超时值,单位毫秒,缺省值不超时
connectionTimeout: 0
+
+sms:
+ enabled: false
+ # 阿里云 dysmsapi.aliyuncs.com
+ # 腾讯云 sms.tencentcloudapi.com
+ endpoint: "dysmsapi.aliyuncs.com"
+ accessKeyId: xxxxxxx
+ accessKeySecret: xxxxxx
+ signName: 测试
+ # 腾讯专用
+ sdkAppId:
diff --git a/pom.xml b/pom.xml
index bb132e6e..54ffd023 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,6 +50,10 @@
8.3.8
4.9.3
+
+ 2.0.9
+ 3.1.500
+
localhost
http://${docker.registry.url}:2375
@@ -248,6 +252,24 @@
${okhttp.version}
+
+ com.aliyun
+ dysmsapi20170525
+ ${aliyun.sms.version}
+
+
+
+ com.tencentcloudapi
+ tencentcloud-sdk-java
+ ${tencent.sms.version}
+
+
+ com.squareup.okio
+ okio
+
+
+
+
com.google.guava
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
index 69acd258..2399841b 100644
--- a/ruoyi-common/pom.xml
+++ b/ruoyi-common/pom.xml
@@ -28,6 +28,7 @@
ruoyi-common-oss
ruoyi-common-idempotent
ruoyi-common-mail
+ ruoyi-common-sms
ruoyi-common
diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml
index 32380bba..18395e26 100644
--- a/ruoyi-common/ruoyi-common-bom/pom.xml
+++ b/ruoyi-common/ruoyi-common-bom/pom.xml
@@ -126,6 +126,12 @@
${project.version}
+
+ com.ruoyi
+ ruoyi-common-sms
+ ${project.version}
+
+
diff --git a/ruoyi-common/ruoyi-common-sms/pom.xml b/ruoyi-common/ruoyi-common-sms/pom.xml
new file mode 100644
index 00000000..87b22b21
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/pom.xml
@@ -0,0 +1,38 @@
+
+
+
+ com.ruoyi
+ ruoyi-common
+ 0.10.0
+
+ 4.0.0
+
+ ruoyi-common-sms
+
+
+ ruoyi-common-sms 短信模块
+
+
+
+
+
+ com.ruoyi
+ ruoyi-common-core
+
+
+
+ com.aliyun
+ dysmsapi20170525
+ true
+
+
+
+ com.tencentcloudapi
+ tencentcloud-sdk-java
+ true
+
+
+
+
diff --git a/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/config/SmsAutoConfiguration.java b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/config/SmsAutoConfiguration.java
new file mode 100644
index 00000000..d58e127c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/config/SmsAutoConfiguration.java
@@ -0,0 +1,46 @@
+package com.ruoyi.common.sms.config;
+
+import com.ruoyi.common.sms.config.properties.SmsProperties;
+import com.ruoyi.common.sms.core.AliyunSmsTemplate;
+import com.ruoyi.common.sms.core.SmsTemplate;
+import com.ruoyi.common.sms.core.TencentSmsTemplate;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+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")
+@EnableConfigurationProperties(SmsProperties.class)
+public class SmsAutoConfiguration {
+
+ @Configuration
+ @ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
+ static class AliyunSmsConfiguration {
+
+ @Bean
+ public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
+ return new AliyunSmsTemplate(smsProperties);
+ }
+
+ }
+
+ @Configuration
+ @ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
+ static class TencentSmsConfiguration {
+
+ @Bean
+ public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
+ return new TencentSmsTemplate(smsProperties);
+ }
+
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/config/properties/SmsProperties.java b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/config/properties/SmsProperties.java
new file mode 100644
index 00000000..c0afb0b6
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/config/properties/SmsProperties.java
@@ -0,0 +1,46 @@
+package com.ruoyi.common.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
+@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-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/core/AliyunSmsTemplate.java b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/core/AliyunSmsTemplate.java
new file mode 100644
index 00000000..7d36aa7e
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/core/AliyunSmsTemplate.java
@@ -0,0 +1,65 @@
+package com.ruoyi.common.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.core.utils.JsonUtils;
+import com.ruoyi.common.core.utils.StringUtils;
+import com.ruoyi.common.sms.config.properties.SmsProperties;
+import com.ruoyi.common.sms.entity.SmsResult;
+import com.ruoyi.common.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-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/core/SmsTemplate.java b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/core/SmsTemplate.java
new file mode 100644
index 00000000..eb61b863
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/core/SmsTemplate.java
@@ -0,0 +1,26 @@
+package com.ruoyi.common.sms.core;
+
+import com.ruoyi.common.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-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/core/TencentSmsTemplate.java b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/core/TencentSmsTemplate.java
new file mode 100644
index 00000000..0827d10c
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/core/TencentSmsTemplate.java
@@ -0,0 +1,80 @@
+package com.ruoyi.common.sms.core;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.ruoyi.common.core.utils.StringUtils;
+import com.ruoyi.common.sms.config.properties.SmsProperties;
+import com.ruoyi.common.sms.entity.SmsResult;
+import com.ruoyi.common.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-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/entity/SmsResult.java b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/entity/SmsResult.java
new file mode 100644
index 00000000..b1a7b28f
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/entity/SmsResult.java
@@ -0,0 +1,29 @@
+package com.ruoyi.common.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-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/exception/SmsException.java b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/exception/SmsException.java
new file mode 100644
index 00000000..f855a29a
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/java/com/ruoyi/common/sms/exception/SmsException.java
@@ -0,0 +1,16 @@
+package com.ruoyi.common.sms.exception;
+
+/**
+ * Sms异常类
+ *
+ * @author Lion Li
+ */
+public class SmsException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public SmsException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring.factories b/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..1058e084
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-sms/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+ com.ruoyi.common.sms.config.SmsAutoConfiguration