如何在 Laravel 中 “规范” 的开发验证码发送功能【研发日记十一】

2017-07-14 (597)

今天社交系统ThinkSNS研发团队分享的是「如何在 Laravel 中“规范”的开发验证码发送功能」,之所以把「规范」二字扩起来,是因为没有绝对规范,这只是我们技术人员使用 Laravel 研发ThinkSNS+时,理解的在 Laravel 中的「规范」。

顺便发个小通知:7月15日ThinkSNS+开源版发布,同时非开源的APP也走出内测阶段,体验二维码也全面发布体验。

什么是ThinkSNS ?

ThinkSNS(简称TS),一款全平台综合性社交系统,为国内外大中小企业和创业者提供社会化软件研发及技术解决方案,目前最新版本为ThinkSNS+(简称TS+)。

社交系统ThinkSNS团队拥有超过九年的移动互联网软件/网站/APP开发经验,涉及行业:粉丝社群、直播互动、移动医疗、电子商务、生活服务、在线教育、旅游出行、移动办公、电子杂志、休闲娱乐、企业信息等。

我们的服务包括

快速建站、提供技术及产品解决方案、技术支持维护、技术孵化服务,同时,我们也提供TS的二次开发服务以及网站开发、Android APP开发、iOS APP开发、H5开发、微信公众号开发、小程序开发等定制开发服务,更多TS问题请咨询QQ:3298713109。

需求场景

发送「验证码」或者「消息通知」,可发送到手机或邮箱中。

完成

首先,在 Laravel 中的规范就是使用 Laravel 的「消息通知」,这里基于场景为「验证码」。这个需求几乎所有软件系统都有使用到。

创建通知场景

第一步,使用 php artisan make:notification 创建一个通知类,创建成功后默认已经存在了三个方法 via、toMail 和 toArray ,因为是发送验证码,姑将这个控制类命名为 VerificationCode 

然后创建一个验证码数据模型和数据表迁移,可以使用 php artisan make:model \"VerificationCode\" -m 直接快速创建数据模型和迁移。

ThinkSNS+的迁移如下:

<?php 
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateVerificationCodesTable extends Migration{
   /**     
    * Run the migrations.
    *
    * @return void
    */    
   public function up()
   {
       Schema::create('verification_codes', function (Blueprint $table) {
          $table->increments('id');
          $table->integer('user_id')->nullable()->default(null)->comment('关联用户');
          $table->string('channel', 50)->comment('发送频道,例如 mail, sms');
          $table->string('account', 100)->comment('发送账户');
          $table->string('code', 20)->comment('发送验证码');
          $table->tinyInteger('state')->nullable()->default(0)->comment('状态');
          $table->timestamps();
          $table->softDeletes();
          $table->index('account');
          $table->index('user_id');
       });
      }
      /**
       * Reverse the migrations.
       *
       * @return void
       */
      public function down()
      {
          Schema::dropIfExists('verification_codes');
       }
}

第二步,打开数据模型类,在里面添加 Illuminate\\Notifications\\Notifiable 性状:

<?php
namespace Zhiyi\Plus\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
class VerificationCode extends Model{
   use Notifiable, SoftDeletes;
   /**
    * Get the notification routing information for the given driver.
    *
    * @return mixed
    */
   public function routeNotificationFor()
   {
       return $this->account;
    }
    /**
     * Has User.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasOne
     *
    public function user()
    {
        return $this->hasOne(User::class, 'id', 'user_id');
     }
}

从代码里面,可以看到我们还添加了「软删除」,因为是基于手机号或者邮箱的验证码发送,所以不需要其他的内置花花肠子,也不需要记录到 「消息通知数据表」中,所以 routeNotificationFor 方法我们选择直接返回需要发送的账号(手机号或邮箱)。

加入工厂模式,快捷发送

打开 database/factories/ModelFactory.php 在里面添加一个关于通知数据模型的工厂定义:

$factory->define(Zhiyi\Plus\Models\VerificationCode::class, function (Faker\\Generator $faker) {
    return [
        'user_id' => null,
        'channel' => 'mail',
        'account' => $faker->safeEmail,
        'code' => $faker->numberBetween(1000, 999999),
        'state' => 0,
    ];
});

这样,我们就可以通过 factory(\\Zhiyi\\Plus\\Models\\VerificationCode::class) 工厂函数快捷的创建验证码并发送通知。

为什么在验证码数据模型增加通知性状?

首先 Illuminate\\Notifications\\Notifiable 这份性状,Laravel 默认添加到 User 模型中的,所以通过 $user->notify() 可以快速的给用户发送一个通知,但是在规范文档中有这么一句话:

Remember, you may use the Illuminate\\Notifications\\Notifiable trait on any of your models. You are not limited to only including it on your User model.

这是Laravel官方文档原话,意思就是Illuminate\\Notifications\\Notifiable不仅仅是用在 User 模型上。

所以我们在验证码模型中添加 Illuminate\\Notifications\\Notifiable 是完全符合Laravel通知的正确使用的。

开发通知类

首先,在数据表迁移中存在一个字段 channel 也就是通知频道标识,我们可根据这个值来决定用什么方式发送验证码,而这个操作在通知类的 via 中实现的:

public function via(VerificationCodeModel $notifiable){
   return [$notifiable->channel];
}

我们选择方式就是直接返回 channel 值,这个值可以是任何值,只要我们实现了这个通知频道,都可以发送,而Laravel已经内置和一些发送频道 database、mail 和 nexmo

完成邮件验证码发送

其实,这个步骤我们要做的事情已经很少了,生产通知类的时候,已经完成了 toMail 方法,所以,我们直接修改其消息内容即可。

完成短信验证码发送

短信发送我们采用 overtrue/easy-sms 包,这是安正超开发的一个短信发送客户端,已经内置了很多短信平台,实现也很优秀。(吐槽:虽然有些细节有问题,例如不按照契约调用方法传递网关)

首先依赖短信发送客户端包composer require overtrue/easy-sms然后新建配置 /config/sms.php ,内容嘛,就按照 easy-sms 首页的说明增加即可,先贴出我们的配置内容(为了减少文章字数,只保留阿里大于配置):

<?php
    return [
    /*
    |--------------------------------------------------------------------------
    | HTTP 请求的超时时间
    |--------------------------------------------------------------------------
    |
    | 设置 HTTP 请求超时时间,单位为「秒」。可以为 int 或者 float。
    |
    */
    'timeout' => 5.0,
    /*
    |--------------------------------------------------------------------------
    | 默认发送配置
    |--------------------------------------------------------------------------
    |
    | strategy 为策略器,默认使用「顺序策略器」,可选值有:
    |       - \\Overtrue\\EasySms\\Strategies\\OrderStrategy::class  顺序策略器
    |       - \\Overtrue\\EasySms\\Strategies\\RandomStrategy::class 随机策略器
    |
    | gateways 设置可用的发送网关,可用网关:
    |       - alidayu 阿里云信
    |       - alidayu 阿里大于
    |       - yunpian 云片
    |       - submail Submail
    |       - luosimao 螺丝帽
    |       - yuntongxun 容联云通讯
    |       - huyi 互亿无线
    |       - juhe 聚合数据
    |       - sendcloud SendCloud
    |       - baidu 百度云
    |
    */
    'default' => [
        'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class,
        'gateways' => ['alidayu'],
     ],
     /*
     |--------------------------------------------------------------------------
     | 发送网关配置
     |--------------------------------------------------------------------------
     |
     | 可用的发送网关,基于网关列表,这里配置可用的发送网关必要的数据信息。
     |
     */
     'gateways' => [
         'alidayu' => [
             'app_key' => null,
             'app_secret' => null,
             'sign_name' => null,
          ],
      ],
      /*
      |--------------------------------------------------------------------------
      | 消息支持频道
      |--------------------------------------------------------------------------
      |
      | 发送消息可用根据不同频道配置注入不同频道配置数据。
      |
      */
      'channels' => [
          // 验证码频道
          'code' => [
              'alidayu' => [
              'template' => null,
           ],
       ],
   ],
];

我门增加了一个 channel 配置,用于不同场景,例如验证码场景 code 以方便消息器读取配置。

然后打开 AppServiceProvider.php 在 register 中增加如下:

public function register(){
    $this->app->singleton(\Overtrue\EasySms\EasySms::class, function ($app) {
        return new \Overtrue\EasySms\EasySms(
            $app->config['sms']\r\n            
         );
     });
}

至此 EasySms 在 Laravel 中的集成已经完成,但是还没有开发实际功能,我们接着往下看。

开发 sms 发送频道

为什么要开发?首先,easy-sms 支持的很多,可以考虑单独为每个发送平台开发一个通知发送频道类,也可以采用只开发一个 sms 发送频道类,我们选择开发一个sms通知发送类,通过 easy-sms 的策略机制去多平台发送验证码。

首先,新建一个 app/Notifications/Channels/SmsChannel.php 文件,因为 Laravel 没有提供生成函数,这个需要自己创建哟,只要实现 send 方法即可。 SmsChannel 内容如下:

<?php
namespace Zhiyi\Plus\Notifications\Channels;
use Overtrue\EasySms\EasySms;
use Illuminate\Notifications\Notification;
class SmsChannel{
    /**
     * The SMS notification driver.
     *
     * @var \Overtrue\EasySms\EasySms
     */
     protected $sms;
     /**
     * Create the SMS notification channel instance.
     *
     * @param \Overtrue\EasySms\EasySms $sms
     * @author Seven Du <shiweidu@outlook.com>
     */
     public function __construct(EasySms $sms)
     {
         $this->sms = $sms;
      }
      /**
       * Send the given notification.
       *
       * @param  mixed  $notifiable
       * @param  \Illuminate\Notifications\Notification  $notification
       * @return \\Nexmo\\Message\\Message
       */
       public function send($notifiable, Notification $notification)
       {
           if (! $to = $notifiable->routeNotificationFor('sms')) {
               return;
           }
           $message = $notification->toSms($notifiable, $this->sms->getConfig());
           return $this->sms->send($to, $message);
        }
 }

