@ -0,0 +1,239 @@ | |||
# 短信验证码登录功能使用指南 | |||
## 功能概述 | |||
在 `AppletLoginServiceImpl` 中实现了完整的短信验证码登录功能,包括: | |||
- 发送验证码 (`phoneSendCode`) | |||
- 验证码登录 (`phoneLogin`) | |||
## 实现特性 | |||
### ✅ 已实现功能 | |||
1. **手机号格式验证** | |||
- 验证11位数字格式 | |||
- 以1开头的中国大陆手机号 | |||
2. **验证码生成与发送** | |||
- 生成6位随机数字验证码 | |||
- 通过短信服务发送到用户手机 | |||
- 验证码5分钟内有效 | |||
3. **防刷机制** | |||
- 同一手机号60秒内只能发送一次验证码 | |||
- 避免短信轰炸 | |||
4. **验证码验证** | |||
- 登录时验证验证码的正确性 | |||
- 验证码使用后自动失效 | |||
- 过期验证码自动清理 | |||
5. **安全机制** | |||
- Redis存储验证码,自动过期 | |||
- 验证成功后立即删除验证码 | |||
- 完整的错误处理和日志记录 | |||
## API接口 | |||
### 1. 发送验证码 | |||
**接口**: `POST /api/xxx/phoneSendCode` | |||
**参数**: | |||
- `phone`: 手机号码 | |||
**响应示例**: | |||
```json | |||
{ | |||
"success": true, | |||
"message": "验证码发送成功", | |||
"code": 200 | |||
} | |||
``` | |||
### 2. 验证码登录 | |||
**接口**: `POST /api/xxx/phoneLogin` | |||
**参数**: | |||
- `phone`: 手机号码 | |||
- `code`: 验证码 | |||
**响应示例**: | |||
```json | |||
{ | |||
"success": true, | |||
"message": "登录成功", | |||
"code": 200, | |||
"result": { | |||
"userInfo": { | |||
"id": "用户ID", | |||
"phone": "手机号", | |||
"nickName": "昵称" | |||
}, | |||
"token": "JWT令牌" | |||
} | |||
} | |||
``` | |||
## 使用流程 | |||
### 前端调用流程 | |||
1. **发送验证码** | |||
```javascript | |||
// 发送验证码 | |||
function sendSmsCode(phone) { | |||
fetch('/api/xxx/phoneSendCode', { | |||
method: 'POST', | |||
headers: { | |||
'Content-Type': 'application/x-www-form-urlencoded', | |||
}, | |||
body: `phone=${phone}` | |||
}) | |||
.then(response => response.json()) | |||
.then(data => { | |||
if (data.success) { | |||
alert('验证码发送成功'); | |||
// 开始倒计时 | |||
startCountdown(); | |||
} else { | |||
alert(data.message); | |||
} | |||
}); | |||
} | |||
``` | |||
2. **验证码登录** | |||
```javascript | |||
// 验证码登录 | |||
function loginWithSms(phone, code) { | |||
fetch('/api/xxx/phoneLogin', { | |||
method: 'POST', | |||
headers: { | |||
'Content-Type': 'application/x-www-form-urlencoded', | |||
}, | |||
body: `phone=${phone}&code=${code}` | |||
}) | |||
.then(response => response.json()) | |||
.then(data => { | |||
if (data.success) { | |||
// 保存用户信息和token | |||
localStorage.setItem('token', data.result.token); | |||
localStorage.setItem('userInfo', JSON.stringify(data.result.userInfo)); | |||
// 跳转到主页 | |||
window.location.href = '/main.html'; | |||
} else { | |||
alert(data.message); | |||
} | |||
}); | |||
} | |||
``` | |||
## 配置说明 | |||
### 短信服务配置 | |||
确保在 `application-dev.yml` 中配置了短信服务: | |||
```yaml | |||
##配置短信服务 | |||
sms: | |||
userName: csxinyuan | |||
clientPassword: HwmY9q4F3fhi | |||
interfacePassword: pGUKjsiVkaNF | |||
apiUrl: http://agent.izjun.com:8001/sms/api/sendMessageOne | |||
``` | |||
### Redis配置 | |||
验证码存储依赖Redis,确保Redis服务正常运行。 | |||
## 关键常量配置 | |||
```java | |||
// 验证码Redis前缀 | |||
private static final String SMS_CODE_PREFIX = "SMS_CODE:"; | |||
// 验证码过期时间(秒) | |||
private static final int SMS_CODE_EXPIRE_TIME = 300; // 5分钟 | |||
// 验证码长度 | |||
private static final int SMS_CODE_LENGTH = 6; | |||
``` | |||
## 错误处理 | |||
### 常见错误码 | |||
| 错误信息 | 说明 | 解决方案 | | |||
|---------|------|----------| | |||
| "手机号格式不正确" | 手机号不符合11位数字格式 | 检查手机号输入 | | |||
| "发送过于频繁,请稍后再试" | 60秒内重复发送 | 等待60秒后重试 | | |||
| "验证码已过期,请重新获取" | 验证码超过5分钟 | 重新获取验证码 | | |||
| "验证码错误" | 输入的验证码不正确 | 检查验证码输入 | | |||
| "电话号码不存在" | 数据库中没有该手机号用户 | 用户需要先注册 | | |||
## 日志监控 | |||
### 关键日志 | |||
```java | |||
log.info("验证码发送成功,手机号:{},验证码:{}", phone, verifyCode); | |||
log.error("验证码发送失败,手机号:{},错误信息:{}", phone, smsResponse.getMessage()); | |||
log.error("发送验证码异常,手机号:{}", phone, e); | |||
``` | |||
### 监控指标 | |||
- 验证码发送成功率 | |||
- 验证码验证成功率 | |||
- 短信服务响应时间 | |||
- 用户登录成功率 | |||
## 扩展建议 | |||
### 1. 图形验证码 | |||
可以添加图形验证码,防止机器人刷验证码: | |||
```java | |||
// 发送前验证图形验证码 | |||
if (!validateCaptcha(captcha)) { | |||
return Result.error("图形验证码错误"); | |||
} | |||
``` | |||
### 2. IP限制 | |||
可以添加IP维度的限制: | |||
```java | |||
// IP限制 | |||
String ipLimitKey = "SMS_IP_LIMIT:" + clientIp; | |||
if (redisUtil.get(ipLimitKey) > 10) { | |||
return Result.error("该IP发送次数过多,请稍后再试"); | |||
} | |||
``` | |||
### 3. 短信模板化 | |||
可以使用短信模板,支持多种场景: | |||
```java | |||
public enum SmsTemplate { | |||
LOGIN("您的登录验证码是:%s,%d分钟内有效。"), | |||
REGISTER("您的注册验证码是:%s,%d分钟内有效。"), | |||
RESET_PASSWORD("您的密码重置验证码是:%s,%d分钟内有效。"); | |||
} | |||
``` | |||
## 注意事项 | |||
1. **生产环境安全** | |||
- 不要在日志中输出真实验证码 | |||
- 考虑添加更严格的频率限制 | |||
- 监控异常发送行为 | |||
2. **用户体验** | |||
- 提供清晰的错误提示 | |||
- 倒计时显示发送间隔 | |||
- 支持语音验证码备选方案 | |||
3. **成本控制** | |||
- 监控短信发送量 | |||
- 设置日发送上限 | |||
- 异常情况下的熔断机制 |
@ -0,0 +1,115 @@ | |||
# Common 通用工具模块 | |||
## 目录结构 | |||
本目录用于存放项目中的通用工具和服务组件,按功能模块进行分类组织。 | |||
``` | |||
org.jeecg.common/ | |||
├── sms/ # 短信服务模块 | |||
│ ├── config/ # 配置类 | |||
│ │ └── SmsConfig.java | |||
│ ├── entity/ # 实体类/数据对象 | |||
│ │ ├── SmsMessage.java | |||
│ │ ├── SmsRequest.java | |||
│ │ ├── SmsResponse.java | |||
│ │ └── SmsResponseData.java | |||
│ ├── service/ # 服务实现类 | |||
│ │ └── SmsService.java | |||
│ └── controller/ # REST控制器 | |||
│ └── SmsController.java | |||
└── README.md # 说明文档 | |||
``` | |||
## 现有模块 | |||
### 📱 SMS 短信服务模块 | |||
- **路径**: `org.jeecg.common.sms` | |||
- **功能**: 提供短信发送服务,支持单发、批量发送、定时发送 | |||
- **配置**: 在 `application-dev.yml` 中配置短信服务参数 | |||
- **接口**: `/common/sms/**` | |||
#### 主要类说明: | |||
- `SmsConfig`: 短信配置类,读取yml配置 | |||
- `SmsService`: 短信服务实现,核心业务逻辑 | |||
- `SmsController`: REST接口控制器 | |||
- `SmsMessage`: 短信消息实体 | |||
- `SmsRequest/Response`: 请求响应实体 | |||
## 扩展新模块 | |||
当需要添加新的通用工具或服务时,请遵循以下规范: | |||
### 1. 目录结构规范 | |||
``` | |||
org.jeecg.common.{模块名}/ | |||
├── config/ # 配置类(如需要) | |||
├── entity/ # 实体类/数据对象 | |||
├── service/ # 服务实现类 | |||
├── controller/ # REST控制器(如需要) | |||
├── util/ # 工具类(如需要) | |||
└── constants/ # 常量定义(如需要) | |||
``` | |||
### 2. 命名规范 | |||
- **包名**: 使用小写字母,多个单词用点分隔 | |||
- **类名**: 使用驼峰命名,见名知意 | |||
- **配置类**: 以 `Config` 结尾 | |||
- **服务类**: 以 `Service` 结尾 | |||
- **控制器**: 以 `Controller` 结尾 | |||
- **实体类**: 使用具体业务名称 | |||
### 3. 示例:添加邮件服务模块 | |||
``` | |||
org.jeecg.common.email/ | |||
├── config/ | |||
│ └── EmailConfig.java | |||
├── entity/ | |||
│ ├── EmailMessage.java | |||
│ ├── EmailRequest.java | |||
│ └── EmailResponse.java | |||
├── service/ | |||
│ └── EmailService.java | |||
└── controller/ | |||
└── EmailController.java | |||
``` | |||
### 4. 配置文件 | |||
- 在 `application-dev.yml` 中添加相应配置 | |||
- 使用统一的配置前缀,如:`email:`、`file:`等 | |||
### 5. API接口 | |||
- 统一使用 `/common/{模块名}/**` 作为接口路径 | |||
- 添加 Swagger 文档注解 | |||
## 注意事项 | |||
1. **依赖管理**: 确保添加的依赖不与现有项目冲突 | |||
2. **异常处理**: 统一使用项目的异常处理机制 | |||
3. **日志记录**: 使用 SLF4J 进行日志记录 | |||
4. **测试**: 为新模块编写相应的单元测试 | |||
5. **文档**: 更新本README文档,说明新模块的用途和使用方法 | |||
## 使用示例 | |||
### 短信服务使用示例 | |||
```java | |||
@Autowired | |||
private SmsService smsService; | |||
// 发送单条短信 | |||
SmsResponse response = smsService.sendSms("13800138000", "验证码:123456"); | |||
// 批量发送 | |||
List<SmsMessage> messages = Arrays.asList( | |||
new SmsMessage("13800138000", "消息1"), | |||
new SmsMessage("13800138001", "消息2") | |||
); | |||
SmsResponse response = smsService.sendSmsBatch(messages); | |||
``` | |||
## 联系方式 | |||
如有问题或建议,请联系开发团队。 |
@ -0,0 +1,213 @@ | |||
# Java 8 兼容性修改说明 | |||
## 问题描述 | |||
原始的短信服务类使用了 Java 11 的 `java.net.http` 包,导致在 Java 8 环境下编译失败: | |||
``` | |||
java: 程序包java.net.http不存在 | |||
``` | |||
## 解决方案 | |||
### 1. 替换 HTTP 客户端 | |||
**原始代码(Java 11)**: | |||
```java | |||
import java.net.http.HttpClient; | |||
import java.net.http.HttpRequest; | |||
import java.net.http.HttpResponse; | |||
private final HttpClient httpClient = HttpClient.newHttpClient(); | |||
private String sendHttpRequest(SmsRequest request) throws IOException, InterruptedException { | |||
String jsonBody = objectMapper.writeValueAsString(request); | |||
HttpRequest httpRequest = HttpRequest.newBuilder() | |||
.uri(URI.create(smsConfig.getApiUrl())) | |||
.header("Accept", "application/json") | |||
.header("Content-Type", "application/json;charset=utf-8") | |||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody)) | |||
.build(); | |||
HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); | |||
if (response.statusCode() != 200) { | |||
throw new IOException("HTTP请求失败,状态码:" + response.statusCode()); | |||
} | |||
return response.body(); | |||
} | |||
``` | |||
**修改后的代码(Java 8 兼容)**: | |||
```java | |||
import java.io.BufferedReader; | |||
import java.io.DataOutputStream; | |||
import java.io.InputStreamReader; | |||
import java.net.HttpURLConnection; | |||
import java.net.URL; | |||
import java.nio.charset.StandardCharsets; | |||
private String sendHttpRequest(SmsRequest request) throws IOException { | |||
String jsonBody = objectMapper.writeValueAsString(request); | |||
URL url = new URL(smsConfig.getApiUrl()); | |||
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); | |||
try { | |||
// 设置请求方法和属性 | |||
connection.setRequestMethod("POST"); | |||
connection.setRequestProperty("Accept", "application/json"); | |||
connection.setRequestProperty("Content-Type", "application/json;charset=utf-8"); | |||
connection.setDoOutput(true); | |||
connection.setDoInput(true); | |||
connection.setUseCaches(false); | |||
// 发送请求体 | |||
try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { | |||
byte[] input = jsonBody.getBytes(StandardCharsets.UTF_8); | |||
os.write(input, 0, input.length); | |||
os.flush(); | |||
} | |||
// 检查响应状态码 | |||
int responseCode = connection.getResponseCode(); | |||
if (responseCode != 200) { | |||
throw new IOException("HTTP请求失败,状态码:" + responseCode); | |||
} | |||
// 读取响应 | |||
try (BufferedReader br = new BufferedReader( | |||
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { | |||
StringBuilder response = new StringBuilder(); | |||
String responseLine; | |||
while ((responseLine = br.readLine()) != null) { | |||
response.append(responseLine.trim()); | |||
} | |||
return response.toString(); | |||
} | |||
} finally { | |||
connection.disconnect(); | |||
} | |||
} | |||
``` | |||
### 2. 修改异常处理 | |||
**原始代码**: | |||
```java | |||
} catch (Exception e) { | |||
logger.error("短信发送失败", e); | |||
// ... | |||
} | |||
``` | |||
**修改后**: | |||
```java | |||
} catch (IOException e) { | |||
logger.error("短信发送网络异常", e); | |||
SmsResponse errorResponse = new SmsResponse(); | |||
errorResponse.setCode(-1); | |||
errorResponse.setMessage("网络异常:" + e.getMessage()); | |||
return errorResponse; | |||
} catch (Exception e) { | |||
logger.error("短信发送失败", e); | |||
// ... | |||
} | |||
``` | |||
## 主要变更 | |||
### ✅ 移除的 Java 11 特性 | |||
1. `java.net.http.HttpClient` | |||
2. `java.net.http.HttpRequest` | |||
3. `java.net.http.HttpResponse` | |||
4. `InterruptedException` 异常处理 | |||
### ✅ 使用的 Java 8 兼容特性 | |||
1. `java.net.HttpURLConnection` - 传统的 HTTP 客户端 | |||
2. `java.io.*` 类进行流处理 | |||
3. `java.nio.charset.StandardCharsets` - UTF-8 编码 | |||
4. try-with-resources 语句(Java 7+) | |||
## 兼容性说明 | |||
### ✅ 支持的 Java 版本 | |||
- Java 8+ | |||
- Java 11+ | |||
- Java 17+ | |||
### 📊 性能对比 | |||
| 特性 | Java 11 HttpClient | Java 8 HttpURLConnection | | |||
|------|-------------------|---------------------------| | |||
| **连接池** | 内置 | 需要手动管理 | | |||
| **异步支持** | 原生支持 | 需要额外实现 | | |||
| **HTTP/2** | 支持 | 不支持 | | |||
| **内存使用** | 较高 | 较低 | | |||
| **兼容性** | Java 11+ | Java 1.1+ | | |||
### 🔧 功能保持 | |||
- ✅ POST 请求发送 | |||
- ✅ JSON 请求体 | |||
- ✅ HTTP 头设置 | |||
- ✅ 响应状态码检查 | |||
- ✅ 响应内容读取 | |||
- ✅ 连接管理 | |||
- ✅ 错误处理 | |||
## 测试建议 | |||
### 单元测试 | |||
```java | |||
@Test | |||
public void testSendSms() { | |||
SmsService smsService = new SmsService(); | |||
SmsResponse response = smsService.sendSms("13800138000", "测试消息"); | |||
assertNotNull(response); | |||
} | |||
``` | |||
### 集成测试 | |||
1. 验证 HTTP 请求格式是否正确 | |||
2. 测试网络异常处理 | |||
3. 验证响应解析功能 | |||
## 后续升级建议 | |||
如果将来项目升级到 Java 11+,可以考虑以下优化: | |||
### 1. 重新启用 Java 11 HttpClient | |||
```java | |||
// 可选:使用连接池提升性能 | |||
HttpClient client = HttpClient.newBuilder() | |||
.connectTimeout(Duration.ofSeconds(10)) | |||
.build(); | |||
``` | |||
### 2. 异步发送支持 | |||
```java | |||
// 异步发送短信 | |||
CompletableFuture<SmsResponse> future = smsService.sendSmsAsync(phone, content); | |||
``` | |||
### 3. HTTP/2 支持 | |||
```java | |||
// 启用 HTTP/2 | |||
HttpClient client = HttpClient.newBuilder() | |||
.version(HttpClient.Version.HTTP_2) | |||
.build(); | |||
``` | |||
## 注意事项 | |||
1. **连接管理**: 确保在 `finally` 块中调用 `connection.disconnect()` | |||
2. **编码处理**: 使用 `StandardCharsets.UTF_8` 确保字符编码正确 | |||
3. **资源释放**: 使用 try-with-resources 自动关闭流 | |||
4. **超时设置**: 可以通过 `connection.setConnectTimeout()` 设置连接超时 | |||
## 总结 | |||
通过将 Java 11 的 `java.net.http` 替换为 Java 8 兼容的 `HttpURLConnection`,成功解决了编译错误,同时保持了所有原有功能。修改后的代码在 Java 8+ 环境下都能正常运行。 |
@ -0,0 +1,230 @@ | |||
# SMS 短信服务模块 | |||
## 概述 | |||
短信服务模块提供完整的短信发送功能,支持单条发送、批量发送和定时发送。 | |||
## 目录结构 | |||
``` | |||
org.jeecg.common.sms/ | |||
├── config/ | |||
│ └── SmsConfig.java # 配置类 | |||
├── entity/ | |||
│ ├── SmsMessage.java # 短信消息实体 | |||
│ ├── SmsRequest.java # 请求实体 | |||
│ ├── SmsResponse.java # 响应实体 | |||
│ └── SmsResponseData.java # 响应数据实体 | |||
├── service/ | |||
│ └── SmsService.java # 服务实现类 | |||
└── controller/ | |||
└── SmsController.java # REST控制器 | |||
``` | |||
## 配置说明 | |||
在 `application-dev.yml` 中的配置: | |||
```yaml | |||
##配置短信服务 | |||
sms: | |||
# 短信服务配置 | |||
userName: csxinyuan | |||
clientPassword: HwmY9q4F3fhi | |||
interfacePassword: pGUKjsiVkaNF | |||
apiUrl: http://agent.izjun.com:8001/sms/api/sendMessageOne | |||
``` | |||
## 使用方法 | |||
### 1. 在其他类中注入服务 | |||
```java | |||
@Autowired | |||
private SmsService smsService; | |||
``` | |||
### 2. 发送单条短信 | |||
```java | |||
// 即时发送 | |||
SmsResponse response = smsService.sendSms("13800138000", "您的验证码是:123456"); | |||
// 定时发送 | |||
String sendTime = "2024-12-25 10:30:00"; | |||
SmsResponse response = smsService.sendSms("13800138000", "圣诞快乐!", sendTime); | |||
``` | |||
### 3. 批量发送短信 | |||
```java | |||
List<SmsMessage> messageList = Arrays.asList( | |||
new SmsMessage("13800138000", "消息内容1"), | |||
new SmsMessage("13800138001", "消息内容2"), | |||
new SmsMessage("13800138002", "消息内容3") | |||
); | |||
SmsResponse response = smsService.sendSmsBatch(messageList); | |||
``` | |||
### 4. 参数验证 | |||
```java | |||
// 验证手机号格式 | |||
boolean isValidPhone = smsService.isValidPhone("13800138000"); | |||
// 验证短信内容 | |||
boolean isValidContent = smsService.isValidContent("短信内容"); | |||
// 格式化时间 | |||
String timeStr = smsService.formatSendTime(LocalDateTime.now()); | |||
``` | |||
## REST API接口 | |||
### 发送单条短信 | |||
- **URL**: `POST /common/sms/send` | |||
- **参数**: | |||
- `phone`: 手机号 | |||
- `content`: 短信内容 | |||
- **示例**: | |||
```bash | |||
curl -X POST "http://localhost:8002/novel-admin/common/sms/send" \ | |||
-d "phone=13800138000&content=测试短信" | |||
``` | |||
### 发送定时短信 | |||
- **URL**: `POST /common/sms/sendScheduled` | |||
- **参数**: | |||
- `phone`: 手机号 | |||
- `content`: 短信内容 | |||
- `sendTime`: 定时时间 (yyyy-MM-dd HH:mm:ss) | |||
- **示例**: | |||
```bash | |||
curl -X POST "http://localhost:8002/novel-admin/common/sms/sendScheduled" \ | |||
-d "phone=13800138000&content=定时短信&sendTime=2024-12-25 10:30:00" | |||
``` | |||
### 批量发送短信 | |||
- **URL**: `POST /common/sms/sendBatch` | |||
- **Content-Type**: `application/json` | |||
- **Body**: | |||
```json | |||
[ | |||
{ | |||
"phone": "13800138000", | |||
"content": "消息内容1" | |||
}, | |||
{ | |||
"phone": "13800138001", | |||
"content": "消息内容2" | |||
} | |||
] | |||
``` | |||
### 获取当前时间格式 | |||
- **URL**: `GET /common/sms/currentTime` | |||
- **返回**: 当前时间的格式化字符串 | |||
## 响应格式 | |||
### 成功响应 | |||
```json | |||
{ | |||
"success": true, | |||
"message": "短信发送成功", | |||
"code": 200, | |||
"result": { | |||
"code": 0, | |||
"message": "成功", | |||
"data": [ | |||
{ | |||
"code": 0, | |||
"message": "成功", | |||
"phone": "13800138000", | |||
"msgId": 1234567890, | |||
"smsCount": 1 | |||
} | |||
], | |||
"smsCount": 1 | |||
} | |||
} | |||
``` | |||
### 错误响应 | |||
```json | |||
{ | |||
"success": false, | |||
"message": "短信发送失败:错误描述", | |||
"code": 500 | |||
} | |||
``` | |||
## 注意事项 | |||
1. **手机号格式**: 必须是11位数字,以1开头 | |||
2. **短信内容**: 不能超过500字符 | |||
3. **批量限制**: 批量发送最多支持1000条消息 | |||
4. **定时限制**: 定时发送时间限制在15天以内 | |||
5. **签名算法**: MD5(userName + timestamp + MD5(password)) | |||
## 错误码说明 | |||
| 错误码 | 说明 | | |||
|--------|------| | |||
| 0 | 成功 | | |||
| -1 | 系统异常 | | |||
| 其他 | 具体错误信息请参考接口文档 | | |||
## 业务集成示例 | |||
### 用户注册发送验证码 | |||
```java | |||
@Service | |||
public class UserService { | |||
@Autowired | |||
private SmsService smsService; | |||
public boolean sendRegistrationCode(String phone) { | |||
// 生成验证码 | |||
String code = generateRandomCode(); | |||
// 构建短信内容 | |||
String content = String.format("您的注册验证码是:%s,5分钟内有效。", code); | |||
// 发送短信 | |||
SmsResponse response = smsService.sendSms(phone, content); | |||
if (response.isSuccess()) { | |||
// 保存验证码到缓存 | |||
saveCodeToCache(phone, code); | |||
return true; | |||
} else { | |||
logger.error("发送注册验证码失败:{}", response.getMessage()); | |||
return false; | |||
} | |||
} | |||
} | |||
``` | |||
### 系统通知批量发送 | |||
```java | |||
@Service | |||
public class NotificationService { | |||
@Autowired | |||
private SmsService smsService; | |||
public void sendSystemNotification(List<String> phoneList, String message) { | |||
List<SmsMessage> messageList = phoneList.stream() | |||
.map(phone -> new SmsMessage(phone, message)) | |||
.collect(Collectors.toList()); | |||
SmsResponse response = smsService.sendSmsBatch(messageList); | |||
if (response.isSuccess()) { | |||
logger.info("系统通知发送成功,共发送{}条", response.getSmsCount()); | |||
} else { | |||
logger.error("系统通知发送失败:{}", response.getMessage()); | |||
} | |||
} | |||
} |
@ -0,0 +1,64 @@ | |||
package org.jeecg.common.sms.config; | |||
import org.springframework.boot.context.properties.ConfigurationProperties; | |||
import org.springframework.stereotype.Component; | |||
/** | |||
* 短信服务配置类 | |||
*/ | |||
@Component | |||
@ConfigurationProperties(prefix = "sms") | |||
public class SmsConfig { | |||
/** | |||
* 用户名 | |||
*/ | |||
private String userName; | |||
/** | |||
* 客户端密码 | |||
*/ | |||
private String clientPassword; | |||
/** | |||
* 接口密码 | |||
*/ | |||
private String interfacePassword; | |||
/** | |||
* API地址 | |||
*/ | |||
private String apiUrl; | |||
public String getUserName() { | |||
return userName; | |||
} | |||
public void setUserName(String userName) { | |||
this.userName = userName; | |||
} | |||
public String getClientPassword() { | |||
return clientPassword; | |||
} | |||
public void setClientPassword(String clientPassword) { | |||
this.clientPassword = clientPassword; | |||
} | |||
public String getInterfacePassword() { | |||
return interfacePassword; | |||
} | |||
public void setInterfacePassword(String interfacePassword) { | |||
this.interfacePassword = interfacePassword; | |||
} | |||
public String getApiUrl() { | |||
return apiUrl; | |||
} | |||
public void setApiUrl(String apiUrl) { | |||
this.apiUrl = apiUrl; | |||
} | |||
} |
@ -0,0 +1,152 @@ | |||
package org.jeecg.common.sms.controller; | |||
import io.swagger.annotations.Api; | |||
import io.swagger.annotations.ApiOperation; | |||
import io.swagger.annotations.ApiParam; | |||
import org.jeecg.common.sms.entity.SmsMessage; | |||
import org.jeecg.common.sms.entity.SmsResponse; | |||
import org.jeecg.common.sms.service.SmsService; | |||
import org.jeecg.common.api.vo.Result; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.web.bind.annotation.*; | |||
import java.time.LocalDateTime; | |||
import java.util.List; | |||
/** | |||
* 短信发送控制器 | |||
*/ | |||
@RestController | |||
@RequestMapping("/common/sms") | |||
@Api(tags = "短信发送接口") | |||
public class SmsController { | |||
private static final Logger logger = LoggerFactory.getLogger(SmsController.class); | |||
@Autowired | |||
private SmsService smsService; | |||
/** | |||
* 发送单条短信 | |||
*/ | |||
@PostMapping("/send") | |||
@ApiOperation("发送单条短信") | |||
public Result<SmsResponse> sendSms( | |||
@ApiParam("手机号") @RequestParam String phone, | |||
@ApiParam("短信内容") @RequestParam String content) { | |||
try { | |||
// 验证参数 | |||
if (!smsService.isValidPhone(phone)) { | |||
return Result.error("手机号格式不正确"); | |||
} | |||
if (!smsService.isValidContent(content)) { | |||
return Result.error("短信内容不能为空且不能超过500字符"); | |||
} | |||
// 发送短信 | |||
SmsResponse response = smsService.sendSms(phone, content); | |||
if (response.isSuccess()) { | |||
return Result.OK("短信发送成功", response); | |||
} else { | |||
return Result.error("短信发送失败:" + response.getMessage()); | |||
} | |||
} catch (Exception e) { | |||
logger.error("发送短信异常", e); | |||
return Result.error("发送短信异常:" + e.getMessage()); | |||
} | |||
} | |||
/** | |||
* 发送定时短信 | |||
*/ | |||
@PostMapping("/sendScheduled") | |||
@ApiOperation("发送定时短信") | |||
public Result<SmsResponse> sendScheduledSms( | |||
@ApiParam("手机号") @RequestParam String phone, | |||
@ApiParam("短信内容") @RequestParam String content, | |||
@ApiParam("定时发送时间,格式:yyyy-MM-dd HH:mm:ss") @RequestParam String sendTime) { | |||
try { | |||
// 验证参数 | |||
if (!smsService.isValidPhone(phone)) { | |||
return Result.error("手机号格式不正确"); | |||
} | |||
if (!smsService.isValidContent(content)) { | |||
return Result.error("短信内容不能为空且不能超过500字符"); | |||
} | |||
// 发送定时短信 | |||
SmsResponse response = smsService.sendSms(phone, content, sendTime); | |||
if (response.isSuccess()) { | |||
return Result.OK("定时短信设置成功", response); | |||
} else { | |||
return Result.error("定时短信设置失败:" + response.getMessage()); | |||
} | |||
} catch (Exception e) { | |||
logger.error("发送定时短信异常", e); | |||
return Result.error("发送定时短信异常:" + e.getMessage()); | |||
} | |||
} | |||
/** | |||
* 批量发送短信 | |||
*/ | |||
@PostMapping("/sendBatch") | |||
@ApiOperation("批量发送短信") | |||
public Result<SmsResponse> sendBatchSms( | |||
@ApiParam("短信消息列表") @RequestBody List<SmsMessage> messageList) { | |||
try { | |||
// 验证参数 | |||
if (messageList == null || messageList.isEmpty()) { | |||
return Result.error("短信消息列表不能为空"); | |||
} | |||
if (messageList.size() > 1000) { | |||
return Result.error("批量发送短信数量不能超过1000条"); | |||
} | |||
// 验证每条消息 | |||
for (SmsMessage message : messageList) { | |||
if (!smsService.isValidPhone(message.getPhone())) { | |||
return Result.error("手机号格式不正确:" + message.getPhone()); | |||
} | |||
if (!smsService.isValidContent(message.getContent())) { | |||
return Result.error("短信内容不正确:" + message.getContent()); | |||
} | |||
} | |||
// 批量发送短信 | |||
SmsResponse response = smsService.sendSmsBatch(messageList); | |||
if (response.isSuccess()) { | |||
return Result.OK("批量短信发送成功", response); | |||
} else { | |||
return Result.error("批量短信发送失败:" + response.getMessage()); | |||
} | |||
} catch (Exception e) { | |||
logger.error("批量发送短信异常", e); | |||
return Result.error("批量发送短信异常:" + e.getMessage()); | |||
} | |||
} | |||
/** | |||
* 获取格式化的当前时间(用于定时发送) | |||
*/ | |||
@GetMapping("/currentTime") | |||
@ApiOperation("获取当前时间格式") | |||
public Result<String> getCurrentTime() { | |||
String currentTime = smsService.formatSendTime(LocalDateTime.now()); | |||
return Result.OK("当前时间", currentTime); | |||
} | |||
} |
@ -0,0 +1,80 @@ | |||
package org.jeecg.common.sms.entity; | |||
import com.fasterxml.jackson.annotation.JsonProperty; | |||
/** | |||
* 短信消息对象 | |||
*/ | |||
public class SmsMessage { | |||
/** | |||
* 发送手机号码 | |||
*/ | |||
@JsonProperty("phone") | |||
private String phone; | |||
/** | |||
* 短信内容 | |||
*/ | |||
@JsonProperty("content") | |||
private String content; | |||
/** | |||
* 附带通道扩展码(可选) | |||
*/ | |||
@JsonProperty("extcode") | |||
private String extcode; | |||
/** | |||
* 用户回传数据,最大长度64(可选) | |||
*/ | |||
@JsonProperty("callData") | |||
private String callData; | |||
public SmsMessage() { | |||
} | |||
public SmsMessage(String phone, String content) { | |||
this.phone = phone; | |||
this.content = content; | |||
} | |||
public SmsMessage(String phone, String content, String extcode, String callData) { | |||
this.phone = phone; | |||
this.content = content; | |||
this.extcode = extcode; | |||
this.callData = callData; | |||
} | |||
public String getPhone() { | |||
return phone; | |||
} | |||
public void setPhone(String phone) { | |||
this.phone = phone; | |||
} | |||
public String getContent() { | |||
return content; | |||
} | |||
public void setContent(String content) { | |||
this.content = content; | |||
} | |||
public String getExtcode() { | |||
return extcode; | |||
} | |||
public void setExtcode(String extcode) { | |||
this.extcode = extcode; | |||
} | |||
public String getCallData() { | |||
return callData; | |||
} | |||
public void setCallData(String callData) { | |||
this.callData = callData; | |||
} | |||
} |
@ -0,0 +1,90 @@ | |||
package org.jeecg.common.sms.entity; | |||
import com.fasterxml.jackson.annotation.JsonProperty; | |||
import java.util.List; | |||
/** | |||
* 短信发送请求对象 | |||
*/ | |||
public class SmsRequest { | |||
/** | |||
* 帐号用户名 | |||
*/ | |||
@JsonProperty("userName") | |||
private String userName; | |||
/** | |||
* 短信消息列表 | |||
*/ | |||
@JsonProperty("messageList") | |||
private List<SmsMessage> messageList; | |||
/** | |||
* 当前时间戳,精确到毫秒 | |||
*/ | |||
@JsonProperty("timestamp") | |||
private Long timestamp; | |||
/** | |||
* 签名,MD5(userName + timestamp + MD5(password)) | |||
*/ | |||
@JsonProperty("sign") | |||
private String sign; | |||
/** | |||
* 短信定时发送时间,格式:yyyy-MM-dd HH:mm:ss(可选) | |||
*/ | |||
@JsonProperty("sendTime") | |||
private String sendTime; | |||
public SmsRequest() { | |||
} | |||
public SmsRequest(String userName, List<SmsMessage> messageList, Long timestamp, String sign) { | |||
this.userName = userName; | |||
this.messageList = messageList; | |||
this.timestamp = timestamp; | |||
this.sign = sign; | |||
} | |||
public String getUserName() { | |||
return userName; | |||
} | |||
public void setUserName(String userName) { | |||
this.userName = userName; | |||
} | |||
public List<SmsMessage> getMessageList() { | |||
return messageList; | |||
} | |||
public void setMessageList(List<SmsMessage> messageList) { | |||
this.messageList = messageList; | |||
} | |||
public Long getTimestamp() { | |||
return timestamp; | |||
} | |||
public void setTimestamp(Long timestamp) { | |||
this.timestamp = timestamp; | |||
} | |||
public String getSign() { | |||
return sign; | |||
} | |||
public void setSign(String sign) { | |||
this.sign = sign; | |||
} | |||
public String getSendTime() { | |||
return sendTime; | |||
} | |||
public void setSendTime(String sendTime) { | |||
this.sendTime = sendTime; | |||
} | |||
} |
@ -0,0 +1,76 @@ | |||
package org.jeecg.common.sms.entity; | |||
import com.fasterxml.jackson.annotation.JsonProperty; | |||
import java.util.List; | |||
/** | |||
* 短信发送响应对象 | |||
*/ | |||
public class SmsResponse { | |||
/** | |||
* 处理结果,0为成功,其他失败 | |||
*/ | |||
@JsonProperty("code") | |||
private Integer code; | |||
/** | |||
* 处理结果描述 | |||
*/ | |||
@JsonProperty("message") | |||
private String message; | |||
/** | |||
* 处理结果的数据数组 | |||
*/ | |||
@JsonProperty("data") | |||
private List<SmsResponseData> data; | |||
/** | |||
* 消耗此次请求的计费总数 | |||
*/ | |||
@JsonProperty("smsCount") | |||
private Integer smsCount; | |||
public SmsResponse() { | |||
} | |||
public Integer getCode() { | |||
return code; | |||
} | |||
public void setCode(Integer code) { | |||
this.code = code; | |||
} | |||
public String getMessage() { | |||
return message; | |||
} | |||
public void setMessage(String message) { | |||
this.message = message; | |||
} | |||
public List<SmsResponseData> getData() { | |||
return data; | |||
} | |||
public void setData(List<SmsResponseData> data) { | |||
this.data = data; | |||
} | |||
public Integer getSmsCount() { | |||
return smsCount; | |||
} | |||
public void setSmsCount(Integer smsCount) { | |||
this.smsCount = smsCount; | |||
} | |||
/** | |||
* 检查响应是否成功 | |||
*/ | |||
public boolean isSuccess() { | |||
return code != null && code == 0; | |||
} | |||
} |
@ -0,0 +1,82 @@ | |||
package org.jeecg.common.sms.entity; | |||
import com.fasterxml.jackson.annotation.JsonProperty; | |||
/** | |||
* 短信响应数据对象 | |||
*/ | |||
public class SmsResponseData { | |||
/** | |||
* 处理结果,0为成功,其他失败 | |||
*/ | |||
@JsonProperty("code") | |||
private Integer code; | |||
/** | |||
* 处理结果描述 | |||
*/ | |||
@JsonProperty("message") | |||
private String message; | |||
/** | |||
* 发送手机号码 | |||
*/ | |||
@JsonProperty("phone") | |||
private String phone; | |||
/** | |||
* 唯一消息Id | |||
*/ | |||
@JsonProperty("msgId") | |||
private Long msgId; | |||
/** | |||
* 此号码的计费数 | |||
*/ | |||
@JsonProperty("smsCount") | |||
private Integer smsCount; | |||
public SmsResponseData() { | |||
} | |||
public Integer getCode() { | |||
return code; | |||
} | |||
public void setCode(Integer code) { | |||
this.code = code; | |||
} | |||
public String getMessage() { | |||
return message; | |||
} | |||
public void setMessage(String message) { | |||
this.message = message; | |||
} | |||
public String getPhone() { | |||
return phone; | |||
} | |||
public void setPhone(String phone) { | |||
this.phone = phone; | |||
} | |||
public Long getMsgId() { | |||
return msgId; | |||
} | |||
public void setMsgId(Long msgId) { | |||
this.msgId = msgId; | |||
} | |||
public Integer getSmsCount() { | |||
return smsCount; | |||
} | |||
public void setSmsCount(Integer smsCount) { | |||
this.smsCount = smsCount; | |||
} | |||
} |
@ -0,0 +1,236 @@ | |||
package org.jeecg.common.sms.service; | |||
import com.fasterxml.jackson.databind.ObjectMapper; | |||
import org.jeecg.common.sms.config.SmsConfig; | |||
import org.jeecg.common.sms.entity.SmsMessage; | |||
import org.jeecg.common.sms.entity.SmsRequest; | |||
import org.jeecg.common.sms.entity.SmsResponse; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
import java.io.BufferedReader; | |||
import java.io.DataOutputStream; | |||
import java.io.IOException; | |||
import java.io.InputStreamReader; | |||
import java.net.HttpURLConnection; | |||
import java.net.URL; | |||
import java.nio.charset.StandardCharsets; | |||
import java.security.MessageDigest; | |||
import java.security.NoSuchAlgorithmException; | |||
import java.time.LocalDateTime; | |||
import java.time.format.DateTimeFormatter; | |||
import java.util.Arrays; | |||
import java.util.List; | |||
/** | |||
* 短信发送服务类 | |||
*/ | |||
@Service | |||
public class SmsService { | |||
private static final Logger logger = LoggerFactory.getLogger(SmsService.class); | |||
@Autowired | |||
private SmsConfig smsConfig; | |||
private final ObjectMapper objectMapper = new ObjectMapper(); | |||
/** | |||
* 发送单条短信 | |||
* | |||
* @param phone 手机号 | |||
* @param content 短信内容 | |||
* @return 发送结果 | |||
*/ | |||
public SmsResponse sendSms(String phone, String content) { | |||
return sendSms(phone, content, null); | |||
} | |||
/** | |||
* 发送定时短信 | |||
* | |||
* @param phone 手机号 | |||
* @param content 短信内容 | |||
* @param sendTime 定时发送时间,格式:yyyy-MM-dd HH:mm:ss | |||
* @return 发送结果 | |||
*/ | |||
public SmsResponse sendSms(String phone, String content, String sendTime) { | |||
SmsMessage message = new SmsMessage(phone, content); | |||
List<SmsMessage> messageList = Arrays.asList(message); | |||
return sendSmsBatch(messageList, sendTime); | |||
} | |||
/** | |||
* 批量发送短信 | |||
* | |||
* @param messageList 短信消息列表 | |||
* @return 发送结果 | |||
*/ | |||
public SmsResponse sendSmsBatch(List<SmsMessage> messageList) { | |||
return sendSmsBatch(messageList, null); | |||
} | |||
/** | |||
* 批量发送定时短信 | |||
* | |||
* @param messageList 短信消息列表 | |||
* @param sendTime 定时发送时间,格式:yyyy-MM-dd HH:mm:ss | |||
* @return 发送结果 | |||
*/ | |||
public SmsResponse sendSmsBatch(List<SmsMessage> messageList, String sendTime) { | |||
try { | |||
// 构建请求参数 | |||
SmsRequest request = buildSmsRequest(messageList, sendTime); | |||
// 发送HTTP请求 | |||
String responseJson = sendHttpRequest(request); | |||
// 解析响应 | |||
SmsResponse response = objectMapper.readValue(responseJson, SmsResponse.class); | |||
logger.info("短信发送完成,响应:{}", responseJson); | |||
return response; | |||
} catch (IOException e) { | |||
logger.error("短信发送网络异常", e); | |||
SmsResponse errorResponse = new SmsResponse(); | |||
errorResponse.setCode(-1); | |||
errorResponse.setMessage("网络异常:" + e.getMessage()); | |||
return errorResponse; | |||
} catch (Exception e) { | |||
logger.error("短信发送失败", e); | |||
SmsResponse errorResponse = new SmsResponse(); | |||
errorResponse.setCode(-1); | |||
errorResponse.setMessage("短信发送异常:" + e.getMessage()); | |||
return errorResponse; | |||
} | |||
} | |||
/** | |||
* 构建短信请求对象 | |||
*/ | |||
private SmsRequest buildSmsRequest(List<SmsMessage> messageList, String sendTime) { | |||
long timestamp = System.currentTimeMillis(); | |||
String sign = generateSign(smsConfig.getUserName(), timestamp, smsConfig.getInterfacePassword()); | |||
SmsRequest request = new SmsRequest(); | |||
request.setUserName(smsConfig.getUserName()); | |||
request.setMessageList(messageList); | |||
request.setTimestamp(timestamp); | |||
request.setSign(sign); | |||
if (sendTime != null && !sendTime.trim().isEmpty()) { | |||
request.setSendTime(sendTime); | |||
} | |||
return request; | |||
} | |||
/** | |||
* 生成签名 | |||
* 签名规则:MD5(userName + timestamp + MD5(password)) | |||
*/ | |||
private String generateSign(String userName, long timestamp, String password) { | |||
try { | |||
String passwordMd5 = md5(password); | |||
String signString = userName + timestamp + passwordMd5; | |||
return md5(signString); | |||
} catch (Exception e) { | |||
logger.error("生成签名失败", e); | |||
throw new RuntimeException("生成签名失败", e); | |||
} | |||
} | |||
/** | |||
* MD5加密 | |||
*/ | |||
private String md5(String input) throws NoSuchAlgorithmException { | |||
MessageDigest md = MessageDigest.getInstance("MD5"); | |||
byte[] digest = md.digest(input.getBytes()); | |||
StringBuilder sb = new StringBuilder(); | |||
for (byte b : digest) { | |||
sb.append(String.format("%02x", b)); | |||
} | |||
return sb.toString(); | |||
} | |||
/** | |||
* 发送HTTP请求 | |||
*/ | |||
private String sendHttpRequest(SmsRequest request) throws IOException { | |||
String jsonBody = objectMapper.writeValueAsString(request); | |||
logger.info("发送短信请求:{}", jsonBody); | |||
URL url = new URL(smsConfig.getApiUrl()); | |||
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); | |||
try { | |||
// 设置请求方法和属性 | |||
connection.setRequestMethod("POST"); | |||
connection.setRequestProperty("Accept", "application/json"); | |||
connection.setRequestProperty("Content-Type", "application/json;charset=utf-8"); | |||
connection.setDoOutput(true); | |||
connection.setDoInput(true); | |||
connection.setUseCaches(false); | |||
// 发送请求体 | |||
try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { | |||
byte[] input = jsonBody.getBytes(StandardCharsets.UTF_8); | |||
os.write(input, 0, input.length); | |||
os.flush(); | |||
} | |||
// 检查响应状态码 | |||
int responseCode = connection.getResponseCode(); | |||
if (responseCode != 200) { | |||
throw new IOException("HTTP请求失败,状态码:" + responseCode); | |||
} | |||
// 读取响应 | |||
try (BufferedReader br = new BufferedReader( | |||
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { | |||
StringBuilder response = new StringBuilder(); | |||
String responseLine; | |||
while ((responseLine = br.readLine()) != null) { | |||
response.append(responseLine.trim()); | |||
} | |||
return response.toString(); | |||
} | |||
} finally { | |||
connection.disconnect(); | |||
} | |||
} | |||
/** | |||
* 验证手机号格式 | |||
*/ | |||
public boolean isValidPhone(String phone) { | |||
if (phone == null || phone.trim().isEmpty()) { | |||
return false; | |||
} | |||
// 简单的手机号验证,11位数字,以1开头 | |||
return phone.matches("^1[0-9]{10}$"); | |||
} | |||
/** | |||
* 验证短信内容 | |||
*/ | |||
public boolean isValidContent(String content) { | |||
if (content == null || content.trim().isEmpty()) { | |||
return false; | |||
} | |||
// 内容不能超过500字符 | |||
return content.length() <= 500; | |||
} | |||
/** | |||
* 格式化定时发送时间 | |||
*/ | |||
public String formatSendTime(LocalDateTime dateTime) { | |||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | |||
return dateTime.format(formatter); | |||
} | |||
} |