SpringCloud 搭建企业级开发框架之实现多租户多平台短信通知服务(微服务实战)

2022-07-20,,,,

目前系统集成短信似乎是必不可少的部分,由于各种云平台都提供了不同的短信通道,这里我们增加多租户多通道的短信验证码,并增加配置项,使系统可以支持多家云平台提供的短信服务。这里以阿里云和腾讯云为例,集成短信通知服务。

1、在gitegg-platform中新建gitegg-platform-sms基础工程,定义抽象方法和配置类
smssendservice发送短信抽象接口:

/**
 * 短信发送接口
 */
public interface smssendservice {

    /**
     * 发送单个短信
     * @param smsdata
     * @param phonenumber
     * @return
     */
    default smsresponse sendsms(smsdata smsdata, string phonenumber){
        if (strutil.isempty(phonenumber)) {
            return new smsresponse();
        }
        return this.sendsms(smsdata, collections.singletonlist(phonenumber));
    }

    /**
     * 群发发送短信
     * @param smsdata
     * @param phonenumbers
     * @return
     */
    smsresponse sendsms(smsdata smsdata, collection<string> phonenumbers);
    
}

smsresultcodeenum定义短信发送结果

/**
 * @classname: resultcodeenum
 * @description: 自定义返回码枚举
 * @author gitegg
 * @date 2020年09月19日 下午11:49:45
 */
@getter
@allargsconstructor
public enum smsresultcodeenum {

    /**
     * 成功
     */
    success(200, "操作成功"),

    /**
     * 系统繁忙,请稍后重试
     */
    error(429, "短信发送失败,请稍后重试"),

    /**
     * 系统错误
     */
    phone_number_error(500, "手机号错误");

    public int code;

    public string msg;
}

2、新建gitegg-platform-sms-aliyun工程,实现阿里云短信发送接口
aliyunsmsproperties配置类

@data
@component
@configurationproperties(prefix = "sms.aliyun")
public class aliyunsmsproperties {

    /**
     * product
     */
    private string product = "dysmsapi";

    /**
     * domain
     */
    private string domain = "dysmsapi.aliyuncs.com";

    /**
     * regionid
     */
    private string regionid = "cn-hangzhou";

    /**
     * accesskeyid
     */
    private string accesskeyid;

    /**
     * accesskeysecret
     */
    private string accesskeysecret;

    /**
     * 短信签名
     */
    private string signname;
}

aliyunsmssendserviceimpl阿里云短信发送接口实现类

/**
 * 阿里云短信发送
 */
@slf4j
@allargsconstructor
public class aliyunsmssendserviceimpl implements smssendservice {

    private static final string successcode = "ok";

    private final aliyunsmsproperties properties;

    private final iacsclient acsclient;

    @override
    public smsresponse sendsms(smsdata smsdata, collection<string> phonenumbers) {
        smsresponse smsresponse = new smsresponse();
        sendsmsrequest request = new sendsmsrequest();
        request.setsysmethod(methodtype.post);
        request.setphonenumbers(strutil.join(",", phonenumbers));
        request.setsignname(properties.getsignname());
        request.settemplatecode(smsdata.gettemplateid());
        request.settemplateparam(jsonutils.maptojson(smsdata.getparams()));
        try {
            sendsmsresponse sendsmsresponse = acsclient.getacsresponse(request);
            if (null != sendsmsresponse && !stringutils.isempty(sendsmsresponse.getcode())) {
                if (this.successcode.equals(sendsmsresponse.getcode())) {
                    smsresponse.setsuccess(true);
                } else {
                    log.error("send aliyun sms fail: [code={}, message={}]", sendsmsresponse.getcode(), sendsmsresponse.getmessage());
                }
                smsresponse.setcode(sendsmsresponse.getcode());
                smsresponse.setmessage(sendsmsresponse.getmessage());
            }
        } catch (exception e) {
            e.printstacktrace();
            log.error("send aliyun sms fail: {}", e);
            smsresponse.setmessage("send aliyun sms fail!");
        }
        return smsresponse;
    }

}

3、新建gitegg-platform-sms-tencent工程,实现腾讯云短信发送接口
tencentsmsproperties配置类

@data
@component
@configurationproperties(prefix = "sms.tencent")
public class tencentsmsproperties {

    /* 填充请求参数,这里 request 对象的成员变量即对应接口的入参
     * 您可以通过官网接口文档或跳转到 request 对象的定义处查看请求参数的定义
     * 基本类型的设置:
     * 帮助链接:
     * 短信控制台:https://console.cloud.tencent.com/smsv2
     * sms helper:https://cloud.tencent.com/document/product/382/3773 */
    /* 短信应用 id: 在 [短信控制台] 添加应用后生成的实际 sdkappid,例如1400006666 */
    private string smssdkappid;

