最近要做一个小程序,业务流程最后需要下发短信给客户,其中短信内容含地址变量。
经调研后,使用OSS短信服务进行短信下发。并采用rocketmq与OSS SDK组合方式进行下发短信。
step1 开通OSS短信服务
登录阿里云工作台,开通短信服务。工作台链接:阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台
选择开通短信服务,这一步省略,开通后,进行购买套餐,页面如下图所示(已经购买了)
step2 设置短信签名和模板
点击添加签名,按照要求填写
点击模板管理,点击添加模板
模板创建完成后,会有模板code,在代码中会使用到他。
以上短信模板配置完成,接下来通过代码下发短信。
step3 代码(mq与sms结合)
3-1 nacos配置(也可以放到项目文件)yml
aliyun:
sms-enable: true
sms:
access-key-id: #开通时获取的
access-key-secret: #开通是获取的
endpoint: dysmsapi.aliyuncs.com #固定的
region: cn-hangzhou #oss服务器地
common-code-sign-name: 测试签名 #控制台的签名管理
common-code-template-code: SMS_DASD #模板CODE
topic:
sms: sms # mq的topic
rocketmq: #mq 配置
active: dev #开发环境
enable: true
endAppendEnv: true
enableAcl: false
accessKey:
secretKey:
access-channel-str:
consumer:
consumer-list:
- namesrv-addr: 127.0.0.1:9876 #mq地址
consumer-group: smsConsumerGroup #消费者组名
topic: sms
sub-expression:
pull-batch-size: 10
model: CLUSTERING
handler: com.test.oss.consumer.SmsConsumer #消费者的包位置
producer:
namesrv-addr: 127.0.0.1:9876
group: testProducerGroup
3-2 发送消息接口
/**
* @author ming
*/
public interface SmsService {
/**
* 通过mq发送短信
*
* @param msgDTO
*/
void sendSmsToMq(MsgDTO msgDTO);
/**
* 直接发送同步短信
*
* @param phoneNumber 接收短信的手机号码
* @param signName 短信签名名称
* @param templateCode 短信模板CODE
* @param templateParam 短信模板变量对应的实际值- ${path} ${suffix} 两个变量对应的值
* 可以先建一个对象包含着两个字段,将值放进去后,转成String,再传参
* @param outId 外部流水扩展字段
* @return
*/
boolean syncSendMsg(String phoneNumber, String signName, String templateCode,
String templateParam, String outId);
/**
* 直接发送异步短信
*
* @param phoneNumber 接收短信的手机号码
* @param signName 短信签名名称
* @param templateCode 短信模板CODE
* @param templateParam 短信模板变量对应的实际值
* @param outId 外部流水扩展字段
* @return
*/
void asyncSendMsg(String phoneNumber, String signName, String templateCode,
String templateParam, String outId);
}
@Data
public class MsgDTO implements Serializable {
/**
* 消息内容
*/
private String content;
/**
* 消息类型枚举
*/
private MsgTypeEnum msgTypeEnum;
}
public enum MsgTypeEnum {
/** 短信验证码 */
SMS_VERIFICATION_CODE,
/** 短信注册通知 */
SMS_REGISTER_NOTICE,
/** 短信通用通知 */
SMS_COMMON_NOTICE,
}
@Slf4j
@Service
@ConditionalOnProperty(prefix = "aliyun", name = "sms-enable", havingValue = "true")
public class SmsServiceImpl implements SmsService {
private final RocketMqProducer rocketMqProducer;
private final com.aliyun.dysmsapi20170525.Client smsSyncClient;
private final com.aliyun.sdk.service.dysmsapi20170525.AsyncClient smsAsyncClient;
public SmsServiceImpl(RocketMqProducer rocketMqProducer, Client smsSyncClient, AsyncClient smsAsyncClient) {
this.rocketMqProducer = rocketMqProducer;
this.smsSyncClient = smsSyncClient;
this.smsAsyncClient = smsAsyncClient;
}
@Value("${topic.sms}")
private String smsTopicName;
@Override
public void sendSmsToMq(MsgDTO msgDTO) {
//mq发送短信
rocketMqProducer.sendMsg(msgDTO.getContent(), msgDTO.getMsgTypeEnum().toString(), smsTopicName);
}
@Override
public boolean syncSendMsg(String phoneNumber, String signName, String templateCode, String templateParam, String outId) {
//直接发送短信
SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setSignName(signName)
.setTemplateCode(templateCode)
.setPhoneNumbers(phoneNumber)
.setTemplateParam(templateParam);
if (StringUtils.hasLength(outId)) {
sendSmsRequest.setOutId(outId);
}
RuntimeOptions runtime = new RuntimeOptions();
try {
SendSmsResponse sendSmsResponse = smsSyncClient.sendSmsWithOptions(sendSmsRequest, runtime);
if (sendSmsResponse != null) {
return "OK".equals(sendSmsResponse.getBody().code);
}
} catch (Exception error) {
log.error("aliyun sms sync error.", error);
}
return false;
}
@Override
public void asyncSendMsg(String phoneNumber, String signName, String templateCode, String templateParam, String outId) {
//直接发送短信
com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsRequest.Builder builder = com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsRequest.builder()
.signName(signName)
.templateCode(templateCode)
.phoneNumbers(phoneNumber)
.templateParam(templateParam);
if (StringUtils.hasLength(outId)) {
builder.outId(outId);
}
smsAsyncClient.sendSms(builder.build());
}
}
mq producer
@Slf4j
@Component
@ConditionalOnProperty(prefix = "rocketmq", name = "enable", havingValue = "true")
public class RocketMqProducer {
private final RocketMQProperties rocketMQProperties;
private final RocketMQProducerProperties producerProperties;
public RocketMqProducer(RocketMQProperties rocketMQProperties, RocketMQProducerProperties producerProperties) {
this.rocketMQProperties = rocketMQProperties;
this.producerProperties = producerProperties;
}
private DefaultMQProducer mqProducer;
public DefaultMQProducer getMqProducer() {
return mqProducer;
}
@PostConstruct
public void initMqProducer() {
//初始加载
if (ObjectUtil.equal(rocketMQProperties.getEnableAcl(), Boolean.TRUE)) {
mqProducer = new DefaultMQProducer(producerProperties.getGroup(), new AclClientRPCHook(new SessionCredentials(rocketMQProperties.getAccessKey(), rocketMQProperties.getSecretKey())));
} else {
mqProducer = new DefaultMQProducer(producerProperties.getGroup());
}
mqProducer.setNamesrvAddr(producerProperties.getNamesrvAddr());
mqProducer.setAccessChannel(rocketMQProperties.getAccessChannel());
mqProducer.setVipChannelEnabled(false);
try {
mqProducer.start();
} catch (MQClientException e) {
log.error("rocketmq producer start error.", e);
}
}
@PreDestroy
public void destroy() {
//执行完 销毁
mqProducer.shutdown();
}
/**
* 发送同步消息
*
* @param msgContent 消息内容
* @param tags tags
* @param topic topic
*/
public void sendMsg(String msgContent, String tags, String topic) {
//根据环境获取topic名称 细看mqUtil,以此区分环境
topic = ObjectUtil.equal(rocketMQProperties.getEndAppendEnv(), Boolean.TRUE) ? MqUtil.getInstance().getMqTopicNameByAppProfileEnv(topic) : topic;
log.info("send [{}:{}] msg {}", topic, tags, msgContent);
try {
//通过topic和tags发送mq消息
Message msg = new Message(topic, tags, msgContent.getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = this.mqProducer.send(msg);
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
throw new CommonException(sendResult.getSendStatus().toString());
}
} catch (Exception e) {
log.error("send [{}:{}] msg {} error.", topic, tags, msgContent);
}
}
}
mq消费 consumer
@Slf4j
public class SmsConsumer implements Consumer<List<MessageExt>> {
private final SmsService smsService = SpringUtil.getBean(SmsServiceImpl.class);
@Override
public void accept(List<MessageExt> messageExts) {
try {
for (MessageExt messageExt : messageExts) {
//消费mq消息,并获取发送的msgContent
String msgContent = StrUtil.str(messageExt.getBody(), RemotingHelper.DEFAULT_CHARSET);
SmsMsg smsMsg = JSONUtil.toBean(msgContent, SmsMsg.class);
log.info("messageExt: {}", msgContent);
smsService.asyncSendMsg(smsMsg.getPhoneNumber(),
aliyunSmsProperties.getCommonNoticeCodeSignName(),
aliyunSmsProperties.getCommonNoticeTemplateCode(),
JSONUtil.toJsonStr(smsMsg.getTemplateParam()), null);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
public class MqUtil {
private final ApplicationProfileEnvProperties applicationProfileEnvProperties = SpringUtil.getBean(ApplicationProfileEnvProperties.class);
private static volatile MqUtil instance;
private MqUtil() {
}
/**
* getInstance方法会进行两次判空操作;第一次,判断是否实例化了,有实例对象则直接返回,不需要其他操作。
* 如果没有实例化,则说明是程序第一次去获取实例对像,会进行一次加锁操作,只允许一个线程进入方法,
* 进入方法之后的判空操作是为了仅允许第一个进入的线程进行实例化,其他线程不允许实例化。
* 因为实例化操作仅需要进行一次同步,所以可以用第一次判空操作来进行避免。至于第二次判空操作,则是为了保证单例。
*
* @return
*/
public static MqUtil getInstance() {
if (instance == null) {
synchronized (MqUtil.class) {
if (instance == null) {
instance = new MqUtil();
}
}
}
return instance;
}
/**
* 通过环境env,获取mq得topic名字
*
* @param topicName
* @return 原始topic名字-env名称,例sms-test, sms-prod
*/
public String getMqTopicNameByAppProfileEnv(String topicName) {
return topicName + Constants.UNDER_LINE + applicationProfileEnvProperties.getActive();
}
/**
* 通过环境env,获取mq得consumerGroup名字
*
* @param consumerGroup
* @return 原始consumerGroup名字-env名称,例algorithmTaskOvertimeConsumerGroup-test, 例algorithmTaskOvertimeConsumerGroup-prod
*/
public String getMqConsumerGroupByAppProfileEnv(String consumerGroup) {
return consumerGroup + Constants.UNDER_LINE + applicationProfileEnvProperties.getActive();
}
}
@Data
@Component
@ConditionalOnProperty(prefix = "rocketmq", name = "enable", havingValue = "true")
public class ApplicationProfileEnvProperties {
//环境(dev test prod)
private String active;
}