dev
parent
8ca81101ab
commit
2829d082ae
|
|
@ -1,7 +1,6 @@
|
||||||
package org.qinan.safetyeval.adapter.web;
|
package org.qinan.safetyeval.adapter.web;
|
||||||
|
|
||||||
import cn.hutool.json.JSONUtil;
|
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.MessageSendCmd;
|
||||||
import com.jjb.saas.message.client.message.request.MessageTargetCmd;
|
import com.jjb.saas.message.client.message.request.MessageTargetCmd;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
|
|
@ -9,11 +8,11 @@ import io.swagger.annotations.ApiOperation;
|
||||||
import io.swagger.annotations.ApiParam;
|
import io.swagger.annotations.ApiParam;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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.api.AccountApi;
|
||||||
import org.qinan.safetyeval.client.co.AccountCO;
|
import org.qinan.safetyeval.client.co.AccountCO;
|
||||||
import org.qinan.safetyeval.client.dto.*;
|
import org.qinan.safetyeval.client.dto.*;
|
||||||
import org.qinan.safetyeval.domain.exception.BizException;
|
import org.qinan.safetyeval.domain.exception.BizException;
|
||||||
|
import org.qinan.safetyeval.infrastructure.remote.SmsRemote;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
@ -39,11 +38,9 @@ public class AccountController {
|
||||||
@Resource
|
@Resource
|
||||||
private AccountApi accountApi;
|
private AccountApi accountApi;
|
||||||
|
|
||||||
@DubboReference
|
@Resource
|
||||||
private MessageFacade messageFacade;
|
private SmsRemote smsRemote;
|
||||||
|
|
||||||
@Value("${sms.sign.sourceCode:}")
|
|
||||||
private String sourceCode;
|
|
||||||
|
|
||||||
@ApiOperation("新增用户")
|
@ApiOperation("新增用户")
|
||||||
@PostMapping("/save")
|
@PostMapping("/save")
|
||||||
|
|
@ -78,27 +75,7 @@ public class AccountController {
|
||||||
@ApiOperation("发送短信")
|
@ApiOperation("发送短信")
|
||||||
@PostMapping("/sendMessage")
|
@PostMapping("/sendMessage")
|
||||||
public com.alibaba.cola.dto.SingleResponse<Boolean> sendMessage(@Validated @RequestBody SendMessageCmd cmd) {
|
public com.alibaba.cola.dto.SingleResponse<Boolean> sendMessage(@Validated @RequestBody SendMessageCmd cmd) {
|
||||||
if (StringUtils.isBlank(sourceCode)) {
|
return smsRemote.sign(cmd.getMobile());
|
||||||
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<String, Object> codeMap = new HashMap<>();
|
|
||||||
codeMap.put("valid_code", "123456");
|
|
||||||
messageSendCmd.setParams(codeMap);
|
|
||||||
|
|
||||||
log.info("发送短信参数: messageSendCmd: {}", JSONUtil.toJsonStr(messageSendCmd));
|
|
||||||
com.alibaba.cola.dto.SingleResponse<Boolean> result = messageFacade.send(messageSendCmd);
|
|
||||||
log.info("发送短信结果: result: {}", JSONUtil.toJsonStr(result));
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation("用户同步gbs")
|
@ApiOperation("用户同步gbs")
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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)远程调用
|
||||||
|
* <p>
|
||||||
|
* 接口说明:
|
||||||
|
* - 地址: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<String> sendRegisterVerificationCode(String mobile, String validCode) {
|
||||||
|
// 模板内容:[秦安安全]您正在注册本系统,验证码:{valid_code},5分钟内有效,请勿泄露给他人,如非本人操作请忽略本条短信。
|
||||||
|
String content = smsProperties.getSignature()
|
||||||
|
+ "您正在注册本系统,验证码:" + validCode
|
||||||
|
+ ",5分钟内有效,请勿泄露给他人,如非本人操作请忽略本条短信。";
|
||||||
|
return sendSms(mobile, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SingleResponse<Boolean> 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<String> sendSms(String mobile, String content) {
|
||||||
|
log.info("助通短信发送请求 - mobile={}, content={}", mobile, content);
|
||||||
|
SingleResponse<String> 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<String> requestEntity = new HttpEntity<>(jsonBody, headers);
|
||||||
|
|
||||||
|
ResponseEntity<String> 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<Boolean> 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<String, Object> codeMap = new HashMap<>();
|
||||||
|
codeMap.put("valid_code", "123456");
|
||||||
|
messageSendCmd.setParams(codeMap);
|
||||||
|
|
||||||
|
log.info("发送短信参数: messageSendCmd: {}", JSONUtil.toJsonStr(messageSendCmd));
|
||||||
|
com.alibaba.cola.dto.SingleResponse<Boolean> result = messageFacade.send(messageSendCmd);
|
||||||
|
log.info("发送短信结果: result: {}", JSONUtil.toJsonStr(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package org.qinan.safetyeval.start.sms;
|
package org.qinan.safetyeval.infrastructure.remote;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
Loading…
Reference in New Issue