    /* 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */
    private string senderid;

    /* 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper] */
    private string extendcode;

    /**
     * 短信签名
     */
    private string signname;
}

tencentsmssendserviceimpl腾讯云短信发送接口实现类

/**
 * 腾讯云短信发送
 */
@slf4j
@allargsconstructor
public class tencentsmssendserviceimpl implements smssendservice {

    private static final string successcode = "ok";

    private final tencentsmsproperties properties;

    private final smsclient client;

    @override
    public smsresponse sendsms(smsdata smsdata, collection<string> phonenumbers) {
        smsresponse smsresponse = new smsresponse();

        sendsmsrequest request = new sendsmsrequest();
        request.setsmssdkappid(properties.getsmssdkappid());
        /* 短信签名内容: 使用 utf-8 编码,必须填写已审核通过的签名,可登录 [短信控制台] 查看签名信息 */
        request.setsign(properties.getsignname());
        /* 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */
        if (!stringutils.isempty(properties.getsenderid()))
        {
            request.setsenderid(properties.getsenderid());
        }
        request.settemplateid(smsdata.gettemplateid());
        /* 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号]
         * 例如+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/
        string[] phonenumbersarray = (string[]) phonenumbers.toarray();
        request.setphonenumberset(phonenumbersarray);
        /* 模板参数: 若无模板参数,则设置为空*/
        string[] templateparams = new string[]{};
        if (!collectionutils.isempty(smsdata.getparams())) {
            templateparams = (string[]) smsdata.getparams().values().toarray();
        }
        request.settemplateparamset(templateparams);
        try {
            /* 通过 client 对象调用 sendsms 方法发起请求。注意请求方法名与请求对象是对应的
             * 返回的 res 是一个 sendsmsresponse 类的实例,与请求对象对应 */
            sendsmsresponse sendsmsresponse = client.sendsms(request);
            //如果是批量发送,那么腾讯云短信会返回每条短信的发送状态,这里默认返回第一条短信的状态
            if (null != sendsmsresponse && null != sendsmsresponse.getsendstatusset()) {
                sendstatus sendstatus = sendsmsresponse.getsendstatusset()[0];
                if (this.successcode.equals(sendstatus.getcode()))
                {
                    smsresponse.setsuccess(true);
                }
                else
                {
                    smsresponse.setcode(sendstatus.getcode());
                    smsresponse.setmessage(sendstatus.getmessage());
                }
            }
        } catch (exception e) {
            e.printstacktrace();
            log.error("send aliyun sms fail: {}", e);
            smsresponse.setmessage("send aliyun sms fail!");
        }
        return smsresponse;
    }
}

4、在gitegg-cloud中新建业务调用方法,这里要考虑到不同租户调用不同的短信配置进行短信发送,所以新建smsfactory短信接口实例化工厂,根据不同的租户实例化不同的短信发送接口,这里以实例化com.gitegg.service.extension.sms.factory.smsaliyunfactory类为例,进行实例化操作,实际使用中,这里需要配置和租户的对应关系,从租户的短信配置中获取。

@component
public class smsfactory {

    private final ismstemplateservice smstemplateservice;

    /**
     * smssendservice 缓存
     */
    private final map<long, smssendservice=""> smssendservicemap = new concurrenthashmap<>();

    public smsfactory(ismstemplateservice smstemplateservice) {
        this.smstemplateservice = smstemplateservice;
    }

    /**
     * 获取 smssendservice
     *
     * @param smstemplatedto 短信模板
     * @return smssendservice
     */
    public smssendservice getsmssendservice(smstemplatedto smstemplatedto) {

        //根据channelid获取对应的发送短信服务接口,channelid是唯一的,每个租户有其自有的channelid
        long channelid = smstemplatedto.getchannelid();
        smssendservice smssendservice = smssendservicemap.get(channelid);
        if (null == smssendservice) {
            class cls = null;
            try {
                cls = class.forname("com.gitegg.service.extension.sms.factory.smsaliyunfactory");
                method staticmethod = cls.getdeclaredmethod("getsmssendservice", smstemplatedto.class);
                smssendservice = (smssendservice) staticmethod.invoke(cls,smstemplatedto);
                smssendservicemap.put(channelid, smssendservice);
            } catch (classnotfoundexception | nosuchmethodexception e) {
                e.printstacktrace();
            } catch (illegalaccessexception e) {
                e.printstacktrace();
            } catch (invocationtargetexception e) {
                e.printstacktrace();
            }

        }
        return smssendservice;
    }
}
/**
 * 阿里云短信服务接口工厂类
 */
public class smsaliyunfactory {

