diff --git a/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/SmsTestController.java b/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/SmsTestController.java new file mode 100644 index 0000000..fbcedc9 --- /dev/null +++ b/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/SmsTestController.java @@ -0,0 +1,87 @@ +package org.qinan.safetyeval.start.sms; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * 助通短信平台测试控制器 + * + * @author safety-eval + */ +@Slf4j +@Api(tags = "助通短信测试接口") +@RestController +@RequestMapping("/test/sms") +public class SmsTestController { + + private final ZtSmsService ztSmsService; + + public SmsTestController(ZtSmsService ztSmsService) { + this.ztSmsService = ztSmsService; + } + + /** + * 发送验证码短信测试接口 + * + * @param mobile 手机号 + * @return 发送结果 + */ + @ApiOperation("发送验证码短信") + @PostMapping("/sendCode") + public Map sendVerificationCode(@RequestParam String mobile) { + log.info("收到验证码发送请求,手机号: {}", mobile); + + // 生成6位随机验证码 + String validCode = ztSmsService.generateVerificationCode(); + + // 发送验证码短信 + Map result = ztSmsService.sendVerificationCode(mobile, validCode); + + // 返回验证码供测试验证 + result.put("validCode", validCode); + result.put("templateId", "793059"); + result.put("templateName", "安全评价监管服务系统注册验证码"); + + return result; + } + + /** + * 发送自定义内容短信测试接口 + * + * @param mobile 手机号 + * @param content 短信内容 + * @return 发送结果 + */ + @ApiOperation("发送自定义短信") + @PostMapping("/sendCustom") + public Map sendCustomSms(@RequestParam String mobile, @RequestParam String content) { + log.info("收到自定义短信发送请求,手机号: {}, 内容: {}", mobile, content); + return ztSmsService.sendSms(mobile, content); + } + + /** + * 获取短信配置信息 + * + * @return 配置信息 + */ + @ApiOperation("获取短信配置信息") + @GetMapping("/config") + public Map getConfig() { + Map config = new java.util.HashMap<>(); + config.put("username", "qhdzyhy"); + config.put("templateId", "793059"); + config.put("templateName", "安全评价监管服务系统注册验证码"); + config.put("templateContent", "[秦安安全]您正在注册本系统,验证码:{valid_code},5分钟内有效,请勿泄露给他人,如非本人操作请忽略本条短信。"); + config.put("templateVariable", "valid_code"); + config.put("apiUrl", "http://www.ztsms.cn/sendNSms.do"); + return config; + } +} \ No newline at end of file diff --git a/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsMainTest.java b/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsMainTest.java new file mode 100644 index 0000000..0a789f0 --- /dev/null +++ b/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsMainTest.java @@ -0,0 +1,210 @@ +package org.qinan.safetyeval.start.sms; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.HashMap; +import java.util.Map; + +/** + * 助通短信平台(融合云 v2 接口)main 方法测试类 + * 直接运行无需启动 Spring Boot 容器 + * + * 接口说明: + * - 融合云 v2 接口,Content-Type: application/json + * - tKey:Unix 时间戳(秒) + * - password:md5(原始密码 + tKey),单次 md5 小写 + * + * @author safety-eval + */ +public class ZtSmsMainTest { + + /** + * 助通短信平台配置信息 + */ + private static final String USERNAME = "qhdzyhy"; + private static final String PASSWORD = "Zcloud@88888"; + private static final String API_URL = "https://api-shss.zthysms.com/v2/sendSms"; + + /** + * 测试手机号 - 请替换为实际接收短信的手机号 + */ + private static final String TEST_MOBILE = "19122631081"; +// private static final String TEST_MOBILE = "18375715233"; + + public static void main(String[] args) { + System.out.println("========== 助通短信平台测试开始 =========="); + System.out.println("用户名: " + USERNAME); + System.out.println("接口地址: " + API_URL); + System.out.println(); + + // 生成6位随机验证码 + String validCode = generateVerificationCode(); + System.out.println("生成验证码: " + validCode); + + // 构建短信内容(与模板一致:模板ID 793059) + // 注意:融合云签名必须使用全角【】,不能使用半角[] + String content = "【秦安安全】您正在注册本系统,验证码:" + validCode + ",5分钟内有效,请勿泄露给他人,如非本人操作请忽略本条短信。"; + System.out.println("短信内容: " + content); + System.out.println("内容字数: " + content.length()); + + // 发送短信 + Map result = sendSms(TEST_MOBILE, content); + + System.out.println(); + System.out.println("========== 发送结果 =========="); + for (Map.Entry entry : result.entrySet()) { + System.out.println(entry.getKey() + ": " + entry.getValue()); + } + System.out.println("========== 助通短信平台测试结束 =========="); + } + + /** + * 发送短信(融合云 v2 接口,使用 JDK 原生 HttpURLConnection,无需任何依赖) + * + * @param mobile 手机号 + * @param content 短信内容 + * @return 发送结果 + */ + public static Map sendSms(String mobile, String content) { + Map result = new HashMap<>(); + try { + // 融合云 v2 tKey:当前 Unix 时间戳(秒,10位) + String tKey = String.valueOf(System.currentTimeMillis() / 1000); + + // 融合云 v2 password:md5(md5(原始密码) + tKey),两次 md5 小写 + String md5Password = md5(PASSWORD); + String finalPassword = md5(md5Password + tKey); + + System.out.println("tKey(时间戳): " + tKey); + System.out.println("md5(password): " + md5Password); + System.out.println("最终 password: " + finalPassword); + + // 构建融合云 v2 JSON 请求体(字段名:username / password / tKey / mobile / content) + // content 为明文内容,无需 base64 编码 + String jsonBody = "{" + + "\"username\":\"" + USERNAME + "\"," + + "\"password\":\"" + finalPassword + "\"," + + "\"tKey\":\"" + tKey + "\"," + + "\"mobile\":\"" + mobile + "\"," + + "\"content\":\"" + escapeJson(content) + "\"" + + "}"; + + System.out.println("JSON 请求体: " + jsonBody); + + // 创建连接 + java.net.URL url = new java.net.URL(API_URL); + java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + conn.setRequestProperty("Accept-Charset", "UTF-8"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + + // 发送请求 + try (java.io.OutputStream os = conn.getOutputStream()) { + os.write(jsonBody.getBytes(StandardCharsets.UTF_8)); + } + + // 读取响应 + int responseCode = conn.getResponseCode(); + System.out.println("HTTP 响应码: " + responseCode); + + java.io.InputStream inputStream; + if (responseCode >= 400) { + inputStream = conn.getErrorStream(); + } else { + inputStream = conn.getInputStream(); + } + + StringBuilder response = new StringBuilder(); + try (java.io.BufferedReader reader = new java.io.BufferedReader( + new java.io.InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + } + + String responseBody = response.toString(); + System.out.println("响应内容: " + responseBody); + + result.put("success", responseCode == 200); + result.put("responseCode", responseCode); + result.put("response", responseBody); + result.put("message", responseCode == 200 ? "短信发送成功" : "短信发送失败"); + + } catch (Exception e) { + System.err.println("短信发送异常: " + e.getMessage()); + e.printStackTrace(); + result.put("success", false); + result.put("message", "短信发送异常: " + e.getMessage()); + } + return result; + } + + /** + * MD5 加密(小写) + * + * @param input 待加密字符串 + * @return 32位小写 MD5 字符串 + */ + private static String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } catch (Exception e) { + throw new RuntimeException("MD5加密失败", e); + } + } + + /** + * 生成6位随机验证码 + * + * @return 6位验证码字符串 + */ + private static String generateVerificationCode() { + return String.format("%06d", (int) (Math.random() * 1000000)); + } + + /** + * JSON 字符串转义(处理 " \ \n \r \t 等) + * + * @param input 原始字符串 + * @return 转义后的字符串 + */ + private static String escapeJson(String input) { + if (input == null) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + switch (c) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + default: + sb.append(c); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsProperties.java b/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsProperties.java new file mode 100644 index 0000000..d89e3e2 --- /dev/null +++ b/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsProperties.java @@ -0,0 +1,41 @@ +package org.qinan.safetyeval.start.sms; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 助通短信平台配置属性 + * + * @author safety-eval + */ +@Data +@Component +@ConfigurationProperties(prefix = "zt.sms") +public class ZtSmsProperties { + + /** + * 助通短信平台用户名 + */ + private String username = "qhdzyhy"; + + /** + * 助通短信平台密码 + */ + private String password = "Zcloud@88888"; + + /** + * 助通短信平台接口地址 + */ + private String apiUrl = "http://www.ztsms.cn/sendNSms.do"; + + /** + * 短信模板ID + */ + private String templateId = "793059"; + + /** + * 短信签名 + */ + private String signature = "【秦安安全】"; +} \ No newline at end of file diff --git a/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsService.java b/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsService.java new file mode 100644 index 0000000..faa4d20 --- /dev/null +++ b/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsService.java @@ -0,0 +1,138 @@ +package org.qinan.safetyeval.start.sms; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * 助通短信平台服务 + * API文档参考: http://www.ztsms.cn + * + * @author safety-eval + */ +@Slf4j +@Service +public class ZtSmsService { + + private final ZtSmsProperties ztSmsProperties; + private final RestTemplate restTemplate; + + public ZtSmsService(ZtSmsProperties ztSmsProperties) { + this.ztSmsProperties = ztSmsProperties; + this.restTemplate = new RestTemplate(); + } + + /** + * 发送验证码短信 + * + * @param mobile 手机号 + * @param validCode 验证码 + * @return 发送结果 + */ + public Map sendVerificationCode(String mobile, String validCode) { + // 构建短信内容:[秦安安全]您正在注册本系统,验证码:{valid_code},5分钟内有效,请勿泄露给他人,如非本人操作请忽略本条短信。 + String content = ztSmsProperties.getSignature() + "您正在注册本系统,验证码:" + validCode + ",5分钟内有效,请勿泄露给他人,如非本人操作请忽略本条短信。"; + return sendSms(mobile, content); + } + + /** + * 发送短信 + * + * @param mobile 手机号 + * @param content 短信内容 + * @return 发送结果 + */ + public Map sendSms(String mobile, String content) { + Map result = new HashMap<>(); + try { + // 生成tkey:当前时间,格式yyyyMMddHHmmss + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + String tkey = sdf.format(new Date()); + + // 生成password:md5(md5(password) + tkey) + String md5Password = md5(ztSmsProperties.getPassword()); + String finalPassword = md5(md5Password + tkey); + + // 构建请求参数 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("username", ztSmsProperties.getUsername()); + params.add("tkey", tkey); + params.add("password", finalPassword); + params.add("mobile", mobile); + params.add("content", content); + + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity> requestEntity = new HttpEntity<>(params, headers); + + log.info("助通短信发送请求 - 手机号: {}, 内容: {}", mobile, content); + log.info("助通短信发送请求 - tkey: {}, password: {}", tkey, finalPassword); + + // 发送请求 + ResponseEntity response = restTemplate.postForEntity( + ztSmsProperties.getApiUrl(), + requestEntity, + String.class + ); + + String responseBody = response.getBody(); + log.info("助通短信发送响应: {}", responseBody); + + result.put("success", true); + result.put("mobile", mobile); + result.put("response", responseBody); + result.put("message", "短信发送成功"); + + } catch (Exception e) { + log.error("助通短信发送失败", e); + result.put("success", false); + result.put("mobile", mobile); + result.put("message", "短信发送失败: " + e.getMessage()); + } + return result; + } + + /** + * MD5加密 + * + * @param input 待加密字符串 + * @return 加密后的小写字符串 + */ + private String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } catch (Exception e) { + throw new RuntimeException("MD5加密失败", e); + } + } + + /** + * 生成6位随机验证码 + * + * @return 6位验证码 + */ + public String generateVerificationCode() { + return String.format("%06d", (int) (Math.random() * 1000000)); + } +} \ No newline at end of file