TP5 封装多业务的发送短信功能(包括国际短信)

1、准备工作

1.1、准备依赖包

1、短信方面 我这里用到 packagist 里面的 overtrue/easy-sms

image

下载依赖包之前记得要看下依赖的PHP版本、短信平台等。

我们可以看到这个依赖包提供的平台有以下:


image.png

2、Redis方面 我用到了Predis的包

image

另外他包里面提供了使用方法和类型,需要去看下,不过本人进行了代码封装。
安装这些包的话直接打开cmd命令行输入 composer require XXX 即可,
XXX是对应包名,XXX后面如果加 :1.*就相当于版本号,包里面会介绍相关的操作。

2、代码封装

2.1、封装的文件
  • 包括两个 服务类config.php ,用于封装不同业务类型的存储和发送的方法,如下图
image
2.2、配置文件
  • config.php 文件,用于保存短信配置和白名单等,内容如下
//通用发送短信配置(短信消息配置)
'easy_sms' => [
    // HTTP 请求的超时时间(秒)
    'timeout' => 5.0,

    // 默认发送配置
    'default' => [
        // 网关调用策略,默认:顺序调用
        'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class,

        // 默认可用的发送网关
        'gateways' => [
            'yunpian',
        ],
    ],

    // 可用的网关配置
    'gateways' => [
        'errorlog' => [
            'file' => '/tmp/easy-sms.log',
        ],
        //云片
        'yunpian' => [
            'api_key' => '',
        ],
        //阿里云
        'aliyun' => [
            'access_key_id' => '',
            'access_key_secret' => '',
            'sign_name' => 'aa',
        ],
        //...
    ],
],

//发送消息限制配置
'sms_limit' => [
    'white_list' => [ //发送短信白名单
        '13512341234',
        '18664337604',
        '13450681681',
    ],
    'save_time' => 3600, //设置保存时间 默认一小时
    'send_total' => 5, //限制时间内最多发送5条
    'expires' => 900, //设置验证码过期时间 默认15分钟
],

//Redis配置
'redis_host' => Env::get('redis.hostname','192.168.2.168'),
'redis_password' => Env::get('redis.password',''),
'redis_port' => Env::get('redis.hostport','6379'),
'redis_prefix' => Env::get('redis.prefix','su::'),

其中 Env 是对应的配置文件,你也可以直接在第二个参数填写默认的配置即可。

2.3、Redis服务类
<?php

namespace app\common\service;

use Predis\Client;

class RedisService
{
    //Redis保存的key
    //短信部分
    const SU_SMS_LOGIN = 'sms::login::'; //短信验证码|登录和找回密码(后面跟 国际区号-用户手机号)
    const SU_SMS_RESET_PWD = 'sms::reset::pwd::'; //短信验证码|重置账号密码(后面跟 国际区号-用户手机号)
    const SU_SMS_RESET_MOBILE = 'sms::reset::mobile::'; //短信验证码|重置手机号码(后面跟 国际区号-用户手机号)
    const SU_SMS_CREATE_ACCOUNT_MOBILE = 'sms::create::account::mobile::'; //创建子账号|修改子账号手机号(后面跟 国际区号-用户手机号)
    const SU_SMS_OTHER = 'sms::other::'; //短信验证码|其他情况(后面跟 国际区号-用户手机号)
    const SU_SMS_NUM = 'sms::num::'; //手机短信发送次数(后面跟 国际区号-用户手机号)

    private static $prefix = '';
    private static $client;

    /**
     * 单例模式获取redis连接实例
     * @return Client
     */
    public static function connect()
    {
        if (!self::$client) {
            self::$prefix = config('redis_prefix');
            $config = [
                'scheme' => 'tcp',
                'host' => config('redis_host'),
                'port' => config('redis_port'),
            ];
            //没有配置密码时,不传入密码项参数
            if (config('redis_password')) $config['password'] = config('redis_password');

            self::$client = new Client($config, ['prefix' => self::$prefix]);
        }

        return self::$client;
    }

