diff --git a/app/Http/Controllers/Api/V1/SmsCodeController.php b/app/Http/Controllers/Api/V1/SmsCodeController.php new file mode 100644 index 00000000..1da346b6 --- /dev/null +++ b/app/Http/Controllers/Api/V1/SmsCodeController.php @@ -0,0 +1,49 @@ +isLocal()) { + $captchaService->validatePhrase( + (string) $request->input('captcha_key'), + (string) $request->input('captcha_phrase') + ); + } + + try { + $smsCodeService->send( + $request->input('phone'), + $request->input('type'), + app()->isProduction() ? mt_rand(100000, 999999) : '666666', + ); + } catch (BizException $e) { + throw $e; + } catch (Throwable $e) { + report($e); + } + + return response()->noContent(); + } +} diff --git a/app/Http/Requests/Api/V1/SmsCode/StoreRequest.php b/app/Http/Requests/Api/V1/SmsCode/StoreRequest.php new file mode 100644 index 00000000..12908329 --- /dev/null +++ b/app/Http/Requests/Api/V1/SmsCode/StoreRequest.php @@ -0,0 +1,33 @@ + ['bail', 'required', new PhoneNumber()], + 'type' => ['bail', 'required', 'int'], + 'captcha_phrase' => ['bail', 'required', 'string'], + ]; + } +} diff --git a/app/Models/SmsCode.php b/app/Models/SmsCode.php new file mode 100644 index 00000000..c7f65c89 --- /dev/null +++ b/app/Models/SmsCode.php @@ -0,0 +1,84 @@ + false, + ]; + + /** + * @var array + */ + protected $fillable = [ + 'user_id', + 'phone', + 'type', + 'code', + 'is_use', + 'expires_at', + ]; + + /** + * @var array + */ + protected $casts = [ + 'type' => 'int', + 'is_use' => 'bool', + 'expires_at' => 'datetime', + ]; + + /** + * 允许发送短信的验证码类型 + * + * @var array + */ + public static $allowedTypes = [ + self::TYPE_REGISTER, + ]; + + /** + * {@inheritdoc} + */ + protected static function booted() + { + static::created(function ($smsCode) { + if (app()->isProduction()) { + $smsCode->notify(new SmsCodeCreated($smsCode)); + } + }); + } + + /** + * 确认此验证码是否无效 + * + * @return bool + */ + public function isInvalid(): bool + { + return $this->is_use || ($this->expires_at && $this->expires_at->lte(now())); + } + + /** + * @param \Illuminate\Notifications\Notification $notification + * @return string + */ + public function routeNotificationForSms($notification): string + { + return $this->phone; + } +} diff --git a/app/Notifications/Channels/SmsChannel.php b/app/Notifications/Channels/SmsChannel.php new file mode 100644 index 00000000..5dc2c034 --- /dev/null +++ b/app/Notifications/Channels/SmsChannel.php @@ -0,0 +1,42 @@ +toSms($notifiable); + $phone = $notifiable->routeNotificationFor('sms', $notification); + + if (! $phone && ! $message instanceof Message) { + return; + } + + try { + $this->easySms->send($phone, $message); + } catch (NoGatewayAvailableException $e) { + foreach ($e->getExceptions() as $exception) { + report($exception); + } + } + } +} diff --git a/app/Notifications/Messages/SmsCodeMessage.php b/app/Notifications/Messages/SmsCodeMessage.php new file mode 100644 index 00000000..ae198ba0 --- /dev/null +++ b/app/Notifications/Messages/SmsCodeMessage.php @@ -0,0 +1,17 @@ +smsCode); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index cc8108ac..86af802a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,7 @@ namespace App\Providers; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; use Illuminate\Support\ServiceProvider; +use Overtrue\EasySms\EasySms; class AppServiceProvider extends ServiceProvider { @@ -15,6 +16,7 @@ class AppServiceProvider extends ServiceProvider */ public function register() { + $this->registerEasySms(); $this->registerRequestRealIp(); } @@ -30,6 +32,18 @@ class AppServiceProvider extends ServiceProvider ]); } + /** + * 注册短信发送服务 + * + * @return void + */ + protected function registerEasySms() + { + $this->app->singleton(EasySms::class, function ($app) { + return new EasySms($app['config']->get('easysms')); + }); + } + /** * 在请求上注册 realIp 宏 * diff --git a/app/Services/SmsCodeService.php b/app/Services/SmsCodeService.php new file mode 100644 index 00000000..d84c5bbe --- /dev/null +++ b/app/Services/SmsCodeService.php @@ -0,0 +1,89 @@ +exists()) { + throw new BizException(__('The phone number is already registered')); + } + } + + if (! $this->cache->add("sms_lock_{$type}_{$phone}", 1, $decaySeconds)) { + throw new BizException(__('Sending too frequently, please try again later')); + } + + return SmsCode::create([ + 'phone' => $phone, + 'code' => $code, + 'type' => $type, + 'expires_at' => now()->addSeconds($this->expires), + ]); + } + + /** + * 校验验证码是否正确 + * + * @param string $phone + * @param int $type + * @param string $code + * @return void + * + * @throws \App\Exceptions\BizException + */ + public function validate(string $phone, int $type, string $code): void + { + $smsCode = SmsCode::where('phone', $phone) + ->where('type', $type) + ->latest('id') + ->first(); + + if ($smsCode === null || $smsCode->code !== $code || $smsCode->isInvalid()) { + throw new BizException(__('Invalid verification code')); + } + + $smsCode->update([ + 'is_use' => true, + ]); + } +} diff --git a/composer.json b/composer.json index 4e32b0c1..255000ee 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "kalnoy/nestedset": "^6.0", "laravel/framework": "^8.65", "laravel/sanctum": "^2.12", - "laravel/tinker": "^2.5" + "laravel/tinker": "^2.5", + "overtrue/easy-sms": "^2.0" }, "require-dev": { "facade/ignition": "^2.5", diff --git a/composer.lock b/composer.lock index c1d8b148..2c5f946f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "865b93f26360dff74bb3e2c2c6dac800", + "content-hash": "d93e7912aa10ac47d7b1923ce5dd28aa", "packages": [ { "name": "asm89/stack-cors", @@ -2858,6 +2858,81 @@ }, "time": "2021-04-09T13:42:10+00:00" }, + { + "name": "overtrue/easy-sms", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/overtrue/easy-sms.git", + "reference": "8a9d45cdd090dc66b26faad127614a24c6c1b049" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/overtrue/easy-sms/zipball/8a9d45cdd090dc66b26faad127614a24c6c1b049", + "reference": "8a9d45cdd090dc66b26faad127614a24c6c1b049", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.2 || ^7.0", + "php": ">=5.6" + }, + "require-dev": { + "brainmaestro/composer-git-hooks": "^2.8", + "friendsofphp/php-cs-fixer": "^3.0", + "jetbrains/phpstorm-attributes": "^1.0", + "mockery/mockery": "~1.3.3 || ^1.4.2", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^5.7 || ^7.5 || ^8.5.19 || ^9.5.8", + "vimeo/psalm": "^4.10" + }, + "type": "library", + "extra": { + "hooks": { + "pre-commit": [ + "composer check-style", + "composer psalm", + "composer test" + ], + "pre-push": [ + "composer check-style" + ] + } + }, + "autoload": { + "psr-4": { + "Overtrue\\EasySms\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "i@overtrue.me" + } + ], + "description": "The easiest way to send short message.", + "support": { + "issues": "https://github.com/overtrue/easy-sms/issues", + "source": "https://github.com/overtrue/easy-sms/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/overtrue", + "type": "github" + } + ], + "time": "2021-11-19T03:46:38+00:00" + }, { "name": "phpoption/phpoption", "version": "1.8.0", diff --git a/config/easysms.php b/config/easysms.php new file mode 100644 index 00000000..29e72004 --- /dev/null +++ b/config/easysms.php @@ -0,0 +1,23 @@ + 5.0, + + // 默认发送配置 + 'default' => [ + // 网关调用策略,默认:顺序调用 + 'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class, + + // 默认可用的发送网关 + 'gateways' => [ + // + ], + ], + // 可用的网关配置 + 'gateways' => [ + 'errorlog' => [ + 'file' => storage_path('logs/easysms.log'), + ], + ], +]; diff --git a/database/migrations/2021_11_23_153624_create_sms_codes_table.php b/database/migrations/2021_11_23_153624_create_sms_codes_table.php new file mode 100644 index 00000000..4dbf451d --- /dev/null +++ b/database/migrations/2021_11_23_153624_create_sms_codes_table.php @@ -0,0 +1,39 @@ +id(); + $table->unsignedBigInteger('user_id')->nullable(); + $table->string('phone', 20)->comment('手机号'); + $table->tinyInteger('type')->comment('验证码类型'); + $table->string('code')->comment('验证码'); + $table->boolean('is_use')->default(0)->comment('是否使用'); + $table->timestamp('expires_at')->nullable()->comment('过期时间'); + $table->timestamps(); + + $table->index(['phone', 'type']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('sms_codes'); + } +} diff --git a/resources/lang/zh_CN.json b/resources/lang/zh_CN.json index 768f84be..dc0b26cd 100644 --- a/resources/lang/zh_CN.json +++ b/resources/lang/zh_CN.json @@ -2,5 +2,9 @@ ":resource not found": ":resource 未找到", "Invalid captcha": "无效的验证码", "Invalid invitation code": "无效的邀请码", - "Registration failed, please try again": "注册失败,请重试" + "Invalid verification code": "无效的验证码", + "Invalid verification code type": "无效的验证码类型", + "Registration failed, please try again": "注册失败,请重试", + "Sending too frequently, please try again later": "发送过于频繁,请稍后再试", + "The phone number is already registered": "手机号码已被注册" } diff --git a/routes/api/v1/common.php b/routes/api/v1/common.php index b260c50c..e1d8ea9a 100644 --- a/routes/api/v1/common.php +++ b/routes/api/v1/common.php @@ -1,7 +1,10 @@