master
parent
97380a7d60
commit
29d0fca345
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Contract;
|
||||||
|
|
||||||
|
interface SmsSender {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信验证码
|
||||||
|
*
|
||||||
|
* @param string $mobile 手机号
|
||||||
|
* @param string $code 验证码
|
||||||
|
* @param int $activeMinute 验证码有效期
|
||||||
|
*/
|
||||||
|
public function sendCode($mobile, $code, $activeMinute = 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信
|
||||||
|
*
|
||||||
|
* @param string $mobile 手机号
|
||||||
|
* @param string $content 短信内容
|
||||||
|
*/
|
||||||
|
public function send($mobile, $content);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量发送短信
|
||||||
|
*
|
||||||
|
* @param array $mobiles 手机号
|
||||||
|
* @param string $content 短信内容
|
||||||
|
*/
|
||||||
|
public function sendBatch(array $mobiles, $content);
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Exception\BusinessException;
|
||||||
|
use App\Helper\Queue;
|
||||||
|
use App\Job\SmsJob;
|
||||||
|
use App\Service\SmsService;
|
||||||
|
|
||||||
|
class SmsController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var SmsService
|
||||||
|
*/
|
||||||
|
protected $smsService;
|
||||||
|
|
||||||
|
public function __construct(SmsService $smsService)
|
||||||
|
{
|
||||||
|
$this->smsService = $smsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send()
|
||||||
|
{
|
||||||
|
$mobile = $this->request->input('mobile', null);
|
||||||
|
$options = $this->request->input('options', []);
|
||||||
|
if (!$mobile) {
|
||||||
|
throw new BusinessException('缺少参数[mobile]');
|
||||||
|
}
|
||||||
|
if (!isset($options['type'])) {
|
||||||
|
throw new BusinessException('缺少参数options[type]');
|
||||||
|
}
|
||||||
|
Queue::push(SmsJob::class, [
|
||||||
|
'mobile' => $mobile,
|
||||||
|
'options' => $options,
|
||||||
|
]);
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check()
|
||||||
|
{
|
||||||
|
$mobile = $this->request->input('mobile', '');
|
||||||
|
$code = $this->request->input('code', '');
|
||||||
|
|
||||||
|
if (!$mobile) {
|
||||||
|
throw new BusinessException('缺少参数[mobile]');
|
||||||
|
}
|
||||||
|
if (!$code) {
|
||||||
|
throw new BusinessException('缺少参数[code]');
|
||||||
|
}
|
||||||
|
if (!$this->smsService->check($mobile, $code)) {
|
||||||
|
throw new BusinessException('验证失败');
|
||||||
|
}
|
||||||
|
return $this->success('验证成功');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Factory;
|
||||||
|
|
||||||
|
use App\Helper\SmsSender\JhSmsSender;
|
||||||
|
use App\Helper\SmsSender\XgSmsSender;
|
||||||
|
use App\Helper\SmsSender\ZwSmsSender;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use App\Model\Tool;
|
||||||
|
|
||||||
|
class SmsSenderFactory
|
||||||
|
{
|
||||||
|
protected $senders = [
|
||||||
|
'sms_set' => XgSmsSender::class,
|
||||||
|
'zhongwang' => ZwSmsSender::class,
|
||||||
|
'juhedata' => JhSmsSender::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __invoke(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
$tool = Tool::getActiveByGroup('sms');
|
||||||
|
if (!$tool) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$senderClass = $this->getSenderClass($tool);
|
||||||
|
if ($senderClass) {
|
||||||
|
return make($senderClass, compact('tool'));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSenderClass(Tool $tool)
|
||||||
|
{
|
||||||
|
return $this->senders[$tool->name] ?? null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Helper\SmsSender;
|
||||||
|
|
||||||
|
use App\Contract\SmsSender;
|
||||||
|
use App\Exception\BusinessException;
|
||||||
|
use App\Model\Tool;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Hyperf\Guzzle\CoroutineHandler;
|
||||||
|
use GuzzleHttp\HandlerStack;
|
||||||
|
|
||||||
|
class JhSmsSender implements SmsSender
|
||||||
|
{
|
||||||
|
|
||||||
|
public $channel = 'juhe';
|
||||||
|
|
||||||
|
protected $baseUrl = 'http://v.juhe.cn';
|
||||||
|
protected $key;
|
||||||
|
protected $tplId;
|
||||||
|
protected $client;
|
||||||
|
|
||||||
|
public function __construct(Tool $tool)
|
||||||
|
{
|
||||||
|
$this->key = $tool->get('key');
|
||||||
|
$this->tplId = $tool->get('tpl_id');
|
||||||
|
$this->initClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendCode($mobile, $code, $activeMinute = 10)
|
||||||
|
{
|
||||||
|
$tplValue = urlencode('#code#=' . $code . '&#m#=' . $activeMinute);
|
||||||
|
$data = [
|
||||||
|
'mobile' => $mobile,
|
||||||
|
'tpl_id' => $this->tplId,
|
||||||
|
'tpl_value' => $tplValue,
|
||||||
|
'key' => $this->key,
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->post('/sms/send', $this->parseData($data));
|
||||||
|
$this->parseResult($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send($mobile, $content)
|
||||||
|
{
|
||||||
|
throw new BusinessException('该短信服务不可用');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendBatch(array $mobiles, $content)
|
||||||
|
{
|
||||||
|
throw new BusinessException('该短信服务不可用');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseData($data)
|
||||||
|
{
|
||||||
|
return http_build_query($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseResult($result)
|
||||||
|
{
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if (!$result) {
|
||||||
|
throw new BusinessException('系统异常');
|
||||||
|
}
|
||||||
|
if (!$result['error_code'] == 0) {
|
||||||
|
throw new BusinessException($result['reason']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function post($uri, $data)
|
||||||
|
{
|
||||||
|
$response = $this->client->post($uri, [
|
||||||
|
'verify' => false,
|
||||||
|
'body' => $data
|
||||||
|
]);
|
||||||
|
return (string)$response->getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initClient()
|
||||||
|
{
|
||||||
|
$this->client = new Client([
|
||||||
|
'base_uri' => $this->baseUrl,
|
||||||
|
'handler' => HandlerStack::create(new CoroutineHandler()),
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||||
|
],
|
||||||
|
'swoole' => [
|
||||||
|
'timeout' => 10,
|
||||||
|
'socket_buffer_size' => 1024 * 1024 * 2,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Helper\SmsSender;
|
||||||
|
|
||||||
|
use App\Contract\SmsSender;
|
||||||
|
use App\Exception\BusinessException;
|
||||||
|
use App\Model\Tool;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Hyperf\Guzzle\CoroutineHandler;
|
||||||
|
use GuzzleHttp\HandlerStack;
|
||||||
|
|
||||||
|
class XgSmsSender implements SmsSender
|
||||||
|
{
|
||||||
|
|
||||||
|
public $channel = 'xigu';
|
||||||
|
|
||||||
|
protected $baseUrl = 'http://api.vlpush.com';
|
||||||
|
protected $appId;
|
||||||
|
protected $apiKey;
|
||||||
|
protected $templateId;
|
||||||
|
protected $client;
|
||||||
|
|
||||||
|
public function __construct(Tool $tool)
|
||||||
|
{
|
||||||
|
$this->appId = $tool->get('smtp');
|
||||||
|
$this->apiKey = $tool->get('smtp_account');
|
||||||
|
$this->templateId = $tool->get('smtp_password');
|
||||||
|
$this->initClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendCode($mobile, $code, $activeMinute = 10)
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'appid' => $this->appId,
|
||||||
|
'apikey' => $this->apiKey,
|
||||||
|
'templateid' => $this->templateId,
|
||||||
|
'phone' => $mobile,
|
||||||
|
'param' => $code . ',' . $activeMinute,
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $this->post('/sms/send_sms', $this->parseData($data));
|
||||||
|
$this->parseResult($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send($mobile, $content)
|
||||||
|
{
|
||||||
|
throw new BusinessException('该短信服务不可用');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendBatch(array $mobiles, $content)
|
||||||
|
{
|
||||||
|
throw new BusinessException('该短信服务不可用');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseData($data)
|
||||||
|
{
|
||||||
|
return http_build_query($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseResult($result)
|
||||||
|
{
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if (!$result) {
|
||||||
|
throw new BusinessException('系统异常');
|
||||||
|
}
|
||||||
|
if ($result['code'] != 200) {
|
||||||
|
throw new BusinessException($result['msg']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function post($uri, $data)
|
||||||
|
{
|
||||||
|
$response = $this->client->post($uri, [
|
||||||
|
'verify' => false,
|
||||||
|
'body' => $data
|
||||||
|
]);
|
||||||
|
return (string)$response->getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function initClient()
|
||||||
|
{
|
||||||
|
$this->client = new Client([
|
||||||
|
'base_uri' => $this->baseUrl,
|
||||||
|
'handler' => HandlerStack::create(new CoroutineHandler()),
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||||
|
],
|
||||||
|
'swoole' => [
|
||||||
|
'timeout' => 10,
|
||||||
|
'socket_buffer_size' => 1024 * 1024 * 2,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Job;
|
||||||
|
|
||||||
|
use App\Exception\BusinessException;
|
||||||
|
use App\Service\SmsService;
|
||||||
|
|
||||||
|
class SmsJob extends Job
|
||||||
|
{
|
||||||
|
public $params;
|
||||||
|
|
||||||
|
protected $queueName = 'sms';
|
||||||
|
|
||||||
|
public function __construct($params)
|
||||||
|
{
|
||||||
|
$this->params = $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$mobile = $this->params['mobile'] ?? null;
|
||||||
|
$options = $this->params['options'] ?? [];
|
||||||
|
$type = $options['type'] ?? '';
|
||||||
|
if (!$mobile) {
|
||||||
|
throw new BusinessException('缺少手机号');
|
||||||
|
}
|
||||||
|
if (!$type) {
|
||||||
|
throw new BusinessException('缺少类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var SmsService $smsService
|
||||||
|
*/
|
||||||
|
$smsService = make(SmsService::class);
|
||||||
|
if ($type == 'content') {
|
||||||
|
$content = $options['content'] ?? '';
|
||||||
|
$smsService->send($mobile, $content);
|
||||||
|
} elseif ($type == 'code') {
|
||||||
|
$clientIp = $options['client_ip'] ?? '';
|
||||||
|
$smsService->sendCode($mobile, ['client_ip' => $clientIp]);
|
||||||
|
} elseif ($type == 'batch') {
|
||||||
|
if (!is_array($mobile)) {
|
||||||
|
$mobile = [$mobile];
|
||||||
|
}
|
||||||
|
$content = $options['content'] ?? '';
|
||||||
|
$smsService->sendBatch($mobile, $content);
|
||||||
|
} else {
|
||||||
|
throw new BusinessException('类型错误');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Model;
|
||||||
|
|
||||||
|
class Sms extends Model
|
||||||
|
{
|
||||||
|
const STATUS_SUCCESS = 1;
|
||||||
|
const STATUS_ERROR = 2;
|
||||||
|
const ACTIVE_MINUTE = 10;
|
||||||
|
|
||||||
|
protected $table = 'tab_sms_logs';
|
||||||
|
|
||||||
|
public function isExpired()
|
||||||
|
{
|
||||||
|
$expiredTime = strtotime($this->created_at) + self::ACTIVE_MINUTE * 60;
|
||||||
|
if (time() > $expiredTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Process;
|
||||||
|
|
||||||
|
use Hyperf\AsyncQueue\Process\ConsumerProcess;
|
||||||
|
use Hyperf\Process\Annotation\Process;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Process()
|
||||||
|
*/
|
||||||
|
class SmsQueueConsumer extends ConsumerProcess
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $queue = 'sms';
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Contract\SmsSender;
|
||||||
|
use App\Model\Sms;
|
||||||
|
use App\Exception\BusinessException;
|
||||||
|
use App\Helper\Log;
|
||||||
|
|
||||||
|
class SmsService extends Service
|
||||||
|
{
|
||||||
|
const INTERVAL_MINUTE = 1;
|
||||||
|
const IP_DAY_COUNT = 100;
|
||||||
|
|
||||||
|
public function send($mobile, $content)
|
||||||
|
{
|
||||||
|
$smsSender = make(SmsSender::class);
|
||||||
|
|
||||||
|
if (!$smsSender) {
|
||||||
|
throw new BusinessException('系统异常,请联系客服');
|
||||||
|
}
|
||||||
|
$smsSender->send($mobile, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendBatch(array $mobile, $content)
|
||||||
|
{
|
||||||
|
$smsSender = make(SmsSender::class);
|
||||||
|
|
||||||
|
if (!$smsSender) {
|
||||||
|
throw new BusinessException('系统异常,请联系客服');
|
||||||
|
}
|
||||||
|
|
||||||
|
$smsSender->sendBatch($mobile, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendCode($mobile, array $options)
|
||||||
|
{
|
||||||
|
$clientIp = $options['client_ip'] ?? '';
|
||||||
|
$smsSender = make(SmsSender::class);
|
||||||
|
|
||||||
|
if (!$smsSender) {
|
||||||
|
throw new BusinessException('系统异常,请联系客服');
|
||||||
|
}
|
||||||
|
if ($clientIp && $this->isIpOutOfDayCount($clientIp)) {
|
||||||
|
throw new BusinessException('每天发送数量不能超过' . self::IP_DAY_COUNT . '条');
|
||||||
|
}
|
||||||
|
if ($this->isOutOfRate($mobile)) {
|
||||||
|
throw new BusinessException('发送过于频繁,请稍后再试');
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = $this->getCode();
|
||||||
|
$sms = new Sms();
|
||||||
|
$sms->code = $code;
|
||||||
|
$sms->mobile = $mobile;
|
||||||
|
$sms->client_ip = $clientIp;
|
||||||
|
$sms->channel = $smsSender->channel;
|
||||||
|
$sms->save();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$smsSender->sendCode($mobile, $code, Sms::ACTIVE_MINUTE);
|
||||||
|
$sms->status = Sms::STATUS_SUCCESS;
|
||||||
|
$sms->result = 'success';
|
||||||
|
$sms->save();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$sms->status = Sms::STATUS_ERROR;
|
||||||
|
$sms->result = 'error';
|
||||||
|
$sms->save();
|
||||||
|
Log::error(
|
||||||
|
'SMS_SEND_ERROR: ' . $e->getMessage(),
|
||||||
|
['mobile' => $mobile, 'options' => $options],
|
||||||
|
'sms'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check($mobile, $code): bool
|
||||||
|
{
|
||||||
|
$sms = Sms::where('mobile', $mobile)
|
||||||
|
->where('is_checked', 0)
|
||||||
|
->where('status', Sms::STATUS_SUCCESS)
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->first();
|
||||||
|
if (!$sms) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($sms->isExpired()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($sms->code == $code) {
|
||||||
|
$sms->is_checked = 1;
|
||||||
|
$sms->save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCode()
|
||||||
|
{
|
||||||
|
return rand(100000, 999999);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isOutOfRate($mobile): bool
|
||||||
|
{
|
||||||
|
$lastSms = Sms::select(['created_at'])->where(['mobile' => $mobile])->orderBy('created_at', 'desc')->first();
|
||||||
|
if ($lastSms && strtotime($lastSms->created_at) + self::INTERVAL_MINUTE * 60 > time()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isIpOutOfDayCount($ip): bool
|
||||||
|
{
|
||||||
|
$count = Sms::where('client_ip', $ip)
|
||||||
|
->where('status', Sms::STATUS_SUCCESS)
|
||||||
|
->whereBetween('created_at', [date('Y-m-d 00:00:00'), date('Y-m-d 23:59:59')])
|
||||||
|
->count();
|
||||||
|
return $count > self::IP_DAY_COUNT;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue