一、总述
短信和支付一样,会涉及费率、发送频率、安全稳定等问题。故短信平台需要对接阿里云和畅卓等多家短信服务方。
二、数模设计
-- 短信发送记录表
CREATE TABLE `t_sms_send_log` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`mobile` varchar(16) NOT NULL COMMENT '手机号',
`template_no` varchar(16) NOT NULL COMMENT '模板编号, 关联表t_sms_template.template_no',
`sms_content` varchar(500) CHARACTER SET utf8mb4 NOT NULL COMMENT '短信内容',
`send_status` smallint(6) NOT NULL COMMENT '短信发送状态:1待发送,2发送中,3发送成功,4发送失败',
`third_id` smallint(6) NOT NULL COMMENT '第三方发送方ID',
`third_name` varchar(32) NOT NULL COMMENT '第三方发送方名称',
`third_trade_no` varchar(64) DEFAULT NULL COMMENT '第三方发送交易号',
`ctime` int(11) NOT NULL COMMENT '创建时间',
`utime` int(11) NOT NULL COMMENT '修改时间',
`remark` varchar(128) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `idx_template_id` (`mobile`,`template_no`),
KEY `idx_ctime` (`ctime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='短信发送记录表';
-- 短信模板表
CREATE TABLE `t_sms_template` (
`template_id` bigint(20) NOT NULL COMMENT '主键ID',
`template_type` smallint(6) NOT NULL COMMENT '模板类型:1通知,2验证码',
`template_no` varchar(32) DEFAULT NULL COMMENT '模板编号',
`template_content` varchar(500) CHARACTER SET utf8mb4 NOT NULL COMMENT '模板内容',
`template_description` varchar(200) DEFAULT NULL COMMENT '模板描述',
`ctime` int(11) NOT NULL COMMENT '创建时间',
`utime` int(11) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`template_id`),
UNIQUE KEY `idx_template_no` (`template_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='短信模板表';
三、用到的设计模式
类设计.png工厂、模板、策略三个设计模式的实用示例。
模板,体现在抽象类的抽象方法交由子类去具体实现,发短信,具体怎么做是不用理会的。写了一个实现的发短信的方法,第一步根据模板编号获取模板,组装好短信内容;第二步调用子类的具体发送短信;第三步解析发送结果,并保存短信发送记录。模板模式需要学会钩子方法的妙用。能够把子类方法的变动级联到抽象类里。
策略,体现在工厂类的getSmsService()方法上,可以根据具体的路由规则,返回发送短信的实现类。
Boolean result = smsFactoryService.getSmsService(new RandomRule(mobile, brandId, 1)).
sendSms(mobile, brandId, templateNo, firstVarValue, secondVarValue);
四、主要的类源码
/**
* 发送短信的抽象类
*/
public abstract class AbstractSmsService implements SmsService {
@Autowired
private SmsTemplateService smsTemplateService;
@Autowired
private SmsSendLogService smsSendLogService;
@Autowired
private CacheClient cacheClient;
private static final Pattern regexVar = Pattern.compile("\\$\\{(.*?)\\}");
/**
* 获取短信模板
*
* @param templateNo
* @return
*/
protected SmsTemplate getSmsTemplate(String templateNo) {
if (StringUtils.isEmpty(templateNo)) {
return null;
}
return smsTemplateService.getSmsTemplateByNo(templateNo);
}
/**
* 返回第三方短信服务商
*
* @return
*/
protected abstract SmsThirdEnum getSmsThird();
/**
* 发送短信
*
*/
protected abstract SmsSendResult send(String mobile, Long brandId, String templateNo, Map<String, Object> params);
/**
* 发送短信.
* 最多支持两个变量
*/
@Override
public Boolean sendSms(String mobile, Long brandId, String templateNo, String firstVarValue, String secondVarValue) {
Map<String, Object> params = Maps.newHashMap();
SmsTemplate smsTemplate = getSmsTemplate(templateNo);
if (null == smsTemplate) {
GwsLogger.error("短信模板编号{}不能为空!", templateNo);
return false;
}
/**模板内容*/
String templateContent = smsTemplate.getTemplateContent();
Matcher matcher = regexVar.matcher(templateContent);
int iVariables = 0;
while (matcher.find()) {
iVariables++;
if (iVariables == 1) {
params.put(matcher.group(1), firstVarValue);
} else if (iVariables == 2) {
params.put(matcher.group(1), secondVarValue);
}
}
SmsSendResult sendResult = send(mobile, brandId, templateNo, params);
sendResult.setSmsContent(formatSmsContent(smsTemplate.getTemplateContent(), params));
parseSendResult(sendResult, smsTemplate.getTemplateType());
return sendResult.getSuccess();
}
/**
* 格式化短信模板
*
* @param templateContent
* @param params
* @return
*/
protected String formatSmsContent(String templateContent, Map<String, Object> params) {
if (StringUtils.isEmpty(templateContent)) {
return null;
}
if (CollectionUtils.isEmpty(params)) {
return templateContent;
}
for (String key : params.keySet()) {
templateContent = templateContent.replaceAll("\\$\\{" + key + "}",
params.get(key).toString());
}
return templateContent;
}
/**
* 解析短信发送结果
*
* @param sendResult
* @return
*/
private void parseSendResult(SmsSendResult sendResult, Integer templateType) {
Boolean success = sendResult.getSuccess();
Integer sendStatus = success ? SendStatusEnum.SEND_SUCCESS.getCode() : SendStatusEnum.SEND_FAIL.getCode();
SmsThirdEnum smsThirdEnum = getSmsThird();
String mobile = sendResult.getMobile();
/**短信验证码, 需要记录手机上一次的短信服务商是阿里云还是畅卓,用于轮询发送短信*/
if (TemplateTypeEnum.VCODE.getCode().equals(templateType)) {
cacheClient.set(CachePrefix.MOBILE_VCODE_SMS_SEND_THIRD, mobile,
smsThirdEnum.getCode(), 60 * 60 * 24 * 1L);
}
saveSmsSendLog(sendResult.getMobile(), sendResult.getTemplateNo(), sendResult.getSmsContent(),
sendStatus, smsThirdEnum.getCode(),
smsThirdEnum.getMessage(), sendResult.getThirdTradeNo());
}
/**
* 保存发送记录
*
* @param mobile
* @param templateNo
* @param smsContent
* @param sendStatus
* @param thirdId
* @param thirdName
* @param thirdTradeNo
*/
public void saveSmsSendLog(String mobile, String templateNo, String smsContent,
Integer sendStatus, Integer thirdId, String thirdName,
String thirdTradeNo) {
SmsSendLog smsSendLog = new SmsSendLog();
smsSendLog.setMobile(mobile);
smsSendLog.setTemplateNo(templateNo);
smsSendLog.setSmsContent(smsContent);
smsSendLog.setSendStatus(sendStatus);
smsSendLog.setThirdId(thirdId);
smsSendLog.setThirdName(thirdName);
smsSendLog.setThirdTradeNo(thirdTradeNo);
smsSendLogService.saveSmsSendLog(smsSendLog);
}
}
/**
* 【阿里云短信服务类】
*
* @author yangjh 06/05/2017.
*/
@Service(value = "aliyunSmsService")
public class AliyunSmsServiceImpl extends AbstractSmsService {
private static IAcsClient acsClient;
private static final String SIGN_NAME = "游戏猫";
/**
* 产品名称:云通信短信API产品
*/
private static final String product = "Dysmsapi";
/**
* 产品域名
*/
private static final String domain = "dysmsapi.aliyuncs.com";
private static final String SUCCESS_FLAG = "OK";
@Value("${aliyun.sms.regionId}")
private String smsRegionId;
@Value("${aliyun.sms.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.sms.accessKeySecret}")
private String accessKeySecret;
@Autowired
private SmsBrandSignService smsBrandSignService;
/**
* 初始化阿里云短信客户端
* IAcsClient 是线程安全的
*/
@PostConstruct
public void init() {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
try {
IClientProfile profile = DefaultProfile.getProfile(smsRegionId, accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint(smsRegionId, smsRegionId, product, domain);
acsClient = new DefaultAcsClient(profile);
} catch (ClientException e) {
GwsLogger.error("初始化阿里云短信客户端出现异常{}", e);
}
}
@Override
public SmsSendResult send(String mobile, Long brandId, String templateNo, Map<String, Object> params) {
SmsSendResult sendResult = new SmsSendResult();
sendResult.setMobile(mobile);
sendResult.setTemplateNo(templateNo);
Boolean success = false;
try {
SendSmsRequest request = new SendSmsRequest();
//必填:待发送手机号, 支持多个手机号, 使用逗号隔开.
request.setPhoneNumbers(mobile);
//必填:短信签名-可在短信控制台中找到
//SmsSignEnum smsSign = SmsSignEnum.getEnum(brandId);
SmsBrandSign smsBrandSign = smsBrandSignService.getSmsBrandSign(brandId);
request.setSignName(null == smsBrandSign ? SIGN_NAME : smsBrandSign.getSmsSign());
//必填:短信模板-可在短信控制台中找到
request.setTemplateCode(templateNo);
// json格式
if (!CollectionUtils.isEmpty(params)) {
request.setTemplateParam(JSON.toJSONString(params));
}
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
if (null != sendSmsResponse) {
GwsLogger.info("阿里云--手机{}发送短信模板{},品牌ID是{},返回报文是{}", mobile, templateNo, brandId, JSON.toJSONString(sendSmsResponse));
if (SUCCESS_FLAG.equalsIgnoreCase(sendSmsResponse.getCode())) {
success = true;
sendResult.setThirdTradeNo(sendSmsResponse.getBizId());
GwsLogger.info("阿里云--手机{}发送短信模板{}成功!,品牌ID是{}", mobile, templateNo, brandId);
} else {
GwsLogger.info("阿里云--手机{}发送短信模板{}失败!,品牌ID是{}", mobile, templateNo, brandId);
}
}
} catch (Exception e) {
GwsLogger.error("阿里云--手机{}发送短信异常,模板编号是{},品牌ID是{},详细错误是" + e, mobile, templateNo, brandId);
}
sendResult.setSuccess(success);
return sendResult;
}
@Override
protected SmsThirdEnum getSmsThird() {
return SmsThirdEnum.ALIYUN;
}
}
/**
* 畅卓发短信
*/
@Service(value = "chanzorSmsService")
public class ChanzorSmsServiceImpl extends AbstractSmsService {
private static final String SMS_SERVICE_URL = "http://api.chanzor.com/sms.aspx";
private static final String SUCCESS_FLAG = "Success";
public static final String CHARSET = "utf-8";
public static final String SMS_SIGN = "【游戏猫】";
@Override
protected SmsThirdEnum getSmsThird() {
return SmsThirdEnum.CHAN_ZOR;
}
@Override
protected SmsSendResult send(String mobile, Long brandId, String templateNo, Map<String, Object> params) {
SmsSendResult sendResult = new SmsSendResult();
sendResult.setMobile(mobile);
sendResult.setTemplateNo(templateNo);
Boolean success = false;
try {
SmsTemplate smsTemplate = getSmsTemplate(templateNo);
if (null == smsTemplate) {
GwsLogger.error("畅卓--手机{}发送短信模板{}不存在!");
return null;
}
/**短信内容, 注意畅卓需要额外传签名数据*/
String content = formatSmsContent(smsTemplate.getTemplateContent(), params) + SMS_SIGN;
/**发送短信*/
String retStr = chanzorSms(buildHttpPostData(mobile, content), SMS_SERVICE_URL);
GwsLogger.info("畅卓--手机{}发送短信模板{}返回报文内容是{}", mobile, templateNo, retStr);
/** 如果返回报文为空, 则退出 */
if (StringUtils.isEmpty(retStr)) {
return sendResult;
}
Map<String, String> resultMap = XmlUtils.toMap(retStr.getBytes(CHARSET), CHARSET);
String returnstatus = resultMap.get("returnstatus");
if (SUCCESS_FLAG.equals(returnstatus)) {
success = true;
String taskId = resultMap.get("taskid");
sendResult.setThirdTradeNo(taskId);
GwsLogger.info("畅卓--手机{}发送短信模板{}成功!", mobile, templateNo);
} else {
GwsLogger.info("畅卓--手机{}发送短信模板{}失败!", mobile, templateNo);
}
} catch (Exception e) {
GwsLogger.error("畅卓--手机{}发送短信异常,模板编号是{},详细错误是{}", mobile, templateNo, e);
}
sendResult.setSuccess(success);
return sendResult;
}
/**
* 发送短信
*
* @param postData
* @param postUrl
* @return
*/
private String chanzorSms(String postData, String postUrl) {
try {
//发送POST请求
URL url = new URL(postUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setUseCaches(false);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Length", "" + postData.length());
OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
out.write(postData);
out.flush();
out.close();
//获取响应状态
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
GwsLogger.error("畅卓发送短信{}失败, http连接失败,返回响应码是{}", postData, conn.getResponseCode());
return "";
}
//获取响应内容体
String line = "";
String result = "";
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
while ((line = in.readLine()) != null) {
result += line + "\n";
}
in.close();
return result;
} catch (IOException e) {
GwsLogger.error("畅卓发送短信异常,短信内容是{}", postData);
}
return "";
}
public static String buildHttpPostData(String mobile, String content) {
String action = "send";
String account = "youximao-tz";
String password = "153948";
StringBuilder httpPostData = new StringBuilder("action=").append(action)
.append("&account=").append(account)
.append("&password=").append(password)
.append("&mobile=").append(mobile)
.append("&content=").append(content)
.append("&sendTime=");
return httpPostData.toString();
}
}
public interface SmsService {
/**
* 发送短信.
* 最多支持两个变量
*
* @param mobile
* @param brandId
* @param templateNo
* @param firstVarValue
* @param secondVarValue
* @return
*/
Boolean sendSms(String mobile, Long brandId, String templateNo, String firstVarValue,
String secondVarValue);
}
/**
* 短信发送的简单工厂类.
* 发送的策略:根据短信类型/服务商的优先级等
*/
@Service
public class SmsFactoryService {
@Autowired
@Qualifier(value = "aliyunSmsService")
private SmsService aliyunSmsService;
@Autowired
@Qualifier(value = "chanzorSmsService")
private SmsService chanzorSmsService;
/**
* 根据发送规则,选择出具体的SmsService实现类.
* @return
*/
public SmsService getSmsService(Rule rule) {
}
}
网友评论