| @ -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); | |||||
| } | |||||
| } | |||||