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

1、准备工作

1.1、准备依赖包
  • 这个网站提供的都是PHP包,挺有用的 https://packagist.org/
  • 另外还可以在GitHub上面下载 https://github.com/

1、短信方面 我这里用到 packagist 里面的 overtrue/easy-sms
TP5 封装多业务的发送短信功能(包括国际短信)_第1张图片

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

我们可以看到这个依赖包提供的平台有以下:
TP5 封装多业务的发送短信功能(包括国际短信)_第2张图片

2、Redis方面 我用到了Predis的包
TP5 封装多业务的发送短信功能(包括国际短信)_第3张图片

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

  • 包括两个 服务类config.php ,用于封装不同业务类型的存储和发送的方法,如下图

TP5 封装多业务的发送短信功能(包括国际短信)_第4张图片

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服务类
 '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、发送短信服务类
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);

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

你可能感兴趣的:(PHP延伸扩展,php,thinkphp,封装)