    public static smssendservice getsmssendservice(smstemplatedto sms) {
        aliyunsmsproperties aliyunsmsproperties = new aliyunsmsproperties();
        aliyunsmsproperties.setaccesskeyid(sms.getsecretid());
        aliyunsmsproperties.setaccesskeysecret(sms.getsecretkey());
        aliyunsmsproperties.setregionid(sms.getregionid());
        aliyunsmsproperties.setsignname(sms.getsignname());
        iclientprofile profile = defaultprofile.getprofile(aliyunsmsproperties.getregionid(), aliyunsmsproperties.getaccesskeyid(), aliyunsmsproperties.getaccesskeysecret());
        iacsclient acsclient = new defaultacsclient(profile);
        return new aliyunsmssendserviceimpl(aliyunsmsproperties, acsclient);
    }

}
/**
 * 腾讯云短信服务接口工厂类
 */
public class smstencentfactory {

    public static smssendservice getsmssendservice(smstemplatedto sms) {

        tencentsmsproperties tencentsmsproperties = new tencentsmsproperties();
        tencentsmsproperties.setsmssdkappid(sms.getsecretid());
        tencentsmsproperties.setextendcode(sms.getsecretkey());
        tencentsmsproperties.setsenderid(sms.getregionid());
        tencentsmsproperties.setsignname(sms.getsignname());

        /* 必要步骤:
         * 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretid 和 secretkey
         * 本示例采用从环境变量读取的方式,需要预先在环境变量中设置这两个值
         * 您也可以直接在代码中写入密钥对,但需谨防泄露,不要将代码复制、上传或者分享给他人
         * cam 密钥查询:https://console.cloud.tencent.com/cam/capi
         */
        credential cred = new credential(sms.getsecretid(), sms.getsecretkey());
        // 实例化一个 http 选项,可选,无特殊需求时可以跳过
        httpprofile httpprofile = new httpprofile();
        // 设置代理
//        httpprofile.setproxyhost("host");
//        httpprofile.setproxyport(port);
        /* sdk 默认使用 post 方法。
         * 如需使用 get 方法,可以在此处设置,但 get 方法无法处理较大的请求 */
        httpprofile.setreqmethod("post");
        /* sdk 有默认的超时时间,非必要请不要进行调整
         * 如有需要请在代码中查阅以获取最新的默认值 */
        httpprofile.setconntimeout(60);
        /* sdk 会自动指定域名,通常无需指定域名,但访问金融区的服务时必须手动指定域名
         * 例如 sms 的上海金融区域名为 sms.ap-shanghai-fsi.tencentcloudapi.com */
        if (!stringutils.isempty(sms.getregionid()))
        {
            httpprofile.setendpoint(sms.getregionid());
        }

        /* 非必要步骤:
         * 实例化一个客户端配置对象,可以指定超时时间等配置 */
        clientprofile clientprofile = new clientprofile();
        /* sdk 默认用 tc3-hmac-sha256 进行签名
         * 非必要请不要修改该字段 */
        clientprofile.setsignmethod("hmacsha256");
        clientprofile.sethttpprofile(httpprofile);
        /* 实例化 sms 的 client 对象
         * 第二个参数是地域信息,可以直接填写字符串 ap-guangzhou,或者引用预设的常量 */
        smsclient client = new smsclient(cred, "",clientprofile);

        return new tencentsmssendserviceimpl(tencentsmsproperties, client);
    }
}

5、定义短信发送接口及实现类
ismsservice业务短信发送接口定义

/**
 * <p>
 * 短信发送接口定义
 * </p>
 *
 * @author gitegg
 * @since 2021-01-25
 */
public interface ismsservice {


    /**
     * 发送短信
     *
     * @param smscode
     * @param smsdata
     * @param phonenumbers
     * @return
     */
    smsresponse sendsmsnormal(string smscode, string smsdata, string phonenumbers);

    /**
     * 发送短信验证码
     *
     * @param smscode
     * @param phonenumber
     * @return
     */
    smsresponse sendsmsverificationcode( string smscode, string phonenumber);

    /**
     * 校验短信验证码
     *
     * @param smscode
     * @param phonenumber
     * @return
     */
    boolean checksmsverificationcode(string smscode, string phonenumber, string verificationcode);

}

smsserviceimpl 短信发送接口实现类

/**
 * <p>
 * 短信发送接口实现类
 * </p>
 *
 * @author gitegg
 * @since 2021-01-25
 */
@slf4j
@service
@requiredargsconstructor(onconstructor_ = @autowired)
public class smsserviceimpl implements ismsservice {

    private final smsfactory smsfactory;

    private final ismstemplateservice smstemplateservice;

    private final redistemplate redistemplate;