    /**
     * 校验短信验证码
     * @param string $areaCode 手机国际区号
     * @param string $mobile 手机号
     * @param string $smsCode 验证码
     * @param string $prefix 根据业务区分的短信前缀
     * @param bool $isThrowException 是否抛出异常
     * @param bool $isDel 检查完后是否删除该缓存
     * @return array
     */
    public static function checkSmsCode(string $areaCode, string $mobile, string $smsCode, string $prefix = self::SU_SMS_LOGIN, bool $isThrowException = true, bool $isDel = true)
    {
        $res = [true, '短信验证码正确!'];
        if (!self::connect()->exists($prefix .$areaCode . '-' . $mobile)) {
            $isThrowException ? throwResult('手机验证码失效') : $res = [false, '手机验证码失效'];
        } else {
            if (!hash_equals($smsCode, self::connect()->get($prefix . $areaCode . '-' . $mobile)))
                $isThrowException ? throwResult('手机验证码不正确') : $res = [false, '手机验证码不正确'];
        }

        if ($isDel) self::connect()->del($prefix . $mobile);

        return $res;
    }

最上面定义的常量都是用于定义业务类型,后面跟着手机号
throwResult 是封装好的抛出异常的方法,使用自己封装的即可。
connect 方法是用于实例化,每次用redis时候直接RedisService::connect();
checkSmsCode 方法是用于检验验证码是否正确的,其中需要传业务类型前缀。

2.4、发送短信服务类
<?php

namespace app\common\service;

use Overtrue\EasySms\EasySms;
use Overtrue\EasySms\Exceptions\NoGatewayAvailableException;

class SmsService
{
    /**
     * 发送通用短信验证码
     * @param string $areaCode 手机国际区号
     * @param string $mobile 手机号
     * @param int $smsUseType 业务类型:0=注册登录,1=更换密码,2=修改手机号,3=创建子账号,10=其他
     * @param int $userId 用户ID
     * @return int
     * @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException
     * @throws \app\common\exception\WorkException
     */
    public static function sendSmsCode(string $areaCode,string $mobile, int $smsUseType = 0, int $userId = 0)
    {
        $redis = RedisService::connect();

        $countKey = RedisService::SU_SMS_NUM . $mobile; //记录该手机发送的次数

        //验证码业务类型
        switch ($smsUseType) {
            case 0:
                //注册登录
                $smsKey = RedisService::SU_SMS_LOGIN . $areaCode . '-' . $mobile;
                break;
            case 1:
                //重置密码
                $smsKey = RedisService::SU_SMS_RESET_PWD . $areaCode . '-' . $mobile;
                break;
            case 2:
                //修改手机号
                $smsKey = RedisService::SU_SMS_RESET_MOBILE . $areaCode . '-' . $mobile;
                break;
            case 3:
                //创建子账号
                $smsKey = RedisService::SU_SMS_CREATE_ACCOUNT_MOBILE . $areaCode . '-' . $mobile;
                break;
            case 10:
                //其他
                $smsKey = RedisService::SU_SMS_OTHER . $areaCode . '-' . $mobile;
                break;
            default:
                //通用
                $smsKey = RedisService::SU_SMS_OTHER . $areaCode . '-' . $mobile;
                break;
        }

        //白名单可以无限制使用发送次数
        if (!in_array($mobile, config('sms_limit.white_list'))) {
            //防止恶意发送,每小时限制5次
            $count = $redis->get($countKey);
            if ($count && $count >= config('sms_limit.send_total')) {
                throwResult('超过发送次数限制,请稍后再试');
            }
        }

        //检查该手机号一段时间内已发送的次数
        $redisIncr = $redis->incr($countKey);
        if ($redisIncr == 1) $redis->expire($countKey, config('sms_limit.save_time')); //设置保存时间 默认一小时

        //查找如果存在相同键名的短信码则更新采用旧的编码
        $smsCode = config('app_debug') ? 8888 : ($redis->get($smsKey) ?: mt_rand(1000, 9999));

        //保存验证码并设置15分钟有效时间
        $redis->set($smsKey, $smsCode, 'EX', config('sms_limit.expires')); //todo 短信验证码过期时间

        //发送短信并保存记录
        if (!config('app_debug')) self::send($areaCode,$mobile, ['content' => $smsContent], $smsCode, $smsUseType, $userId);

        return $smsCode;
    }

    /**
     * 通用短信发送方法
     * @param string $areaCode 手机国际区号
     * @param string $mobile 手机号码
     * @param string $content 发送内容
     * @param string $smsCode 手机验证码(没有则不需要传参)
     * @param int $smsUseType 0=注册登录,1=更换密码,2=修改手机号,3=创建子账号,10=其他
     * @param int $userId 用户ID 默认为0(用户不存在时候不需要传参)
     * @return bool
     * @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException
     */
    public static function send(string $areaCode, string $mobile, string $content, string $smsCode = '', int $smsUseType = 0, int $userId = 0)
    {
        //发送短信的内容
        $isSuccess = 1;
        try {
            $mobile = new PhoneNumber($mobile, $areaCode);
            $option = [
                'content' => $content, //文字内容,使用在像云片类似的以文字内容发送的平台
                'template' => 'SMS_001', // 模板 ID,使用在以模板ID来发送短信的平台
                'data' =>  //模板变量,使用在以模板ID来发送短信的平台
                    [
                        'code' => 6379
                    ],
            ];
            $easySms = new EasySms(config('easy_sms'));
            $easySms->send($mobile, $option);
        } catch (NoGatewayAvailableException $e) {
            $isSuccess = 3;
        }

        //短信验证码记录成功与失败到表
//        SmsModel::createOne($mobile, $option['content'], $isSuccess, $smsUseType, $smsCode, $userId);

        return $isSuccess == 1 ? true : false;
    }
}

我封装的时候加了一些附加条件:

