Compare commits

...

91 Commits

Author SHA1 Message Date
wind
bc5af3a68a 3.3.5发布 2025-05-10 13:38:46 +08:00
wind
3be4e34d6b 腾讯云,不再强制发送+86号码,如果需要发送非+86手机,可在手机号前拼接 - 号,表示不强制添加+86 2025-04-27 21:38:55 +08:00
风如歌
9baa8bba57
!194 移动云mas的params格式问题修复
Merge pull request !194 from dusttosky/bugfix-mas
2025-04-02 10:44:39 +00:00
Charles7c
03d30271dd 修复部分提示 2025-03-28 20:42:34 +08:00
dusttosky
920f9304ea 移动云mas的params格式问题修复 2025-03-26 16:07:18 +08:00
wind
cedae2696b 3.3.4发布 2025-03-07 21:38:28 +08:00
wind
7582982d44 3.3.4发布 2025-03-07 21:36:32 +08:00
wind
6aa173501e 3.3.4发布 2025-03-07 20:56:01 +08:00
wind
5bc7d0152e 3.3.4发布 2025-03-07 20:55:29 +08:00
wind
bfdc75d538 Merge branch 'dev-3.0.x' of https://gitee.com/dromara/sms4j
# Conflicts:
#	pom.xml
#	sms4j-comm/src/main/java/org/dromara/sms4j/comm/constant/Constant.java
2025-03-07 19:55:56 +08:00
风如歌
4e3845e2ce
!191 add 新增http代理模式
Merge pull request !191 from Bleachtred/dev-3.0.x
2025-03-03 11:16:24 +00:00
bleachtred
b168d5235f add 新增http代理模式 2025-03-03 16:43:33 +08:00
wind
bc8e329bfa 更新版本号 2025-02-26 19:50:13 +08:00
wind
09a3dba016 修复,异常状态下 短信重试无限次重试问题,布丁云短信群发采用自实现 2025-02-26 19:40:51 +08:00
wind
a62bca40b6 优化,每日最大发送量计时截止每日0点,而不是持续24小时 2025-02-26 19:25:31 +08:00
wind
8498fafa65 修复,云片短信无限重试问题 2025-02-26 19:16:39 +08:00
风如歌
aeecaaa8f5
!190 sms4j-solon-plugin: 优化适配,保持与 spring 的体验一致
Merge pull request !190 from 西东/dev-3.0.x
2025-02-06 09:14:41 +00:00
noear
2ef232d9ff sms4j-solon-plugin: 优化适配,保持与 spring 的体验一致 2025-01-05 08:15:05 +08:00
风如歌
e6173c5d2f
!189 sms4j-solon-plugin:重新适配(同步 springboot 的适配代码)
Merge pull request !189 from 西东/dev-3.0.x
2025-01-04 11:07:50 +00:00
noear
44287e62d4 sms4j-solon-plugin-example: springboot 的测试代码 2025-01-04 18:44:27 +08:00
noear
49c5af9f70 sms4j-solon-plugin:重新适配(同步 springboot 的适配代码) 2025-01-04 18:33:57 +08:00
noear
98e779376d sms4j-solon-plugin:优化 SupplierConfig 适配类 2025-01-03 17:19:58 +08:00
noear
ff4d6d8bf4 solon 升为 3.0.1(兼容 2.x) 2025-01-03 16:28:38 +08:00
noear
76e9ec060e sms4j-solon-plugin-example 简化测试代码 2025-01-03 16:27:46 +08:00
风如歌
d58f2d0eaf
!188 【修复】发送oa消息通知,线程池资源在while循环中未能释放,导致服务器cpu占用过高
Merge pull request !188 from 东风/oa-fix-high-cpu
2024-12-24 08:26:20 +00:00
风如歌
ec501e441d
!187 修复luosimao对接手机号+86前缀报错问题
Merge pull request !187 from LiYaoheng/dev-3.0.x
2024-12-24 08:25:58 +00:00
dongfeng
f9f197935a 修复oa导致的cpu过高的问题 2024-11-11 19:33:25 +08:00
风如歌
197dc65cf8
!186 优化导入依赖
Merge pull request !186 from handy/dev-3.0.x
2024-11-05 00:40:04 +00:00
LiYaoheng
1c0d518fdb fix luosimao的手机号不需要+86前缀 2024-11-04 17:07:09 +08:00
handy
46f3ccfbf2 优化导入依赖 2024-10-25 14:27:42 +08:00
bleachtred
109f50cfd1 fix 两个邮件插件核心代码不同步 #I9CWJI 2024-10-23 13:21:58 +08:00
bleachtred
30c4488cde update 包名adepter => adaptor #I98OR2 2024-10-23 11:27:07 +08:00
bleachtred
8a0dd4bc3d fix 修复调用百度接口提示缺少请求头 #IAR2RU 2024-10-23 11:23:40 +08:00
Bleachtred
8aca0fd957
update sms4j-provider/src/main/java/org/dromara/sms4j/mas/utils/MasUtils.java.
fix 修复中国移动 云MAS addSerial参数为空时没有放到map中导致发送失败

Signed-off-by: Bleachtred <bleachtred@163.com>
2024-10-23 03:13:00 +00:00
Bleachtred
1a31bb5189
update sms4j-provider/src/main/java/org/dromara/sms4j/submail/service/SubMailSmsImpl.java.
fix 修复SubMail获取timestamp时,由于取值key错误导致无法获取timestamp,导致短信无法发送

Signed-off-by: Bleachtred <bleachtred@163.com>
2024-10-23 03:07:44 +00:00
风如歌
e81eeffe40
!185 重提PR(179 解决 Sms 和 Oa 的厂商配置信息Map会在同时存在时发生冲突的Bug)
Merge pull request !185 from 东风/oa-fix-bean
2024-10-21 06:36:51 +00:00
dongfeng
95601131cd 添加 Qualifier 指定配置Map注入Bean,解决OA与Sms同时存在Bug 2024-10-17 22:39:01 +08:00
dongfeng
3f9c22a04d 修复oa的yml案例 2024-10-17 22:36:52 +08:00
风如歌
463f3e568b
!181 add 联通一信通平台短信通道
Merge pull request !181 from moatg/dev-3.0.x
2024-09-23 01:20:53 +00:00
wind
298e7c48e5 3.3.3升级
1. https://gitee.com/dromara/sms4j/issues/IAN87I
2. 解决腾讯云多参数发送报错问题
修复异常执行器再未进行自定义的情况下,会将异常堆栈吞掉的问题
3. 天翼云eop-date参数实际生产少8小时
2024-09-07 17:55:58 +08:00
wind
d7589aa7a1 Merge branch 'refs/heads/dev-3.0.x'
# Conflicts:
#	README.md
2024-09-07 17:53:19 +08:00
wind
2ee0389eda #https://gitee.com/dromara/sms4j/issues/IAN87I
在调用SmsBlend.sendMessage方法时,传入三个参数,第三个参数map,当这个参数大小不为0的时候,该方法一直返回null
2024-09-06 15:04:12 +08:00
风如歌
36348b54b1
!184 fix 天翼云eop-date参数实际生产少8小时 bug #IAKMPG
Merge pull request !184 from Bleachtred/dev-3.0.x
2024-08-28 12:14:58 +00:00
wind
2df3db09d9 修复异常执行器再未进行自定义的情况下,会将异常堆栈吞掉的问题 2024-08-28 17:38:39 +08:00
bleachtred
ea6e6a9fa6 fix 天翼云eop-date参数实际生产少8小时 bug #IAKMPG 2024-08-19 19:10:33 +08:00
风如歌
eea34ace98
!183 修改文档地址
Merge pull request !183 from handy/dev-3.0.x
2024-08-16 07:39:04 +00:00
风如歌
319a0d2cd1
update README.md.
Signed-off-by: 风如歌 <wzsf1810@qq.com>
2024-08-15 02:12:31 +00:00
handy
21fe49f627 修改文档地址 2024-08-15 10:10:12 +08:00
风如歌
cf8b977a9e
update README.md.
Signed-off-by: 风如歌 <wzsf1810@qq.com>
2024-08-14 03:13:28 +00:00
wind
506acbf906 3.3.2 修复开启短信拦截后导致发送失败的问题 2024-08-08 19:24:07 +08:00
wind
12ab72d6d1 Merge branch 'refs/heads/dev-3.0.x' 2024-08-08 19:19:56 +08:00
wind
b045df8be0 修复 开启拦截器后导致短信发送失败的问题 2024-08-08 19:19:28 +08:00
moat
d921679af4 fix: 增加短信前置签名编号配置项 2024-08-02 10:09:30 +08:00
风如歌
2dec57ad80
update README.md.
Signed-off-by: 风如歌 <wzsf1810@qq.com>
2024-08-02 01:21:12 +00:00
wind
4fbea6e6a1 升级3.3.0 2024-08-01 19:26:11 +08:00
wind
a8e2ddb420 升级3.3.1 修复错误PR导致的yml配置加载失败问题 2024-08-01 17:57:51 +08:00
wind
1a3abc4be3 修复错误PR导致的yml配置加载失败问题 2024-08-01 15:52:12 +08:00
moat
f0e018b855 add 联通一信通平台短信通道 2024-07-31 11:45:13 +08:00
wind
8ec8cf8bd6 拦截器相关优化,用户可以自定义添加拦截器并且实现相关能力 2024-07-29 11:19:36 +08:00
wind
5020771a7a 拦截器相关优化,用户可以自定义添加拦截器并且实现相关能力 2024-07-29 10:58:32 +08:00
Bleachtred
9623b28b31 !180 add 添加螺丝帽短信接入 并添加时间日期工具类
* add danmi短信接入
* update 补充配置案例
* add 添加SUBMAIL短信接入
* fix 腾讯云请求签名时间赋值错误
* update 提取短信异常发送结果
* add 添加螺丝帽短信接入 并添加时间日期工具类
2024-06-24 07:53:49 +00:00
wind
98fa90ee1f 修复错误的方法 2024-06-17 12:12:01 +08:00
风如歌
780caef8cd
!177 极光短信 创蓝短信
Merge pull request !177 from Bleachtred/dev_jg
2024-05-27 01:58:14 +00:00
风如歌
f5ce5b9be4
!176 sms4j-solon-plugin: 优化 SupplierConfig 处理
Merge pull request !176 from 西东/dev-3.0.x
2024-05-27 01:55:47 +00:00
bleachtred
a2a740a339 update 通用返回 2024-05-13 03:09:22 +08:00
bleachtred
f7c53c5f4e update 更新测试用例 2024-05-13 01:09:04 +08:00
bleachtred
4a5e342d15 update 优化 2024-05-13 00:53:33 +08:00
bleachtred
bea0c20e67 update 优化报错 2024-05-13 00:43:55 +08:00
bleachtred
fb198304a0 update 添加作者 2024-05-12 17:30:45 +08:00
bleachtred
f3327c33fa fix 创蓝 2024-05-12 17:27:19 +08:00
bleachtred
b5744a92ed fix 极光短信 2024-05-12 17:04:53 +08:00
风如歌
2b36985012
!173 解决分钟限制和每日限制存在的一个计数问题
Merge pull request !173 from kratos/dev-3.0.x
2024-05-10 06:46:27 +00:00
noear
154b7c3d3e 添加 sms4j-solon-plugin-example 模块 2024-05-08 12:24:53 +08:00
noear
525166c102 sms4j-solon-plugin: 优化 SupplierConfig 处理 2024-05-08 12:24:20 +08:00
kratos
2d1c50337b fix: 解决分钟限制和每日限制存在的一个计数问题
问题1:日最大限制未达到,但分钟计数达到限制时,此时短信发送异常,但日限制计数已经+1
此问题可能会导致分钟限制触发,分钟内多次请求发送短信,导致日限额超过,但实际未收到短信
问题2:日志打印,引起误导,应该是手误复制后忘记改动
2024-04-29 15:16:30 +08:00
风如歌
a5f7ba37a4
!171 add 百度智能云 sms
Merge pull request !171 from Bleachtred/dev_M
2024-04-29 04:00:03 +00:00
bleachtred
b19287cd58 add 百度智能云 sms 2024-04-25 20:25:44 +08:00
Bleachtred
81341fcf7c !170 add 中国移动 云MAS短信配置
* Merge branch 'dev-3.0.x' of https://gitee.com/Bleachtred/sms4j into dev-3.0.x
* add 中国移动 云MAS短信配置器
2024-04-24 02:27:16 +00:00
风如歌
9366a4d0f5
!169 常量使用大写
Merge pull request !169 from handy/dev-3.0.x
2024-04-23 02:16:46 +00:00
handy
7efa3319c7 常量使用大写 2024-04-22 10:36:06 +08:00
风如歌
ef44c1a2df
!168 格式化代码
Merge pull request !168 from 致远/dev-3.0.x
2024-04-19 06:27:09 +00:00
oddfar
d4b213c8f2 格式化代码 2024-04-18 22:19:58 +08:00
风如歌
f54331acc5
!167 修复网易云信短信群发时发送消息与模板id参数传参顺序错误问题
Merge pull request !167 from 骑着蜗牛码代码/fix-dev.3.x
2024-04-17 01:37:59 +00:00
cwg
6c29ca195d fix:修复网易云信短信群发时发送消息与模板id参数传参顺序错误问题 2024-04-15 14:09:26 +08:00
Bleachtred
fc4830b58c !165 update 简化map
* update 简化map
* Merge branch 'dev-3.0.x' of https://gitee.com/Bleachtred/sms4j into dev-3.0.x
* update 简化map
2024-04-08 10:09:30 +00:00
wind
067c08ab46 1.修复群发短信始终抛出异常的问题 2024-04-08 15:51:56 +08:00
风如歌
c2adc018e0
!159 添加 布丁云V2 短信Provider
Merge pull request !159 from NicholasLD/dev-3.0.x-budingv2
贡献者:NicholasLD
2024-04-03 07:06:19 +00:00
NicholasLD
157a19bc8e Merge branch 'dev-3.0.x' of gitee.com:dromara/sms4j into dev-3.0.x-budingv2
Signed-off-by: NicholasLD <nicholasld505@gmail.com>
2024-04-02 03:51:36 +00:00
wind
18fe81286a 1.升级hutool工具版本。
2.修复被异常执行器吞掉的Exception。
3.华为云无参数模板发送报错的问题。
2024-03-29 16:54:40 +08:00
wind
40e0460e15 添加一个校验手机号是否合法的接口,用户可以通过实现此接口进行手机号合法性校验 2024-03-28 15:40:44 +08:00
NicholasLD
ca13119acc 添加 布丁云短信V2 Provider 2024-03-21 02:52:20 +08:00
138 changed files with 6801 additions and 794 deletions

View File

