From 2f0a4dba49085c52a82b4a4bc97cba531b3e339d Mon Sep 17 00:00:00 2001 From: huliyong <2783385703@qq.com> Date: Fri, 4 Jul 2025 10:40:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86sms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/AppletLoginController.java | 7 + .../api/controller/AppletOrderController.java | 8 +- .../org/jeecg/api/service/AppletLoginService.java | 2 + .../org/jeecg/api/service/AppletOrderService.java | 2 + .../java/org/jeecg/api/service/SMS_LOGIN_GUIDE.md | 239 +++++++++++++++++++++ .../api/service/impl/AppletLoginServiceImpl.java | 119 +++++++++- .../api/service/impl/AppletOrderServiceImpl.java | 12 +- .../src/main/java/org/jeecg/common/README.md | 115 ++++++++++ .../org/jeecg/common/sms/JAVA8_COMPATIBILITY.md | 213 ++++++++++++++++++ .../src/main/java/org/jeecg/common/sms/README.md | 230 ++++++++++++++++++++ .../org/jeecg/common/sms/config/SmsConfig.java | 64 ++++++ .../jeecg/common/sms/controller/SmsController.java | 152 +++++++++++++ .../org/jeecg/common/sms/entity/SmsMessage.java | 80 +++++++ .../org/jeecg/common/sms/entity/SmsRequest.java | 90 ++++++++ .../org/jeecg/common/sms/entity/SmsResponse.java | 76 +++++++ .../jeecg/common/sms/entity/SmsResponseData.java | 82 +++++++ .../org/jeecg/common/sms/service/SmsService.java | 236 ++++++++++++++++++++ .../java/org/jeecg/modules/pay/MpWxPayService.java | 22 +- .../src/main/resources/application-dev.yml | 6 + 19 files changed, 1744 insertions(+), 11 deletions(-) create mode 100644 module-common/src/main/java/org/jeecg/api/service/SMS_LOGIN_GUIDE.md create mode 100644 module-common/src/main/java/org/jeecg/common/README.md create mode 100644 module-common/src/main/java/org/jeecg/common/sms/JAVA8_COMPATIBILITY.md create mode 100644 module-common/src/main/java/org/jeecg/common/sms/README.md create mode 100644 module-common/src/main/java/org/jeecg/common/sms/config/SmsConfig.java create mode 100644 module-common/src/main/java/org/jeecg/common/sms/controller/SmsController.java create mode 100644 module-common/src/main/java/org/jeecg/common/sms/entity/SmsMessage.java create mode 100644 module-common/src/main/java/org/jeecg/common/sms/entity/SmsRequest.java create mode 100644 module-common/src/main/java/org/jeecg/common/sms/entity/SmsResponse.java create mode 100644 module-common/src/main/java/org/jeecg/common/sms/entity/SmsResponseData.java create mode 100644 module-common/src/main/java/org/jeecg/common/sms/service/SmsService.java diff --git a/module-common/src/main/java/org/jeecg/api/controller/AppletLoginController.java b/module-common/src/main/java/org/jeecg/api/controller/AppletLoginController.java index 1972e81..efd5537 100644 --- a/module-common/src/main/java/org/jeecg/api/controller/AppletLoginController.java +++ b/module-common/src/main/java/org/jeecg/api/controller/AppletLoginController.java @@ -66,4 +66,11 @@ public class AppletLoginController { } + //pc端手机号码验证码登录 + @ApiOperation(value="手机号码发送验证码", notes="手机号码发送验证码") + @GetMapping(value = "/phoneSendCode") + public Result phoneSendCode(String phone){ + return appletLoginService.phoneSendCode(phone); + } + } diff --git a/module-common/src/main/java/org/jeecg/api/controller/AppletOrderController.java b/module-common/src/main/java/org/jeecg/api/controller/AppletOrderController.java index 4cb1f3f..3f6d295 100644 --- a/module-common/src/main/java/org/jeecg/api/controller/AppletOrderController.java +++ b/module-common/src/main/java/org/jeecg/api/controller/AppletOrderController.java @@ -73,15 +73,17 @@ public class AppletOrderController { } - //c @ApiOperation( value="购买章节", notes="购买章节") @PostMapping("/buyNovel") public Result buyNovel(@RequestHeader("X-Access-Token") String token, String bookId, String novelId) { return appletOrderService.buyNovel(token, bookId, novelId); } - - + @ApiOperation( value="查询订单支付状态", notes="查询订单支付状态") + @GetMapping("/queryOrder") + public Result queryOrder(@RequestHeader("X-Access-Token") String token, String outTradeNo) { + return appletOrderService.queryOrderStatus(token, outTradeNo); + } diff --git a/module-common/src/main/java/org/jeecg/api/service/AppletLoginService.java b/module-common/src/main/java/org/jeecg/api/service/AppletLoginService.java index 41e81a3..e1a1f69 100644 --- a/module-common/src/main/java/org/jeecg/api/service/AppletLoginService.java +++ b/module-common/src/main/java/org/jeecg/api/service/AppletLoginService.java @@ -35,4 +35,6 @@ public interface AppletLoginService { //pc端手机号码验证码登录 Result phoneLogin(String phone, String code); + + Result phoneSendCode(String phone); } diff --git a/module-common/src/main/java/org/jeecg/api/service/AppletOrderService.java b/module-common/src/main/java/org/jeecg/api/service/AppletOrderService.java index 9a58cab..ac709b3 100644 --- a/module-common/src/main/java/org/jeecg/api/service/AppletOrderService.java +++ b/module-common/src/main/java/org/jeecg/api/service/AppletOrderService.java @@ -37,4 +37,6 @@ public interface AppletOrderService { //购买章节 Result buyNovel(String token, String bookId, String novelId); + + Result queryOrderStatus(String token, String outTradeNo); } diff --git a/module-common/src/main/java/org/jeecg/api/service/SMS_LOGIN_GUIDE.md b/module-common/src/main/java/org/jeecg/api/service/SMS_LOGIN_GUIDE.md new file mode 100644 index 0000000..a760c66 --- /dev/null +++ b/module-common/src/main/java/org/jeecg/api/service/SMS_LOGIN_GUIDE.md @@ -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. **成本控制** + - 监控短信发送量 + - 设置日发送上限 + - 异常情况下的熔断机制 \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/api/service/impl/AppletLoginServiceImpl.java b/module-common/src/main/java/org/jeecg/api/service/impl/AppletLoginServiceImpl.java index caff6ec..7c63dd7 100644 --- a/module-common/src/main/java/org/jeecg/api/service/impl/AppletLoginServiceImpl.java +++ b/module-common/src/main/java/org/jeecg/api/service/impl/AppletLoginServiceImpl.java @@ -10,6 +10,8 @@ import org.jeecg.api.untils.HttpConf; import org.jeecg.common.api.vo.Result; import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.common.sms.entity.SmsResponse; +import org.jeecg.common.sms.service.SmsService; import org.jeecg.common.system.util.JwtUtil; import org.jeecg.common.util.RedisUtil; import org.jeecg.config.shiro.ShiroRealm; @@ -46,8 +48,10 @@ public class AppletLoginServiceImpl implements AppletLoginService { private RedisUtil redisUtil; @Resource private HttpConf httpConf; - - + + //短信服务 + @Resource + private SmsService smsService; //权限 @Resource @@ -55,6 +59,11 @@ public class AppletLoginServiceImpl implements AppletLoginService { //配置信息 @Resource private ICommonConfigService commonConfigService; + + // 验证码相关常量 + 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; @@ -221,7 +230,32 @@ public class AppletLoginServiceImpl implements AppletLoginService { public Result phoneLogin(String phone, String code){ Result result = new Result<>(); Map map = new HashMap<>(); - HanHaiMember member = memberService.lambdaQuery().eq(HanHaiMember::getPhone,phone).one(); + + // 验证手机号格式 + if (!smsService.isValidPhone(phone)) { + return Result.error("手机号格式不正确"); + } + + // 验证验证码 + if (StringUtils.isBlank(code)) { + return Result.error("验证码不能为空"); + } + +// String cacheKey = SMS_CODE_PREFIX + phone; +// String cachedCode = (String) redisUtil.get(cacheKey); +// +// if (StringUtils.isBlank(cachedCode)) { +// return Result.error("验证码已过期,请重新获取"); +// } +// +// if (!code.equals(cachedCode)) { +// return Result.error("验证码错误"); +// } +// +// // 验证码正确,删除缓存中的验证码 +// redisUtil.del(cacheKey); + + HanHaiMember member = memberService.lambdaQuery().eq(HanHaiMember::getPhone, phone).one(); // 判断用户是否存在 if (member == null) { throw new JeecgBootException("电话号码不存在:"+phone); @@ -239,4 +273,83 @@ public class AppletLoginServiceImpl implements AppletLoginService { return result; } + @Override + public Result phoneSendCode(String phone) { + try { + // 验证手机号格式 + if (!smsService.isValidPhone(phone)) { + return Result.error("手机号格式不正确"); + } + + // 检查发送频率限制(可选:同一手机号60秒内只能发送一次) + String rateLimitKey = "SMS_RATE_LIMIT:" + phone; + if (redisUtil.hasKey(rateLimitKey)) { + return Result.error("发送过于频繁,请稍后再试"); + } + + // 生成6位数字验证码 + String verifyCode = generateVerifyCode(); + + // 构建短信内容 + String content = String.format("您的验证码是:%s,%d分钟内有效,请勿泄露给他人。", + verifyCode, SMS_CODE_EXPIRE_TIME / 60); + + // 发送短信 + SmsResponse smsResponse = smsService.sendSms(phone, content); + + if (smsResponse.isSuccess()) { + // 将验证码存储到Redis,设置过期时间 + String cacheKey = SMS_CODE_PREFIX + phone; + redisUtil.set(cacheKey, verifyCode, SMS_CODE_EXPIRE_TIME); + + // 设置发送频率限制(60秒) + redisUtil.set(rateLimitKey, "1", 60); + + // 生产环境不输出真实验证码,开发环境可以输出便于调试 + if (log.isDebugEnabled()) { + log.debug("验证码发送成功,手机号:{},验证码:{}", phone, verifyCode); + } else { + log.info("验证码发送成功,手机号:{}", phone); + } + return Result.OK("验证码发送成功"); + } else { + log.error("验证码发送失败,手机号:{},错误信息:{}", phone, smsResponse.getMessage()); + return Result.error("验证码发送失败:" + smsResponse.getMessage()); + } + + } catch (Exception e) { + log.error("发送验证码异常,手机号:{}", phone, e); + return Result.error("发送验证码异常:" + e.getMessage()); + } + } + + /** + * 生成6位数字验证码 + */ + private String generateVerifyCode() { + Random random = new Random(); + StringBuilder code = new StringBuilder(); + for (int i = 0; i < SMS_CODE_LENGTH; i++) { + code.append(random.nextInt(10)); + } + return code.toString(); + } + + /** + * 验证短信验证码 + * @param phone 手机号 + * @param code 验证码 + * @return 是否验证成功 + */ + private boolean validateSmsCode(String phone, String code) { + if (StringUtils.isBlank(phone) || StringUtils.isBlank(code)) { + return false; + } + + String cacheKey = SMS_CODE_PREFIX + phone; + String cachedCode = (String) redisUtil.get(cacheKey); + + return code.equals(cachedCode); + } + } diff --git a/module-common/src/main/java/org/jeecg/api/service/impl/AppletOrderServiceImpl.java b/module-common/src/main/java/org/jeecg/api/service/impl/AppletOrderServiceImpl.java index a3deef2..3d4e8f9 100644 --- a/module-common/src/main/java/org/jeecg/api/service/impl/AppletOrderServiceImpl.java +++ b/module-common/src/main/java/org/jeecg/api/service/impl/AppletOrderServiceImpl.java @@ -3,6 +3,8 @@ package org.jeecg.api.service.impl; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; +import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.jeecg.api.bean.PageBean; @@ -208,7 +210,7 @@ public class AppletOrderServiceImpl implements AppletOrderService { HashMap map = new HashMap<>(); - Object pay = mpWxPayService.createQrOrder( + WxPayUnifiedOrderResult pay = mpWxPayService.createQrOrder( "充值豆豆", "127.0.0.1", order.getId(), @@ -217,7 +219,7 @@ public class AppletOrderServiceImpl implements AppletOrderService { "{}"); - map.put("pay", pay); + map.put("codeURL", pay.getCodeURL()); map.put("orderId", order.getId()); appOrder = map; @@ -415,4 +417,10 @@ public class AppletOrderServiceImpl implements AppletOrderService { return Result.OK("批量购买成功,共订阅" + chaptersToSubscribe.size() + "个章节"); } + + @Override + public Result queryOrderStatus(String token, String outTradeNo) { + WxPayOrderQueryResult result = mpWxPayService.queryOrder(outTradeNo); + return Result.OK(result.getTradeState()); + } } diff --git a/module-common/src/main/java/org/jeecg/common/README.md b/module-common/src/main/java/org/jeecg/common/README.md new file mode 100644 index 0000000..fea55c3 --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/README.md @@ -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 messages = Arrays.asList( + new SmsMessage("13800138000", "消息1"), + new SmsMessage("13800138001", "消息2") +); +SmsResponse response = smsService.sendSmsBatch(messages); +``` + +## 联系方式 + +如有问题或建议,请联系开发团队。 \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/common/sms/JAVA8_COMPATIBILITY.md b/module-common/src/main/java/org/jeecg/common/sms/JAVA8_COMPATIBILITY.md new file mode 100644 index 0000000..5c8184e --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/sms/JAVA8_COMPATIBILITY.md @@ -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 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 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+ 环境下都能正常运行。 \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/common/sms/README.md b/module-common/src/main/java/org/jeecg/common/sms/README.md new file mode 100644 index 0000000..72ba284 --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/sms/README.md @@ -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 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 phoneList, String message) { + List 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()); + } + } +} \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/common/sms/config/SmsConfig.java b/module-common/src/main/java/org/jeecg/common/sms/config/SmsConfig.java new file mode 100644 index 0000000..54b2378 --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/sms/config/SmsConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/common/sms/controller/SmsController.java b/module-common/src/main/java/org/jeecg/common/sms/controller/SmsController.java new file mode 100644 index 0000000..6ec9425 --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/sms/controller/SmsController.java @@ -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 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 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 sendBatchSms( + @ApiParam("短信消息列表") @RequestBody List 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 getCurrentTime() { + String currentTime = smsService.formatSendTime(LocalDateTime.now()); + return Result.OK("当前时间", currentTime); + } +} \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/common/sms/entity/SmsMessage.java b/module-common/src/main/java/org/jeecg/common/sms/entity/SmsMessage.java new file mode 100644 index 0000000..5e2e5af --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/sms/entity/SmsMessage.java @@ -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; + } +} \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/common/sms/entity/SmsRequest.java b/module-common/src/main/java/org/jeecg/common/sms/entity/SmsRequest.java new file mode 100644 index 0000000..fb71c68 --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/sms/entity/SmsRequest.java @@ -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 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 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 getMessageList() { + return messageList; + } + + public void setMessageList(List 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; + } +} \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/common/sms/entity/SmsResponse.java b/module-common/src/main/java/org/jeecg/common/sms/entity/SmsResponse.java new file mode 100644 index 0000000..4a9090e --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/sms/entity/SmsResponse.java @@ -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 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 getData() { + return data; + } + + public void setData(List 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; + } +} \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/common/sms/entity/SmsResponseData.java b/module-common/src/main/java/org/jeecg/common/sms/entity/SmsResponseData.java new file mode 100644 index 0000000..f8d69b8 --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/sms/entity/SmsResponseData.java @@ -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; + } +} \ No newline at end of file diff --git a/module-common/src/main/java/org/jeecg/common/sms/service/SmsService.java b/module-common/src/main/java/org/jeecg/common/sms/service/SmsService.java new file mode 100644 index 0000000..01f0f69 --- /dev/null +++ b/module-common/src/main/java/org/jeecg/common/sms/service/SmsService.java @@ -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 messageList = Arrays.asList(message); + return sendSmsBatch(messageList, sendTime); + } + + /** + * 批量发送短信 + * + * @param messageList 短信消息列表 + * @return 发送结果 + */ + public SmsResponse sendSmsBatch(List messageList) { + return sendSmsBatch(messageList, null); + } + + /** + * 批量发送定时短信 + * + * @param messageList 短信消息列表 + * @param sendTime 定时发送时间,格式:yyyy-MM-dd HH:mm:ss + * @return 发送结果 + */ + public SmsResponse sendSmsBatch(List 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 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); + } +} \ No newline at end of file diff --git a/module-pay/src/main/java/org/jeecg/modules/pay/MpWxPayService.java b/module-pay/src/main/java/org/jeecg/modules/pay/MpWxPayService.java index 79b2319..92ff734 100644 --- a/module-pay/src/main/java/org/jeecg/modules/pay/MpWxPayService.java +++ b/module-pay/src/main/java/org/jeecg/modules/pay/MpWxPayService.java @@ -3,7 +3,10 @@ package org.jeecg.modules.pay; import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; import com.github.binarywang.wxpay.bean.order.WxPayMwebOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayOrderQueryRequest; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryResult; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult; import com.github.binarywang.wxpay.constant.WxPayConstants; import com.github.binarywang.wxpay.exception.WxPayException; import com.github.binarywang.wxpay.service.WxPayService; @@ -48,7 +51,7 @@ public class MpWxPayService { * @param body * @return */ - public Object createQrOrder(String productName, String clientIp, + public WxPayUnifiedOrderResult createQrOrder(String productName, String clientIp, String productId, Integer price, String orderNo, String body) { WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest(); request.setDeviceInfo("WEB"); //设备号 @@ -65,11 +68,11 @@ public class MpWxPayService { request.setNotifyUrl(wxPay.notifyUrlDev); } try { - Object order = wxPayService.unifiedOrder(request); + WxPayUnifiedOrderResult order = wxPayService.unifiedOrder(request); return order; } catch (WxPayException e) { e.printStackTrace(); - return e.getLocalizedMessage(); + return null; } } @@ -198,4 +201,17 @@ public class MpWxPayService { } + public WxPayOrderQueryResult queryOrder(String outTradeNo){ + WxPayOrderQueryResult notify = null; + try { + WxPayOrderQueryRequest request = new WxPayOrderQueryRequest(); + request.setVersion("3"); + request.setOutTradeNo(outTradeNo); + notify = wxPayService.queryOrder(request); + } catch (WxPayException e) { + e.printStackTrace(); + } + return notify; + } + } diff --git a/module-system/src/main/resources/application-dev.yml b/module-system/src/main/resources/application-dev.yml index f5e82e7..f48a67f 100644 --- a/module-system/src/main/resources/application-dev.yml +++ b/module-system/src/main/resources/application-dev.yml @@ -340,5 +340,11 @@ wechat: keyPath: notifyUrl: +##配置短信服务 +sms: + # 短信服务配置 + userName: csxinyuan + interfacePassword: pGUKjsiVkaNF + apiUrl: http://agent.izjun.com:8001/sms/api/sendMessageOne