  • 业务类型 我们根据不同的业务类型进行不同的 键值 保存和操作。

  • 限制次数 白名单用户可以无限次发送,而普通用户限制每小时最多发送多少次,主要通过 SU_SMS_NUM 这个redis 键值去判断,这个键值需要根据具体业务设置个时间,比如1小时。

  • 验证码时长 验证码需要存入redis中并设置一个时间,保证验证码的有效时长。

  • 稳定验证码 每次发送验证码之前需要去查看是否存在 有效验证码 ,有的话则直接发送该验证码。

3、实现过程

3.1、发送短信
/**
 * @ApiTitle    (发送短信验证码)
 * @ApiSummary  (用于发送短信验证码)
 * @ApiMethod   (POST)
 * @ApiRoute    (/api/User/sendSmsByPhone)
 * @ApiHeaders  (name=Authorization, type=string, required=false, description="用户Token,其中修改手机号需要传")
 * @ApiParams   (name="mobile", type="string", required=true, description="手机号")
     * @ApiParams   (name="area_code", type="string", required=true, description="国际区号")
 * @ApiParams   (name="sms_type", type="string", required=false, description="业务类型:0=注册登录,1=更换密码,2=修改手机号,3=创建子账号和修改子账号手机号,10=其他,默认为0")
 */
public function sendSmsByPhone()
{
    $areaCode = input('area_code'); //国际区号
    $mobile = input('mobile'); //手机号
    $smsType = intval(input('sms_type')) ?? 0; //短信类型

    //验证数据
    $this->validate([
        'mobile' => $mobile,
        'area_code' => $areaCode,
        'sms_type' => $smsType,
    ], 'UserValidate.send_sms_by_phone');

    //业务类型判断
    if ($smsType == 1) { //重置、找回密码
//            $user = $this->auth->getUser();
//            if (!$user) $this->error(MSG_NEED_LOGIN);

    } elseif ($smsType == 2) { //修改手机号需要登录和验证手机号
        $user = $this->auth->getUser();
        if (!$user) $this->error(MSG_NEED_LOGIN);

        $oldMobile = $user->toArray()['mobile'];
        $oldAreaCode = $user->toArray()['area_code'];
        if ($areaCode . $mobile == $oldAreaCode . $oldMobile) $this->error('修改的手机号和原手机号相同!');
    } elseif ($smsType == 3) { //创建子账号
        //判断账户是否存在
        $id = db('user')->where('mobile', $mobile)->where('area_code', $areaCode)->value('id');
        if ($id) {
            $this->error('该账户已存在');
        }
    }

    //验证该手机号是否存在,存在则验证状态 状态:0=正常,1=检测中,2=已冻结,3=封号
    $user = (new UserModel)->where('mobile', $mobile)->field(['id', 'status'])->find();
    $userid = 0;
    if ($user) {
        if ($user['status'] == 2) $this->error(MSG_USER_STATUS_FREEZE);
        if ($user['status'] == 3) $this->error(MSG_USER_STATUS_BAN);
        $userid = $user['id'];
    }

    //判断业务类型
    $smsCode = SmsService::sendSmsCode($areaCode, $mobile, $smsType, $userid);

    //调试模式时,接口返回短信验证码的值
    $res['mobile'] = $mobile;
    $res['area_code'] = $areaCode;
    if (config('app_debug')) $res['_sms_code'] = $smsCode;

    $this->success(MSG_OK, $res, '手机验证码发送成功');
}

3.2、验证短信并登陆
/**
     * @ApiTitle    (会员手机号注册和登录)
     * @ApiSummary  (用户用于手机号注册和登录平台)
     * @ApiMethod   (POST)
     * @ApiRoute    (/api/User/mobileLogin)
     * @ApiParams   (name="area_code", type="string", required=true, description="手机国际区号")
     * @ApiParams   (name="mobile", type="string", required=true, description="手机号")
     * @ApiParams   (name="sms_code", type="string", required=true, description="手机验证码")
     * @ApiParams   (name="type", type="int", required=true, description="类型:0=未知设备,1=安卓APP,2=IOSAPP,3=微信小程序,4=H5页面,5=PC端")
     */
    public function mobileLogin()
    {
        $areaCode = input('area_code'); //手机国际区号
        $mobile = input('mobile'); //手机号
        $smsCode = input('sms_code'); //手机验证码
        $type = input('type') ?? 0; //类型:0=未知设备,1=安卓APP,2=IOSAPP,3=微信小程序,4=H5页面,5=PC端

        //验证数据
        $this->validate([
            'mobile' => $mobile,
            'sms_code' => $smsCode,
            'area_code' => $areaCode,
        ], 'UserValidate.mobile_login');

        //发送短信检测
        RedisService::checkSmsCode(areaCode,$mobile, $smsCode, RedisService::SU_SMS_LOGIN);

        $userId = (new UserModel)->where('mobile', $mobile)->where('area_code', $areaCode)->value('id');
        $ip = request()->ip();
        $time = time();
        $redis = new Redis();

        if (!$userId) {
            //如果不存在则直接注册
            $data = [
                'area_code' => $areaCode, //手机国际区号
                'mobile' => $mobile, //手机号
                'score' => 0, //积分(考虑第一次注册是否有积分)
                'logintime' => $time, //登录时间
                'loginip' => $ip, //登录IP
                'prevtime' => $time, //上次登录时间
                'status' => 0 //状态:0=正常,1=检测中,2=已冻结,3=封号
            ];

            //账号注册时需要开启事务,避免出现垃圾数据
            Db::startTrans();
            try {
                //插入用户表
                $userId = (new UserModel)->insertGetId($data);
                $rand1 = rand(10000000, 99999999);
                $rand2 = rand(10, 99);
                $username = 'SuLink-' . substr($rand1 . $userId . $rand2, -11); //取10位数

                //再设置用户昵称
                (new UserModel)->where('id', $userId)->update([
                    'nickname' => $username, //昵称
                    'username' => $username, //用户名
                    'avatar' => letter_avatar($username)
                ]);

                //插入登录记录
                (new UserLoginLogModel)->insert([
                    'user_id' => $userId,
                    'type' => $type,
                    'user_agent' => \request()->header('User-Agent'),
                    'login_ip' => $ip,
                ]);

                //新增用户设置表
                (new UserSettingModel)->insert(['user_id' => $userId]);

                //设置Token
                $token = Random::uuid();
                $redis->set($token, $userId, config('token.keep_time'));

                Db::commit();
            } catch (Exception $e) {
                Db::rollback();
                $this->error(MSG_ERR, null, $e->getMessage());
            }

        } else {
            $token = $this->loginSuccess($userId, $type, $time);
        }

        $this->success(MSG_OK, ['id' => $userId, 'mobile' => $mobile, 'token' => $token], '登录成功');
    }
3.3、总结
  • 从上面我们可以提取出主要的部分,调用时候主要使用两个部分就行了

1、发送验证码只需要简单的调用短信服务类方法 sendSmsCode

//判断业务类型
$smsCode = SmsService::sendSmsCode($mobile, $smsType, $userid);

2、验证验证码只需要简单的调用Redis服务类方法 checkSmsCode

//发送短信检测
RedisService::checkSmsCode($mobile, $smsCode, RedisService::SU_SMS_LOGIN);

如果觉得那部分不清楚的可以评论发出提问,或者哪里写的不好的也可以提出来。
谢谢大家的观赏,楼上的方法都是本人亲自封装。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • 本来就打算针对一些固定的特别点的业务(QQ与网易邮件、拦截设计、短信、定时器等等)来进行记录以及解析原理,这些会比...
    JackFrost_fuzhu阅读 2,515评论 3 38
  • 2017年6月7日 星期三 晴间多云 今天下午看见皮老师发的视力检查表,心里有点不安。赶紧给皮老师打电话问了问具...
    巭Pro阅读 241评论 0 0
  • 毛孔粗大有黑头怎么办?鼻子上有黑头怎么去除呢?想必这是很多女生一直为此苦恼的问题肌肤了,脸部毛孔粗大有黑头很容易影...
    小青瓜bb阅读 1,698评论 0 5
  • 我不想承认,我体内种种色彩 是通行世界的艺术 我以为我是蓝色的,像荒凉 俗不可耐的大海,鱼群 一个滚接一个滚 就让...
    宇斯阅读 2,959评论 4 8
  • 你说 我应该享受孤独 我尝试自己走夜路 你说 我应该直面懦弱 我学着得过又且过 你说 我应该懂得抉择 我开始一遍遍...
    芥墨阅读 201评论 0 0