@ -1,9 +1,9 @@
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sms4j v3.2.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sms4j v3.3.5</h1>
<h4 align="center" style="margin: 30px 0 30px; font-weight: bold;">sms4j -- 让发送短信变的更简单</h4>
<p align="center">
<a href="https://gitee.com/dromara/sms4j/stargazers"><img src="https://gitee.com/dromara/sms4j/badge/star.svg?theme=gvp"></a>
<a href="https://gitee.com/dromara/sms4j/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-green"></a>
<a href="https://gitee.com/dromara/sms4j"><img src="https://img.shields.io/badge/version-v3.2.0-blue"></a>
<a href="https://gitee.com/dromara/sms4j"><img src="https://img.shields.io/badge/version-v3.3.5-blue"></a>
</p>
<img src="/public/logo.png">
@ -16,8 +16,9 @@
如果我们的项目对你产生了帮助或者你觉得还算值得鼓励请用你发财的小手帮助点上一个start
[gitee](https://gitee.com/dromara/sms4j)
[github](https://github.com/dromara/sms4j)
[gitcode](https://gitcode.com/dromara/SMS4J)
#### [官方文档](http://wind.kim)
#### [官方文档](https://sms4j.com)
#### [JavaDoc文档](https://apidoc.gitee.com/dromara/sms4j/)
## 支持厂商一览
@ -35,6 +36,13 @@
- **[助通短信](https://www.ztinfo.cn/products/sms)**
- **[联麓短信](https://console.shlianlu.com/#/document/smsDoc)**
- **[鼎众短信](http://demoapi.321sms.com:8201/index.html)**
- **[创蓝短信](https://doc.chuanglan.com/document/HAQYSZKH9HT5Z50L)**
- **[极光短信](https://docs.jiguang.cn/jsms)**
- **[布丁云短信](https://console-docs.apipost.cn/preview/986c24caf79228ed/d3d8a6d5faf6ef51)**
- **[中国移动 云MAS短信](https://mas.10086.cn/)**
- **[螺丝帽短信](https://luosimao.com)**
- **[SUBMAIL短信](https://www.mysubmail.com/)**
- **[单米科技短信](https://www.danmi.com/)**
## 在SpringBoot环境集成
@ -145,6 +153,7 @@ sms:
## 贡献原则
- 我们原则上欢迎任何人为sms4j添加加瓦贡献代码
- 贡献代码应注释完备按照javaDoc标准对 类,方法,变量,参数,返回值等信息说明
- 如新增短信厂商需要同時以MD格式编写厂商的使用文档
- 新增的方法模块不能破坏原有结构和兼容性
- 如果我们关闭了你的issues或者pr请查看回复内容我们会在回复中做出解释

50
pom.xml
View File

@ -17,8 +17,9 @@
<module>sms4j-provider</module>
<module>sms4j-core</module>
<module>sms4j-spring-boot-starter</module>
<module>sms4j-solon-plugin</module>
<module>sms4j-spring-boot-example</module>
<module>sms4j-solon-plugin</module>
<module>sms4j-solon-plugin-example</module>
<module>sms4j-javase-plugin</module>
<module>sms4j-Email-plugin</module>
<module>sms4j-oa-plugin</module>
@ -52,15 +53,15 @@
</scm>
<properties>
<revision>3.2.0</revision>
<revision>3.3.5</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring.boot.version>2.7.18</spring.boot.version>
<solon.version>2.6.5</solon.version>
<solon.version>3.0.1</solon.version>
<redisson.version>3.17.0</redisson.version>
<jdcloud.version>1.3.3</jdcloud.version>
<hutool.version>5.8.25</hutool.version>
<hutool.version>5.8.28</hutool.version>
<xmlblend.version>2.3.0</xmlblend.version>
<activation.version>1.1.1</activation.version>
<mail.version>1.6.2</mail.version>
@ -242,20 +243,20 @@
</execution>
</executions>
</plugin>
<!-- &lt;!&ndash; GPG &ndash;&gt;-->
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-gpg-plugin</artifactId>-->
<!-- <version>1.6</version>-->
<!-- <executions>-->
<!-- <execution>-->
<!-- <phase>verify</phase>-->
<!-- <goals>-->
<!-- <goal>sign</goal>-->
<!-- </goals>-->
<!-- </execution>-->
<!-- </executions>-->
<!-- </plugin>-->
<!-- GPG -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<!--Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@ -313,6 +314,19 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.6.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>ossrh</publishingServerId>
<!-- 自动发布 -->
<autoPublish>true</autoPublish>
<!-- 等待发布 -->
<waitUntil>published</waitUntil>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -17,7 +17,7 @@ import java.util.Map;
* 2023/6/8 22:35
**/
public class MailFactory{
private static final Map<Object,MailSmtpConfig> configs = new HashMap<>();
private static final Map<Object,MailSmtpConfig> CONFIGS = new HashMap<>();
/**
* createMailClient
@ -27,7 +27,7 @@ public class MailFactory{
*/
public static MailClient createMailClient(Object key){
try {
return MailBuild.build(configs.get(key));
return MailBuild.build(CONFIGS.get(key));
} catch (MessagingException e) {
throw new MailException(e);
}
@ -43,7 +43,7 @@ public class MailFactory{
*/
public static MailClient createMailClient(Object key, Blacklist blacklist){
try {
return MailBuild.build(configs.get(key),blacklist);
return MailBuild.build(CONFIGS.get(key),blacklist);
} catch (MessagingException e) {
throw new MailException(e);
}
@ -57,7 +57,7 @@ public class MailFactory{
* @author :Wind
*/
public static void put(Object key, MailSmtpConfig config){
configs.put(key,config);
CONFIGS.put(key,config);
}
}

View File

@ -45,6 +45,7 @@ public interface SmsBlend {
/**
* sendMessage
* <p>说明发送固定消息模板多模板参数短信
*
* @param phone 接收短信的手机号
* @param messages 模板内容
* @author :Wind
@ -190,7 +191,7 @@ public interface SmsBlend {
* @param phones 需要加入黑名单的手机号数组
* @author :sh1yu
*/
default void batchJoinBlacklist(List<String > phones) {
default void batchJoinBlacklist(List<String> phones) {
}
/**
@ -200,6 +201,6 @@ public interface SmsBlend {
* @param phones 需要移除黑名单的手机号数组
* @author :sh1yu
*/
default void batchRemovalFromBlacklist(List<String > phones) {
default void batchRemovalFromBlacklist(List<String> phones) {
}
}

View File

@ -22,6 +22,9 @@ public interface CoreMethodProcessor extends SmsProcessor {
return param;
}
if (NumberOfParasmeters.THREE == NumberOfParasmeters.getNumberOfParasmetersEnum(parameterCount)) {
if (null == param[2]){
param[2] = new LinkedHashMap<>();
}
sendMessageByTemplatePreProcess((String)param[0],(String) param[1],(LinkedHashMap<String, String>)param[2]);
return param;
}
@ -32,6 +35,9 @@ public interface CoreMethodProcessor extends SmsProcessor {
return param;
}
if (NumberOfParasmeters.THREE == NumberOfParasmeters.getNumberOfParasmetersEnum(parameterCount)) {
if (null == param[2]){
param[2] = new LinkedHashMap<>();
}
massTextingByTemplatePreProcess((List<String>)param[0],(String)param[1],(LinkedHashMap<String, String>)param[2]);
return param;
}

View File

@ -1,12 +1,13 @@
package org.dromara.sms4j.api.proxy;
/**
* 排序接口
*
* 排序接口 用户拦截器排序并进行有序执行请注意排序值应大于等于0
* 拦截器的排序依照从小到大的规则进行排序
* @author sh1yu
* @since 2023/10/27 13:03
*/
public interface Order {
default public int getOrder(){
default int getOrder(){
return 999;
}
}

View File

@ -1,6 +1,9 @@
package org.dromara.sms4j.api.proxy;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import java.lang.reflect.Method;
/**
* 执行器接口
@ -9,15 +12,40 @@ import java.lang.reflect.Method;
* @since 2023/10/27 13:03
*/
public interface SmsProcessor extends Order {
/**
* preProcessor
* <p> 前置拦截方法 此方法将在短信发送方法之前进行执行但请勿在此方法中修改实例对象或者方法对象否则可能会导致并发问题
* @param method 方法对象
* @param source 实例对象
* @param param 参数列表
* @author :Wind
*/
default Object[] preProcessor(Method method, Object source, Object[] param) {
return null;
}
default Object postProcessor(Object result, Object[] param) {
/**
* postProcessor
* <p> 后置拦截方法 此方法执行在发送方法执行完毕之后获取到返回值之后
* @param result 返回值
* @param param 参数列表
* @author :Wind
*/
default Object postProcessor(SmsResponse result, Object[] param) {
return null;
}
default Object exceptionHandleProcessor(Method method, Object source, Object[] param,Exception exception) {
return null;
/**
* exceptionHandleProcessor
* <p> 异常拦截执行器在发送方法执行过程中发生异常将会通过此方法进行反馈
* <p> 请注意此方法一旦捕捉到相应异常并抛出新异常后会中断后续执行器的进行 如果在所有的异常执行器中均没有抛出异常则后续会进入后置方法执行器
* @param method 方法对象
* @param source 实例对象
* @param param 参数列表
* @param exception 异常
* @author :Wind
*/
default void exceptionHandleProcessor(Method method, Object source, Object[] param, Exception exception) throws RuntimeException {
throw new SmsBlendException(exception);
}
}

View File

@ -0,0 +1,27 @@
package org.dromara.sms4j.api.universal;
import lombok.Data;
import java.io.Serializable;
/**
* 短信代理配置类
*/
@Data
public class ProxyConfig implements Serializable {
/**
* 是否启用代理 默认不启用
*/
private Boolean enable = false;
/**
* 代理服务器地址
*/
private String host;
/**
* 代理服务器端口
*/
private Integer port;
}

View File

@ -22,4 +22,9 @@ public interface SupplierConfig {
*/
String getSupplier();
/**
* 获取代理配置
*
*/
ProxyConfig getProxy();
}

View File

@ -0,0 +1,52 @@
package org.dromara.sms4j.api.utils;
import org.dromara.sms4j.api.entity.SmsResponse;
public class SmsRespUtils {
private SmsRespUtils() {
} //私有构造防止实例化
public static SmsResponse error(){
return error("error no response", null);
}
public static SmsResponse error(String configId){
return error("error no response", configId);
}
public static SmsResponse error(String detailMessage, String configId){
return resp(detailMessage, false, configId);
}
public static SmsResponse success(){
return success(null);
}
public static SmsResponse success(Object data){
return success(data, null);
}
public static SmsResponse resp(Object data, boolean success){
return resp(data, success, null);
}
public static SmsResponse success(Object data, String configId){
return resp(data, true, configId);
}
public static SmsResponse resp(boolean success){
return success ? success() : error();
}
public static SmsResponse resp(boolean success, String configId){
return resp(null, success, configId);
}
public static SmsResponse resp(Object data, boolean success, String configId){
SmsResponse error = new SmsResponse();
error.setSuccess(success);
error.setData(data);
error.setConfigId(configId);
return error;
}
}

View File

@ -0,0 +1,23 @@
package org.dromara.sms4j.api.verify;
/**
* PhoneVerify
* <p> 实现校验手机号合规的接口
* @author :Wind
* 2024/3/28 14:15
**/
public interface PhoneVerify{
/**
* verifyPhone
* <p>用于校验手机号是否合理的规则方法可以尝试重写此方法以改变规则例如你可以选择使用正则表达式来进行
* 一系列更加精准和严格的校验此校验优先级最高会在黑名单和其他拦截之前执行
* 当此校验触发时候将会直接以异常形式进行抛出并终止后续向厂商请求的动作故而不会有返回值
* <p>当校验手机号合格时应返回 true 否则返回 false
* @param phone 被校验的手机号
* @author :Wind
*/
default boolean verifyPhone(String phone){
return phone.length() == 11;
}
}

View File

@ -11,27 +11,51 @@ public abstract class Constant {
/**
* 项目版本号
*/
public static final String VERSION = "V 3.0.1";
public static final String VERSION = "V 3.3.5";
/**
* 用于格式化鉴权头域,"Authorization"参数赋值
*/
public static final String HUAWEI_AUTH_HEADER_VALUE = "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\"";
/**
* 用于格式化鉴权头域,"X-WSSE"参数赋值
*/
public static final String HUAWEI_WSSE_HEADER_FORMAT = "UsernameToken Username=\"%s\",PasswordDigest=\"%s\",Nonce=\"%s\",Created=\"%s\"";
/**
* 华为云国内短信访问URI
*/
public static final String HUAWEI_REQUEST_URL = "/sms/batchSendSms/v1";
/**
* Content-Type
*/
public static final String FROM_URLENCODED = "application/x-www-form-urlencoded";
public static final String CONTENT_TYPE = "Content-Type";
public static final String ACCEPT = "application/json";
/**
* Authorization
*/
public static final String AUTHORIZATION = "Authorization";
/**
* Accept
*/
public static final String ACCEPT = "Accept";
/**
* x-www-form-urlencoded
*/
public static final String APPLICATION_FROM_URLENCODED = "application/x-www-form-urlencoded";
/**
* application/json
*/
public static final String APPLICATION_JSON = "application/json";
/**
* application/json; charset=utf-8
*/
public static final String APPLICATION_JSON_UTF8 = "application/json; charset=utf-8";
/**
@ -42,7 +66,7 @@ public abstract class Constant {
/**
* 云片短信国内短信请求地址
*/
public static final String YUNPIAN_URL = "https://sms.yunpian.com/v2";
public static final String YUNPIAN_URL = Constant.HTTPS_PREFIX + "sms.yunpian.com/v2";
/**
* https请求前缀

View File

@ -10,7 +10,7 @@ public abstract class SupplierConstant {
*/
public static final String ALIBABA = "alibaba";
/**
*
*
*/
public static final String CLOOPEN = "cloopen";
/**
@ -49,16 +49,52 @@ public abstract class SupplierConstant {
* 助通
*/
public static final String ZHUTONG = "zhutong";
/**
* 联麓
*/
public static final String LIANLU = "lianlu";
/**
* 鼎众
*/
public static final String DINGZHONG = "dingzhong";
/**
* 七牛云
*/
public static final String QINIU = "qiniu";
/**
* 创蓝
*/
public static final String CHUANGLAN = "chuanglan";
/**
* 极光
*/
public static final String JIGUANG = "jiguang";
/**
* 布丁云V2
*/
public static final String BUDING_V2 = "buding_v2";
/**
* 中国移动 云MAS
*/
public static final String MAS = "mas";
/**
* 百度云 sms
*/
public static final String BAIDU = "baidu";
/**
* 螺丝帽 sms
*/
public static final String LUO_SI_MAO = "luosimao";
/**
* SUBMAIL sms
*/
public static final String MY_SUBMAIL = "mysubmail";
/**
* danmi sms
*/
public static final String DAN_MI = "danmi";
/**
* 联通一信通 sms
*/
public static final String YIXINTONG = "yixintong";
}

View File

@ -1,4 +1,4 @@
package org.dromara.sms4j.comm.enumerate;
package org.dromara.sms4j.comm.enums;
import lombok.Getter;

View File

@ -22,4 +22,9 @@ public class SmsBlendException extends RuntimeException{
this.code = code;
this.requestId = requestId;
}
public SmsBlendException(Exception e){
super(e);
this.message = e.getMessage();
}
}

View File

@ -0,0 +1,215 @@
package org.dromara.sms4j.comm.utils;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* <p>类名: SmsDateUtils
* <p>说明 时间日期工具类
*
* @author :bleachtred
* 2024/6/21 23:59
**/
public class SmsDateUtils extends DateUtil {
private SmsDateUtils() {
}
/**
* 格林威治标准时间GMT或世界协调时间UTC
*/
private static final String GMT = "GMT";
/**
* 东八区
*/
private static final String GMT_8 = SmsDateUtils.GMT + "+8:00";
/**
* 天翼云七牛云时间格式
*/
private static final String PURE_DATE_UTC_PATTERN = "yyyyMMdd'T'HHmmss'Z'";
/**
* 获取格林威治标准时间GMT或世界协调时间UTC
* @return TimeZone
*/
public static TimeZone gmt(){
return getTimeZone(GMT);
}
/**
* 获取东八区时区
* @return TimeZone
*/
public static TimeZone gmt8(){
return getTimeZone(GMT_8);
}
/**
* 获取时区
* @param zoneId zoneId
* @return TimeZone
*/
public static TimeZone getTimeZone(String zoneId){
return TimeZone.getTimeZone(zoneId);
}
/**
* 获取SimpleDateFormat
* @param pattern 时间格式
* @return SimpleDateFormat
*/
public static SimpleDateFormat sdfGmt(String pattern){
return sdf(pattern, gmt());
}
/**
* 获取SimpleDateFormat
* @param pattern 时间格式
* @return SimpleDateFormat
*/
public static SimpleDateFormat sdfGmt8(String pattern){
return sdf(pattern, gmt8());
}
/**
* 获取SimpleDateFormat
* @param pattern 时间格式
* @param timeZone 时区
* @return 获取SimpleDateFormat
*/
public static SimpleDateFormat sdf(String pattern, TimeZone timeZone){
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
sdf.setTimeZone(timeZone);
return sdf;
}
/**
* 格式化时间
* @param date 时间
* @param pattern 时间格式
* @return String
*/
public static String formatGmtDateToStr(Date date, String pattern){
SimpleDateFormat sdf = sdfGmt(pattern);
return sdf.format(date);
}
/**
* 格式化时间
* @param date 时间
* @param pattern 时间格式
* @return String
*/
public static String formatGmt8DateToStr(Date date, String pattern){
SimpleDateFormat sdf = sdfGmt8(pattern);
return sdf.format(date);
}
/**
* 格式化时间
* @param date 时间
* @param pattern 时间格式
* @param timeZone 时区
* @return String
*/
public static String formatDateToStr(Date date, String pattern, TimeZone timeZone){
SimpleDateFormat sdf = sdf(pattern, timeZone);
return sdf.format(date);
}
/**
* 日期格式yyyy-MM-dd'T'HH:mm:ss'Z'
* @param date 时间
* @return 时间字符串
*/
public static String utcGmt(Date date){
return formatGmtDateToStr(date, DatePattern.UTC_PATTERN);
}
/**
* 日期格式yyyy-MM-dd'T'HH:mm:ss'Z'
* @param date 时间
* @return 时间字符串
*/
public static String utcGmt8(Date date){
return formatGmt8DateToStr(date, DatePattern.UTC_PATTERN);
}
/**
* 日期格式yyyyMMdd
* @param date 时间
* @return 时间字符串
*/
public static String pureDateGmt(Date date){
return formatGmtDateToStr(date, DatePattern.PURE_DATE_PATTERN);
}
/**
* 日期格式yyyyMMdd
* @param date 时间
* @return 时间字符串
*/
public static String pureDateGmt8(Date date){
return formatGmt8DateToStr(date, DatePattern.PURE_DATE_PATTERN);
}
/**
* 天翼云七牛云时间格式yyyyMMdd'T'HHmmss'Z'
* @param date 时间
* @return 时间字符串
*/
public static String pureDateUtcGmt(Date date){
return formatGmtDateToStr(date, PURE_DATE_UTC_PATTERN);
}
/**
* 天翼云七牛云时间格式yyyyMMdd'T'HHmmss'Z'
* @param date 时间
* @return 时间字符串
*/
public static String pureDateUtcGmt8(Date date){
return formatGmt8DateToStr(date, PURE_DATE_UTC_PATTERN);
}
/**
* 日期格式yyyy-MM-dd
* @param date 时间
* @return 时间字符串
*/
public static String normDateGmt(Date date){
return formatGmtDateToStr(date, DatePattern.NORM_DATE_PATTERN);
}
/**
* 日期格式yyyy-MM-dd
* @param date 时间
* @return 时间字符串
*/
public static String normDateGmt8(Date date){
return formatGmt8DateToStr(date, DatePattern.NORM_DATE_PATTERN);
}
/**
* 日期格式yyyy-MM-dd HH:mm:ss
* @param date 时间
* @return 时间字符串
*/
public static String normDatetimeGmt(Date date){
return formatGmtDateToStr(date, DatePattern.NORM_DATETIME_PATTERN);
}
/**
* 日期格式yyyy-MM-dd HH:mm:ss
* @param date 时间
* @return 时间字符串
*/
public static String normDatetimeGmt8(Date date){
return formatGmt8DateToStr(date, DatePattern.NORM_DATETIME_PATTERN);
}
}

View File

@ -1,9 +1,11 @@
package org.dromara.sms4j.comm.utils;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.Method;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.dromara.sms4j.comm.exception.SmsBlendException;
@ -12,15 +14,108 @@ import java.util.Map;
public class SmsHttpUtils {
/**
* 是否启用代理 默认不启用
*/
private final Boolean enable;
/**
* 代理服务器地址
*/
private final String host;
/**
* 代理服务器端口
*/
private final Integer port;
// 无代理单例饿汉式加载
private static final SmsHttpUtils NON_PROXY_INSTANCE = new SmsHttpUtils();
// 代理单例双重校验锁延迟加载
private static volatile SmsHttpUtils PROXY_INSTANCE;
// 无代理构造方法
private SmsHttpUtils() {
this.enable = false;
this.host = null;
this.port = null;
}
private static class SmsHttpHolder {
private static final SmsHttpUtils INSTANCE = new SmsHttpUtils();
// 代理构造方法
private SmsHttpUtils(String host, Integer port) {
this.enable = true;
this.host = host;
this.port = port;
}
/**
* 获取无代理单例
*/
public static SmsHttpUtils instance() {
return SmsHttpHolder.INSTANCE;
return NON_PROXY_INSTANCE;
}
/**
* 获取代理单例线程安全 + 参数校验
*/
public static SmsHttpUtils instance(String host, Integer port) {
if (PROXY_INSTANCE == null) {
synchronized (SmsHttpUtils.class) {
if (PROXY_INSTANCE == null) {
validateProxyParams(host, port);
PROXY_INSTANCE = new SmsHttpUtils(host, port);
}
}
} else {
// 二次调用时校验参数一致性
if (!PROXY_INSTANCE.host.equals(host) || !PROXY_INSTANCE.port.equals(port)) {
throw new IllegalStateException("Proxy parameters cannot be modified after initialization");
}
}
return PROXY_INSTANCE;
}
// 代理参数校验
private static void validateProxyParams(String host, Integer port) {
if (StrUtil.isBlank(host) || port == null || port <= 0) {
throw new IllegalArgumentException("Invalid proxy host or port");
}
}
/**
* 配置请求 是否走代理
* @param url 请求地址
* @return HttpRequest
*/
private HttpRequest request(String url){
HttpRequest request = HttpRequest.of(url);
if (enable){
request.setHttpProxy(host, port);
}
return request;
}
/**
* 构造post请求
* @param url 请求地址
* @return HttpRequest
*/
private HttpRequest post(String url){
HttpRequest post = request(url);
post.setMethod(Method.POST);
return post;
}
/**
* 构造get请求
* @param url 请求地址
* @return HttpRequest
*/
private HttpRequest get(String url){
HttpRequest get = request(url);
get.setMethod(Method.GET);
return get;
}
/**
@ -32,7 +127,7 @@ public class SmsHttpUtils {
* @return 返回体
*/
public JSONObject postJson(String url, Map<String, String> headers, String body) {
try (HttpResponse response = HttpRequest.post(url)
try (HttpResponse response = post(url)
.addHeaders(headers)
.body(body)
.execute()) {
@ -63,7 +158,7 @@ public class SmsHttpUtils {
* @return 返回体
*/
public JSONObject postFrom(String url, Map<String, String> headers, Map<String, Object> body) {
try (HttpResponse response = HttpRequest.post(url)
try (HttpResponse response = post(url)
.addHeaders(headers)
.form(body)
.execute()) {
@ -73,6 +168,27 @@ public class SmsHttpUtils {
}
}
/**
* 发送post form 请求
*
* @param url 请求地址
* @param headers 请求头
* @param body 请求体(map格式请求体)
* @param username 用户名
* @param password 密码
* @return 返回体
*/
public JSONObject postBasicFrom(String url, Map<String, String> headers, String username, String password, Map<String, Object> body) {
try (HttpResponse response = post(url)
.addHeaders(headers)
.basicAuth(username, password)
.form(body)
.execute()) {
return JSONUtil.parseObj(response.body());
} catch (Exception e) {
throw new SmsBlendException(e.getMessage());
}
}
/**
* 发送post url 参数拼装url传输
@ -84,7 +200,7 @@ public class SmsHttpUtils {
*/
public JSONObject postUrl(String url, Map<String, String> headers, Map<String, Object> params) {
String urlWithParams = url + "?" + URLUtil.buildQuery(params, null);
try (HttpResponse response = HttpRequest.post(urlWithParams)
try (HttpResponse response = post(urlWithParams)
.addHeaders(headers)
.execute()) {
return JSONUtil.parseObj(response.body());
@ -93,6 +209,37 @@ public class SmsHttpUtils {
}
}
/**
* 发送get
*
* @param url 请求地址
* @return 返回体
*/
public JSONObject getBasic(String url, String username, String password) {
try (HttpResponse response = get(url)
.basicAuth(username, password)
.execute()) {
return JSONUtil.parseObj(response.body());
} catch (Exception e) {
throw new SmsBlendException(e.getMessage());
}
}
/**
* 发送get
*
* @param url 请求地址
* @return 返回体
*/
public JSONObject getUrl(String url) {
try (HttpResponse response = get(url)
.execute()) {
return JSONUtil.parseObj(response.body());
} catch (Exception e) {
throw new SmsBlendException(e.getMessage());
}
}
/**
* 线程睡眠
*

View File

@ -2,15 +2,22 @@ package org.dromara.sms4j.comm.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* @author wind
@ -113,18 +120,38 @@ public class SmsUtils {
* @param list 要转换的list
* @author :Wind
*/
public static String listToString(List<String> list) {
return CollUtil.join(list, ",");
public static String joinComma(List<String> list) {
return CollUtil.join(list, StrUtil.COMMA);
}
/**
* conjunction 为分隔符将集合转换为字符串
* 切分字符串
*
* @param list 集合
* @param str 被切分的字符串
* @return 分割后的数据列表
*/
public static List<String> splitTrimComma(String str) {
return StrUtil.splitTrim(str, StrUtil.COMMA);
}
/**
* 将手机号码 添加+86中国的电话国际区号前缀
*
* @param phones 手机号码集合
* @return 结果字符串
*/
public static String arrayToString(List<String> list) {
return CollUtil.join(list, ",", str -> StrUtil.addPrefixIfNot(str, "+86"));
public static String addCodePrefixIfNot(List<String> phones) {
return CollUtil.join(phones, StrUtil.COMMA, SmsUtils::addCodePrefixIfNot);
}
/**
* 将手机号码 添加+86电话区号前缀
*
* @param phone 手机号码
* @return 结果字符串
*/
public static String addCodePrefixIfNot(String phone) {
return StrUtil.addPrefixIfNot(phone, "+86");
}
/**
@ -133,10 +160,10 @@ public class SmsUtils {
* @param list 集合
* @return 结果字符串
*/
public static String[] listToArray(List<String> list) {
public static String[] addCodePrefixIfNotToArray(List<String> list) {
List<String> toStr = new ArrayList<>();
for (String s : list) {
toStr.add(StrUtil.addPrefixIfNot(s, "+86"));
toStr.add(addCodePrefixIfNot(s));
}
return toStr.toArray(new String[list.size()]);
}
@ -144,20 +171,20 @@ public class SmsUtils {
/**
* 将Map中所有key的分隔符转换为新的分隔符
* @param map map对象
* @param seperator 旧分隔符
* @param newSeperator 新分隔符
* @param separator 旧分隔符
* @param newSeparator 新分隔符
*/
public static void replaceKeysSeperator(Map<String, Object> map, String seperator, String newSeperator) {
public static void replaceKeysSeparator(Map<String, Object> map, String separator, String newSeparator) {
if(CollUtil.isEmpty(map)) {
return;
}
List<String> keySet = new ArrayList<>(map.keySet());
for(String key : keySet) {
if(StrUtil.isEmpty(key) || !key.contains(seperator)) {
if(StrUtil.isEmpty(key) || !key.contains(separator)) {
continue;
}
String value = String.valueOf(map.get(key));
String newKey = key.replaceAll(seperator, newSeperator);
String newKey = key.replaceAll(separator, newSeparator);
map.putIfAbsent(newKey, value);
map.remove(key);
}
@ -172,4 +199,76 @@ public class SmsUtils {
}
}
public static LinkedHashMap<String, String> buildMessageByAmpersand(String message) {
if (isEmpty(message)){
return new LinkedHashMap<>();
}
String[] split = message.split("&");
LinkedHashMap<String, String> map = new LinkedHashMap<>(split.length);
for (int i = 0; i < split.length; i++) {
map.put(String.valueOf(i), split[i]);
}
return map;
}
/**
* 将任意类型集合转成想要的数组
* @param list 需要转换的集合
* @param predicate 过滤条件
* @param mapper 对此流的元素执行函数
* @param array 想要的数组
* @return 数组
* @param <T> 集合泛型
* @param <E> 想要的数组类型
*/
public static <E, T> E[] toArray(Collection<T> list, Predicate<T> predicate, Function<? super T, ? extends E> mapper, E[] array) {
if (isEmpty(list)) {
return array.clone();
}
return list.stream().filter(predicate).map(mapper).toArray(size -> array.clone());
}
/**
* 将map的value转成数组
* @param map Map
* @return 数组
*/
public static String[] toArray(Map<String, String> map){
if (isEmpty(map)) {
return new String[0];
}
return toArray(map.values(), SmsUtils::isNotEmpty, s -> s, new String[map.size()]);
}
/**
* 将所有提交的参数升序排列并排除部分key字段后将key与value用"="连接起来 组成"key=value" + "&"连接符+ "key=value" 的方式
* @param params 参数Map
* @param excludes 排除的key
* @return String
*/
public static String sortedParamsAsc(Map<String, Object> params, String... excludes) {
if (MapUtil.isEmpty(params)){
return StrUtil.EMPTY;
}
List<String> keys = new ArrayList<>(params.keySet());
if (CollUtil.isEmpty(keys)){
return StrUtil.EMPTY;
}
if (ArrayUtil.isNotEmpty(excludes)){
ArrayList<String> excludeKeys = CollUtil.toList(excludes);
keys.removeIf(key -> excludeKeys.stream().anyMatch(exclude -> exclude.equals(key)));
if (CollUtil.isEmpty(keys)){
return StrUtil.EMPTY;
}
}
Collections.sort(keys);
StringBuilder sb = new StringBuilder();
for (String key : keys) {
sb.append(key).append("=").append(Convert.toStr(params.get(key))).append("&");
}
if (sb.length() > 0) {
sb.setLength(sb.length() - 1); // Remove the last '&'
}
return sb.toString();
}
}

View File

@ -20,7 +20,7 @@ public class SmsLoad {
// 服务器列表每个服务器有一个权重和当前权重
private final List<LoadServer> LoadServers = new ArrayList<>();
private static final SmsLoad smsLoad = new SmsLoad();
private static final SmsLoad SMS_LOAD = new SmsLoad();
private SmsLoad() {
}
@ -100,15 +100,15 @@ public class SmsLoad {
public static void starConfig(SmsBlend smsBlend, SupplierConfig supplierConfig) {
Map<String, Object> supplierConfigMap = BeanUtil.beanToMap(supplierConfig);
Object weight = supplierConfigMap.getOrDefault("weight", 1);
smsLoad.addLoadServer(smsBlend, Integer.parseInt(weight.toString()));
SMS_LOAD.addLoadServer(smsBlend, Integer.parseInt(weight.toString()));
}
public static void starConfig(SmsBlend smsBlend,Integer weight) {
smsLoad.addLoadServer(smsBlend,weight);
SMS_LOAD.addLoadServer(smsBlend,weight);
}
public static SmsLoad getBeanLoad() {
return smsLoad;
return SMS_LOAD;
}
}

View File

@ -2,11 +2,12 @@ package org.dromara.sms4j.core.proxy;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.proxy.SmsProcessor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.Objects;
/**
@ -18,12 +19,10 @@ import java.util.LinkedList;
@Slf4j
public class SmsInvocationHandler implements InvocationHandler {
private final SmsBlend smsBlend;
private final LinkedList<SmsProcessor> processors;
public SmsInvocationHandler(SmsBlend smsBlend, LinkedList<SmsProcessor> processors) {
public SmsInvocationHandler(SmsBlend smsBlend) {
this.smsBlend = smsBlend;
this.processors = processors;
}
@Override
@ -38,30 +37,40 @@ public class SmsInvocationHandler implements InvocationHandler {
doErrorHandleProcess(smsBlend, method, objects,e);
}
//后置执行器
doPostrocess(smsBlend, method, objects, result);
return result;
return doPostrocess(smsBlend, method, objects, result);
}
/**
* 前置执行器
* */
public Object[] doPreProcess(Object o, Method method, Object[] objects) {
for (SmsProcessor processor : processors) {
for (SmsProcessor processor : SmsProxyFactory.getProcessors()) {
objects = processor.preProcessor(method, o, objects);
}
return objects;
}
public void doErrorHandleProcess(Object o, Method method, Object[] objects,Exception e) {
for (SmsProcessor processor : processors) {
/**
* 异常执行器
* */
public void doErrorHandleProcess(Object o, Method method, Object[] objects,Exception e) throws RuntimeException{
for (SmsProcessor processor : SmsProxyFactory.getProcessors()) {
processor.exceptionHandleProcessor(method, o, objects,e);
}
}
/**
* 后置执行器
* */
public Object doPostrocess(Object o, Method method, Object[] objects, Object result) {
for (SmsProcessor processor : processors) {
Object overrideResult = processor.postProcessor(result, objects);
for (SmsProcessor processor : SmsProxyFactory.getProcessors()) {
if (Objects.nonNull(result) && result instanceof SmsResponse){
Object overrideResult = processor.postProcessor((SmsResponse)result, objects);
if (overrideResult != null) {
return overrideResult;
}
}
}
return result;
}
}

View File

@ -10,6 +10,8 @@ import org.dromara.sms4j.api.proxy.SuppotFilter;
import org.dromara.sms4j.api.proxy.aware.SmsBlendConfigAware;
import org.dromara.sms4j.api.proxy.aware.SmsConfigAware;
import org.dromara.sms4j.api.proxy.aware.SmsDaoAware;
import org.dromara.sms4j.api.verify.PhoneVerify;
import org.dromara.sms4j.core.proxy.processor.CoreMethodParamValidateProcessor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@ -17,32 +19,65 @@ import java.lang.reflect.Proxy;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* SmsBlend代理工厂
*
* 代理工厂
* 可用于增加和移除拦截器
* @author sh1yu
* @since 2023/10/27 13:03
*/
@Slf4j
public abstract class SmsProxyFactory {
private static final LinkedList<SmsProcessor> processors = new LinkedList<>();
private static final LinkedList<SmsProcessor> PROCESSORS = new LinkedList<>();
public static SmsBlend getProxySmsBlend(SmsBlend smsBlend) {
LinkedList<SmsProcessor> ownerProcessors = processors.stream().filter(processor -> !shouldSkipProcess(processor,smsBlend)).collect(Collectors.toCollection(LinkedList::new));
return (SmsBlend) Proxy.newProxyInstance(smsBlend.getClass().getClassLoader(), new Class[]{SmsBlend.class}, new SmsInvocationHandler(smsBlend, ownerProcessors));
// LinkedList<SmsProcessor> ownerProcessors = PROCESSORS.stream().filter(processor -> !shouldSkipProcess(processor,smsBlend)).collect(Collectors.toCollection(LinkedList::new));
return (SmsBlend) Proxy.newProxyInstance(smsBlend.getClass().getClassLoader(), new Class[]{SmsBlend.class}, new SmsInvocationHandler(smsBlend));
}
/**
* 增加拦截器
*/
public static void addProcessor(SmsProcessor processor) {
public static void addPreProcessor(SmsProcessor processor) {
//校验拦截器是否正确
processorValidate(processor);
awareTransfer(processor);
processors.add(processor);
processors.sort(Comparator.comparingInt(Order::getOrder));
PROCESSORS.add(processor);
PROCESSORS.sort(Comparator.comparingInt(Order::getOrder));
}
/**
* removeProcessor
* <p> 移除拦截器
* @param processor 拦截器对象
* @author :Wind
*/
public static void removePreProcessor(SmsProcessor processor) {
PROCESSORS.remove(processor);
}
/**
* getProcessors
* <p> 获取全部拦截器
* @author :Wind
*/
public static LinkedList<SmsProcessor> getProcessors() {
return PROCESSORS;
}
/**
* setPhoneProcessor
* <p> 添加手机号验证器手机号验证器只且只能存在一个如果重复置入则会替换先前的验证器
* 如果在验证器之后还需进行额外操作请参考使用前置拦截器进行处理
* @param phoneVerify 手机号验证器
* @author :Wind
*/
public static void setPhoneProcessor(PhoneVerify phoneVerify) {
PROCESSORS.forEach(processor -> {
if (processor instanceof CoreMethodParamValidateProcessor){
((CoreMethodParamValidateProcessor) processor).setPhoneVerify(phoneVerify);
}
});
}
/*
@ -101,7 +136,7 @@ public abstract class SmsProxyFactory {
Class<?> clazz = Class.forName(className);
Method getSmsDao = clazz.getMethod("getSmsDao", null);
SmsDao smsDao = (SmsDao) getSmsDao.invoke(null, null);
log.info("{}:加载SmsDao成功使用{}", frameworkName,smsDao.getClass().getName());
log.debug("{}:加载SmsDao成功使用{}", frameworkName,smsDao.getClass().getName());
return smsDao;
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.debug("{}:尝试其他框架加载......", frameworkName);

View File

@ -55,7 +55,7 @@ public class BlackListProcessor implements CoreMethodProcessor, SmsDaoAware {
}
for (String phone : phones) {
if (blackList.stream().anyMatch(black -> black.replace("-","").equals(phone))) {
throw new SmsBlendException("The phone:", phone + " hit global blacklist");
throw new SmsBlendException(String.format("The phone: %s hit global blacklist", phone));
}
}
}

View File

@ -1,8 +1,10 @@
package org.dromara.sms4j.core.proxy.processor;
import cn.hutool.core.util.StrUtil;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.proxy.CoreMethodProcessor;
import org.dromara.sms4j.api.verify.PhoneVerify;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import java.util.LinkedHashMap;
@ -16,11 +18,34 @@ import java.util.Objects;
* @author sh1yu
* @since 2023/10/27 13:03
*/
@Setter
@Slf4j
public class CoreMethodParamValidateProcessor implements CoreMethodProcessor {
/**
* -- SETTER --
* 设置 phoneVerify
*/
private PhoneVerify phoneVerify;
public CoreMethodParamValidateProcessor(PhoneVerify phoneVerify) {
this.phoneVerify = phoneVerify;
}
/**
* setter
* <p> 用于设置手机号验证器可以在通过重写验证器规则来实现自己的验证器逻辑
* <p> 默认验证规则仅仅验证手机号是否为空和手机号是否为11位
* @param phoneVerify 手机号验证器
* @author :Wind
*/
public void setPhoneVerify(PhoneVerify phoneVerify) {
this.phoneVerify = phoneVerify;
}
@Override
public int getOrder() {
return -1;
return -100;
}
@Override
@ -63,6 +88,9 @@ public class CoreMethodParamValidateProcessor implements CoreMethodProcessor {
if (StrUtil.isBlank(phone)) {
throw new SmsBlendException("cant send message to null!");
}
if (phoneVerify != null && !phoneVerify.verifyPhone(phone)){
throw new SmsBlendException("The mobile phone number format is invalid!");
}
}
public void validatePhones(List<String> phones) {
@ -71,10 +99,11 @@ public class CoreMethodParamValidateProcessor implements CoreMethodProcessor {
}
for (String phone : phones) {
if (StrUtil.isNotBlank(phone)) {
return;
if (phoneVerify != null && !phoneVerify.verifyPhone(phone)){
throw new SmsBlendException("The mobile phone number format is invalid!");
}
}
}
throw new SmsBlendException("cant send message to null!");
}
public void validateMessages(String templateId, LinkedHashMap<String, String> messages) {

View File

@ -10,6 +10,8 @@ import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.provider.config.SmsConfig;
import org.dromara.sms4j.provider.factory.BeanFactory;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@ -26,7 +28,6 @@ import java.util.Objects;
@Slf4j
public class RestrictedProcessor implements CoreMethodProcessor, SmsDaoAware {
static Long minTimer = 60 * 1000L;
static Long accTimer = 24 * 60 * 60 * 1000L;
private static final String REDIS_KEY = "sms:restricted:";
/**
@ -59,44 +60,69 @@ public class RestrictedProcessor implements CoreMethodProcessor, SmsDaoAware {
doRestricted(phones);
}
private long calculateExpiryTime() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime tomorrowMidnight = now.toLocalDate().plusDays(1).atStartOfDay();
Duration duration = Duration.between(now, tomorrowMidnight);
return duration.getSeconds();
}
public void doRestricted(List<String> phones) {
if (Objects.isNull(smsDao)) {
throw new SmsBlendException("The smsDao tool could not be found");
}
SmsConfig config = BeanFactory.getSmsConfig();
// 如果未开始限制则不做处理
if (!config.getRestricted()){
return;
}
// 每日最大发送量
Integer accountMax = config.getAccountMax();
// 每分钟最大发送量
Integer minuteMax = config.getMinuteMax();
// 配置了每日最大发送量
boolean dailyMaxLimitExists = SmsUtils.isNotEmpty(accountMax);
// 配置了每分钟最大发送量
boolean perMinuteLimitExists = SmsUtils.isNotEmpty(minuteMax);
// 如果未开启限制或未配置任何限制发送量不做处理
boolean isNoProcessing = !config.getRestricted() || (!dailyMaxLimitExists && !perMinuteLimitExists);
if (isNoProcessing) {
return;
}
for (String phone : phones) {
// 分钟发送量缓存key
String minuteMaxKey = REDIS_KEY + phone;
// 天发送量缓存key
String accountMaxKey = minuteMaxKey.concat("max");
// 是否配置了每日限制
if (SmsUtils.isNotEmpty(accountMax)) {
Integer i = (Integer) smsDao.get(REDIS_KEY + phone + "max");
if (SmsUtils.isEmpty(i)) {
smsDao.set(REDIS_KEY + phone + "max", 1, accTimer / 1000);
} else if (i >= accountMax) {
log.info("The phone: {},number of short messages reached the maximum today", phone);
throw new SmsBlendException("The phone: {},number of short messages reached the maximum today", phone);
if (dailyMaxLimitExists) {
Integer dailyCount = (Integer) smsDao.get(accountMaxKey);
if (SmsUtils.isEmpty(dailyCount)) {
smsDao.set(accountMaxKey, 1, calculateExpiryTime());
} else if (dailyCount >= accountMax) {
log.info("The phone: {} number of short messages reached the maximum today", phone);
throw new SmsBlendException(String.format("The phone: %s number of short messages reached the maximum today", phone));
} else {
smsDao.set(REDIS_KEY + phone + "max", i + 1, accTimer / 1000);
smsDao.set(accountMaxKey, dailyCount + 1, calculateExpiryTime());
}
}
// 是否配置了每分钟最大限制
if (SmsUtils.isNotEmpty(minuteMax)) {
Integer o = (Integer) smsDao.get(REDIS_KEY + phone);
if (SmsUtils.isNotEmpty(o)) {
if (o < minuteMax) {
smsDao.set(REDIS_KEY + phone, o + 1, minTimer / 1000);
if (perMinuteLimitExists) {
Integer minuteCount = (Integer) smsDao.get(REDIS_KEY + phone);
if (SmsUtils.isNotEmpty(minuteCount)) {
if (minuteCount < minuteMax) {
smsDao.set(minuteMaxKey, minuteCount + 1, minTimer / 1000);
} else {
log.info("The phone: {},number of short messages reached the maximum today", phone);
throw new SmsBlendException("The phone: {} Text messages are sent too often", phone);
//如果能走到这里且存在每日限制说明每日限制已经计数这里将之前的计数减一次
if (dailyMaxLimitExists) {
Integer dailyCount = (Integer) smsDao.get(accountMaxKey);
if (dailyCount > 1) {
smsDao.set(accountMaxKey, dailyCount - 1, calculateExpiryTime());
} else {
smsDao.remove(accountMaxKey);
}
}
log.info("The phone: {} text messages are sent too often", phone);
throw new SmsBlendException(String.format("The phone: %s text messages are sent too often", phone));
}
} else {
smsDao.set(REDIS_KEY + phone, 1, minTimer / 1000);
smsDao.set(minuteMaxKey, 1, minTimer / 1000);
}
}
}

View File

@ -1,72 +0,0 @@
package org.dromara.sms4j.core.proxy.processor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.dao.SmsDao;
import org.dromara.sms4j.api.proxy.SmsProcessor;
import org.dromara.sms4j.api.proxy.aware.SmsBlendConfigAware;
import org.dromara.sms4j.api.proxy.aware.SmsDaoAware;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import java.lang.reflect.Method;
import java.util.Map;
/**
* 短信发送渠道级上限前置拦截执行器
*
* @author sh1yu
* @since 2023/10/27 13:03
*/
@Setter
@Slf4j
public class SingleBlendRestrictedProcessor implements SmsProcessor, SmsDaoAware, SmsBlendConfigAware {
private static final String REDIS_KEY = "sms:restricted:";
/**
* 缓存实例
*/
private SmsDao smsDao;
Map smsBlendsConfig;
@Override
public int getOrder() {
return 2;
}
@Override
public Object[] preProcessor(Method method, Object source, Object[] param) {
String name = method.getName();
if (!"sendMessage".equals(name) && !"massText".equals(name)) {
return param;
}
SmsBlend smsBlend = (SmsBlend) source;
String configId = smsBlend.getConfigId();
Map targetConfig = (Map) smsBlendsConfig.get(configId);
Object maximumObj = targetConfig.get("maximum");
if (SmsUtils.isEmpty(maximumObj)) {
return param;
}
int maximum = 0;
try{
maximum = Integer.parseInt(String.valueOf(maximumObj)) ;
}catch (Exception e){
log.error("获取厂商级发送上限参数错误!请检查!");
throw new IllegalArgumentException("获取厂商级发送上限参数错误");
}
Integer i = (Integer) smsDao.get(REDIS_KEY + configId + "maximum");
if (SmsUtils.isEmpty(i)) {
smsDao.set(REDIS_KEY + configId + "maximum", 1);
} else if (i >= maximum) {
log.info("The channel: {},messages reached the maximum", configId);
throw new SmsBlendException("The channel: {},messages reached the maximum", configId);
} else {
smsDao.set(REDIS_KEY + configId + "maximum", i + 1);
}
return param;
}
}

View File

@ -0,0 +1,6 @@
/**
* <p> 邮件插件api模块
* @author :Wind
* 2024/10/23 10:58
**/
package org.dromara.email.jakarta.api;

View File

@ -3,6 +3,7 @@ package org.dromara.email.jakarta.comm.entity;
import lombok.Getter;
import org.dromara.email.jakarta.comm.utils.ReflectUtil;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
@ -27,6 +28,12 @@ public class MailMessage {
/** html模板文件的输入流可来自任意可读取位置*/
private InputStream htmlInputStream;
/** html内容可以存在模板变量*/
private String htmlContent;
/** html 模板文件的File对象*/
private File htmlFile;
/** html 模板参数*/
private Map<String,String> htmlValues;
@ -141,6 +148,18 @@ public class MailMessage {
return this;
}
/** html模板文件的File对象*/
public MailsBuilder html(File htmlFile){
mailMessage.htmlFile = htmlFile;
return this;
}
/** html内容直接输入*/
public MailsBuilder htmlContent(String htmlContent){
mailMessage.htmlContent = htmlContent;
return this;
}
/** html 模板参数*/
public MailsBuilder htmlValues(String key, String value){
if (mailMessage.htmlValues == null){

View File

@ -0,0 +1,6 @@
/**
* <p> 邮件插件通用模块
* @author :Wind
* 2024/10/23 10:58
**/
package org.dromara.email.jakarta.comm;

View File

@ -32,8 +32,8 @@ public final class HtmlUtil {
* @param name 模板文件名
* @author :Wind
*/
public static List<String> readHtml(String name) throws MailException {
try (InputStream is = HtmlUtil.class.getResourceAsStream("/template/" + name)) {
public static List<String> readHtml(String name,Class<?> clazz) throws MailException {
try (InputStream is = clazz.getResourceAsStream("/template/" + name)) {
return readHtml(is);
} catch (IOException e) {
throw new MailException(e);
@ -75,6 +75,12 @@ public final class HtmlUtil {
}
} catch (IOException e) {
throw new MailException(e);
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}

View File

@ -17,7 +17,7 @@ import java.util.Map;
* 2023/6/8 22:35
**/
public class MailFactory{
private static final Map<Object,MailSmtpConfig> configs = new HashMap<>();
private static final Map<Object,MailSmtpConfig> CONFIGS = new HashMap<>();
/**
* createMailClient
@ -27,7 +27,7 @@ public class MailFactory{
*/
public static MailClient createMailClient(Object key){
try {
return MailBuild.build(configs.get(key));
return MailBuild.build(CONFIGS.get(key));
} catch (MessagingException e) {
throw new MailException(e);
}
@ -43,7 +43,7 @@ public class MailFactory{
*/
public static MailClient createMailClient(Object key, Blacklist blacklist){
try {
return MailBuild.build(configs.get(key),blacklist);
return MailBuild.build(CONFIGS.get(key),blacklist);
} catch (MessagingException e) {
throw new MailException(e);
}
@ -57,7 +57,7 @@ public class MailFactory{
* @author :Wind
*/
public static void put(Object key, MailSmtpConfig config){
configs.put(key,config);
CONFIGS.put(key,config);
}
}

View File

@ -26,6 +26,7 @@ import org.dromara.email.jakarta.comm.utils.ZipUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -50,8 +51,14 @@ public class MailService implements MailClient {
if (mailMessage.getHtmlInputStream() != null) {
html = HtmlUtil.readHtml(mailMessage.getHtmlInputStream());
}
if (StrUtil.isNotBlank(mailMessage.getHtmlPath())){
html = HtmlUtil.readHtml(mailMessage.getHtmlPath());
if (StrUtil.isNotBlank(mailMessage.getHtmlPath())) {
html = HtmlUtil.readHtml(mailMessage.getHtmlPath(), MailService.class);
}
if (mailMessage.getHtmlFile() != null) {
html = HtmlUtil.readHtml(mailMessage.getHtmlFile());
}
if (StrUtil.isNotBlank(mailMessage.getHtmlContent())) {
html = Arrays.asList(mailMessage.getHtmlContent().split("\n"));
}
send(mailMessage.getMailAddress(),
mailMessage.getTitle(),
@ -186,11 +193,17 @@ public class MailService implements MailClient {
message.setSubject(title);
Multipart multipart = new MimeMultipart("alternative");
if (CollUtil.isNotEmpty(html) && MapUtil.isNotEmpty(parameter)) {
if (CollUtil.isNotEmpty(html)) {
String htmlData;
List<String> strings;
if (MapUtil.isNotEmpty(parameter)) {
//读取模板并进行变量替换
List<String> strings = HtmlUtil.replacePlaceholder(html, parameter);
strings = HtmlUtil.replacePlaceholder(html, parameter);
//拼合HTML数据
String htmlData = HtmlUtil.pieceHtml(strings);
htmlData = HtmlUtil.pieceHtml(strings);
}else {
htmlData = HtmlUtil.pieceHtml(html);
}
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlData, "text/html;charset=UTF-8");
multipart.addBodyPart(htmlPart);

View File

@ -15,6 +15,10 @@ import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.dao.SmsDao;
import org.dromara.sms4j.api.dao.SmsDaoDefaultImpl;
import org.dromara.sms4j.api.universal.SupplierConfig;
import org.dromara.sms4j.api.verify.PhoneVerify;
import org.dromara.sms4j.baidu.config.BaiduFactory;
import org.dromara.sms4j.budingyun.config.BudingV2Factory;
import org.dromara.sms4j.chuanglan.config.ChuangLanFactory;
import org.dromara.sms4j.cloopen.config.CloopenFactory;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.exception.SmsBlendException;
@ -26,21 +30,27 @@ import org.dromara.sms4j.core.proxy.processor.BlackListProcessor;
import org.dromara.sms4j.core.proxy.processor.BlackListRecordingProcessor;
import org.dromara.sms4j.core.proxy.processor.CoreMethodParamValidateProcessor;
import org.dromara.sms4j.core.proxy.processor.RestrictedProcessor;
import org.dromara.sms4j.core.proxy.processor.SingleBlendRestrictedProcessor;
import org.dromara.sms4j.ctyun.config.CtyunFactory;
import org.dromara.sms4j.danmi.config.DanMiFactory;
import org.dromara.sms4j.dingzhong.config.DingZhongFactory;
import org.dromara.sms4j.emay.config.EmayFactory;
import org.dromara.sms4j.huawei.config.HuaweiFactory;
import org.dromara.sms4j.javase.util.YamlUtils;
import org.dromara.sms4j.jdcloud.config.JdCloudFactory;
import org.dromara.sms4j.jg.config.JgFactory;
import org.dromara.sms4j.lianlu.config.LianLuFactory;
import org.dromara.sms4j.luosimao.config.LuoSiMaoFactory;
import org.dromara.sms4j.mas.config.MasFactory;
import org.dromara.sms4j.netease.config.NeteaseFactory;
import org.dromara.sms4j.provider.config.SmsConfig;
import org.dromara.sms4j.provider.factory.BaseProviderFactory;
import org.dromara.sms4j.provider.factory.BeanFactory;
import org.dromara.sms4j.provider.factory.ProviderFactoryHolder;
import org.dromara.sms4j.qiniu.config.QiNiuFactory;
import org.dromara.sms4j.submail.config.SubMailFactory;
import org.dromara.sms4j.tencent.config.TencentFactory;
import org.dromara.sms4j.unisms.config.UniFactory;
import org.dromara.sms4j.yixintong.config.YiXintongFactory;
import org.dromara.sms4j.yunpian.config.YunPianFactory;
import org.dromara.sms4j.zhutong.config.ZhutongFactory;
@ -51,6 +61,7 @@ import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
/**
* 初始化类
@ -128,11 +139,16 @@ public class SEInitializer {
EnvirmentHolder.frozenEnvirmet(smsConfig, blends);
//注册执行器实现
SmsProxyFactory.addProcessor(new RestrictedProcessor());
SmsProxyFactory.addProcessor(new BlackListProcessor());
SmsProxyFactory.addProcessor(new BlackListRecordingProcessor());
SmsProxyFactory.addProcessor(new SingleBlendRestrictedProcessor());
SmsProxyFactory.addProcessor(new CoreMethodParamValidateProcessor());
SmsProxyFactory.addPreProcessor(new RestrictedProcessor());
SmsProxyFactory.addPreProcessor(new BlackListProcessor());
SmsProxyFactory.addPreProcessor(new BlackListRecordingProcessor());
//如果手机号校验器存在实现则注册手机号校验器
ServiceLoader<PhoneVerify> loader = ServiceLoader.load(PhoneVerify.class);
if (loader.iterator().hasNext()) {
loader.forEach(f -> SmsProxyFactory.addPreProcessor(new CoreMethodParamValidateProcessor(f)));
} else {
SmsProxyFactory.addPreProcessor(new CoreMethodParamValidateProcessor(null));
}
}catch (Exception e){
log.error("配置对象转换配置信息失败但不影响基础功能的使用。【注意】未加载SMS4J扩展功能模块拦截器参数校验可能失效");
}
@ -194,11 +210,16 @@ public class SEInitializer {
EnvirmentHolder.frozenEnvirmet(smsConfig, blends);
//注册执行器实现
SmsProxyFactory.addProcessor(new RestrictedProcessor());
SmsProxyFactory.addProcessor(new BlackListProcessor());
SmsProxyFactory.addProcessor(new BlackListRecordingProcessor());
SmsProxyFactory.addProcessor(new SingleBlendRestrictedProcessor());
SmsProxyFactory.addProcessor(new CoreMethodParamValidateProcessor());
SmsProxyFactory.addPreProcessor(new RestrictedProcessor());
SmsProxyFactory.addPreProcessor(new BlackListProcessor());
SmsProxyFactory.addPreProcessor(new BlackListRecordingProcessor());
//如果手机号校验器存在实现则注册手机号校验器
ServiceLoader<PhoneVerify> loader = ServiceLoader.load(PhoneVerify.class);
if (loader.iterator().hasNext()) {
loader.forEach(f -> SmsProxyFactory.addPreProcessor(new CoreMethodParamValidateProcessor(f)));
} else {
SmsProxyFactory.addPreProcessor(new CoreMethodParamValidateProcessor(null));
}
for (String configId : blends.keySet()) {
Map<String, Object> configMap = blends.get(configId);
Object supplierObj = configMap.get(Constant.SUPPLIER_KEY);
@ -210,7 +231,7 @@ public class SEInitializer {
continue;
}
configMap.put("config-id", configId);
SmsUtils.replaceKeysSeperator(configMap, "-", "_");
SmsUtils.replaceKeysSeparator(configMap, "-", "_");
JSONObject configJson = new JSONObject(configMap);
SupplierConfig supplierConfig = JSONUtil.toBean(configJson, providerFactory.getConfigClass());
SmsFactory.createSmsBlend(supplierConfig);
@ -233,6 +254,16 @@ public class SEInitializer {
ProviderFactoryHolder.registerFactory(ZhutongFactory.instance());
ProviderFactoryHolder.registerFactory(LianLuFactory.instance());
ProviderFactoryHolder.registerFactory(DingZhongFactory.instance());
ProviderFactoryHolder.registerFactory(QiNiuFactory.instance());
ProviderFactoryHolder.registerFactory(ChuangLanFactory.instance());
ProviderFactoryHolder.registerFactory(JgFactory.instance());
ProviderFactoryHolder.registerFactory(BudingV2Factory.instance());
ProviderFactoryHolder.registerFactory(MasFactory.instance());
ProviderFactoryHolder.registerFactory(BaiduFactory.instance());
ProviderFactoryHolder.registerFactory(LuoSiMaoFactory.instance());
ProviderFactoryHolder.registerFactory(SubMailFactory.instance());
ProviderFactoryHolder.registerFactory(DanMiFactory.instance());
ProviderFactoryHolder.registerFactory(YiXintongFactory.instance());
if (SmsUtils.isClassExists("com.jdcloud.sdk.auth.CredentialsProvider")) {
ProviderFactoryHolder.registerFactory(JdCloudFactory.instance());
}

View File

@ -3,6 +3,7 @@ package org.dromara.oa.core.config;
import org.dromara.oa.api.OaSender;
import org.dromara.oa.core.provider.config.OaConfig;
import org.dromara.oa.core.provider.factory.OaBaseProviderFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
@ -31,7 +32,7 @@ public class OaSupplierConfig {
protected OaBlendsInitializer smsOasInitializer(
List<OaBaseProviderFactory<? extends OaSender, ? extends org.dromara.oa.comm.config.OaSupplierConfig>> factoryList,
OaConfig oaConfig,
Map<String, Map<String, Object>> oas) {
@Qualifier("oas") Map<String, Map<String, Object>> oas) {
return new OaBlendsInitializer(factoryList,oaConfig,oas);
}
}

View File

@ -16,13 +16,13 @@ import java.util.concurrent.ConcurrentHashMap;
public class ProviderFactoryHolder {
private static final Map<String, OaBaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig>> factories = new ConcurrentHashMap<>();
private static final Map<String, OaBaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig>> FACTORIES = new ConcurrentHashMap<>();
public static void registerFactory(OaBaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig> factory) {
if (factory == null) {
throw new OaException("注册供应商工厂失败,工厂实例不能为空");
}
factories.put(factory.getSupplier(), factory);
FACTORIES.put(factory.getSupplier(), factory);
}
public static void registerFactory(List<OaBaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig>> factoryList) {
@ -38,6 +38,6 @@ public class ProviderFactoryHolder {
}
public static OaBaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig> requireForSupplier(String supplier) {
return factories.getOrDefault(supplier, null);
return FACTORIES.getOrDefault(supplier, null);
}
}

View File

@ -2,6 +2,7 @@ package org.dromara.oa.core.provider.service;
import cn.hutool.core.util.StrUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.dromara.oa.api.OaCallBack;
import org.dromara.oa.api.OaSender;
import org.dromara.oa.comm.config.OaSupplierConfig;
@ -19,6 +20,7 @@ import java.util.concurrent.PriorityBlockingQueue;
* @author dongfeng
* 2023-10-22 21:03
*/
@Slf4j
public abstract class AbstractOaBlend<C extends OaSupplierConfig> implements OaSender {
@Getter
@ -57,12 +59,16 @@ public abstract class AbstractOaBlend<C extends OaSupplierConfig> implements OaS
pool.execute(() -> {
Thread.currentThread().setName("oa-priorityQueueMap-thread");
while (!Thread.currentThread().isInterrupted()) {
Request request = priorityQueueMap.poll();
if (!Objects.isNull(request)) {
try{
Request request = priorityQueueMap.take() ;
pool.execute(() -> {
System.out.println("优先级为"+request.getPriority()+"已发送");
log.info("优先级为"+request.getPriority()+"已发送");
sender(request, request.getMessageType());
});
}catch (InterruptedException e){
log.info("[Dispatcher]-priorityQueueMap-task-dispatcher has been interrupt to close.");
Thread.currentThread().interrupt();
break;
}
}
});

View File

@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.aliyun.config.AlibabaConfig;
import org.dromara.sms4j.aliyun.utils.AliyunUtils;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
@ -57,7 +58,7 @@ public class AlibabaSmsImpl extends AbstractSmsBlend<AlibabaConfig> {
@Override
public SmsResponse sendMessage(String phone, String message) {
LinkedHashMap<String, String> map = new LinkedHashMap<>();
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put(getConfig().getTemplateName(), message);
return sendMessage(phone, getConfig().getTemplateId(), map);
}
@ -92,7 +93,7 @@ public class AlibabaSmsImpl extends AbstractSmsBlend<AlibabaConfig> {
messages = new LinkedHashMap<>();
}
String messageStr = JSONUtil.toJsonStr(messages);
return getSmsResponse(SmsUtils.arrayToString(phones), messageStr, templateId);
return getSmsResponse(SmsUtils.addCodePrefixIfNot(phones), messageStr, templateId);
}
private SmsResponse getSmsResponse(String phone, String message, String templateId) {
@ -108,16 +109,14 @@ public class AlibabaSmsImpl extends AbstractSmsBlend<AlibabaConfig> {
log.debug("requestUrl {}", requestUrl);
Map<String, String> headers = MapUtil.newHashMap(1, true);
headers.put("Content-Type", Constant.FROM_URLENCODED);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_FROM_URLENCODED);
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postJson(requestUrl, headers, paramStr));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) {
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
@ -132,11 +131,7 @@ public class AlibabaSmsImpl extends AbstractSmsBlend<AlibabaConfig> {
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("OK".equals(resJson.getStr("Code")));
smsResponse.setData(resJson);
smsResponse.setConfigId(getConfigId());
return smsResponse;
return SmsRespUtils.resp(resJson, "OK".equals(resJson.getStr("Code")), getConfigId());
}
}

View File

@ -4,15 +4,14 @@ import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import org.dromara.sms4j.aliyun.config.AlibabaConfig;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.utils.SmsDateUtils;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SimpleTimeZone;
import java.util.TreeMap;
import java.util.UUID;
@ -27,18 +26,15 @@ public class AliyunUtils {
*/
private static final String ALGORITHM = "HMAC-SHA1";
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
public static String generateSendSmsRequestUrl(AlibabaConfig alibabaConfig, String message, String phone, String templateId) throws Exception {
// 这里一定要设置GMT时区
SDF.setTimeZone(new SimpleTimeZone(0, "GMT"));
Map<String, String> paras = new HashMap<>();
// 1. 公共请求参数
paras.put("SignatureMethod", ALGORITHM);
paras.put("SignatureNonce", UUID.randomUUID().toString());
paras.put("AccessKeyId", alibabaConfig.getAccessKeyId());
paras.put("SignatureVersion", "1.0");
paras.put("Timestamp", SDF.format(new Date()));
paras.put("Timestamp", SmsDateUtils.utcGmt(new Date()));
paras.put("Format", "JSON");
paras.put("Action", alibabaConfig.getAction());
paras.put("Version", alibabaConfig.getVersion());
@ -110,10 +106,10 @@ public class AliyunUtils {
/**
* 生成请求参数body字符串
*
* @param alibabaConfig
* @param phone
* @param message
* @param templateId
* @param alibabaConfig 配置数据
* @param phone 手机号
* @param message 短信内容
* @param templateId 模板id
*/
public static String generateParamBody(AlibabaConfig alibabaConfig, String phone, String message, String templateId) throws Exception {
Map<String, String> paramMap = generateParamMap(alibabaConfig, phone, message, templateId);

View File

@ -0,0 +1,54 @@
package org.dromara.sms4j.baidu.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* <p>类名: BaiduConfig
* <p>说明百度智能云 sms
*
* @author :bleachtred
* 2024/4/25 13:40
**/
@Data
@EqualsAndHashCode(callSuper = true)
public class BaiduConfig extends BaseConfig {
/**
* 请求地址
*/
private String host = Constant.HTTPS_PREFIX + "smsv3.bj.baidubce.com";
/**
* 接口名称
*/
private String action = "/api/v3/sendSms";
/**
* 模板变量名称
*/
private String templateName;
/**
* 用户自定义参数格式为字符串状态回调时会回传该值
*/
private String custom;
/**
* 通道自定义扩展码
*/
private String userExtId;
/**
* 获取供应商
*
* @since 3.0.0
*/
@Override
public String getSupplier() {
return SupplierConstant.BAIDU;
}
}

View File

@ -0,0 +1,49 @@
package org.dromara.sms4j.baidu.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.baidu.service.BaiduSmsImpl;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
/**
* <p>类名: BaiduFactory
* <p>说明百度智能云 sms
*
* @author :bleachtred
* 2024/4/25 13:40
**/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BaiduFactory extends AbstractProviderFactory<BaiduSmsImpl, BaiduConfig> {
private static final BaiduFactory INSTANCE = new BaiduFactory();
/**
* 获取建造者实例
* @return 建造者实例
*/
public static BaiduFactory instance() {
return INSTANCE;
}
/**
* createSms
* <p> 建造一个短信实现对像
*
* @author :bleachtred
*/
@Override
public BaiduSmsImpl createSms(BaiduConfig baiduConfig) {
return new BaiduSmsImpl(baiduConfig);
}
/**
* 获取供应商
* @return 供应商
*/
@Override
public String getSupplier() {
return SupplierConstant.BAIDU;
}
}

View File

@ -0,0 +1,181 @@
package org.dromara.sms4j.baidu.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.baidu.config.BaiduConfig;
import org.dromara.sms4j.baidu.utils.BaiduUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* <p>类名: BaiduSmsImpl
* <p>说明百度智能云 sms
*
* @author :bleachtred
* 2024/4/25 13:40
**/
@Slf4j
public class BaiduSmsImpl extends AbstractSmsBlend<BaiduConfig> {
private int retry = 0;
public BaiduSmsImpl(BaiduConfig config, Executor pool, DelayedTime delayedTime) {
super(config, pool, delayedTime);
}
public BaiduSmsImpl(BaiduConfig config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.BAIDU;
}
@Override
public SmsResponse sendMessage(String phone, String message) {
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put(getConfig().getTemplateName(), message);
return sendMessage(phone, getConfig().getTemplateId(), map);
}
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) {
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return sendMessage(phone, getConfig().getTemplateId(), messages);
}
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponse(phone, templateId, messages);
}
@Override
public SmsResponse massTexting(List<String> phones, String message) {
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put(getConfig().getTemplateName(), message);
return massTexting(phones, getConfig().getTemplateId(), map);
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponse(SmsUtils.addCodePrefixIfNot(phones), templateId, messages);
}
private SmsResponse getSmsResponse(String phone, String templateId, LinkedHashMap<String, String> messages) {
return getSmsResponseWithClientToken(phone, templateId, messages, null);
}
private void checkClientToken(String clientToken){
if (StrUtil.isBlank(clientToken)){
log.error("clientToken is required.");
throw new SmsBlendException("clientToken is required.");
}
}
public SmsResponse sendMessageWithClientToken(String phone, String message, String clientToken) {
checkClientToken(clientToken);
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put(getConfig().getTemplateName(), message);
return sendMessageWithClientToken(phone, getConfig().getTemplateId(), map, clientToken);
}
public SmsResponse sendMessageWithClientToken(String phone, LinkedHashMap<String, String> messages, String clientToken) {
checkClientToken(clientToken);
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return sendMessageWithClientToken(phone, getConfig().getTemplateId(), messages, clientToken);
}
public SmsResponse sendMessageWithClientToken(String phone, String templateId, LinkedHashMap<String, String> messages, String clientToken) {
checkClientToken(clientToken);
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponseWithClientToken(phone, templateId, messages, clientToken);
}
public SmsResponse massTextingWithClientToken(List<String> phones, String message, String clientToken) {
checkClientToken(clientToken);
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
map.put(getConfig().getTemplateName(), message);
return massTextingWithClientToken(phones, getConfig().getTemplateId(), map, clientToken);
}
public SmsResponse massTextingWithClientToken(List<String> phones, String templateId, LinkedHashMap<String, String> messages, String clientToken) {
checkClientToken(clientToken);
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponseWithClientToken(SmsUtils.addCodePrefixIfNot(phones), templateId, messages, clientToken);
}
private SmsResponse getSmsResponseWithClientToken(String phone, String templateId, LinkedHashMap<String, String> messages, String clientToken) {
BaiduConfig config = getConfig();
if (StrUtil.isBlank(config.getSignature())){
log.error("signatureId is required.");
throw new SmsBlendException("signatureId is required.");
}
if (StrUtil.isBlank(templateId)){
log.error("template is required.");
throw new SmsBlendException("template is required.");
}
if (StrUtil.isBlank(phone)){
log.error("mobile is required.");
throw new SmsBlendException("mobile is required.");
}
Map<String, String> headers;
Map<String, Object> body;
try {
headers = BaiduUtils.buildHeaders(config, clientToken);
body = BaiduUtils.buildBody(phone, templateId, config.getSignature(), messages, config.getCustom(), config.getUserExtId());
} catch (Exception e) {
log.error("baidu sms buildHeaders or buildBody error", e);
throw new SmsBlendException(e.getMessage());
}
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postJson(config.getHost() + config.getAction(), headers, body));
} catch (SmsBlendException e) {
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == config.getMaxRetries()) {
retry = 0;
return smsResponse;
}
return requestRetry(phone, templateId, messages, clientToken);
}
private SmsResponse requestRetry(String phone, String templateId, LinkedHashMap<String, String> messages, String clientToken) {
http.safeSleep(getConfig().getRetryInterval());
retry ++;
log.warn("The SMS has been resent for the {}th time.", retry);
return getSmsResponseWithClientToken(phone, templateId, messages, clientToken);
}
private SmsResponse getResponse(JSONObject resJson) {
return SmsRespUtils.resp(resJson, "1000".equals(resJson.getStr("code")), getConfigId());
}
}

View File

@ -0,0 +1,133 @@
package org.dromara.sms4j.baidu.utils;
import cn.hutool.core.net.URLEncodeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.baidu.config.BaiduConfig;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.utils.SmsDateUtils;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BaiduUtils {
/**
* 创建前缀字符串
* @param accessKeyId 访问密钥ID
* @return bce-auth-v1/{accessKeyId}/{timestamp}/{expirationPeriodInSeconds }
*/
private static String authStringPrefix(String accessKeyId){
return "bce-auth-v1/" + accessKeyId + "/" + SmsDateUtils.utcGmt(new Date()) + "/1800";
}
/**
* 创建规范请求
* @param host Host域
* @param action 接口名称
* @param clientToken 幂等性参数
* @return HTTP Method + "\n" + CanonicalURI + "\n" + CanonicalQueryString + "\n" + CanonicalHeaders
*/
private static String canonicalRequest(String host, String action, String clientToken){
return "POST\n" + canonicalURI(action) + "\n" + canonicalQueryString(clientToken) + "\n" + canonicalHeaders(host);
}
/**
* Formatting the URL with signing protocol.
* @param action URI
* @return UriEncodeExceptSlash
*/
private static String canonicalURI(String action){
return URLEncodeUtil.encode(action, StandardCharsets.UTF_8);
}
/**
* Formatting the query string with signing protocol.
* @param clientToken 幂等性参数
* @return String
*/
private static String canonicalQueryString(String clientToken){
if (StrUtil.isBlank(clientToken)) {
return StrUtil.EMPTY;
}
return "clientToken=" + URLEncodeUtil.encode(clientToken, StandardCharsets.UTF_8);
}
/**
* Formatting the headers from the request based on signing protocol.
* @param host only host
* @return String
*/
private static String canonicalHeaders(String host){
return URLEncodeUtil.encode("host", StandardCharsets.UTF_8) + ":" + URLEncodeUtil.encode(host, StandardCharsets.UTF_8);
}
/**
* HMAC-SHA256-HEX
* @param key 密钥
* @param str 要加密的字符串
* @return 小写形式的十六进制字符串
*/
private static String sha256Hex(String key, String str) {
HMac hMac = new HMac(HmacAlgorithm.HmacSHA256, key.getBytes(StandardCharsets.UTF_8));
return hMac.digestHex(str, StandardCharsets.UTF_8);
}
/**
* 构造 HTTP Headers请求头
* @param config 百度智能云配置
* @param clientToken 幂等性参数
* @return Headers请求头
*/
public static Map<String, String> buildHeaders(BaiduConfig config, String clientToken) {
// 创建前缀字符串
String authStringPrefix = authStringPrefix(config.getAccessKeyId());
// 生成派生密钥
String signingKey = sha256Hex(config.getAccessKeySecret(), authStringPrefix(config.getAccessKeyId()));
// 生成签名摘要及认证字符串
String signature = sha256Hex(signingKey, canonicalRequest(config.getHost(), config.getAction(), clientToken));
// 认证字符串
String authorization = authStringPrefix + "/" + "/" + signature;
Map<String, String> headers = new HashMap<>(2);
headers.put(Constant.AUTHORIZATION, authorization);
headers.put("host", config.getHost());
headers.put("x-bce-date", SmsDateUtils.normDateGmt8(new Date()));
return headers;
}
/**
* 构造 HTTP Body 请求体
* @param mobile 手机号码 支持单个或多个手机号多个手机号之间以英文逗号分隔
* @param template 短信模板ID模板申请成功后自动创建全局内唯一
* @param signatureId 短信签名ID签名表申请成功后自动创建全局内唯一
* @param contentVar 模板变量内容用于替换短信模板中定义的变量
* @param custom 用户自定义参数格式为字符串状态回调时会回传该值
* @param userExtId 通道自定义扩展码上行回调时会回传该值其格式为纯数字串默认为不开通请求时无需设置该参数如需开通请联系SMS帮助申请
* @return Body 请求体
*/
public static Map<String, Object> buildBody(String mobile, String template, String signatureId,
LinkedHashMap<String, String> contentVar, String custom, String userExtId) {
Map<String, Object> body = new HashMap<>(4);
body.put("mobile", mobile);
body.put("template", template);
body.put("signatureId", signatureId);
body.put("contentVar", contentVar);
if (StrUtil.isNotBlank(custom)){
body.put("custom", custom);
}
if (StrUtil.isNotBlank(userExtId)){
body.put("userExtId", userExtId);
}
return body;
}
}

View File

@ -0,0 +1,40 @@
package org.dromara.sms4j.budingyun.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* BudingV2Config
* <p> 布丁云V2短信配置
*
* @author NicholaslD
* @date 2024/03/21 12:00
* */
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class BudingV2Config extends BaseConfig {
/**
* 签名密钥
* 就是发短信的时候的签名比如布丁云
*/
private String signKey;
/**
* 变量列表
* 用于替换短信模板中的变量
*/
private String[] args;
/**
* 获取供应商
*/
@Override
public String getSupplier() {
return SupplierConstant.BUDING_V2;
}
}

View File

@ -0,0 +1,33 @@
package org.dromara.sms4j.budingyun.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.budingyun.service.BudingV2SmsImpl;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
/**
* BudingV2Factory
* <p> 布丁云V2短信对象建造
*
* @author NicholaslD
* @date 2024/03/21 12:00
* */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BudingV2Factory extends AbstractProviderFactory<BudingV2SmsImpl, BudingV2Config> {
private static final BudingV2Factory INSTANCE = new BudingV2Factory();
public static BudingV2Factory instance() {
return INSTANCE;
}
@Override
public BudingV2SmsImpl createSms(BudingV2Config budingV2Config) {
return new BudingV2SmsImpl(budingV2Config);
}
@Override
public String getSupplier() {
return SupplierConstant.BUDING_V2;
}
}

View File

@ -0,0 +1,168 @@
package org.dromara.sms4j.budingyun.service;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.budingyun.config.BudingV2Config;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* BudingV2SmsImpl 布丁云V2短信实现
* @author NicholasLD
* @createTime 2024/3/21 01:28
*/
@Slf4j
public class BudingV2SmsImpl extends AbstractSmsBlend<BudingV2Config> {
/**
* 重试次数
*/
private int retry = 0;
private static final String URL = Constant.HTTPS_PREFIX + "smsapi.idcbdy.com";
protected BudingV2SmsImpl(BudingV2Config config, Executor pool, DelayedTime delayed) {
super(config, pool, delayed);
}
public BudingV2SmsImpl(BudingV2Config config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.BUDING_V2;
}
@Override
public SmsResponse sendMessage(String phone, String message) {
Map<String, Object> body = new HashMap<>();
System.out.println(getConfig().getSignKey());
System.out.println(getConfig().getSignature());
if (getConfig().getSignKey() == null && getConfig().getSignature() == null) {
throw new SmsBlendException("签名秘钥不能为空");
}
if (getConfig().getSignKey() == null) {
body.put("sign", getConfig().getSignature());
}
body.put("key", getConfig().getAccessKeyId());
body.put("to", phone);
body.put("content", message);
Map<String, String> headers = getHeaders();
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postFrom(URL + "/Api/Sent", headers, body));
} catch (SmsBlendException e) {
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
return requestRetry(phone, message);
}
private SmsResponse requestRetry(String phone, String message) {
http.safeSleep(getConfig().getRetryInterval());
retry++;
log.warn("短信第 {" + retry + "} 次重新发送");
return sendMessage(phone, message);
}
private SmsResponse getResponse(JSONObject resJson) {
if (resJson == null) {
return SmsRespUtils.error(getConfigId());
}
return SmsRespUtils.resp(resJson, resJson.getBool("bool"), getConfigId());
}
/**
* 发送多条短信
* @param phone 手机号
* @param messages 消息内容
* @return 发送结果
*/
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) {
int failed = 0;
for (String message : messages.values()) {
SmsResponse smsResponse = sendMessage(phone, message);
if (!smsResponse.isSuccess()) {
failed++;
}
}
return SmsRespUtils.resp(failed == 0, getConfigId());
}
/**
* 发送多条短信 (布丁云V2暂不支持模板短信)
* @param phone 手机号
* @param templateId 模板ID (布丁云V2暂不支持模板短信此参数无效)
* @param messages 模板参数
* @return 发送结果
*/
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
return sendMessage(phone, messages);
}
/**
* 群发短信
* @param phones 手机号列表
* @param message 消息内容
* @return 发送结果
*/
@Override
public SmsResponse massTexting(List<String> phones, String message) {
int failed = 0;
for (String phone : phones) {
SmsResponse smsResponse = sendMessage(phone, message);
if (!smsResponse.isSuccess()) {
failed++;
}
}
return SmsRespUtils.resp(failed == 0, getConfigId());
}
/**
* 群发短信 (布丁云V2暂不支持模板短信此方法无效)
* @param phones 手机号列表
* @param templateId 模板ID (布丁云V2暂不支持模板短信此参数无效)
* @param messages 模板参数
* @return 发送结果
*/
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
List<SmsResponse> list = new ArrayList<>();
for (String phone : phones) {
SmsResponse smsResponse = sendMessage(phone, templateId, messages);
list.add(smsResponse);
}
return SmsRespUtils.resp(list, true, getConfigId());
}
private Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put(Constant.ACCEPT, Constant.APPLICATION_JSON_UTF8);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_FROM_URLENCODED);
return headers;
}
}

View File

@ -0,0 +1,34 @@
package org.dromara.sms4j.chuanglan.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* @author YYM
* @Date: 2024/1/31 17:56 44
* @描述: ChuangLanConfig
**/
@EqualsAndHashCode(callSuper = true)
@Data
public class ChuangLanConfig extends BaseConfig {
/**
* 基础路径
*/
private String baseUrl = Constant.HTTPS_PREFIX + "smssh1.253.com/msg";
/**
* 短信发送路径
* 普通短信发送 /v1/send/json 此接口支持单发群发短信
* 变量短信发送 /variable/json 单号码对应单内容批量下发
*/
private String msgUrl = "/variable/json";
@Override
public String getSupplier() {
return SupplierConstant.CHUANGLAN;
}
}

View File

@ -0,0 +1,37 @@
package org.dromara.sms4j.chuanglan.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.chuanglan.service.ChuangLanSmsImpl;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
/**
* @author YYM
* @Date: 2024/2/1 9:03 44
* @描述: ChuangLanFactory
**/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ChuangLanFactory extends AbstractProviderFactory<ChuangLanSmsImpl, ChuangLanConfig> {
private static final ChuangLanFactory INSTANCE = new ChuangLanFactory();
/**
* 获取建造者实例
*
* @return 建造者实例
*/
public static ChuangLanFactory instance() {
return INSTANCE;
}
@Override
public ChuangLanSmsImpl createSms(ChuangLanConfig chuangLanConfig) {
return new ChuangLanSmsImpl(chuangLanConfig);
}
@Override
public String getSupplier() {
return SupplierConstant.CHUANGLAN;
}
}

View File

@ -0,0 +1,131 @@
package org.dromara.sms4j.chuanglan.service;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.chuanglan.config.ChuangLanConfig;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.Executor;
/**
* @author YYM
* @Date: 2024/2/1 9:04 27
* @描述: ChuangLanSmsImpl
**/
@Slf4j
public class ChuangLanSmsImpl extends AbstractSmsBlend<ChuangLanConfig> {
private int retry = 0;
public ChuangLanSmsImpl(ChuangLanConfig config, Executor pool, DelayedTime delayed) {
super(config, pool, delayed);
}
public ChuangLanSmsImpl(ChuangLanConfig config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.CHUANGLAN;
}
@Override
public SmsResponse sendMessage(String phone, String message) {
return sendMessage(phone, getConfig().getTemplateId(), SmsUtils.buildMessageByAmpersand(message));
}
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) {
return sendMessage(phone, getConfig().getTemplateId(), messages);
}
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
if (SmsUtils.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
String message = String.join(",", messages.values());
ChuangLanConfig config = getConfig();
LinkedHashMap<String, Object> body = buildBody(config.getAccessKeyId(), config.getAccessKeySecret(), templateId);
body.put("params", phone + "," + message);
return getSmsResponse(body);
}
@Override
public SmsResponse massTexting(List<String> phones, String message) {
return massTexting(phones, getConfig().getTemplateId(), SmsUtils.buildMessageByAmpersand(message));
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
if (SmsUtils.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
String message = String.join(",", messages.values());
StringBuilder param = new StringBuilder();
phones.forEach(phone -> param.append(phone).append(",").append(message).append(";"));
ChuangLanConfig config = getConfig();
LinkedHashMap<String, Object> params = buildBody(config.getAccessKeyId(), config.getAccessKeySecret(), templateId);
params.put("params", param.toString());
return getSmsResponse(params);
}
private static String buildUrl(String baseUrl, String msgUrl){
return baseUrl + msgUrl;
}
private static LinkedHashMap<String, String> buildHeaders(){
LinkedHashMap<String, String> headers = new LinkedHashMap<>(1);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_JSON);
return headers;
}
private static LinkedHashMap<String, Object> buildBody(String accessKeyId, String accessKeySecret, String templateId){
LinkedHashMap<String, Object> body = new LinkedHashMap<>(3);
body.put("account", accessKeyId);
body.put("password", accessKeySecret);
body.put("msg", templateId);
return body;
}
private SmsResponse getSmsResponse(LinkedHashMap<String, Object> body) {
ChuangLanConfig config = getConfig();
SmsResponse smsResponse;
String reqUrl = buildUrl(config.getBaseUrl(), config.getMsgUrl());
try {
smsResponse = getResponse(http.postJson(reqUrl, buildHeaders(), body));
}catch (SmsBlendException e) {
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
http.safeSleep(getConfig().getRetryInterval());
retry++;
log.warn("短信第 {" + retry + "} 次重新发送");
return requestRetry(body);
}
private SmsResponse requestRetry(LinkedHashMap<String, Object> body) {
http.safeSleep(getConfig().getRetryInterval());
retry ++;
log.warn("短信第 {} 次重新发送", retry);
return getSmsResponse(body);
}
private SmsResponse getResponse(JSONObject resJson) {
return SmsRespUtils.resp(resJson, resJson.containsKey("code") && "0".equals(resJson.getStr("code")), getConfigId());
}
}

View File

@ -2,6 +2,7 @@ package org.dromara.sms4j.cloopen.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
@ -18,7 +19,7 @@ public class CloopenConfig extends BaseConfig {
/**
* REST API Base URL
*/
private String baseUrl = "https://app.cloopen.com:8883/2013-12-26";
private String baseUrl = Constant.HTTPS_PREFIX + "app.cloopen.com:8883/2013-12-26";
/**
* 获取供应商

View File

@ -27,7 +27,7 @@ public class CloopenFactory extends AbstractProviderFactory<CloopenSmsImpl, Cloo
}
/**
* 创建容云短信实现对象
* 创建容云短信实现对象
*
* @param cloopenConfig 短信配置对象
* @return 短信实现对象

View File

@ -8,6 +8,7 @@ import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.cloopen.config.CloopenConfig;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.exception.SmsBlendException;
@ -43,16 +44,14 @@ public class CloopenHelper {
config.getAccessKeyId(),
this.generateSign(config.getAccessKeyId(), config.getAccessKeySecret(), timestamp));
Map<String, String> headers = MapUtil.newHashMap(3, true);
headers.put("Accept", Constant.ACCEPT);
headers.put("Content-Type", Constant.APPLICATION_JSON_UTF8);
headers.put("Authorization", this.generateAuthorization(config.getAccessKeyId(), timestamp));
SmsResponse smsResponse = null;
headers.put(Constant.ACCEPT, Constant.APPLICATION_JSON);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_JSON_UTF8);
headers.put(Constant.AUTHORIZATION, this.generateAuthorization(config.getAccessKeyId(), timestamp));
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postJson(url, headers, paramMap));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
smsResponse = SmsRespUtils.error(e.message, config.getConfigId());
}
if (smsResponse.isSuccess() || retry == config.getMaxRetries()) {
retry = 0;
@ -70,11 +69,7 @@ public class CloopenHelper {
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("000000".equals(resJson.getStr("statusCode")));
smsResponse.setData(resJson);
smsResponse.setConfigId(this.config.getConfigId());
return smsResponse;
return SmsRespUtils.resp(resJson, "000000".equals(resJson.getStr("statusCode")), config.getConfigId());
}
/**

View File

@ -2,6 +2,7 @@ package org.dromara.sms4j.ctyun.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
@ -9,7 +10,7 @@ import org.dromara.sms4j.provider.config.BaseConfig;
* <p>类名: CtyunConfig
* <p>说明 天翼云短信差异配置
*
* @author :bleachhtred
* @author :bleachtred
* 2023/5/12 15:06
**/
@Data
@ -24,7 +25,7 @@ public class CtyunConfig extends BaseConfig {
/**
* 请求地址
*/
private String requestUrl = "https://sms-global.ctapi.ctyun.cn/sms/api/v1";
private String requestUrl = Constant.HTTPS_PREFIX + "sms-global.ctapi.ctyun.cn/sms/api/v1";
/**
* 接口名称

View File

@ -10,7 +10,7 @@ import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
* <p>类名: CtyunSmsConfig
* <p>说明 天翼云 云通信短信配置器
*
* @author :bleachhtred
* @author :bleachtred
* 2023/5/12 15:06
**/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@ -30,7 +30,7 @@ public class CtyunFactory extends AbstractProviderFactory<CtyunSmsImpl, CtyunCon
* getCtyunSms
* <p> 建造一个短信实现对像
*
* @author :bleachhtred
* @author :bleachtred
*/
@Override
public CtyunSmsImpl createSms(CtyunConfig ctyunConfig) {

View File

@ -4,6 +4,7 @@ import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
@ -21,7 +22,7 @@ import java.util.concurrent.Executor;
* <p>类名: CtyunSmsImpl
* <p>说明 天翼云短信实现
*
* @author :bleachhtred
* @author :bleachtred
* 2023/5/12 15:06
**/
@Slf4j
@ -79,15 +80,16 @@ public class CtyunSmsImpl extends AbstractSmsBlend<CtyunConfig> {
messages = new LinkedHashMap<>();
}
String messageStr = JSONUtil.toJsonStr(messages);
return getSmsResponse(SmsUtils.arrayToString(phones), messageStr, templateId);
return getSmsResponse(SmsUtils.addCodePrefixIfNot(phones), messageStr, templateId);
}
private SmsResponse getSmsResponse(String phone, String message, String templateId) {
CtyunConfig config = getConfig();
String requestUrl;
String paramStr;
try {
requestUrl = getConfig().getRequestUrl();
paramStr = CtyunUtils.generateParamJsonStr(getConfig(), phone, message, templateId);
requestUrl = config.getRequestUrl();
paramStr = CtyunUtils.generateParamJsonStr(config, phone, message, templateId);
} catch (Exception e) {
log.error("ctyun send message error", e);
throw new SmsBlendException(e.getMessage());
@ -96,14 +98,12 @@ public class CtyunSmsImpl extends AbstractSmsBlend<CtyunConfig> {
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postJson(requestUrl,
CtyunUtils.signHeader(paramStr, getConfig().getAccessKeyId(), getConfig().getAccessKeySecret()),
CtyunUtils.signHeader(paramStr, config.getAccessKeyId(), config.getAccessKeySecret()),
paramStr));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) {
if (smsResponse.isSuccess() || retry == config.getMaxRetries()) {
retry = 0;
return smsResponse;
}
@ -118,11 +118,7 @@ public class CtyunSmsImpl extends AbstractSmsBlend<CtyunConfig> {
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("OK".equals(resJson.getStr("code")));
smsResponse.setData(resJson);
smsResponse.setConfigId(getConfigId());
return smsResponse;
return SmsRespUtils.resp(resJson, "OK".equals(resJson.getStr("code")), getConfigId());
}
}

View File

@ -1,22 +1,18 @@
package org.dromara.sms4j.ctyun.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsDateUtils;
import org.dromara.sms4j.ctyun.config.CtyunConfig;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -28,8 +24,7 @@ public class CtyunUtils {
* 获取签名时间戳
*/
private static String signatureTime(){
SimpleDateFormat timeFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
return timeFormat.format(new Date());
return SmsDateUtils.pureDateUtcGmt8(new Date());
}
/**
@ -39,9 +34,7 @@ public class CtyunUtils {
Map<String, String> map = new ConcurrentHashMap<>(4);
// 构造时间戳
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
Date now = new Date();
String signatureDate = dateFormat.format(now);
String signatureDate = SmsDateUtils.pureDateGmt8(new Date());
String signatureTime = signatureTime();
// 构造请求流水号
String uuid = UUID.randomUUID().toString();
@ -59,7 +52,7 @@ public class CtyunUtils {
// 构造签名
String signature = Base64.encode(hmacSHA256(signatureStr.getBytes(StandardCharsets.UTF_8), kDate));
String signHeader = String.format("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", key, signature);
map.put("Content-Type", Constant.APPLICATION_JSON_UTF8);
map.put(Constant.CONTENT_TYPE, Constant.APPLICATION_JSON_UTF8);
map.put("ctyun-eop-request-id", uuid);
map.put("Eop-date", signatureTime);
map.put("Eop-Authorization", signHeader);
@ -84,36 +77,11 @@ public class CtyunUtils {
return JSONUtil.toJsonStr(paramMap);
}
private static String toHex(byte[] data) {
StringBuilder sb = new StringBuilder(data.length * 2);
for (byte b : data) {
String hex = Integer.toHexString(b);
if (hex.length() == 1) {
sb.append("0");
} else if (hex.length() == 8) {
hex = hex.substring(6);
}
sb.append(hex);
}
return sb.toString().toLowerCase(Locale.getDefault());
}
private static String getSHA256(String text) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(text.getBytes(StandardCharsets.UTF_8));
return toHex(md.digest());
} catch (NoSuchAlgorithmException var3) {
return null;
}
return DigestUtil.sha256Hex(text);
}
private static byte[] hmacSHA256(byte[] data, byte[] key){
try {
HMac hMac = new HMac(HmacAlgorithm.HmacSHA256, key);
return hMac.digest(data);
} catch (Exception e) {
throw new SmsBlendException(e.getMessage());
}
return SecureUtil.hmacSha256(key).digest(data);
}
}

View File

@ -0,0 +1,44 @@
package org.dromara.sms4j.danmi.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* <p>类名: DanMiConfig
* <p>说明 旦米短信差异配置
*
* @author :bleachtred
* 2024/6/23 17:06
**/
@Data
@EqualsAndHashCode(callSuper = true)
public class DanMiConfig extends BaseConfig {
/**
* 请求地址
*/
private String host = Constant.HTTPS_PREFIX + "openapi.danmi.com/";
/**
* 请求方法
* 短信发送 distributor/sendSMS
* 短信余额查询 distributor/user/query
* 语音验证码发送 voice/voiceCode
* 语音通知文件发送 voice/voiceNotify
* 语音模板通知发送 voice/voiceTemplate
*/
private String action = "distributor/sendSMS";
/**
* 获取供应商
*
* @since 3.0.0
*/
@Override
public String getSupplier() {
return SupplierConstant.DAN_MI;
}
}

View File

@ -0,0 +1,48 @@
package org.dromara.sms4j.danmi.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.danmi.service.DanMiSmsImpl;
import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
/**
* <p>类名: DanMiFactory
*
* @author :bleachtred
* 2024/6/23 17:06
**/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DanMiFactory extends AbstractProviderFactory<DanMiSmsImpl, DanMiConfig> {
private static final DanMiFactory INSTANCE = new DanMiFactory();
/**
* 获取建造者实例
* @return 建造者实例
*/
public static DanMiFactory instance() {
return INSTANCE;
}
/**
* createSms
* <p> 建造一个短信实现对像
*
* @author :bleachtred
*/
@Override
public DanMiSmsImpl createSms(DanMiConfig config) {
return new DanMiSmsImpl(config);
}
/**
* 获取供应商
* @return 供应商
*/
@Override
public String getSupplier() {
return SupplierConstant.DAN_MI;
}
}

View File

@ -0,0 +1,151 @@
package org.dromara.sms4j.danmi.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.danmi.config.DanMiConfig;
import org.dromara.sms4j.danmi.utils.DanMiUtils;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.Executor;
/**
* <p>类名: DanMiSmsImpl
*
* @author :bleachtred
* 2024/6/23 17:06
**/
@Slf4j
public class DanMiSmsImpl extends AbstractSmsBlend<DanMiConfig> {
private int retry = 0;
public DanMiSmsImpl(DanMiConfig config, Executor pool, DelayedTime delayedTime) {
super(config, pool, delayedTime);
}
public DanMiSmsImpl(DanMiConfig config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.DAN_MI;
}
@Override
public SmsResponse sendMessage(String phone, String message) {
if (StrUtil.isBlank(phone)){
log.error("手机号不能为空");
throw new SmsBlendException("手机号不能为空");
}
List<String> phones = phone.contains(StrUtil.COMMA) ? SmsUtils.splitTrimComma(phone) : Collections.singletonList(phone);
return massTexting(phones, message);
}
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) {
throw new SmsBlendException("不支持此方法");
}
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
throw new SmsBlendException("不支持此方法");
}
@Override
public SmsResponse massTexting(List<String> phones, String message) {
return getSmsResponse(phones, message, getConfig().getTemplateId());
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
throw new SmsBlendException("不支持此方法");
}
/**
* 短信余额查询
* 请设置action为 distributor/user/query
*
* @return SmsResponse
*/
public SmsResponse queryBalance() {
return getSmsResponse(null, null, null);
}
/**
* 语音验证码发送
* 请设置action为 voice/voiceCode
*
* @param called 被叫号码
* @param verifyCode 验证码内容(1-8位数字)
* @return SmsResponse
*/
public SmsResponse voiceCode(String called, String verifyCode) {
return getSmsResponse(Collections.singletonList(called), verifyCode, null);
}
/**
* 语音通知文件发送
* 请设置action为 voice/voiceNotify
*
* @param called 被叫号码
* @param notifyFileId 语音文件ID
* @return SmsResponse
*/
public SmsResponse voiceNotify(String called, String notifyFileId) {
return getSmsResponse(Collections.singletonList(called), notifyFileId, null);
}
/**
* 语音模板通知发送
* 请设置action为 voice/voiceTemplate
*
* @param called 被叫号码
* @param templateId 文字模板Id(用户中心创建后产生)
* @param param 模板变量替换的参数(多个变量按英文逗号分开)
* @return SmsResponse
*/
public SmsResponse voiceTemplate(String called, String templateId, String param) {
return getSmsResponse(Collections.singletonList(called), param, templateId);
}
private SmsResponse getSmsResponse(List<String> phones, String message, String templateId) {
DanMiConfig config = getConfig();
SmsResponse smsResponse;
try {
String url = config.getHost() + config.getAction();
smsResponse = getResponse(http.postJson(url,
DanMiUtils.buildHeaders(),
DanMiUtils.buildBody(config, phones, message, templateId)));
} catch (SmsBlendException e) {
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == config.getMaxRetries()) {
retry = 0;
return smsResponse;
}
return requestRetry(phones, message, templateId);
}
private SmsResponse requestRetry(List<String> phones, String message, String templateId) {
http.safeSleep(getConfig().getRetryInterval());
retry ++;
log.warn("短信第 {} 次重新发送", retry);
return getSmsResponse(phones, message, templateId);
}
private SmsResponse getResponse(JSONObject resJson) {
return SmsRespUtils.resp(resJson, "00000".equals(resJson.getStr("respCode")), getConfigId());
}
}

View File

@ -0,0 +1,127 @@
package org.dromara.sms4j.danmi.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.net.URLEncodeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.danmi.config.DanMiConfig;
import java.util.LinkedHashMap;
import java.util.List;
/**
* <p>类名: DanMiUtils
*
* @author :bleachtred
* 2024/6/23 17:06
**/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DanMiUtils {
public static LinkedHashMap<String, String> buildHeaders(){
LinkedHashMap<String, String> headers = new LinkedHashMap<>(1);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_JSON);
return headers;
}
/**
* 生成请求body参数
*
* @param config 配置数据
* @param phones 手机号
* @param message 短信内容 (or 验证码内容(1-8位数字) or 语音文件ID or 模板参数)
* @param templateId 模板id
*/
public static LinkedHashMap<String, Object> buildBody(DanMiConfig config, List<String> phones, String message, String templateId) {
LinkedHashMap<String, Object> body = new LinkedHashMap<>();
body.put("respDataType", "JSON");
body.put("accountSid", config.getAccessKeyId());
switch (config.getAction()){
case "distributor/sendSMS":
if (StrUtil.isAllBlank(message, templateId)){
log.error("message and templateId can not be empty at the same time");
throw new SmsBlendException("message and templateId can not be empty at the same time");
}
if (StrUtil.isNotBlank(templateId)){
body.put("templateid", templateId);
}
if (StrUtil.isNotBlank(message)){
body.put("smsContent", URLEncodeUtil.encode(message));
}
if (CollUtil.isEmpty(phones)){
log.error("phones can not be empty");
throw new SmsBlendException("phones can not be empty");
}
body.put("to", SmsUtils.addCodePrefixIfNot(phones));
break;
case "distributor/user/query":
break;
case "voice/voiceCode":
if (CollUtil.isEmpty(phones) || phones.size() != 1){
log.error("called can not be empty or phone must be only one");
throw new SmsBlendException("called can not be empty or phone must be only one");
}
body.put("called", SmsUtils.addCodePrefixIfNot(phones.get(0)));
if (StrUtil.isBlank(message)){
log.error("verifyCode can not be empty");
throw new SmsBlendException("verifyCode can not be empty");
}
body.put("verifyCode", message);
break;
case "voice/voiceNotify":
if (CollUtil.isEmpty(phones) || phones.size() != 1){
log.error("called can not be empty or phone must be only one");
throw new SmsBlendException("called can not be empty or phone must be only one");
}
body.put("called", SmsUtils.addCodePrefixIfNot(phones.get(0)));
if (StrUtil.isBlank(message)){
log.error("notifyFileId can not be empty");
throw new SmsBlendException("notifyFileId can not be empty");
}
body.put("notifyFileId", message);
break;
case "voice/voiceTemplate":
if (CollUtil.isEmpty(phones) || phones.size() != 1){
log.error("called can not be empty or phone must be only one");
throw new SmsBlendException("called can not be empty or phone must be only one");
}
body.put("called", SmsUtils.addCodePrefixIfNot(phones.get(0)));
if (StrUtil.isEmpty(templateId)){
log.error("templateId can not be empty");
throw new SmsBlendException("templateId can not be empty");
}
body.put("templateId", templateId);
if (StrUtil.isEmpty(message)){
log.error("param can not be empty");
throw new SmsBlendException("param can not be empty");
}
body.put("param", message);
break;
default:
log.error("action not found");
throw new SmsBlendException("action not found");
}
long timestamp = System.currentTimeMillis();
body.put("timestamp", timestamp);
body.put("sig", sign(config.getAccessKeyId(), config.getAccessKeySecret(), timestamp));
return body;
}
/**
* 签名MD5(ACCOUNT SID + AUTH TOKEN + timestamp)共32位小写
* @param accessKeyId ACCOUNT SID
* @param accessKeySecret AUTH TOKEN
* @param timestamp timestamp
* @return 签名MD5 共32位小写
*/
private static String sign(String accessKeyId, String accessKeySecret, long timestamp){
return DigestUtil.md5Hex(accessKeyId + accessKeySecret + timestamp);
}
}

View File

@ -86,11 +86,11 @@ public class DingZhongSmsImpl extends AbstractSmsBlend<DingZhongConfig> {
@Override
public SmsResponse massTexting(List<String> phones, String message) {
return sendMessage(SmsUtils.arrayToString(phones), message);
return sendMessage(SmsUtils.addCodePrefixIfNot(phones), message);
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
return sendMessage(SmsUtils.arrayToString(phones), templateId, messages);
return sendMessage(SmsUtils.addCodePrefixIfNot(phones), templateId, messages);
}
}

View File

@ -4,6 +4,7 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsHttpUtils;
@ -33,15 +34,13 @@ public class DingZhongHelper {
public SmsResponse smsResponse(Map<String, Object> paramMap) {
String url = String.format("%s/%s", config.getRequestUrl(), SmsUtils.isEmpty(paramMap.get("templateId"))?config.getBaseAction():config.getTemplateAction());
Map<String, String> headers = MapUtil.newHashMap(2, true);
headers.put("Accept", Constant.ACCEPT);
headers.put("Content-Type", Constant.FROM_URLENCODED);
SmsResponse smsResponse = null;
headers.put(Constant.ACCEPT, Constant.APPLICATION_JSON);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_FROM_URLENCODED);
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postFrom(url, headers, paramMap));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
smsResponse = SmsRespUtils.error(e.message, config.getConfigId());
}
if (smsResponse.isSuccess() || retry == config.getMaxRetries()) {
retry = 0;
@ -59,10 +58,6 @@ public class DingZhongHelper {
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("0".equals(resJson.getStr("resCode")));
smsResponse.setData(resJson);
smsResponse.setConfigId(this.config.getConfigId());
return smsResponse;
return SmsRespUtils.resp(resJson, "0".equals(resJson.getStr("resCode")), config.getConfigId());
}
}

View File

@ -4,6 +4,7 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
@ -44,20 +45,19 @@ public class EmaySmsImpl extends AbstractSmsBlend<EmayConfig> {
@Override
public SmsResponse sendMessage(String phone, String message) {
String url = getConfig().getRequestUrl();
Map<String, Object> params = EmayBuilder.buildRequestBody(getConfig().getAccessKeyId(), getConfig().getAccessKeySecret(), phone, message);
EmayConfig config = getConfig();
String url = config.getRequestUrl();
Map<String, Object> params = EmayBuilder.buildRequestBody(config.getAccessKeyId(), config.getAccessKeySecret(), phone, message);
Map<String, String> headers = MapUtil.newHashMap(1, true);
headers.put("Content-Type", Constant.FROM_URLENCODED);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_FROM_URLENCODED);
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postUrl(url, headers, params));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) {
if (smsResponse.isSuccess() || retry == config.getMaxRetries()) {
retry = 0;
return smsResponse;
}
@ -97,7 +97,7 @@ public class EmaySmsImpl extends AbstractSmsBlend<EmayConfig> {
if (phones.size() > 500) {
throw new SmsBlendException("单次发送超过最大发送上限建议每次群发短信人数低于500");
}
return sendMessage(SmsUtils.listToString(phones), message);
return sendMessage(SmsUtils.joinComma(phones), message);
}
@Override
@ -109,15 +109,11 @@ public class EmaySmsImpl extends AbstractSmsBlend<EmayConfig> {
for (Map.Entry<String, String> entry : messages.entrySet()) {
list.add(entry.getValue());
}
return sendMessage(SmsUtils.listToString(phones), EmayBuilder.listToString(list));
return sendMessage(SmsUtils.joinComma(phones), EmayBuilder.listToString(list));
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("success".equalsIgnoreCase(resJson.getStr("code")));
smsResponse.setData(resJson);
smsResponse.setConfigId(getConfigId());
return smsResponse;
return SmsRespUtils.resp(resJson, "success".equalsIgnoreCase(resJson.getStr("code")), getConfigId());
}
}

View File

@ -5,6 +5,7 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
@ -20,13 +21,14 @@ import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import static org.dromara.sms4j.huawei.utils.HuaweiBuilder.listToString;
@Slf4j
public class HuaweiSmsImpl extends AbstractSmsBlend<HuaweiConfig> {
private int retry = 0;
private volatile int retry = 0;
public HuaweiSmsImpl(HuaweiConfig config, Executor pool, DelayedTime delayed) {
super(config, pool, delayed);
@ -70,18 +72,16 @@ public class HuaweiSmsImpl extends AbstractSmsBlend<HuaweiConfig> {
String requestBody = HuaweiBuilder.buildRequestBody(getConfig().getSender(), phone, templateId, mess, getConfig().getStatusCallBack(), getConfig().getSignature());
Map<String, String> headers = MapUtil.newHashMap(3, true);
headers.put("Authorization", Constant.HUAWEI_AUTH_HEADER_VALUE);
headers.put(Constant.AUTHORIZATION, Constant.HUAWEI_AUTH_HEADER_VALUE);
headers.put("X-WSSE", HuaweiBuilder.buildWsseHeader(getConfig().getAccessKeyId(), getConfig().getAccessKeySecret()));
headers.put("Content-Type", Constant.FROM_URLENCODED);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_FROM_URLENCODED);
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postJson(url, headers, requestBody));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) {
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
@ -109,11 +109,7 @@ public class HuaweiSmsImpl extends AbstractSmsBlend<HuaweiConfig> {
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("000000".equals(resJson.getStr("code")));
smsResponse.setData(resJson);
smsResponse.setConfigId(getConfigId());
return smsResponse;
return SmsRespUtils.resp(resJson, "000000".equals(resJson.getStr("code")), getConfigId());
}
}

View File

@ -2,23 +2,25 @@ package org.dromara.sms4j.huawei.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.net.URLEncodeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Slf4j
public class HuaweiBuilder {
private HuaweiBuilder() {
}
@ -31,22 +33,13 @@ public class HuaweiBuilder {
*/
public static String buildWsseHeader(String appKey, String appSecret) {
if (null == appKey || null == appSecret || appKey.isEmpty() || appSecret.isEmpty()) {
System.out.println("buildWsseHeader(): appKey or appSecret is null.");
return null;
log.error("buildWsseHeader(): appKey or appSecret is null.");
throw new SmsBlendException("buildWsseHeader(): appKey or appSecret is null.");
}
String time = dateFormat(new Date());
// Nonce
String nonce = UUID.randomUUID().toString().replace("-", "");
MessageDigest md;
byte[] passwordDigest = null;
try {
md = MessageDigest.getInstance("SHA-256");
md.update((nonce + time + appSecret).getBytes());
passwordDigest = md.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
String nonce = UUID.fastUUID().toString(true);
byte[] passwordDigest = DigestUtil.sha256(nonce + time + appSecret);
// PasswordDigest
String passwordDigestBase64Str = Base64.encode(passwordDigest);
//若passwordDigestBase64Str中包含换行符,请执行如下代码进行修正
@ -90,12 +83,11 @@ public class HuaweiBuilder {
*/
public static String buildRequestBody(String sender, String receiver, String templateId, String templateParas,
String statusCallBack, String signature) {
if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
|| templateId.isEmpty()) {
System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
return null;
if (StrUtil.hasBlank(sender, receiver, templateId)) {
log.error("buildRequestBody(): sender, receiver or templateId is null.");
throw new SmsBlendException("buildRequestBody(): sender, receiver or templateId is null.");
}
Map<String, String> map = new HashMap<>();
Map<String, String> map = new HashMap<>(3);
map.put("from", sender);
map.put("to", receiver);
@ -111,21 +103,14 @@ public class HuaweiBuilder {
}
StringBuilder sb = new StringBuilder();
String temp = "";
for (String s : map.keySet()) {
try {
temp = URLEncoder.encode(map.get(s), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
sb.append(s).append("=").append(temp).append("&");
}
map.keySet().forEach(s -> sb.append(s).append("=").append(URLEncodeUtil.encode(map.get(s))).append("&"));
return sb.deleteCharAt(sb.length() - 1).toString();
}
public static String listToString(List<String> list) {
if (null == list || list.isEmpty()) {
return null;
}
StringBuilder stringBuffer = new StringBuilder();
stringBuffer.append("[\"");
for (String s : list) {

View File

@ -6,6 +6,7 @@ import com.jdcloud.sdk.service.sms.model.BatchSendRequest;
import com.jdcloud.sdk.service.sms.model.BatchSendResult;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
@ -96,11 +97,9 @@ public class JdCloudSmsImpl extends AbstractSmsBlend<JdCloudConfig> {
try {
smsResponse = getSmsResponse(result);
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) {
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
@ -121,10 +120,6 @@ public class JdCloudSmsImpl extends AbstractSmsBlend<JdCloudConfig> {
* @return 发送短信返回信息
*/
private SmsResponse getSmsResponse(BatchSendResult res) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess(res.getStatus() != null && res.getStatus());
smsResponse.setData(res);
smsResponse.setConfigId(getConfigId());
return smsResponse;
return SmsRespUtils.resp(res, res.getStatus() != null && res.getStatus(), getConfigId());
}
}

View File

@ -0,0 +1,72 @@
package org.dromara.sms4j.jg.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* <p>类名: JgConfig
* <p>说明极光 sms
*
* @author :SmartFire
* 2024/3/15
**/
@Data
@EqualsAndHashCode(callSuper = true)
public class JgConfig extends BaseConfig {
/**
* 签名 ID该字段为空则使用应用默认签名
*/
private String signId;
/**
* 调用地址
*/
private String requestUrl = Constant.HTTPS_PREFIX + "api.sms.jpush.cn/v1/";
/**
* 默认请求方法 messages
* 发送文本验证码短信 codes
* 发送语音验证码短信 voice_codes
* 验证验证码是否有效 valid
* 注意此处直接写valid即为验证码验证请求 系统会自动补充完整请求地址为codes/{msg_id}/valid (:msg_id 为调用发送验证码 API 的返回值)
* 发送单条模板短信 messages
* 发送批量模板短信 messages/batch
*/
private String action = "messages";
/**
* 模板变量名称
*/
private String templateName;
/**
* action设置为voice_codes有效
* 语音验证码播报语言选择0中文播报1英文播报2中英混合播报
*/
private String voice;
/**
* action设置为voice_codes有效
* 验证码有效期默认为 60
*/
private Integer ttl = 60;
/**
* action设置为messages/batch有效
* 标签
*/
private String tag;
/**
* 获取供应商
*
* @since 3.0.0
*/
@Override
public String getSupplier() {
return SupplierConstant.JIGUANG;
}
}

View File

@ -0,0 +1,48 @@
package org.dromara.sms4j.jg.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.jg.service.JgSmsImpl;
import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
/**
* <p>类名: JgFactory
* <p>说明极光 sms
*
* @author :SmartFire
* 2024/3/15
**/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class JgFactory extends AbstractProviderFactory<JgSmsImpl, JgConfig> {
private static final JgFactory INSTANCE = new JgFactory();
/**
* 获取建造者实例
* @return 建造者实例
*/
public static JgFactory instance() {
return INSTANCE;
}
/**
* 创建短信实现对象
* @param config 短信配置对象
* @return 短信实现对象
*/
@Override
public JgSmsImpl createSms(JgConfig config) {
return new JgSmsImpl(config);
}
/**
* 获取供应商
* @return 供应商
*/
@Override
public String getSupplier() {
return SupplierConstant.JIGUANG;
}
}

View File

@ -0,0 +1,140 @@
package org.dromara.sms4j.jg.service;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.jg.config.JgConfig;
import org.dromara.sms4j.jg.util.JgUtils;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* <p>类名: JgSmsImpl
* <p>说明极光 sms
*
* @author :SmartFire
* 2024/3/15
**/
@Slf4j
public class JgSmsImpl extends AbstractSmsBlend<JgConfig> {
private int retry = 0;
public JgSmsImpl(JgConfig config, Executor pool, DelayedTime delayedTime) {
super(config, pool, delayedTime);
}
public JgSmsImpl(JgConfig config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.JIGUANG;
}
@Override
public SmsResponse sendMessage(String phone, String message) {
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
if (SmsUtils.isNotEmpty(getConfig().getTemplateName()) &&
SmsUtils.isNotEmpty(message)){
map.put(getConfig().getTemplateName(), message);
}
return sendMessage(phone, getConfig().getTemplateId(), map);
}
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) {
if (SmsUtils.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return sendMessage(phone, getConfig().getTemplateId(), messages);
}
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
if (SmsUtils.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponse(phone, messages, templateId, null, null);
}
@Override
public SmsResponse massTexting(List<String> phones, String message) {
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
if (SmsUtils.isNotEmpty(getConfig().getTemplateName()) &&
SmsUtils.isNotEmpty(message)){
map.put(getConfig().getTemplateName(), message);
}
return massTexting(phones, getConfig().getTemplateId(), map);
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
if (SmsUtils.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponse(SmsUtils.addCodePrefixIfNot(phones), messages, templateId, null, null);
}
/**
* 自定义方法
* 发送语音验证码短信 请确保action配置为voice_codes
* @param phone 手机号
* @param code 语音验证码 可不填
*/
public SmsResponse sendVoiceCode(String phone, String code){
return getSmsResponse(phone, null, null, code, null);
}
/**
* 自定义方法
* 验证验证码是否有效 请确保action配置为voice_codes
* @param msgId 为调用发送验证码 API 的返回值
* @param code 验证码
*/
public SmsResponse verifyCode(String code, String msgId){
return getSmsResponse(null, null, null, code, msgId);
}
private SmsResponse getSmsResponse(String phone, LinkedHashMap<String, String> messages,
String templateId, String code, String msgId) {
SmsResponse smsResponse;
JgConfig config = getConfig();
String url = JgUtils.buildUrl(config.getRequestUrl(), config.getAction(), msgId);
Map<String, String> headers = JgUtils.buildHeaders(config.getAccessKeyId(), config.getAccessKeySecret());
Map<String, Object> body= JgUtils.buildBody(phone, messages, templateId, config, code);
String jsonKey = JgUtils.buildJsonKey(config.getAction());
try {
smsResponse = getResponse(http.postJson(url, headers, body), jsonKey);
} catch (SmsBlendException e) {
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
return requestRetry(phone, messages, templateId, code, msgId);
}
private SmsResponse requestRetry(String phone, LinkedHashMap<String, String> messages,
String templateId, String code, String msgId) {
http.safeSleep(getConfig().getRetryInterval());
retry ++;
log.warn("短信第 {} 次重新发送", retry);
return getSmsResponse(phone, messages, templateId, code, msgId);
}
private SmsResponse getResponse(JSONObject resJson, String jsonKey) {
return SmsRespUtils.resp(resJson, resJson.getObj(jsonKey) != null, getConfigId());
}
}

View File

@ -0,0 +1,268 @@
package org.dromara.sms4j.jg.util;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.jg.config.JgConfig;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* <p>类名: JgHelper
* <p>说明极光 sms
*
* @author :SmartFire
* 2024/3/15
**/
@Slf4j
public class JgUtils {
/**
* 构造请求地址
* @param baseUrl 配置的baseUrl
* @param action 请求方法
* @param msgId 验证验证码是否有效时使用 msgId 为调用发送验证码 API 的返回值
* @return url
*/
public static String buildUrl(String baseUrl, String action, String msgId) {
if ("valid".equals(action)){
check(msgId);
return baseUrl + "codes/" + msgId + "/" + action;
}else {
return baseUrl + action;
}
}
/**
* 构造请求头
* @param accessKeyId appKey
* @param accessKeySecret appKey
* @return 请求头
*/
public static Map<String, String> buildHeaders(String accessKeyId, String accessKeySecret){
check(accessKeyId);
check(accessKeySecret);
Map<String, String> headers = new LinkedHashMap<>(3);
headers.put(Constant.ACCEPT, Constant.APPLICATION_JSON);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_JSON_UTF8);
headers.put(Constant.AUTHORIZATION, "Basic " + Base64.encode(accessKeyId + ":" + accessKeySecret, StandardCharsets.UTF_8));
return headers;
}
/**
* 构造请求body
* @param phone 手机号
* @param messages 消息体
* @param templateId 模板 ID
* @param config 配置
* @param code 验证码
* @return 请求body
*/
public static Map<String, Object> buildBody(String phone, LinkedHashMap<String, String> messages,
String templateId, JgConfig config, String code) {
checkAction(config.getAction());
switch (config.getAction()){
case "codes":
return buildBody(phone, config.getSignId(), templateId);
case "voice_codes":
return buildBody(phone, code, config.getVoice(), config.getTtl());
case "valid":
return buildBody(code);
case "messages/batch":
return buildBody(phone, config.getSignId(), templateId, config.getTag(), messages);
default:
return buildBody(phone, config.getSignId(), templateId, messages);
}
}
/**
* 构造返回json验证Key值
* @param action 请求方法
* @return 返回json验证Key值
*/
public static String buildJsonKey(String action){
checkAction(action);
switch (action){
case "valid":
return "is_valid";
case "messages/batch":
return "success_count";
default:
return "msg_id";
}
}
/**
* 构造请求body 发送文本验证码短信
* @param phone 手机号
* @param signId 签名 ID该字段为空则使用应用默认签名
* @param templateId 模板 ID
* @return 请求body
*/
private static Map<String, Object> buildBody(String phone, String signId, String templateId) {
checkSingle(phone);
Map<String, Object> map = new LinkedHashMap<>(2);
map.put("mobile", phone);
check(templateId);
map.put("temp_id", templateId);
if (SmsUtils.isNotEmpty(signId)){
map.put("sign_id", signId);
}
return map;
}
/**
* 构造请求body 发送语音验证码短信
* @param phone 手机号
* @param code 语音验证码的值验证码仅支持 4-8 个数字 可为空
* @param voice 语音验证码播报语言选择0中文播报1英文播报2中英混合播报
* @param ttl 验证码有效期默认为 60
* @return 请求body
*/
private static Map<String, Object> buildBody(String phone, String code, String voice, Integer ttl) {
checkSingle(phone);
Map<String, Object> map = new LinkedHashMap<>(1);
map.put("mobile", phone);
if (SmsUtils.isNotEmpty(code)) {
map.put("code", code);
}
if (SmsUtils.isNotEmpty(voice)){
checkVoice(voice);
map.put("voice_lang", voice);
}
if (ttl == null || ttl <= 0){
map.put("ttl", 60);
}else {
map.put("ttl", ttl);
}
return map;
}
/**
* 构造请求body 验证验证码是否有效
* @param code 验证码
* @return 请求body
*/
private static Map<String, Object> buildBody(String code) {
check(code);
Map<String, Object> map = new LinkedHashMap<>(1);
map.put("code", code);
return map;
}
/**
* 构造请求body 发送单条模板短信
* @param phone 手机号码
* @param signId 签名 ID该字段为空则使用应用默认签名
* @param templateId 模板 ID
* @param messages 模板参数,需要替换的参数名和 value 的键值对 可为空
* @return 请求body
*/
private static Map<String, Object> buildBody(String phone, String signId, String templateId, LinkedHashMap<String, String> messages) {
checkSingle(phone);
Map<String, Object> map = new LinkedHashMap<>(1);
map.put("mobile", phone);
if (SmsUtils.isNotEmpty(signId)){
map.put("sign_id", signId);
}
check(templateId);
map.put("temp_id", templateId);
checkMessages(messages);
map.put("temp_para", messages);
return map;
}
/**
* 构造请求body 发送批量模板短信
* @param phone 手机号码列表
* @param signId 签名 ID该字段为空则使用应用默认签名
* @param templateId 模板 ID
* @param tag 标签 可为空
* @param messages 模板参数,需要替换的参数名和 value 的键值对
* @return 请求body
*/
private static Map<String, Object> buildBody(String phone, String signId, String templateId,
String tag, LinkedHashMap<String, String> messages) {
Set<String> phones = build(phone);
Map<String, Object> map = new LinkedHashMap<>(1);
if (SmsUtils.isNotEmpty(signId)){
map.put("sign_id", signId);
}
if (SmsUtils.isNotEmpty(tag)){
map.put("tag", tag);
}
if (SmsUtils.isEmpty(templateId)){
log.error("templateId is required");
throw new SmsBlendException("templateId is required");
}
map.put("temp_id", templateId);
if (SmsUtils.isEmpty(messages)){
log.error("temp_para is required");
throw new SmsBlendException("temp_para is required");
}
List<Map<String, Object>> recipients = new ArrayList<>(phones.size());
phones.forEach(mobile -> {
Map<String, Object> params = new LinkedHashMap<>(1);
params.put("mobile", StrUtil.addPrefixIfNot(mobile, "+86"));
params.put("temp_para", messages);
recipients.add(params);
});
map.put("recipients", recipients);
return map;
}
private static Set<String> build(String phone){
check(phone);
return Arrays.stream(phone.split(","))
.filter(SmsUtils::isNotEmpty)
.map(String::trim)
.collect(Collectors.toSet());
}
private static void checkSingle(String phone){
Set<String> phones = build(phone);
if (phones.size() > 1) {
log.error("Only a single mobile number is supported");
throw new SmsBlendException("Only a single mobile number is supported");
}
}
private static void checkMessages(LinkedHashMap<String, String> messages){
if (SmsUtils.isEmpty(messages)){
log.error("temp_para is required");
throw new SmsBlendException("temp_para is required");
}
}
private static void checkVoice(String voice){
if (!StrUtil.equalsAny(voice, "0", "1", "2")){
log.error("voice_lang is error, the value of an is only [1,2,3]");
throw new SmsBlendException("voice_lang is error, the value of an is only [1,2,3]");
}
}
private static void checkAction(String action){
if (SmsUtils.isEmpty(action) || !StrUtil.equalsAny(action, "codes", "voice_codes", "valid", "messages", "messages/batch")){
log.error("Unknown action method");
throw new SmsBlendException("Unknown action method");
}
}
private static void check(String str){
if (SmsUtils.isEmpty(str)){
String error = str + " is required";
log.error(error);
throw new SmsBlendException(error);
}
}
}

View File

@ -3,6 +3,7 @@ package org.dromara.sms4j.lianlu.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.lianlu.req.LianLuRequest;
import org.dromara.sms4j.lianlu.utils.LianLuUtils;
@ -10,7 +11,7 @@ import org.dromara.sms4j.provider.config.BaseConfig;
/**
* 联麓短信
* <a href="https://console.shlianlu.com/#/document/smsDoc">官方文档</a>
* <a href=Constant.HTTPS_PREFIX + "console.shlianlu.com/#/document/smsDoc">官方文档</a>
*
* @author lym
*/
@ -35,7 +36,7 @@ public class LianLuConfig extends BaseConfig {
*/
private String signType = LianLuUtils.SIGN_TYPE_MD5;
private String requestUrl = "https://apis.shlianlu.com/sms/trade";
private String requestUrl = Constant.HTTPS_PREFIX + "apis.shlianlu.com/sms/trade";
@Override
public String getSupplier() {

View File

@ -4,6 +4,9 @@ import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.lianlu.service.LianLuSmsImpl;
import org.dromara.sms4j.provider.factory.BaseProviderFactory;
/**
* 联鹿短信
* */
public class LianLuFactory implements BaseProviderFactory<LianLuSmsImpl, LianLuConfig> {
private static final LianLuFactory INSTANCE = new LianLuFactory();

View File

@ -3,6 +3,8 @@ package org.dromara.sms4j.lianlu.service;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
@ -177,8 +179,8 @@ public class LianLuSmsImpl extends AbstractSmsBlend<LianLuConfig> {
try {
Map<String, String> headers = new HashMap<>(2);
headers.put("Content-Type", "application/json;charset=utf-8");
headers.put("Accept", "application/json");
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_JSON_UTF8);
headers.put(Constant.ACCEPT, Constant.APPLICATION_JSON);
SmsResponse smsResponse = this.getResponse(this.http.postJson(reqUrl, headers, requestBody));
if (!smsResponse.isSuccess() && this.retry != this.getConfig().getMaxRetries()) {
return this.requestRetry(req);
@ -199,10 +201,6 @@ public class LianLuSmsImpl extends AbstractSmsBlend<LianLuConfig> {
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess("00".equals(resJson.getStr("status")));
smsResponse.setData(resJson);
smsResponse.setConfigId(this.getConfigId());
return smsResponse;
return SmsRespUtils.resp(resJson, "00".equals(resJson.getStr("status")), getConfigId());
}
}

View File

@ -0,0 +1,42 @@
package org.dromara.sms4j.luosimao.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* <p>类名: LuoSiMaoConfig
* <p>说明 螺丝帽短信差异配置
*
* @author :bleachtred
* 2024/6/21 23:59
**/
@Data
@EqualsAndHashCode(callSuper = true)
public class LuoSiMaoConfig extends BaseConfig {
/**
* 请求地址
*/
private String host = Constant.HTTPS_PREFIX + "sms-api.luosimao.com/v1/";
/**
* 接口名称
* 发送短信接口详细 send.json
* 批量发送接口详细 send_batch.json
* 查询账户余额 status.json
*/
private String action = "send.json";
/**
* 获取供应商
*
* @since 3.0.0
*/
@Override
public String getSupplier() {
return SupplierConstant.LUO_SI_MAO;
}
}

View File

@ -0,0 +1,47 @@
package org.dromara.sms4j.luosimao.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.luosimao.service.LuoSiMaoSmsImpl;
import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
/**
* <p>类名: LuoSiMaoFactory
*
* @author :bleachtred
* 2024/6/21 23:59
**/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LuoSiMaoFactory extends AbstractProviderFactory<LuoSiMaoSmsImpl, LuoSiMaoConfig> {
private static final LuoSiMaoFactory INSTANCE = new LuoSiMaoFactory();
/**
* 获取建造者实例
* @return 建造者实例
*/
public static LuoSiMaoFactory instance() {
return INSTANCE;
}
/**
* <p> 建造一个短信实现对像
*
* @author :bleachtred
*/
@Override
public LuoSiMaoSmsImpl createSms(LuoSiMaoConfig config) {
return new LuoSiMaoSmsImpl(config);
}
/**
* 获取供应商
* @return 供应商
*/
@Override
public String getSupplier() {
return SupplierConstant.LUO_SI_MAO;
}
}

View File

@ -0,0 +1,145 @@
package org.dromara.sms4j.luosimao.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.luosimao.config.LuoSiMaoConfig;
import org.dromara.sms4j.luosimao.utils.LuoSiMaoUtils;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* <p>类名: LuoSiMaoSmsImpl
* <p>说明 螺丝帽短信差异配置
*
* @author :bleachtred
* 2024/6/21 23:59
**/
@Slf4j
public class LuoSiMaoSmsImpl extends AbstractSmsBlend<LuoSiMaoConfig> {
private int retry = 0;
public LuoSiMaoSmsImpl(LuoSiMaoConfig config, Executor pool, DelayedTime delayedTime) {
super(config, pool, delayedTime);
}
public LuoSiMaoSmsImpl(LuoSiMaoConfig config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.LUO_SI_MAO;
}
@Override
public SmsResponse sendMessage(String phone, String message) {
return getSmsResponse(Collections.singletonList(phone), message, null, false, false);
}
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) {
throw new SmsBlendException("不支持此方法");
}
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
throw new SmsBlendException("不支持此方法");
}
@Override
public SmsResponse massTexting(List<String> phones, String message) {
return getSmsResponse(phones, message, null, false, false);
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
throw new SmsBlendException("不支持此方法");
}
/**
* 定时批量发送
* @param phones 手机号
* @param message 信息
* @param date 时间
* @return SmsResponse
*/
public SmsResponse massTextingOnTime(List<String> phones, String message, Date date) {
return getSmsResponse(phones, message, date, true, false);
}
/**
* 查询账户余额 请将接口设置为 status.json
*
* @return SmsResponse
*/
public SmsResponse queryAccountBalance() {
return getSmsResponse(null, null, null, false, true);
}
private SmsResponse getSmsResponse(List<String> phones, String message, Date date, boolean batch, boolean status) {
LuoSiMaoConfig config = getConfig();
SmsResponse smsResponse;
try {
String url = config.getHost() + config.getAction();
LinkedHashMap<String, Object> body;
if (status){
if ("status.json".equals(config.getAction())){
log.error("please set the request interface method to status.json");
throw new SmsBlendException("please set the request interface method to status.json");
}
smsResponse = getResponse(http.getBasic(url, "api", "key-" + config.getAccessKeyId()));
} else {
if (CollUtil.isEmpty(phones)){
log.error("mobile number is required");
throw new SmsBlendException("mobile number is required");
}
if (StrUtil.isBlank(message)){
log.error("message number is required");
throw new SmsBlendException("message number is required");
}
if (batch){
body = LuoSiMaoUtils.buildBody(phones, message, date);
}else {
body = LuoSiMaoUtils.buildBody(phones.get(0), message);
}
smsResponse = getResponse(http.postBasicFrom(url, LuoSiMaoUtils.buildHeaders(), "api", "key-" + config.getAccessKeyId(), body));
}
log.debug("短信发送结果:{}", smsResponse);
} catch (SmsBlendException e) {
log.error(e.message, e);
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == config.getMaxRetries()) {
retry = 0;
return smsResponse;
}
return requestRetry(phones, message, date, batch, status);
}
private SmsResponse requestRetry(List<String> phones, String message, Date date, boolean batch, boolean status) {
http.safeSleep(getConfig().getRetryInterval());
retry ++;
log.warn("短信第 {} 次重新发送", retry);
return getSmsResponse(phones, message, date, batch, status);
}
private SmsResponse getResponse(JSONObject resJson) {
return SmsRespUtils.resp(resJson, Objects.equals(0, resJson.getInt("error")), getConfigId());
}
}

View File

@ -0,0 +1,37 @@
package org.dromara.sms4j.luosimao.utils;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.utils.SmsDateUtils;
import org.dromara.sms4j.comm.utils.SmsUtils;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
@Slf4j
public class LuoSiMaoUtils {
public static LinkedHashMap<String, String> buildHeaders() {
LinkedHashMap<String, String> headers = new LinkedHashMap<>(1);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_FROM_URLENCODED);
return headers;
}
public static LinkedHashMap<String, Object> buildBody(String phone, String message) {
LinkedHashMap<String, Object> body = new LinkedHashMap<>(2);
body.put("mobile", phone);
body.put("message", message);
return body;
}
public static LinkedHashMap<String, Object> buildBody(List<String> phones, String message, Date date) {
LinkedHashMap<String, Object> body = new LinkedHashMap<>(2);
body.put("mobile", SmsUtils.joinComma(phones));
body.put("message", message);
if (date != null) {
body.put("time", SmsDateUtils.normDatetimeGmt8(date));
}
return body;
}
}

View File

@ -0,0 +1,48 @@
package org.dromara.sms4j.mas.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* <p>类名: MasConfig
* <p>说明中国移动 云MAS
*
* @author :bleachtred
* 2024/4/22 13:40
**/
@Data
@EqualsAndHashCode(callSuper = true)
public class MasConfig extends BaseConfig {
/**
* 企业名称
*/
private String ecName;
/**
* 请求地址
*/
private String requestUrl = "http://112.35.1.155:1992/sms/";
/**
* 接口名称
*/
private String action = "tmpsubmit";
/**
* 扩展码
*/
private String addSerial;
/**
* 获取供应商
*
* @since 3.0.0
*/
@Override
public String getSupplier() {
return SupplierConstant.MAS;
}
}

View File

@ -0,0 +1,49 @@
package org.dromara.sms4j.mas.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.mas.service.MasSmsImpl;
import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
/**
* <p>类名: MasFactory
* <p>说明中国移动 云MAS短信配置器
*
* @author :bleachtred
* 2024/4/22 13:40
**/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MasFactory extends AbstractProviderFactory<MasSmsImpl, MasConfig> {
private static final MasFactory INSTANCE = new MasFactory();
/**
* 获取建造者实例
* @return 建造者实例
*/
public static MasFactory instance() {
return INSTANCE;
}
/**
* createSms
* <p> 建造一个短信实现对像
*
* @author :bleachtred
*/
@Override
public MasSmsImpl createSms(MasConfig masConfig) {
return new MasSmsImpl(masConfig);
}
/**
* 获取供应商
* @return 供应商
*/
@Override
public String getSupplier() {
return SupplierConstant.MAS;
}
}

View File

@ -0,0 +1,120 @@
package org.dromara.sms4j.mas.service;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.Executor;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.mas.config.MasConfig;
import org.dromara.sms4j.mas.utils.MasUtils;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
/**
* <p>类名: MasSmsImpl
* <p>说明中国移动 云MAS短信实现
*
* @author :bleachtred
* 2024/4/22 13:40
**/
@Slf4j
public class MasSmsImpl extends AbstractSmsBlend<MasConfig> {
private int retry = 0;
public MasSmsImpl(MasConfig config, Executor pool, DelayedTime delayedTime) {
super(config, pool, delayedTime);
}
public MasSmsImpl(MasConfig config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.MAS;
}
@Override
public SmsResponse sendMessage(String phone, String message) {
return getSmsResponse(phone, message, getConfig().getTemplateId());
}
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) {
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
return getSmsResponse(phone, JSONUtil.toJsonStr(messages.values()), getConfig().getTemplateId());
}
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
String messageStr = JSONUtil.toJsonStr(messages.values());
return getSmsResponse(phone, messageStr, templateId);
}
@Override
public SmsResponse massTexting(List<String> phones, String message) {
return getSmsResponse(SmsUtils.addCodePrefixIfNot(phones), message, getConfig().getTemplateId());
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
if (CollUtil.isEmpty(messages)){
messages = new LinkedHashMap<>();
}
String messageStr = JSONUtil.toJsonStr(messages.values());
return getSmsResponse(SmsUtils.addCodePrefixIfNot(phones), messageStr, templateId);
}
private SmsResponse getSmsResponse(String phone, String message, String templateId) {
String requestUrl;
String base64Code;
try {
MasConfig config = getConfig();
requestUrl = config.getRequestUrl() + config.getAction();
base64Code = MasUtils.base64Code(getConfig(), phone, message, templateId);
} catch (Exception e) {
log.error("mas 10086 send message error", e);
throw new SmsBlendException(e.getMessage());
}
log.debug("requestUrl {}", requestUrl);
SmsResponse smsResponse;
try {
smsResponse = getResponse(http.postJson(requestUrl, null, base64Code));
} catch (SmsBlendException e) {
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
return requestRetry(phone, message, templateId);
}
private SmsResponse requestRetry(String phone, String message, String templateId) {
http.safeSleep(getConfig().getRetryInterval());
retry ++;
log.warn("短信第 {} 次重新发送", retry);
return getSmsResponse(phone, message, templateId);
}
private SmsResponse getResponse(JSONObject resJson) {
return SmsRespUtils.resp(resJson, "success".equals(resJson.getStr("rspcod")) && resJson.getBool("success"), getConfigId());
}
}

View File

@ -0,0 +1,75 @@
package org.dromara.sms4j.mas.utils;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.mas.config.MasConfig;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MasUtils {
public static String base64Code(MasConfig config, String phone, String message, String templateId) {
Map<String, String> map = new HashMap<>(1);
StringBuilder sb = new StringBuilder();
if (StrUtil.isNotEmpty(config.getEcName())){
map.put("ecName", config.getEcName().trim());
sb.append(config.getEcName().trim());
}
if (StrUtil.isNotEmpty(config.getSdkAppId())){
map.put("apId", config.getSdkAppId().trim());
sb.append(config.getSdkAppId().trim());
}
if (StrUtil.isNotEmpty(config.getAccessKeySecret())){
map.put("secretKey", config.getAccessKeySecret().trim());
sb.append(config.getAccessKeySecret().trim());
}
if ("norsubmit".equals(config.getAction()) || "submit".equals(config.getAction())){
if (StrUtil.isNotEmpty(phone)){
map.put("mobiles", phone.trim());
sb.append(phone.trim());
}
if (StrUtil.isNotEmpty(message)){
map.put("content", message.trim());
sb.append(message.trim());
}
}else if ("tmpsubmit".equals(config.getAction())){
if (StrUtil.isNotEmpty(templateId)){
sb.append(templateId.trim());
map.put("templateId", templateId);
}
if (StrUtil.isNotEmpty(phone)){
map.put("mobiles", phone.trim());
sb.append(phone.trim());
}
if (StrUtil.isNotEmpty(message)){
map.put("params", message.trim());
sb.append(message.trim());
}else {
String emptyParams = JSONUtil.toJsonStr(new String[]{""});
map.put("params", emptyParams);
sb.append(emptyParams);
}
}
if (StrUtil.isNotEmpty(config.getSignature())){
map.put("sign", config.getSignature().trim());
sb.append(config.getSignature().trim());
}
if (StrUtil.isNotEmpty(config.getAddSerial())){
map.put("addSerial", config.getAddSerial().trim());
sb.append(config.getAddSerial().trim());
}else {
map.put("addSerial", "");
}
map.put("mac", DigestUtil.md5Hex(sb.toString(), StandardCharsets.UTF_8));
return Base64.encode(JSONUtil.toJsonStr(map), StandardCharsets.UTF_8);
}
}

View File

@ -2,6 +2,7 @@ package org.dromara.sms4j.netease.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
@ -21,18 +22,17 @@ public class NeteaseConfig extends BaseConfig {
/**
* 模板短信请求地址
*/
private String templateUrl = "https://api.netease.im/sms/sendtemplate.action";
private String templateUrl = Constant.HTTPS_PREFIX + "api.netease.im/sms/sendtemplate.action";
/**
* 验证码短信请求地址
*/
private String codeUrl = "https://api.netease.im/sms/sendcode.action";
private String codeUrl = Constant.HTTPS_PREFIX + "api.netease.im/sms/sendcode.action";
/**
* 验证码验证请求地址
*/
private String verifyUrl = "https://api.netease.im/sms/verifycode.action";
private String verifyUrl = Constant.HTTPS_PREFIX + "api.netease.im/sms/verifycode.action";
/**
* 是否需要支持短信上行true:需要false:不需要

View File

@ -9,6 +9,7 @@ import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
@ -97,7 +98,7 @@ public class NeteaseSmsImpl extends AbstractSmsBlend<NeteaseConfig> {
throw new SmsBlendException("单次发送超过最大发送上限建议每次群发短信人数低于100");
}
Optional.ofNullable(getConfig().getTemplateId()).orElseThrow(() -> new SmsBlendException("模板ID不能为空"));
return getSmsResponse(getConfig().getTemplateUrl(), phones, getConfig().getTemplateId(), message);
return getSmsResponse(getConfig().getTemplateUrl(), phones,message, getConfig().getTemplateId());
}
@Override
@ -133,7 +134,7 @@ public class NeteaseSmsImpl extends AbstractSmsBlend<NeteaseConfig> {
body.put("needUp", getConfig().getNeedUp());
Map<String, String> headers = MapUtil.newHashMap(5, true);
headers.put("Content-Type", Constant.FROM_URLENCODED);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_FROM_URLENCODED);
headers.put("AppKey", getConfig().getAccessKeyId());
headers.put("Nonce", nonce);
headers.put("CurTime", curTime);
@ -142,11 +143,9 @@ public class NeteaseSmsImpl extends AbstractSmsBlend<NeteaseConfig> {
try {
smsResponse = getResponse(http.postFrom(requestUrl, headers, body));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) {
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
@ -161,11 +160,7 @@ public class NeteaseSmsImpl extends AbstractSmsBlend<NeteaseConfig> {
}
private SmsResponse getResponse(JSONObject jsonObject) {
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess(jsonObject.getInt("code") <= 200);
smsResponse.setData(jsonObject);
smsResponse.setConfigId(getConfigId());
return smsResponse;
return SmsRespUtils.resp(jsonObject, jsonObject.getInt("code") <= 200, getConfigId());
}
}

View File

@ -1,6 +1,7 @@
package org.dromara.sms4j.provider.config;
import lombok.Data;
import org.dromara.sms4j.api.universal.ProxyConfig;
import org.dromara.sms4j.api.universal.SupplierConfig;
import org.dromara.sms4j.comm.exception.SmsBlendException;
@ -22,6 +23,7 @@ public abstract class BaseConfig implements SupplierConfig {
* Access Key
*/
private String accessKeyId;
/**
* Sdk App Id
*/
@ -55,6 +57,12 @@ public abstract class BaseConfig implements SupplierConfig {
*/
private String configId;
/**
* 代理配置
*
*/
private ProxyConfig proxy;
/**
* 重试间隔单位默认为5秒
*/

View File

@ -1,7 +1,7 @@
package org.dromara.sms4j.provider.config;
public class SmsBanner {
private static final String banner =
private static final String BANNER =
" ________ _____ ______ ________ ___ ___ ___ \n" +
"|\\ ____\\|\\ _ \\ _ \\|\\ ____\\|\\ \\ |\\ \\ |\\ \\ \n" +
"\\ \\ \\___|\\ \\ \\\\\\__\\ \\ \\ \\ \\___|\\ \\ \\\\_\\ \\ \\ \\ \\ \n" +
@ -12,6 +12,6 @@ public class SmsBanner {
" \\|_________| \\|_________| \n";
/** 初始化配置文件时打印banner*/
public static void PrintBanner(String version) {
System.out.println(banner+version);
System.out.println(BANNER +version);
}
}

View File

@ -2,7 +2,7 @@ package org.dromara.sms4j.provider.config;
import lombok.Data;
import org.dromara.sms4j.comm.enumerate.ConfigType;
import org.dromara.sms4j.comm.enums.ConfigType;
import java.util.ArrayList;

View File

@ -17,13 +17,13 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public class ProviderFactoryHolder {
private static final Map<String, BaseProviderFactory<? extends SmsBlend, ? extends SupplierConfig>> factories = new ConcurrentHashMap<>();
private static final Map<String, BaseProviderFactory<? extends SmsBlend, ? extends SupplierConfig>> FACTORIES = new ConcurrentHashMap<>();
public static void registerFactory(BaseProviderFactory<? extends SmsBlend, ? extends SupplierConfig> factory) {
if(factory == null) {
throw new SmsBlendException("注册供应商工厂失败,工厂实例不能为空");
}
factories.put(factory.getSupplier(), factory);
FACTORIES.put(factory.getSupplier(), factory);
}
public static void registerFactory(List<BaseProviderFactory<? extends SmsBlend, ? extends SupplierConfig>> factoryList) {
@ -39,7 +39,7 @@ public class ProviderFactoryHolder {
}
public static BaseProviderFactory<? extends SmsBlend, ? extends SupplierConfig> requireForSupplier(String supplier) {
return factories.getOrDefault(supplier, null);
return FACTORIES.getOrDefault(supplier, null);
}
}

View File

@ -5,7 +5,9 @@ import lombok.Getter;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.callback.CallBack;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.universal.ProxyConfig;
import org.dromara.sms4j.api.universal.SupplierConfig;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.utils.SmsHttpUtils;
import org.dromara.sms4j.provider.factory.BeanFactory;
@ -31,13 +33,19 @@ public abstract class AbstractSmsBlend<C extends SupplierConfig> implements SmsB
protected final DelayedTime delayed;
protected final SmsHttpUtils http = SmsHttpUtils.instance();
protected final SmsHttpUtils http;
protected AbstractSmsBlend(C config, Executor pool, DelayedTime delayed) {
this.configId = StrUtil.isEmpty(config.getConfigId()) ? getSupplier() : config.getConfigId();
this.config = config;
this.pool = pool;
this.delayed = delayed;
ProxyConfig proxy = config.getProxy();
if (proxy != null && proxy.getEnable()){
this.http = SmsHttpUtils.instance(proxy.getHost(), proxy.getPort());
}else {
this.http = SmsHttpUtils.instance();
}
}
protected AbstractSmsBlend(C config) {
@ -45,6 +53,12 @@ public abstract class AbstractSmsBlend<C extends SupplierConfig> implements SmsB
this.config = config;
this.pool = BeanFactory.getExecutor();
this.delayed = BeanFactory.getDelayedTime();
ProxyConfig proxy = config.getProxy();
if (proxy != null && proxy.getEnable()){
this.http = SmsHttpUtils.instance(proxy.getHost(), proxy.getPort());
}else {
this.http = SmsHttpUtils.instance();
}
}
protected C getConfig() {
@ -62,7 +76,6 @@ public abstract class AbstractSmsBlend<C extends SupplierConfig> implements SmsB
* message 消息内容
* @author :Wind
*/
@Override
public abstract SmsResponse sendMessage(String phone, String message);
@ -84,7 +97,6 @@ public abstract class AbstractSmsBlend<C extends SupplierConfig> implements SmsB
* @param messages key为模板变量名称 value为模板变量值
* @author :Wind
*/
@Override
public abstract SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages);
@ -94,7 +106,6 @@ public abstract class AbstractSmsBlend<C extends SupplierConfig> implements SmsB
*
* @author :Wind
*/
@Override
public abstract SmsResponse massTexting(List<String> phones, String message);
@ -104,7 +115,6 @@ public abstract class AbstractSmsBlend<C extends SupplierConfig> implements SmsB
*
* @author :Wind
*/
@Override
public abstract SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages);
@ -145,7 +155,6 @@ public abstract class AbstractSmsBlend<C extends SupplierConfig> implements SmsB
* @param callBack 回调
* @author :Wind
*/
@Override
public final void sendMessageAsync(String phone, String templateId, LinkedHashMap<String, String> messages, CallBack callBack){
CompletableFuture<SmsResponse> smsResponseCompletableFuture = CompletableFuture.supplyAsync(() -> sendMessage(phone,templateId, messages), pool);
@ -240,4 +249,13 @@ public abstract class AbstractSmsBlend<C extends SupplierConfig> implements SmsB
}
}, delayedTime);
}
/**
* 返回异常
* @param errorMsg 异常信息
* @return SmsResponse
*/
public SmsResponse errorResp(String errorMsg){
return SmsRespUtils.error(errorMsg, config.getConfigId());
}
}

View File

@ -2,11 +2,12 @@ package org.dromara.sms4j.qiniu.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* @author Administrator
* @author YYM
* @Date: 2024/1/30 15:56 30
* @描述: QiNiuConfig
**/
@ -18,7 +19,7 @@ public class QiNiuConfig extends BaseConfig {
/**
* 请求地址
*/
private String baseUrl = "https://sms.qiniuapi.com";
private String baseUrl = Constant.HTTPS_PREFIX + "sms.qiniuapi.com";
/**
* 模板变量名称

View File

@ -7,7 +7,7 @@ import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
import org.dromara.sms4j.qiniu.service.QiNiuSmsImpl;
/**
* @author Administrator
* @author YYM
* @Date: 2024/1/30 16:06 29
* @描述: QiNiuFactory
**/

View File

@ -1,11 +1,13 @@
package org.dromara.sms4j.qiniu.service;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import org.dromara.sms4j.qiniu.config.QiNiuConfig;
import org.dromara.sms4j.qiniu.util.QiNiuUtils;
@ -17,7 +19,7 @@ import java.util.Objects;
import java.util.concurrent.Executor;
/**
* @author Administrator
* @author YYM
* @Date: 2024/1/30 16:06 59
* @描述: QiNiuSmsImpl
**/
@ -71,7 +73,6 @@ public class QiNiuSmsImpl extends AbstractSmsBlend<QiNiuConfig> {
return senMassMsg(phones, templateId, messages);
}
/**
* @return SmsResponse
* @author 初拥
@ -79,12 +80,15 @@ public class QiNiuSmsImpl extends AbstractSmsBlend<QiNiuConfig> {
* @Description: 统一处理返回结果
*/
public SmsResponse handleRes(String url, HashMap<String, Object> params) {
JSONObject jsonObject = http.postJson(url, QiNiuUtils.getHeaderAndSign(url, params, getConfig()), params);
SmsResponse smsResponse = new SmsResponse();
smsResponse.setSuccess(ObjectUtil.isEmpty(jsonObject.getStr("error")));
smsResponse.setData(jsonObject);
smsResponse.setConfigId(getConfigId());
if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) {
JSONObject jsonObject;
SmsResponse smsResponse;
try {
jsonObject = http.postJson(url, QiNiuUtils.getHeaderAndSign(url, params, getConfig()), params);
smsResponse = SmsRespUtils.resp(jsonObject, SmsUtils.isEmpty(jsonObject.getStr("error")), getConfigId());
}catch (SmsBlendException e){
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}

View File

@ -8,19 +8,18 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsDateUtils;
import org.dromara.sms4j.qiniu.config.QiNiuConfig;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
/**
* @author Administrator
* @author YYM
* @Date: 2024/1/30 16:37 50
* @描述: QiNiuUtils
**/
@ -35,7 +34,7 @@ public class QiNiuUtils {
StringBuilder dataToSign = new StringBuilder();
dataToSign.append(method.toUpperCase()).append(" ").append(reqUrl.getPath());
dataToSign.append("\nHost: ").append(reqUrl.getHost());
dataToSign.append("\n").append("Content-Type").append(": ").append(Constant.ACCEPT);
dataToSign.append("\n").append(Constant.CONTENT_TYPE).append(": ").append(Constant.APPLICATION_JSON);
dataToSign.append("\n").append("X-Qiniu-Date").append(": ").append(signDate);
dataToSign.append("\n\n");
if (ObjectUtil.isNotEmpty(body)) {
@ -50,9 +49,7 @@ public class QiNiuUtils {
public static Map<String, String> getHeaderAndSign(String url, HashMap<String, Object> hashMap, QiNiuConfig qiNiuConfig) {
String signature;
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
String signDate = dateFormat.format(new Date());
String signDate = SmsDateUtils.pureDateUtcGmt(new Date());
try {
signature = getSignature("POST", url, qiNiuConfig, JSONUtil.toJsonStr(hashMap), signDate);
} catch (Exception e) {
@ -62,9 +59,9 @@ public class QiNiuUtils {
//请求头
Map<String, String> header = new HashMap<>(3);
header.put("Authorization", signature);
header.put(Constant.AUTHORIZATION, signature);
header.put("X-Qiniu-Date", signDate);
header.put("Content-Type", "application/json");
header.put(Constant.CONTENT_TYPE, "application/json");
return header;
}
}

View File

@ -0,0 +1,56 @@
package org.dromara.sms4j.submail.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.config.BaseConfig;
/**
* <p>类名: SubMailConfig
* <p>说明 SUBMAIL短信差异配置
*
* @author :bleachtred
* 2024/6/22 13:59
**/
@Data
@EqualsAndHashCode(callSuper = true)
public class SubMailConfig extends BaseConfig {
/**
* 请求地址
*/
private String host = Constant.HTTPS_PREFIX + "api-v4.mysubmail.com/sms/";
/**
* 接口名称
* 短信发送 send.json
* 短信模板发送 xsend.json
* 短信一对多发送 multisend.json
* 短信模板一对多发送 multixsend.json
* 短信批量群发 batchsend.json
* 短信批量模板群发 batchxsend.json
*/
private String action = "send.json";
/**
* MD5 SHA-1 默认MD5 填写任意值不为即为 密匙明文验证模式
*/
private String signType = "MD5";
/**
* signature加密计算方式
* (当sign_version传2时会忽略某些字段)
*/
private String signVersion;
/**
* 获取供应商
*
* @since 3.0.0
*/
@Override
public String getSupplier() {
return SupplierConstant.MY_SUBMAIL;
}
}

View File

@ -0,0 +1,47 @@
package org.dromara.sms4j.submail.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.provider.factory.AbstractProviderFactory;
import org.dromara.sms4j.submail.service.SubMailSmsImpl;
/**
* <p>类名: SubMailFactory
*
* @author :bleachtred
* 2024/6/22 13:59
**/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SubMailFactory extends AbstractProviderFactory<SubMailSmsImpl, SubMailConfig> {
private static final SubMailFactory INSTANCE = new SubMailFactory();
/**
* 获取建造者实例
* @return 建造者实例
*/
public static SubMailFactory instance() {
return INSTANCE;
}
/**
* <p> 建造一个短信实现对像
*
* @author :bleachtred
*/
@Override
public SubMailSmsImpl createSms(SubMailConfig config) {
return new SubMailSmsImpl(config);
}
/**
* 获取供应商
* @return 供应商
*/
@Override
public String getSupplier() {
return SupplierConstant.MY_SUBMAIL;
}
}

View File

@ -0,0 +1,368 @@
package org.dromara.sms4j.submail.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
import org.dromara.sms4j.comm.utils.SmsUtils;
import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import org.dromara.sms4j.submail.config.SubMailConfig;
import org.dromara.sms4j.submail.utils.SubMailUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.Executor;
/**
* <p>类名: SubMailSmsImpl
*
* @author :bleachtred
* 2023/5/12 15:06
**/
@Slf4j
public class SubMailSmsImpl extends AbstractSmsBlend<SubMailConfig> {
private int retry = 0;
public SubMailSmsImpl(SubMailConfig config, Executor pool, DelayedTime delayedTime) {
super(config, pool, delayedTime);
}
public SubMailSmsImpl(SubMailConfig config) {
super(config);
}
@Override
public String getSupplier() {
return SupplierConstant.MY_SUBMAIL;
}
@Override
public SmsResponse sendMessage(String phone, String content) {
return getSmsResponse(Collections.singletonList(phone), content, getConfig().getTemplateId(), null);
}
@Override
public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> vars) {
if (MapUtil.isEmpty(vars)){
log.error("vars or content must be not null");
throw new SmsBlendException("vars or content must be not null");
}
String content = vars.get("content");
vars.remove("content");
return getSmsResponse(Collections.singletonList(phone), content, getConfig().getTemplateId(), vars);
}
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> vars) {
if (MapUtil.isEmpty(vars)){
log.error("vars or content must be not null");
throw new SmsBlendException("vars or content must be not null");
}
String content = vars.get("content");
vars.remove("content");
return getSmsResponse(Collections.singletonList(phone), content, templateId, vars);
}
@SuppressWarnings("unchecked")
@Override
public SmsResponse massTexting(List<String> phones, String content) {
if (StrUtil.isBlank(content)){
log.error("vars or content must be not null");
throw new SmsBlendException("vars or content must be not null");
}
return massTexting(phones, getConfig().getTemplateId(), BeanUtil.copyProperties(content, LinkedHashMap.class));
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> vars) {
if (MapUtil.isEmpty(vars)){
log.error("vars or content must be not null");
throw new SmsBlendException("vars or content must be not null");
}
String content = vars.get("content");
vars.remove("content");
return getSmsResponse(phones, content, templateId, vars);
}
private SmsResponse getSmsResponse(List<String> phones, String content, String templateId, LinkedHashMap<String, String> vars) {
if (CollUtil.isEmpty(phones)){
log.error("phones must be not null");
throw new SmsBlendException("phones must be not null");
}
SubMailConfig config = getConfig();
SmsResponse smsResponse;
String url = config.getHost() + config.getAction();
LinkedHashMap<String, Object> body;
switch (config.getAction()){
case "send.json":
if (StrUtil.isBlank(content)){
log.error("content must be not null");
throw new SmsBlendException("content must be not null");
}
body = buildSend(phones.get(0), content);
break;
case "xsend.json":
body = buildXSend(phones.get(0), templateId, vars);
break;
case "multisend.json":
if (StrUtil.isBlank(content)){
log.error("content must be not null");
throw new SmsBlendException("content must be not null");
}
if (MapUtil.isEmpty(vars)){
log.error("vars be not null");
throw new SmsBlendException("vars must be not null");
}
body = buildMultiSend(phones, content, vars);
break;
case "multixsend.json":
if (MapUtil.isEmpty(vars)){
log.error("vars be not null");
throw new SmsBlendException("vars must be not null");
}
body = buildMultiXSend(phones, templateId, vars);
break;
case "batchsend.json":
if (StrUtil.isBlank(content)){
log.error("vars or content must be not null");
throw new SmsBlendException("vars or content must be not null");
}
body = buildBatchSend(phones, content);
break;
case "batchxsend.json":
if (MapUtil.isEmpty(vars)){
log.error("vars be not null");
throw new SmsBlendException("vars must be not null");
}
body = buildBatchXSend(phones, templateId, vars);
break;
default:
log.error("不支持的短信发送类型");
throw new SmsBlendException("不支持的短信发送类型");
}
try {
smsResponse = getResponse(http.postJson(url, SubMailUtils.buildHeaders(), body));
log.debug("短信发送结果: {}", JSONUtil.toJsonStr(smsResponse));
} catch (SmsBlendException e) {
log.error(e.message, e);
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == config.getMaxRetries()) {
retry = 0;
return smsResponse;
}
return requestRetry(phones, content, templateId, vars);
}
private SmsResponse requestRetry(List<String> phones, String content, String templateId, LinkedHashMap<String, String> vars) {
http.safeSleep(getConfig().getRetryInterval());
retry ++;
log.warn("短信第 {} 次重新发送", retry);
return getSmsResponse(phones, content, templateId, vars);
}
private SmsResponse getResponse(JSONObject resJson) {
return SmsRespUtils.resp(resJson, "success".equals(resJson.getStr("status")), getConfigId());
}
/**
* SMS/Send - 短信发送
* @param phone 单个手机号
* @param content 短信内容
* @return 参数组装
*/
private LinkedHashMap<String, Object> buildSend(String phone, String content){
SubMailConfig config = getConfig();
LinkedHashMap<String, Object> body = new LinkedHashMap<>();
body.put("appid", config.getAccessKeyId());
body.put("to", StrUtil.addPrefixIfNot(phone, "+86"));
if (StrUtil.isNotBlank(config.getSignature())){
content = StrUtil.addPrefixIfNot(content, "" + config.getSignature() + "") + StrUtil.sub(content, 0, 1000);
}else {
content = StrUtil.sub(content, 0, 1000);
}
body.put("content", content);
body.put("timestamp", timestamp());
body.put("sign_type", config.getSignType());
if (StrUtil.isNotBlank(config.getSignVersion())){
body.put("sign_version", config.getSignVersion());
}
body.put("sign_type", config.getSignType());
String signature = SubMailUtils.signature(body, config.getSignType(), config.getAccessKeyId(), config.getAccessKeySecret(), "content");
body.put("signature", signature);
return body;
}
/**
* SMS/XSend - 短信模板发送
* @param phone 单个手机号
* @param templateId 短信模板ID
* @param vars 使用文本变量动态控制短信中的文本
* @return 参数组装
*/
private LinkedHashMap<String, Object> buildXSend(String phone, String templateId, LinkedHashMap<String, String> vars){
SubMailConfig config = getConfig();
LinkedHashMap<String, Object> body = new LinkedHashMap<>();
body.put("appid", config.getAccessKeyId());
body.put("to", StrUtil.addPrefixIfNot(phone, "+86"));
body.put("project", templateId);
if (MapUtil.isNotEmpty(vars)){
body.put("vars", JSONUtil.toJsonStr(vars));
}
body.put("timestamp", timestamp());
body.put("sign_type", config.getSignType());
if (StrUtil.isNotBlank(config.getSignVersion())){
body.put("sign_version", config.getSignVersion());
}
body.put("sign_type", config.getSignType());
String signature = SubMailUtils.signature(body, config.getSignType(), config.getAccessKeyId(), config.getAccessKeySecret(), "vars");
body.put("signature", signature);
return body;
}
/**
* SMS/MultiSend - 短信一对多发送
* 建议单线程提交数量控制在50个联系人 可以开多个线程同时发送
* @param phones N手机号
* @param content 短信内容
* @param vars 使用文本变量动态控制短信中的文本
* @return 参数组装
*/
private LinkedHashMap<String, Object> buildMultiSend(List<String> phones, String content, LinkedHashMap<String, String> vars){
SubMailConfig config = getConfig();
LinkedHashMap<String, Object> body = new LinkedHashMap<>();
body.put("appid", config.getAccessKeyId());
if (StrUtil.isNotBlank(config.getSignature())){
content = StrUtil.addPrefixIfNot(content, "" + config.getSignature() + "") + StrUtil.sub(content, 0, 1000);
}else {
content = StrUtil.sub(content, 0, 1000);
}
body.put("content", content);
phones = CollUtil.sub(phones, 0, 50);
List<LinkedHashMap<String, Object>> multi = new ArrayList<>(phones.size());
phones.forEach(phone -> {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("to", StrUtil.addPrefixIfNot(phone, "+86"));
map.put("vars", vars);
multi.add(map);
});
body.put("multi", JSONUtil.toJsonStr(multi));
body.put("timestamp", timestamp());
body.put("sign_type", config.getSignType());
if (StrUtil.isNotBlank(config.getSignVersion())){
body.put("sign_version", config.getSignVersion());
}
body.put("sign_type", config.getSignType());
String signature = SubMailUtils.signature(body, config.getSignType(), config.getAccessKeyId(), config.getAccessKeySecret(), "multi", "content");
body.put("signature", signature);
return body;
}
/**
* SMS/MultiXSend - 短信模板一对多发送
* 建议 单线程提交数量控制在50200个联系人 可以开多个线程同时发送
* @param phones N手机号
* @param templateId 短信模板ID
* @param vars 使用文本变量动态控制短信中的文本
* @return 参数组装
*/
private LinkedHashMap<String, Object> buildMultiXSend(List<String> phones, String templateId, LinkedHashMap<String, String> vars){
SubMailConfig config = getConfig();
LinkedHashMap<String, Object> body = new LinkedHashMap<>();
body.put("appid", config.getAccessKeyId());
body.put("project", templateId);
phones = CollUtil.sub(phones, 0, 200);
List<LinkedHashMap<String, Object>> multi = new ArrayList<>(phones.size());
phones.forEach(phone -> {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("to", StrUtil.addPrefixIfNot(phone, "+86"));
map.put("vars", vars);
multi.add(map);
});
body.put("multi", JSONUtil.toJsonStr(multi));
body.put("timestamp", timestamp());
body.put("sign_type", config.getSignType());
if (StrUtil.isNotBlank(config.getSignVersion())){
body.put("sign_version", config.getSignVersion());
}
body.put("sign_type", config.getSignType());
String signature = SubMailUtils.signature(body, config.getSignType(), config.getAccessKeyId(), config.getAccessKeySecret(), "multi", "content");
body.put("signature", signature);
return body;
}
/**
* SMS/BatchSend - 短信批量群发
* 单次请求最大支持 10000
* @param phones N手机号
* @param content 短信内容
* @return 参数组装
*/
private LinkedHashMap<String, Object> buildBatchSend(List<String> phones, String content){
SubMailConfig config = getConfig();
LinkedHashMap<String, Object> body = new LinkedHashMap<>();
body.put("appid", config.getAccessKeyId());
phones = CollUtil.sub(phones, 0, 10000);
body.put("to", SmsUtils.addCodePrefixIfNot(phones));
if (StrUtil.isNotBlank(config.getSignature())){
content = StrUtil.addPrefixIfNot(content, "" + config.getSignature() + "") + StrUtil.sub(content, 0, 1000);
}else {
content = StrUtil.sub(content, 0, 1000);
}
body.put("content", content);
body.put("timestamp", timestamp());
body.put("sign_type", config.getSignType());
if (StrUtil.isNotBlank(config.getSignVersion())){
body.put("sign_version", config.getSignVersion());
}
body.put("sign_type", config.getSignType());
String signature = SubMailUtils.signature(body, config.getSignType(), config.getAccessKeyId(), config.getAccessKeySecret(), "content");
body.put("signature", signature);
return body;
}
/**
* SMS/BatchXSend - 短信批量模板群发
* 单次请求最大支持 10000
* @param phones N手机号
* @param templateId 短信模板ID
* @param vars 使用文本变量动态控制短信中的文本
* @return 参数组装
*/
private LinkedHashMap<String, Object> buildBatchXSend(List<String> phones, String templateId, LinkedHashMap<String, String> vars){
SubMailConfig config = getConfig();
LinkedHashMap<String, Object> body = new LinkedHashMap<>();
body.put("appid", config.getAccessKeyId());
phones = CollUtil.sub(phones, 0, 10000);
body.put("to", SmsUtils.addCodePrefixIfNot(phones));
body.put("project", templateId);
if (MapUtil.isNotEmpty(vars)){
body.put("vars", JSONUtil.toJsonStr(vars));
}
body.put("timestamp", timestamp());
body.put("sign_type", config.getSignType());
if (StrUtil.isNotBlank(config.getSignVersion())){
body.put("sign_version", config.getSignVersion());
}
body.put("sign_type", config.getSignType());
String signature = SubMailUtils.signature(body, config.getSignType(), config.getAccessKeyId(), config.getAccessKeySecret(), "vars");
body.put("signature", signature);
return body;
}
private String timestamp(){
JSONObject resp = http.getUrl("https://api-v4.mysubmail.com/service/timestamp");
return resp.getStr("timestamp");
}
}

View File

@ -0,0 +1,49 @@
package org.dromara.sms4j.submail.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.utils.SmsUtils;
import java.util.LinkedHashMap;
/**
* <p>类名: SubMailUtils
*
* @author :bleachtred
* 2024/6/22 13:59
**/
@Slf4j
public class SubMailUtils {
public static LinkedHashMap<String, String> buildHeaders(){
LinkedHashMap<String, String> headers = new LinkedHashMap<>(1);
headers.put(Constant.ACCEPT, Constant.APPLICATION_JSON);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_JSON);
return headers;
}
public static String signature(LinkedHashMap<String, Object> body, String signType, String accessKeyId, String accessKeySecret, String... excludes){
if (StrUtil.containsAnyIgnoreCase(signType, DigestAlgorithm.MD5.getValue(), DigestAlgorithm.SHA1.getValue())){
StringBuilder sb = new StringBuilder();
sb.append(accessKeyId).append(accessKeySecret);
if ("2".equals(Convert.toStr(body.get("sign_version")))){
sb.append(SmsUtils.sortedParamsAsc(body, excludes));
}else {
sb.append(SmsUtils.sortedParamsAsc(body));
}
sb.append(accessKeyId).append(accessKeySecret);
if (signType.equalsIgnoreCase(DigestAlgorithm.MD5.getValue())){
return DigestUtil.md5Hex(sb.toString());
}else {
return DigestUtil.sha1Hex(sb.toString());
}
}else {
return accessKeySecret;
}
}
}

View File

@ -1,10 +1,12 @@
package org.dromara.sms4j.tencent.service;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
@ -15,10 +17,10 @@ import org.dromara.sms4j.tencent.config.TencentConfig;
import org.dromara.sms4j.tencent.utils.TencentUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
@ -44,12 +46,7 @@ public class TencentSmsImpl extends AbstractSmsBlend<TencentConfig> {
@Override
public SmsResponse sendMessage(String phone, String message) {
String[] split = message.split("&");
LinkedHashMap<String, String> map = new LinkedHashMap<>();
for (int i = 0; i < split.length; i++) {
map.put(String.valueOf(i), split[i]);
}
return sendMessage(phone, getConfig().getTemplateId(), map);
return sendMessage(phone, getConfig().getTemplateId(), SmsUtils.buildMessageByAmpersand(message));
}
@Override
@ -59,38 +56,32 @@ public class TencentSmsImpl extends AbstractSmsBlend<TencentConfig> {
@Override
public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) {
if (Objects.isNull(messages)){
messages = new LinkedHashMap<>();
// 如果包含 - 的话则认为是国际短信 不进行+86拼接
if (phone.contains("-")) {
String result = phone.replace("-", "");
return getSmsResponse(new String[]{result}, SmsUtils.toArray(messages), templateId);
} else {
return getSmsResponse(new String[]{StrUtil.addPrefixIfNot(phone, "+86")}, SmsUtils.toArray(messages), templateId);
}
List<String> list = new ArrayList<>();
for (Map.Entry<String, String> entry : messages.entrySet()) {
list.add(entry.getValue());
}
String[] s = new String[list.size()];
return getSmsResponse(new String[]{StrUtil.addPrefixIfNot(phone, "+86")}, list.toArray(s), templateId);
}
@Override
public SmsResponse massTexting(List<String> phones, String message) {
String[] split = message.split("&");
LinkedHashMap<String, String> map = new LinkedHashMap<>();
for (int i = 0; i < split.length; i++) {
map.put(String.valueOf(i), split[i]);
}
return massTexting(phones, getConfig().getTemplateId(), map);
return massTexting(phones, getConfig().getTemplateId(), SmsUtils.buildMessageByAmpersand(message));
}
@Override
public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) {
if (Objects.isNull(messages)){
messages = new LinkedHashMap<>();
}
List<String> list = new ArrayList<>();
for (Map.Entry<String, String> entry : messages.entrySet()) {
list.add(entry.getValue());
for (String phone : phones) {
if (phone.contains("-")) {
String result = phone.replace("-", "");
list.add(result);
} else {
list.add(StrUtil.addPrefixIfNot(phone, "+86"));
}
String[] s = new String[list.size()];
return getSmsResponse(SmsUtils.listToArray(phones), list.toArray(s), templateId);
}
return getSmsResponse(list.toArray(new String[0]), SmsUtils.toArray(messages), templateId);
}
private SmsResponse getSmsResponse(String[] phones, String[] messages, String templateId) {
@ -112,11 +103,9 @@ public class TencentSmsImpl extends AbstractSmsBlend<TencentConfig> {
try {
smsResponse = getResponse(http.postJson(url, headsMap, requestBody));
} catch (SmsBlendException e) {
smsResponse = new SmsResponse();
smsResponse.setSuccess(false);
smsResponse.setData(e.getMessage());
smsResponse = errorResp(e.message);
}
if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) {
if (smsResponse.isSuccess() || retry >= getConfig().getMaxRetries()) {
retry = 0;
return smsResponse;
}
@ -131,15 +120,12 @@ public class TencentSmsImpl extends AbstractSmsBlend<TencentConfig> {
}
private SmsResponse getResponse(JSONObject resJson) {
SmsResponse smsResponse = new SmsResponse();
JSONObject response = resJson.getJSONObject("Response");
// 根据 Error 判断是否配置错误
String error = response.getStr("Error");
smsResponse.setSuccess(StrUtil.isBlank(error));
boolean success = StrUtil.isBlank(response.getStr("Error"));
// 根据 SendStatusSet 判断是否不为Ok
JSONArray sendStatusSet = response.getJSONArray("SendStatusSet");
if (sendStatusSet != null) {
boolean success = true;
for (Object obj : sendStatusSet) {
JSONObject jsonObject = (JSONObject) obj;
String code = jsonObject.getStr("Code");
@ -148,11 +134,8 @@ public class TencentSmsImpl extends AbstractSmsBlend<TencentConfig> {
break;
}
}
smsResponse.setSuccess(success);
}
smsResponse.setData(resJson);
smsResponse.setConfigId(getConfigId());
return smsResponse;
return SmsRespUtils.resp(resJson, success, getConfigId());
}
}

View File

@ -1,20 +1,19 @@
package org.dromara.sms4j.tencent.utils;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.comm.constant.Constant;
import org.dromara.sms4j.comm.utils.SmsDateUtils;
import org.dromara.sms4j.tencent.config.TencentConfig;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
/**
* @author Richard
@ -31,18 +30,14 @@ public class TencentUtils {
*/
private static final String HTTP_REQUEST_METHOD = "POST";
private static final String CT_JSON = "application/json; charset=utf-8";
private static byte[] hmac256(byte[] key, String msg) {
HMac hMac = new HMac(HmacAlgorithm.HmacSHA256, key);
return hMac.digest(msg.getBytes(StandardCharsets.UTF_8));
return hMac.digest(msg);
}
private static String sha256Hex(String s) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] d = md.digest(s.getBytes(StandardCharsets.UTF_8));
return DatatypeConverter.printHexBinary(d).toLowerCase();
return DatatypeConverter.printHexBinary(DigestUtil.sha256(s)).toLowerCase();
}
/**
@ -52,13 +47,11 @@ public class TencentUtils {
* @param messages 短信内容
* @param phones 手机号
* @param timestamp 时间戳
* @throws Exception
* @throws Exception Exception
*/
public static String generateSignature(TencentConfig tencentConfig, String templateId, String[] messages, String[] phones,
String timestamp) throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String date = sdf.format(new Date(Long.parseLong(timestamp + "000")));
String date = SmsDateUtils.normDateGmt(new Date(Long.parseLong(timestamp + "000")));
String canonicalUri = "/";
String canonicalQueryString = "";
String canonicalHeaders = "content-type:application/json; charset=utf-8\nhost:" + tencentConfig.getRequestUrl() + "\n";
@ -95,8 +88,8 @@ public class TencentUtils {
public static Map<String, String> generateHeadsMap(String authorization, String timestamp, String action,
String version, String territory, String requestUrl) {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", authorization);
headers.put("Content-Type", CT_JSON);
headers.put(Constant.AUTHORIZATION, authorization);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_JSON_UTF8);
headers.put("Host", requestUrl);
headers.put("X-TC-Action", action);
headers.put("X-TC-Timestamp", timestamp);
@ -113,7 +106,7 @@ public class TencentUtils {
* @param signatureName 短信签名
* @param templateId 模板id
* @param templateParamSet 模板参数
* @return
* @return Map
*/
public static Map<String, Object> generateRequestBody(String[] phones, String sdkAppId, String signatureName,
String templateId, String[] templateParamSet) {

View File

@ -1,5 +1,6 @@
package org.dromara.sms4j.unisms.core;
import org.dromara.sms4j.comm.constant.Constant;
/**
* 初始化统一环境的单例类.
@ -8,8 +9,8 @@ public class Uni {
/** 模仿SDK版本*/
public static final String VERSION = "0.0.4";
public static final String signingAlgorithm = "hmac-sha256";
public static String endpoint = System.getenv().getOrDefault("UNI_ENDPOINT", "https://uni.apistd.com");
public static final String SIGNING_ALGORITHM = "hmac-sha256";
public static String endpoint = System.getenv().getOrDefault("UNI_ENDPOINT", Constant.HTTPS_PREFIX + "uni.apistd.com");
public static String accessKeyId = System.getenv("UNI_ACCESS_KEY_ID");
private static String accessKeySecret = System.getenv("UNI_ACCESS_KEY_SECRET");
@ -86,7 +87,7 @@ public class Uni {
builder.isSimple(false);
}
builder.endpoint(Uni.endpoint);
builder.signingAlgorithm(Uni.signingAlgorithm);
builder.signingAlgorithm(Uni.SIGNING_ALGORITHM);
builder.setRetryInterval(retryInterval);
builder.setMaxRetries(maxRetries);
return builder.build();

View File

@ -85,8 +85,8 @@ public class UniClient {
public UniResponse request(final String action, final Map<String, Object> data) throws SmsBlendException {
Map<String, String> headers = new HashMap<>();
headers.put("User-Agent", USER_AGENT);
headers.put("Content-Type", Constant.APPLICATION_JSON_UTF8);
headers.put("Accept", Constant.ACCEPT);
headers.put(Constant.CONTENT_TYPE, Constant.APPLICATION_JSON_UTF8);
headers.put(Constant.ACCEPT, Constant.APPLICATION_JSON);
String url;
if (this.isSimple) {
url = this.endpoint + "?action=" + action + "&accessKeyId=" + this.accessKeyId;

View File

@ -6,7 +6,6 @@ import org.dromara.sms4j.comm.exception.SmsBlendException;
import java.util.Objects;
public class UniResponse {
public static final String REQUEST_ID_HEADER_KEY = "x-uni-request-id";
public String requestId;
public String code;
public String message;

View File

@ -3,6 +3,7 @@ package org.dromara.sms4j.unisms.service;
import cn.hutool.core.map.MapUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.api.utils.SmsRespUtils;
import org.dromara.sms4j.comm.constant.SupplierConstant;
import org.dromara.sms4j.comm.delayedTime.DelayedTime;
import org.dromara.sms4j.comm.exception.SmsBlendException;
@ -96,16 +97,12 @@ public class UniSmsImpl extends AbstractSmsBlend<UniConfig> {
}
private SmsResponse getSmsResponse(Map<String, Object> data) {
SmsResponse smsResponse = new SmsResponse();
try {
UniResponse send = Uni.getClient(getConfig().getRetryInterval(), getConfig().getMaxRetries()).request("sms.message.send", data);
smsResponse.setSuccess("0".equals(send.code));
smsResponse.setData(send);
smsResponse.setConfigId(getConfigId());
return SmsRespUtils.resp(send, "0".equals(send.code), getConfigId());
} catch (Exception e) {
smsResponse.setSuccess(false);
return errorResp(e.getMessage());
}
return smsResponse;
}
}

Some files were not shown because too many files have changed in this diff Show More