美文网首页PHP经验分享
TP5 封装多业务的发送短信功能

TP5 封装多业务的发送短信功能

作者: 华仔233 | 来源:发表于2020-04-29 14:20 被阅读0次

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 $mobile 手机号
     * @param string $smsCode 验证码
     * @param string $prefix 根据业务区分的短信前缀
     * @param bool $isThrowException 是否抛出异常
     * @param bool $isDel 检查完后是否删除该缓存
     * @return array
     */
    public static function checkSmsCode(string $mobile, string $smsCode, string $prefix = self::SU_SMS_LOGIN, bool $isThrowException = true, bool $isDel = true)
    {
        $res = [true, '短信验证码正确!'];
        if (!self::connect()->exists($prefix . $mobile)) {
            $isThrowException ? throwResult('手机验证码失效') : $res = [false, '手机验证码失效'];
        } else {
            if (!hash_equals($smsCode, self::connect()->get($prefix . $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 $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 $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 . $mobile;
                break;
            case 1:
                //重置密码
                $smsKey = RedisService::SU_SMS_RESET_PWD . $mobile;
                break;
            case 2:
                //修改手机号
                $smsKey = RedisService::SU_SMS_RESET_MOBILE . $mobile;
                break;
            case 3:
                //创建子账号
                $smsKey = RedisService::SU_SMS_CREATE_ACCOUNT_MOBILE . $mobile;
                break;
            case 10:
                //其他
                $smsKey = RedisService::SU_SMS_OTHER . $mobile;
                break;
            default:
                //通用
                $smsKey = RedisService::SU_SMS_OTHER . $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($mobile, ['content' => $smsContent], $smsCode, $smsUseType, $userId);

        return $smsCode;
    }

    /**
     * 通用短信发送方法
     * @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 $mobile, string $content, string $smsCode = '', int $smsUseType = 0, int $userId = 0)
    {
        //发送短信的内容
        $isSuccess = 1;
        try {
            $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="sms_type", type="string", required=false, description="业务类型:0=注册登录,1=更换密码,2=修改手机号,3=创建子账号和修改子账号手机号,10=其他,默认为0")
 */
public function sendSmsByPhone()
{
    $mobile = input('mobile'); //手机号
    $smsType = intval(input('sms_type')) ?? 0; //短信类型

    //验证数据
    $this->validate([
        'mobile' => $mobile,
        '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'];
        if ($mobile == $oldMobile) $this->error(MSG_ERR, NULL, '修改的手机号和原手机号相同!');
    } elseif ($smsType == 3) { //创建子账号
        //判断账户是否存在
        $id = db('user')->where('mobile', $mobile)->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($mobile, $smsType, $userid);

    //调试模式时,接口返回短信验证码的值
    $res['mobile'] = $mobile;
    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="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()
    {
        $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,
        ], 'UserValidate.mobile_login');

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

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

        if (!$userId) {
            //如果不存在则直接注册
            $data = [
                '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);

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

相关文章

  • TP5 封装多业务的发送短信功能

    1、准备工作 1.1、准备依赖包 这个网站提供的都是PHP包,挺有用的 https://packagist.org...

  • 短信API实现自动化短信发送

    短信验证码接口示例,如何接入短信API接口实现短信自动发送功能; 网站如何实现自动发送短信验证码的功能,短信验证码...

  • 接入短信API,免费试用

    短信验证码接口示例,如何接入短信API接口实现短信自动发送功能; 网站如何实现自动发送短信验证码的功能,短信验证码...

  • 短信推广二次发送

    短信推广二次发送 一、产品定义、需求描述和使用场景 针对这个功能,围绕“二次发送”这一个核心业务,需要对短信二次发...

  • 使用阿里云的短信服务发送短信

    原文地址使用阿里云的短信服务发送短信 在给客户开发一个信息发送功能的时候,需要涉及到短信的发送,短信发送一般不同的...

  • Thinkjs3 中自定义加载,实现插件目录功能

    近期用 thinkjs3 实验业务的时候,希望拥有『业务插件』功能,如果登录验证码、短信发送、第三方登录等,这样团...

  • iOS发送短信功能

    1、程序外发短信 NSString *phoneStr = @"10086";NSURL *url = [NSUR...

  • 基础架构之分布式任务平台

    项目中除了主流业务,往往带有很多附带功能,比如订单确认给客户发送邮件或者短信,流程审批完成给申请人发送审批完成消息...

  • 跟我学微信小程序之五(云短信篇)

    发送短信在小程序中是一个常见功能,比如注册账号时发送验证码,发送短信通知等等,这些都需要用到云短信功能。使用腾讯云...

  • WPF 互联网发送短信

    发送短信 互联网发送短信是指通过第三方平台,调用其接口,实现短信发送功能。短信内容会受到严格审核,按照平台要求的格...

网友评论

    本文标题:TP5 封装多业务的发送短信功能

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