Laravel 13 可以轻松集成多种短信服务,本文介绍如何构建完整的短信发送系统。
短信配置
多渠道配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <?php
return [ 'default' => env('SMS_DRIVER', 'twilio'), 'drivers' => [ 'twilio' => [ 'sid' => env('TWILIO_SID'), 'token' => env('TWILIO_TOKEN'), 'from' => env('TWILIO_FROM'), ], 'nexmo' => [ 'key' => env('NEXMO_KEY'), 'secret' => env('NEXMO_SECRET'), 'from' => env('NEXMO_FROM'), ], 'aliyun' => [ 'access_key' => env('ALIYUN_ACCESS_KEY'), 'access_secret' => env('ALIYUN_ACCESS_SECRET'), 'sign_name' => env('ALIYUN_SMS_SIGN'), 'region' => env('ALIYUN_SMS_REGION', 'cn-hangzhou'), ], 'tencent' => [ 'secret_id' => env('TENCENT_SECRET_ID'), 'secret_key' => env('TENCENT_SECRET_KEY'), 'app_id' => env('TENCENT_SMS_APP_ID'), 'sign_name' => env('TENCENT_SMS_SIGN'), ], ], 'rate_limits' => [ 'per_minute' => 60, 'per_hour' => 500, 'per_day' => 2000, ], ];
|
短信服务
基础短信接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php
namespace App\Services\SMS;
interface SMSDriverInterface { public function send(string $to, string $message): SMSResult; public function sendTemplate(string $to, string $templateId, array $params): SMSResult; public function sendBatch(array $recipients, string $message): array; }
class SMSResult { public function __construct( public bool $success, public ?string $messageId = null, public ?string $errorCode = null, public ?string $errorMessage = null, public array $metadata = [] ) {} public function isSuccess(): bool { return $this->success; } }
|
Twilio 驱动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| <?php
namespace App\Services\SMS\Drivers;
use App\Services\SMS\SMSDriverInterface; use App\Services\SMS\SMSResult; use Twilio\Rest\Client;
class TwilioDriver implements SMSDriverInterface { protected Client $client; protected string $from; public function __construct(array $config) { $this->client = new Client($config['sid'], $config['token']); $this->from = $config['from']; } public function send(string $to, string $message): SMSResult { try { $result = $this->client->messages->create($to, [ 'from' => $this->from, 'body' => $message, ]); return new SMSResult( success: $result->status !== 'failed', messageId: $result->sid, metadata: [ 'status' => $result->status, 'to' => $result->to, ] ); } catch (\Throwable $e) { return new SMSResult( success: false, errorCode: $e->getCode(), errorMessage: $e->getMessage() ); } } public function sendTemplate(string $to, string $templateId, array $params): SMSResult { $message = $this->compileTemplate($templateId, $params); return $this->send($to, $message); } public function sendBatch(array $recipients, string $message): array { $results = []; foreach ($recipients as $recipient) { $results[$recipient] = $this->send($recipient, $message); } return $results; } protected function compileTemplate(string $templateId, array $params): string { return implode(' ', $params); } }
|
阿里云短信驱动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| <?php
namespace App\Services\SMS\Drivers;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Dysmsapi; use AlibabaCloud\SDK\Dysmsapi\V20170525\Models\SendSmsRequest; use App\Services\SMS\SMSDriverInterface; use App\Services\SMS\SMSResult; use Darabonba\OpenApi\Models\Config;
class AliyunDriver implements SMSDriverInterface { protected Dysmsapi $client; protected string $signName; public function __construct(array $config) { $this->signName = $config['sign_name']; $configObj = new Config([ 'accessKeyId' => $config['access_key'], 'accessKeySecret' => $config['access_secret'], 'regionId' => $config['region'] ?? 'cn-hangzhou', ]); $this->client = new Dysmsapi($configObj); } public function send(string $to, string $message): SMSResult { return $this->sendTemplate($to, 'default', ['message' => $message]); } public function sendTemplate(string $to, string $templateId, array $params): SMSResult { try { $request = new SendSmsRequest([ 'phoneNumbers' => $to, 'signName' => $this->signName, 'templateCode' => $templateId, 'templateParam' => json_encode($params), ]); $result = $this->client->sendSms($request); return new SMSResult( success: $result->body->code === 'OK', messageId: $result->body->bizId, errorCode: $result->body->code, errorMessage: $result->body->message, metadata: [ 'request_id' => $result->body->requestId, ] ); } catch (\Throwable $e) { return new SMSResult( success: false, errorCode: $e->getCode(), errorMessage: $e->getMessage() ); } } public function sendBatch(array $recipients, string $message): array { $results = []; foreach ($recipients as $recipient) { $results[$recipient] = $this->send($recipient, $message); } return $results; } }
|
短信服务管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| <?php
namespace App\Services\SMS;
use App\Services\SMS\Drivers\AliyunDriver; use App\Services\SMS\Drivers\TwilioDriver;
class SMSManager { protected array $drivers = []; protected array $config; public function __construct(array $config) { $this->config = $config; } public function driver(string $name = null): SMSDriverInterface { $name = $name ?? $this->config['default']; if (!isset($this->drivers[$name])) { $this->drivers[$name] = $this->createDriver($name); } return $this->drivers[$name]; } protected function createDriver(string $name): SMSDriverInterface { $config = $this->config['drivers'][$name] ?? []; return match ($name) { 'twilio' => new TwilioDriver($config), 'aliyun' => new AliyunDriver($config), default => throw new \InvalidArgumentException("SMS driver [{$name}] not supported"), }; } public function send(string $to, string $message): SMSResult { return $this->driver()->send($to, $message); } public function sendTemplate(string $to, string $templateId, array $params): SMSResult { return $this->driver()->sendTemplate($to, $templateId, $params); } }
|
短信验证码
验证码服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| <?php
namespace App\Services\SMS;
use Illuminate\Support\Facades\Cache;
class VerificationCodeService { protected SMSManager $sms; protected int $codeLength = 6; protected int $expiryMinutes = 5; protected int $maxAttempts = 5; public function __construct(SMSManager $sms) { $this->sms = $sms; } public function send(string $phone, string $purpose = 'verification'): SMSResult { $this->checkRateLimit($phone, $purpose); $code = $this->generateCode(); $result = $this->sms->sendTemplate($phone, 'verification_code', [ 'code' => $code, ]); if ($result->isSuccess()) { $this->storeCode($phone, $code, $purpose); } return $result; } public function verify(string $phone, string $code, string $purpose = 'verification'): bool { $key = $this->getCacheKey($phone, $purpose); $stored = Cache::get($key); if (!$stored) { return false; } $attempts = $stored['attempts'] ?? 0; if ($attempts >= $this->maxAttempts) { return false; } if ($stored['code'] !== $code) { Cache::put($key, [ 'code' => $stored['code'], 'attempts' => $attempts + 1, ], now()->addMinutes($this->expiryMinutes)); return false; } Cache::forget($key); return true; } protected function generateCode(): string { return str_pad(random_int(0, pow(10, $this->codeLength) - 1), $this->codeLength, '0', STR_PAD_LEFT); } protected function storeCode(string $phone, string $code, string $purpose): void { $key = $this->getCacheKey($phone, $purpose); Cache::put($key, [ 'code' => $code, 'attempts' => 0, ], now()->addMinutes($this->expiryMinutes)); } protected function getCacheKey(string $phone, string $purpose): string { return "sms:verification:{$purpose}:{$phone}"; } protected function checkRateLimit(string $phone, string $purpose): void { $key = "sms:rate:{$purpose}:{$phone}"; $count = Cache::get($key, 0); if ($count >= 5) { throw new \RuntimeException('Too many verification code requests'); } Cache::put($key, $count + 1, now()->addHour()); } }
|
短信模板管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <?php
namespace App\Services\SMS;
use App\Models\SMSTemplate;
class TemplateService { public function get(string $name, string $locale = 'en'): ?SMSTemplate { return SMSTemplate::where('name', $name) ->where('locale', $locale) ->where('is_active', true) ->first(); } public function render(string $name, array $params, string $locale = 'en'): string { $template = $this->get($name, $locale); if (!$template) { throw new \RuntimeException("SMS template [{$name}] not found"); } $content = $template->content; foreach ($params as $key => $value) { $content = str_replace("{{{$key}}}", $value, $content); } return $content; } public function create(string $name, string $content, string $locale = 'en'): SMSTemplate { return SMSTemplate::create([ 'name' => $name, 'content' => $content, 'locale' => $locale, 'is_active' => true, ]); } }
|
短信日志
短信记录模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SMSLog extends Model { protected $fillable = [ 'phone', 'message', 'template_id', 'params', 'driver', 'message_id', 'status', 'error_code', 'error_message', 'sent_at', ]; protected $casts = [ 'params' => 'array', 'sent_at' => 'datetime', ]; public function scopeSuccessful($query) { return $query->where('status', 'success'); } public function scopeFailed($query) { return $query->where('status', 'failed'); } public function scopeToday($query) { return $query->whereDate('created_at', today()); } }
|
日志服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?php
namespace App\Services\SMS;
use App\Models\SMSLog;
class SMSLogger { public function log( string $phone, string $message, SMSResult $result, string $driver, ?string $templateId = null, array $params = [] ): SMSLog { return SMSLog::create([ 'phone' => $phone, 'message' => $message, 'template_id' => $templateId, 'params' => $params, 'driver' => $driver, 'message_id' => $result->messageId, 'status' => $result->isSuccess() ? 'success' : 'failed', 'error_code' => $result->errorCode, 'error_message' => $result->errorMessage, 'sent_at' => $result->isSuccess() ? now() : null, ]); } }
|
短信控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| <?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Services\SMS\SMSManager; use App\Services\SMS\VerificationCodeService; use Illuminate\Http\Request;
class SMSController extends Controller { public function __construct( protected SMSManager $sms, protected VerificationCodeService $verification ) {} public function sendVerificationCode(Request $request) { $request->validate([ 'phone' => 'required|string|regex:/^\+?[1-9]\d{1,14}$/', 'purpose' => 'nullable|string|in:registration,login,password_reset', ]); $result = $this->verification->send( $request->phone, $request->purpose ?? 'verification' ); if ($result->isSuccess()) { return response()->json([ 'message' => 'Verification code sent successfully', ]); } return response()->json([ 'message' => 'Failed to send verification code', 'error' => $result->errorMessage, ], 400); } public function verifyCode(Request $request) { $request->validate([ 'phone' => 'required|string', 'code' => 'required|string|size:6', 'purpose' => 'nullable|string', ]); $valid = $this->verification->verify( $request->phone, $request->code, $request->purpose ?? 'verification' ); if ($valid) { return response()->json([ 'message' => 'Verification successful', ]); } return response()->json([ 'message' => 'Invalid or expired verification code', ], 400); } public function send(Request $request) { $request->validate([ 'phone' => 'required|string', 'message' => 'required|string|max:500', ]); $result = $this->sms->send($request->phone, $request->message); return response()->json([ 'success' => $result->isSuccess(), 'message_id' => $result->messageId, ]); } }
|
短信命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php
namespace App\Console\Commands;
use App\Models\SMSLog; use Illuminate\Console\Command;
class SMSCleanupCommand extends Command { protected $signature = 'sms:cleanup {--days=30 : Days to keep logs}'; protected $description = 'Cleanup old SMS logs'; public function handle(): int { $days = (int) $this->option('days'); $count = SMSLog::where('created_at', '<', now()->subDays($days))->delete(); $this->info("Deleted {$count} old SMS logs"); return self::SUCCESS; } }
|
总结
Laravel 13 的短信集成系统支持多种短信服务商,通过统一的接口和驱动模式,可以灵活切换短信渠道。结合验证码服务、模板管理和日志记录,可以构建完整的短信发送系统。