美文网首页
系统重构--短信网关

系统重构--短信网关

作者: 天草二十六_简村人 | 来源:发表于2019-03-27 16:16 被阅读0次

一、总述

短信和支付一样,会涉及费率、发送频率、安全稳定等问题。故短信平台需要对接阿里云和畅卓等多家短信服务方。

二、数模设计

-- 短信发送记录表
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) {
     }
}

相关文章

网友评论

      本文标题:系统重构--短信网关

      本文链接:https://www.haomeiwen.com/subject/rwuzvqtx.html