diff --git a/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/AccountController.java b/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/AccountController.java index 96fef3b..16e981c 100644 --- a/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/AccountController.java +++ b/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/AccountController.java @@ -1,7 +1,6 @@ package org.qinan.safetyeval.adapter.web; import cn.hutool.json.JSONUtil; -import com.jjb.saas.message.client.message.facede.MessageFacade; import com.jjb.saas.message.client.message.request.MessageSendCmd; import com.jjb.saas.message.client.message.request.MessageTargetCmd; import io.swagger.annotations.Api; @@ -9,11 +8,11 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.dubbo.config.annotation.DubboReference; import org.qinan.safetyeval.client.api.AccountApi; import org.qinan.safetyeval.client.co.AccountCO; import org.qinan.safetyeval.client.dto.*; import org.qinan.safetyeval.domain.exception.BizException; +import org.qinan.safetyeval.infrastructure.remote.SmsRemote; import org.springframework.beans.factory.annotation.Value; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -39,11 +38,9 @@ public class AccountController { @Resource private AccountApi accountApi; - @DubboReference - private MessageFacade messageFacade; + @Resource + private SmsRemote smsRemote; - @Value("${sms.sign.sourceCode:}") - private String sourceCode; @ApiOperation("新增用户") @PostMapping("/save") @@ -78,27 +75,7 @@ public class AccountController { @ApiOperation("发送短信") @PostMapping("/sendMessage") public com.alibaba.cola.dto.SingleResponse sendMessage(@Validated @RequestBody SendMessageCmd cmd) { - if (StringUtils.isBlank(sourceCode)) { - throw new BizException(SMS_SOURCE_CODE_NOT_EXIST); - } - - MessageSendCmd messageSendCmd = new MessageSendCmd(); - messageSendCmd.setBusinessId(UUID.randomUUID().toString().replace("-", "")); - - MessageTargetCmd messageTargetCmd = new MessageTargetCmd(); - messageTargetCmd.setMobile(cmd.getMobile()); - messageSendCmd.setTargetCmd(messageTargetCmd); - messageSendCmd.setNeedTokenEnum(false); - - messageSendCmd.setSourceCode(sourceCode); - Map codeMap = new HashMap<>(); - codeMap.put("valid_code", "123456"); - messageSendCmd.setParams(codeMap); - - log.info("发送短信参数: messageSendCmd: {}", JSONUtil.toJsonStr(messageSendCmd)); - com.alibaba.cola.dto.SingleResponse result = messageFacade.send(messageSendCmd); - log.info("发送短信结果: result: {}", JSONUtil.toJsonStr(result)); - return result; + return smsRemote.sign(cmd.getMobile()); } @ApiOperation("用户同步gbs") diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/remote/SmsProperties.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/remote/SmsProperties.java new file mode 100644 index 0000000..d71480d --- /dev/null +++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/remote/SmsProperties.java @@ -0,0 +1,46 @@ +package org.qinan.safetyeval.infrastructure.remote; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 助通短信平台(融合云 v2)配置属性 + * + * @author safety-eval + */ +@Data +@Component +@ConfigurationProperties(prefix = "zt.sms") +public class SmsProperties { + + /** + * 助通融合云用户名 + */ + private String username = "qhdzyhy"; + + /** + * 助通融合云密码(明文,调用时二次 md5 加密) + */ + private String password = "Zcloud@88888"; + + /** + * 融合云 v2 短信发送接口地址 + */ + private String apiUrl = "https://api-shss.zthysms.com/v2/sendSms"; + + /** + * 短信签名(必须使用全角【】,需与平台报备一致) + */ + private String signature = "【秦安安全】"; + + /** + * 连接超时(毫秒) + */ + private int connectTimeout = 10000; + + /** + * 读取超时(毫秒) + */ + private int readTimeout = 30000; +} diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/remote/SmsRemote.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/remote/SmsRemote.java new file mode 100644 index 0000000..1673381 --- /dev/null +++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/remote/SmsRemote.java @@ -0,0 +1,240 @@ +package org.qinan.safetyeval.infrastructure.remote; + +import cn.hutool.json.JSONUtil; +import com.alibaba.cola.dto.SingleResponse; +import com.jjb.saas.message.client.message.facede.MessageFacade; +import com.jjb.saas.message.client.message.request.MessageSendCmd; +import com.jjb.saas.message.client.message.request.MessageTargetCmd; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.dubbo.config.annotation.DubboReference; +import org.qinan.safetyeval.client.dto.SendMessageCmd; +import org.qinan.safetyeval.domain.exception.BizException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import static org.qinan.safetyeval.domain.exception.ErrorCode.SMS_SOURCE_CODE_NOT_EXIST; + +/** + * 助通短信平台(融合云 v2)远程调用 + *

+ * 接口说明: + * - 地址:https://api-shss.zthysms.com/v2/sendSms + * - 方式:POST,Content-Type: application/json + * - 鉴权:tKey = 当前 Unix 秒级时间戳;password = md5(md5(明文密码) + tKey) + * - 错误码:200 成功;4023 签名错误;4001 用户名错误;4009 密码错误 等 + * + * @author safety-eval + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SmsRemote { + + private final SmsProperties smsProperties; + + private RestTemplate restTemplate; + + @DubboReference + private MessageFacade messageFacade; + + + @Value("${sms.sign.sourceCode:}") + private String sourceCode; + + @PostConstruct + public void init() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(smsProperties.getConnectTimeout()); + factory.setReadTimeout(smsProperties.getReadTimeout()); + this.restTemplate = new RestTemplate(factory); + } + + /** + * 发送注册验证码短信(模板ID 793059) + * + * @param mobile 手机号 + * @param validCode 验证码 + * @return 调用结果 + */ + public SingleResponse sendRegisterVerificationCode(String mobile, String validCode) { + // 模板内容:[秦安安全]您正在注册本系统,验证码:{valid_code},5分钟内有效,请勿泄露给他人,如非本人操作请忽略本条短信。 + String content = smsProperties.getSignature() + + "您正在注册本系统,验证码:" + validCode + + ",5分钟内有效,请勿泄露给他人,如非本人操作请忽略本条短信。"; + return sendSms(mobile, content); + } + + public SingleResponse sign(String mobile) { + String validCode = generateVerificationCode(); + String content = "【秦安安全】您正在注册本系统,验证码:" + validCode + ",5分钟内有效,请勿泄露给他人,如非本人操作请忽略本条短信。"; + sendSms(mobile, content); + return SingleResponse.of(true); + } + + /** + * 发送短信(融合云 v2 接口,明文内容) + * + * @param mobile 手机号 + * @param content 短信内容(需包含全角【】签名) + * @return 调用结果 + */ + public SingleResponse sendSms(String mobile, String content) { + log.info("助通短信发送请求 - mobile={}, content={}", mobile, content); + SingleResponse response; + try { + String jsonBody = buildRequestBody(mobile, content); + log.info("助通短信发送请求体: {}", jsonBody); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8)); + + HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers); + + ResponseEntity resp = + restTemplate.postForEntity(smsProperties.getApiUrl(), requestEntity, String.class); + + String body = resp.getBody(); + log.info("助通短信发送响应: status={}, body={}", resp.getStatusCodeValue(), body); + + // 融合云成功返回 HTTP 200,且响应 JSON 中 code=200 + if (resp.getStatusCodeValue() == 200) { + response = SingleResponse.of(body); + } else { + log.warn("助通短信发送失败 - mobile={}, body={}", mobile, body); + throw new BizException("SMS_SEND_FAIL", "短信发送失败: " + body); + } + } catch (BizException e) { + throw e; + } catch (Exception e) { + log.error("助通短信发送异常 - mobile={}", mobile, e); + throw new BizException("SMS_SEND_ERROR", "短信发送异常: " + e.getMessage()); + } + return response; + } + + /** + * 生成 6 位随机验证码 + * + * @return 6位验证码字符串 + */ + public String generateVerificationCode() { + return String.format("%06d", ThreadLocalRandom.current().nextInt(1000000)); + } + + // ==================== 内部方法 ==================== + + /** + * 构建融合云 v2 JSON 请求体 + */ + private String buildRequestBody(String mobile, String content) { + String tKey = String.valueOf(System.currentTimeMillis() / 1000); + String finalPassword = md5(md5(smsProperties.getPassword()) + tKey); + + return "{" + + "\"username\":\"" + smsProperties.getUsername() + "\"," + + "\"password\":\"" + finalPassword + "\"," + + "\"tKey\":\"" + tKey + "\"," + + "\"mobile\":\"" + mobile + "\"," + + "\"content\":\"" + escapeJson(content) + "\"" + + "}"; + } + + /** + * MD5 加密(小写) + */ + 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 BizException("SMS_MD5_ERROR", "MD5加密失败: " + e.getMessage()); + } + } + + /** + * JSON 字符串转义 + */ + private 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(); + } + + + @ApiOperation("发送短信") + @PostMapping("/sendMessage") + public com.alibaba.cola.dto.SingleResponse sendMessage(@Validated @RequestBody SendMessageCmd cmd) { + if (StringUtils.isBlank(sourceCode)) { + throw new BizException(SMS_SOURCE_CODE_NOT_EXIST); + } + + MessageSendCmd messageSendCmd = new MessageSendCmd(); + messageSendCmd.setBusinessId(UUID.randomUUID().toString().replace("-", "")); + + MessageTargetCmd messageTargetCmd = new MessageTargetCmd(); + messageTargetCmd.setMobile(cmd.getMobile()); + messageSendCmd.setTargetCmd(messageTargetCmd); + messageSendCmd.setNeedTokenEnum(false); + + messageSendCmd.setSourceCode(sourceCode); + Map codeMap = new HashMap<>(); + codeMap.put("valid_code", "123456"); + messageSendCmd.setParams(codeMap); + + log.info("发送短信参数: messageSendCmd: {}", JSONUtil.toJsonStr(messageSendCmd)); + com.alibaba.cola.dto.SingleResponse result = messageFacade.send(messageSendCmd); + log.info("发送短信结果: result: {}", JSONUtil.toJsonStr(result)); + return result; + } +} diff --git a/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsMainTest.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/remote/ZtSmsMainTest.java similarity index 99% rename from safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsMainTest.java rename to safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/remote/ZtSmsMainTest.java index 0a789f0..99d03b3 100644 --- a/safety-eval-start/src/main/java/org/qinan/safetyeval/start/sms/ZtSmsMainTest.java +++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/remote/ZtSmsMainTest.java @@ -1,4 +1,4 @@ -package org.qinan.safetyeval.start.sms; +package org.qinan.safetyeval.infrastructure.remote; import java.nio.charset.StandardCharsets; import java.security.MessageDigest;