这样基于 easy-sms 的 短信通知发送频道已经完成。

开发场景发送消息

这部分完全属于 easy-sms 使用开发,我们新建一个 VerificationCodeMessage.php ,内容如下:

<?php
namespace Zhiyi\Plus\Notifications\Messages;
use Overtrue\EasySms\Message;
use Overtrue\EasySms\Contracts\GatewayInterface;
use Illuminate\Config\Repository as ConfigRepository;
class VerificationCodeMessage extends Message{
    protected $config;
    protected $code;
    protected $gateways = ['alidayu'];

    /**
     * Create the message instance.
     *
     * @param \Illuminate\Config\Repository $config
     * @param int $code
     * @author Seven Du <shiweidu@outlook.com>
     */
    public function __construct(ConfigRepository $config, int $code)
    {
        $this->config = $config;
        $this->code = $code;
    }

    /**
     * Get the message content.
     *
     * @param \Overtrue\EasySms\Contracts\GatewayInterface|null $gateway
     * @return string
     * @author Seven Du <shiweidu@outlook.com>
     */
    public function getContent(GatewayInterface $gateway = null)
    {
        return sprintf('验证码%s,如非本人操作,请忽略本条信息。', $this->code);
    }

    /**
     * Get the message template.
     *
     * @param \Overtrue\EasySms\Contracts\GatewayInterface|null $gateway
     * @return string
     * @author Seven Du <shiweidu@outlook.com>
     */
    public function getTemplate(GatewayInterface $gateway = null)
    {
        return $this->config->get('alidayu.template');
    }

    /**
     * Get the message data.
     *
     * @param \Overtrue\EasySms\Contracts\GatewayInterface|null $gateway
     * @return array
     * @author Seven Du <shiweidu@outlook.com>
     */
    public function getData(GatewayInterface $gateway = null)
    {
        return [
            'code' => strval($this->code),
        ];
    }
  }

然后我们回到 VerificationCode 验证码通知类中,增加 toSms 方法,我的代码如下:

public function toSms(VerificationCodeModel $notifiable, Config $config){
   return new Messages\VerificationCodeMessage(
       new ConfigRepository($config->get('channels.code')),
       $notifiable->code
   );
}

场景配置,例如验证码不同频道的 template 等,这样消息器就可以根据发送网关来判断使用场景的配置是什么。

再次吐槽,easy-sms 的契约设计也应该是这个思想,但是 getContent/getTemplate/getData 在实际网关调用的时候根本没有传递网关过来。。。

好了我们的开发完成了。

发送验证码

在创建验证码数据模型的时候就已经添加到「工厂」中,所以我们可以直接使用 factory 函数了,发送演示:

// sms
$model = factory(\Zhiyi\Plus\Models\VerificationCode::class)->create([
    'account' => '1878xxxx50x',
    'channel' => 'sms',]
);
$model->notify(
    new \Zhiyi\Plus\Notifications\VerificationCode($model)
);
// mail
$model = factory(\Zhiyi\Plus\Models\VerificationCode::class)->create([
    'account' => 'example@example.com',
    'channel' => 'mail',]
);
$model->notify(
    new \Zhiyi\Plus\Notifications\VerificationCode($model)
);

大功告成,easy-sms 是一个很不错的包哟。

上面代码都是来自于 ThinkSNS Plus ,看完整的开发代码可以看仓库:

GitHub: https://github.com/slimkit/thinksns-plus(开源不易,求 Star

源码授权购买

现行稳定系统V4系列全端产品(PC/H5/APP),授权源码购买、产品体验,请致电:18108035545开源版源码官方正版获取唯一渠道:关注公众号“thinksns”,回复“开源版”即可获得最新源码压缩文件地址。

TS+预售活动了解:http://www.thinksns.com/reader/68.html

TS+安装部署:http://www.thinksns.com/reader/100.html

业务合作(授权购买、二次开发、技术孵化、定制开发、产品功能规划、技术支持、商务合作等都找她)

咨询电话:18108035545;028-82884828

市场合作邮箱:lihecong@zhishisoft.com

商务合作QQ:3298713109

往期回顾:

ThinkSNS+基于 Laravel master 分支【研发日记一】》

ThinkSNS+研发中前端的抉择(webpack/Vue)踩坑日记【研发日记二】》

《基于 Laravel Route 的 ThinkSNS+ Component【研发日记三】》

《如何做到 Laravel 配置可以网站后台配置【研发日记四】》

ThinkSNS+ 如何计算字符显示长度【研发日记五】》

《基于 Laravel 的 ThinkSNS+ alpha.2 版本发布【研发日记六】》

《聊聊 Laravel 5.5 的 「自动发现」和此刻心情【研发日记七】

《继:我朝特有需求之--英文字符占 0.5 个,中文字符占 1 个【研发日记八】

《利用 Docker 包 Laradock 服务器部署 Laravel & ThinkSNS+ 等程序实战(多项目)【九】》

关于 Overtrue 的拼音库 overtrue/pinyin 为何 travis 为 error【十】