    @override
    public smsresponse sendsmsnormal(string smscode, string smsdata, string phonenumbers) {
        smsresponse smsresponse = new smsresponse();
        try {
            querysmstemplatedto querysmstemplatedto = new querysmstemplatedto();
            querysmstemplatedto.setsmscode(smscode);
            //获取短信code的相关信息,租户信息会根据mybatis plus插件获取
            smstemplatedto smstemplatedto = smstemplateservice.querysmstemplate(querysmstemplatedto);
            objectmapper mapper = new objectmapper();
            map smsdatamap = mapper.readvalue(smsdata, map.class);

            list<string> phonenumberlist =  jsonutils.jsontolist(phonenumbers, string.class);
            smsdata smsdataparam = new smsdata();
            smsdataparam.settemplateid(smstemplatedto.gettemplateid());
            smsdataparam.setparams(smsdatamap);
            smssendservice smssendservice = smsfactory.getsmssendservice(smstemplatedto);
            smsresponse = smssendservice.sendsms(smsdataparam, phonenumberlist);
        } catch (exception e) {
            smsresponse.setmessage("短信发送失败");
            e.printstacktrace();
        }
        return smsresponse;
    }

    @override
    public smsresponse sendsmsverificationcode(string smscode, string phonenumber) {
        string verificationcode = randomutil.randomnumbers(6);
        map<string, string=""> smsdatamap = new hashmap<>();
        smsdatamap.put(smsconstant.sms_captcha_template_code, verificationcode);
        list<string> phonenumbers = arrays.aslist(phonenumber);
        smsresponse smsresponse = this.sendsmsnormal(smscode, jsonutils.maptojson(smsdatamap), jsonutils.listtojson(phonenumbers));
        if (null != smsresponse && smsresponse.issuccess()) {
            // 将短信验证码存入redis并设置过期时间为5分钟
            redistemplate.opsforvalue().set(smsconstant.sms_captcha_key + smscode + phonenumber, verificationcode, 30,
                    timeunit.minutes);
        }
        return smsresponse;
    }

    @override
    public boolean checksmsverificationcode(string smscode, string phonenumber, string verificationcode) {
        string verificationcoderedis = (string) redistemplate.opsforvalue().get(smsconstant.sms_captcha_key + smscode + phonenumber);
        if (!strutil.isallempty(verificationcoderedis, verificationcode) && verificationcode.equalsignorecase(verificationcoderedis)) {
            return true;
        }
        return false;
    }
}

6、新建smsfeign类,供其他微服务调用发送短信

/**
 * @classname: smsfeign
 * @description: smsfeign前端控制器
 * @author gitegg
 * @date 2019年5月18日 下午4:03:58
 */
@restcontroller
@requestmapping(value = "/feign/sms")
@requiredargsconstructor(onconstructor_ = @autowired)
@api(value = "smsfeign|提供微服务调用接口")
@refreshscope
public class smsfeign {

    private final ismsservice smsservice;

    @getmapping(value = "/send/normal")
    @apioperation(value = "发送普通短信", notes = "发送普通短信")
    result<object> sendsmsnormal(@requestparam("smscode") string smscode, @requestparam("smsdata") string smsdata, @requestparam("phonenumbers") string phonenumbers) {
        smsresponse smsresponse = smsservice.sendsmsnormal(smscode, smsdata, phonenumbers);
        return result.data(smsresponse);
    }

    @getmapping(value = "/send/verification/code")
    @apioperation(value = "发送短信验证码", notes = "发送短信验证码")
    result<object> sendsmsverificationcode(@requestparam("smscode") string smscode, @requestparam("phonenumber") string phonenumber) {
        smsresponse smsresponse = smsservice.sendsmsverificationcode(smscode, phonenumber);
        return result.data(smsresponse);
    }

    @getmapping(value = "/check/verification/code")
    @apioperation(value = "校验短信验证码", notes = "校验短信验证码")
    result<boolean> checksmsverificationcode(@requestparam("smscode") string smscode, @requestparam("phonenumber") string phonenumber, @requestparam("verificationcode") string verificationcode) {
        boolean checkresult = smsservice.checksmsverificationcode(smscode, phonenumber, verificationcode);
        return result.data(checkresult);
    }
}

项目源码:

gitee: https://gitee.com/wmz1930/gitegg

github: https://github.com/wmz1930/gitegg

到此这篇关于springcloud 搭建企业级开发框架之实现多租户多平台短信通知服务的文章就介绍到这了,更多相关springcloud 短信通知服务内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

《SpringCloud 搭建企业级开发框架之实现多租户多平台短信通知服务(微服务实战).doc》

下载本文的Word格式文档,以方便收藏与打印。