最新需要用到发送短信的功能,所以就在网上搜索一些写好的扩展。
扩展地址:
https://github.com/MissMyCat/aliyun-sms
通过composer安装:
composer require mrgoon/aliyun-sms dev-master
在 config/app.php 中 providers 加入:
Mrgoon\AliSms\ServiceProvider::class,
有需求的可以自行添加 aliases。
然后在控制台运行 :
php artisan vendor:publish
默认会在 config 目录下创建一个 aliyunsms.php 文件:
<?php
return [
\'access_key\' => env(\'ALIYUN_SMS_AK\'), // accessKey
\'access_secret\' => env(\'ALIYUN_SMS_AS\'), // accessSecret
\'sign_name\' => env(\'ALIYUN_SMS_SIGN_NAME\'), // 签名
];
然后在 .env 中配置相应参数:
ALIYUN_SMS_AK= ALIYUN_SMS_AS= ALIYUN_SMS_SIGN_NAME=
为了能够方便的发送短信,我们可以在 app 目录下,创建一个Services目录,并添加 AliyunSms.php 文件。
<?php
namespace App\Services;
use Mrgoon\AliSms\AliSms;
/**
* 阿里云短信类
*/
class AliyunSms
{
//验证码
const VERIFICATION_CODE = \'verification_code\';
//模板CODE
public static $templateCodes = [
self::VERIFICATION_CODE => \'SMS_XXXXXXXXXX\',
];
/**
* 发送短信
*/
public static function sendSms($mobile, $scene, $params = [])
{
if (empty($mobile)) {
throw new \Exception(\'手机号不能为空\');
}
if (empty($scene)) {
throw new \Exception(\'场景不能为空\');
}
if (!isset(self::$templateCodes[$scene])) {
throw new \Exception(\'请配置场景的模板CODE\');
}
$template_code = self::$templateCodes[$scene];
try {
$ali_sms = new AliSms();
$response = $ali_sms->sendSms($mobile, $template_code, $params);
if ($response->Code == \'OK\') {
return true;
}
throw new \Exception($response->Message);
} catch (\Throwable $e) {
throw new \Exception($e->getMessage());
}
}
}
为了能够记录每次短信发送的状态,我们可以创建一个 sms_logs 表。
CREATE TABLE `sms_logs` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT \'ID\', `type` tinyint(1) NOT NULL DEFAULT \'0\' COMMENT \'类型(0:短信验证码,1:语音验证码,2:短信消息通知)\', `mobile` varchar(16) NOT NULL DEFAULT \'\' COMMENT \'手机号\', `code` varchar(12) NOT NULL DEFAULT \'\' COMMENT \'验证码\', `checked` tinyint(1) NOT NULL DEFAULT \'0\' COMMENT \'是否验证(0:未验证,1:已验证)\', `status` tinyint(1) NOT NULL DEFAULT \'0\' COMMENT \'状态(0:未发送,1:已发送,2:发送失败)\', `reason` varchar(255) NOT NULL DEFAULT \'\' COMMENT \'失败原因\', `remark` varchar(255) NOT NULL DEFAULT \'\' COMMENT \'备注\', `operator_id` int(11) NOT NULL DEFAULT \'0\' COMMENT \'操作人ID\', `ip` varchar(16) NOT NULL DEFAULT \'\' COMMENT \'操作IP\', `created` int(11) NOT NULL DEFAULT \'0\' COMMENT \'创建时间\', `updated` int(11) NOT NULL DEFAULT \'0\' COMMENT \'更新时间\', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\'短信表\';
然后针对该表,我们创建一个 SmsLog 模型来管理。
<?php
namespace App\Models;
use App\Services\AliyunSms;
class SmsLog extends Model
{
protected $fillable = [
\'type\',
\'mobile\',
\'code\',
\'checked\',
\'status\',
\'reason\',
\'remark\',
\'operator_id\',
\'ip\',
];
//类型(0:短信验证码,1:语音验证码,2:短信消息通知)
const TYPE_CODE = 0;
const TYPE_VOICE = 1;
const TYPE_MESSAGE = 2;
//是否验证(0:未验证,1:已验证)
const CHECKED_UNVERIFIED = 0;
const CHECKED_VERIFIED = 1;
//状态(0:未发送,1:已发送,2:发送失败)
const STATUS_NO_SEND = 0;
const STATUS_SEND = 1;
const STATUS_FAIL = 2;
//短信发送间隔时间,默认60秒
const SEND_INTERVAL_TIME = 60;
/**
* 检测短信验证码
*/
protected function checkCode($mobile, $code)
{
if (!$mobile) {
throw new \Exception(\'手机号不能为空\');
}
if (!checkMobile($mobile)) {
throw new \Exception(\'手机号不正确\');
}
if (!$code) {
throw new \Exception(\'验证码不能为空\');
}
$sms_log = $this->where([
[\'type\', self::TYPE_CODE],
[\'mobile\', $mobile],
[\'status\', self::STATUS_SEND],
[\'checked\', self::CHECKED_UNVERIFIED],
])->orderBy(\'created\', \'desc\')->first();
if (!$sms_log) {
throw new \Exception(\'验证码不存在,请重新获取\');
}
if ($code != $sms_log->code) {
throw new \Exception(\'验证码错误\');
}
$sms_log->checked = self::CHECKED_VERIFIED;
$sms_log->save();
return true;
}
/**
* 检测短信频率
*/
protected function checkRate($mobile)
{
if (!$mobile) {
throw new \Exception(\'手机号不能为空\');
}
$sms_log = $this->where([
[\'mobile\', $mobile],
[\'status\', self::STATUS_SEND],
])->orderBy(\'created\', \'desc\')->first();
$now = time();
if ($sms_log) {
if (($now - strtotime($sms_log->created)) < self::SEND_INTERVAL_TIME) {
throw new \Exception(\'短信发送太频繁,请稍后再试\');
}
}
return true;
}
/**
* 发送短信验证码
*/
protected function sendVerifyCode($mobile)
{
self::checkRate($mobile);
$code = mt_rand(1000, 9999);
$sms_log = $this->create([
\'type\' => self::TYPE_CODE,
\'mobile\' => $mobile,
\'code\' => $code,
\'checked\' => self::CHECKED_UNVERIFIED,
\'status\' => self::STATUS_NO_SEND,
\'ip\' => getRealIp(),
]);
try {
AliyunSms::sendSms($mobile, AliyunSms::VERIFICATION_CODE, [\'code\' => $code]);
$sms_log->status = self::STATUS_SEND;
$sms_log->save();
return true;
} catch (\Exception $e) {
$sms_log->status = self::STATUS_FAIL;
$sms_log->reason = $e->getMessage();
$sms_log->save();
throw new \Exception($e->getMessage());
}
}
}
这样,我们就可以在项目中通过 SmsLog::sendVerifyCode() 发送短信了。
getRealIp() 和 checkMobile() 方法为公共方法,存放在 app/Helpers 的 functions.php 中。
/**
* 获取真实IP地址
*/
function getRealIp()
{
$ip = false;
if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown")) {
$ip = getenv("HTTP_CLIENT_IP");
} else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown")) {
$ips = explode(", ", getenv("HTTP_X_FORWARDED_FOR"));
if ($ip) {
array_unshift($ips, $ip);
$ip = false;
}
$ipscount = count($ips);
for ($i = 0; $i < $ipscount; $i++) {
if (!preg_match("/^(10|172\.16|192\.168)\./i", $ips[$i])) {
$ip = $ips[$i];
break;
}
}
} else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown")) {
$ip = getenv("REMOTE_ADDR");
} else if (isset($_SERVER[\'REMOTE_ADDR\']) && $_SERVER[\'REMOTE_ADDR\'] && strcasecmp($_SERVER[\'REMOTE_ADDR\'], "unknown")) {
$ip = $_SERVER[\'REMOTE_ADDR\'];
} else {
$ip = "unknown";
}
return isIp($ip) ? $ip : "unknown";
}
/**
* 检查是否是合法的IP
*/
function isIp($ip)
{
if (preg_match(\'/^((\d|[1-9]\d|2[0-4]\d|25[0-5]|1\d\d)(?:\.(\d|[1-9]\d|2[0-4]\d|25[0-5]|1\d\d)){3})$/\', $ip)) {
return true;
} else {
return false;
}
}
/**
* 验证手机号
*/
function checkMobile($mobile)
{
return preg_match(\'/^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\d{8}$/i\', $mobile);
}