引言:解决短信集成的核心痛点
在实际业务开发中,短信功能集成常面临多重挑战:多家服务商API差异、故障转移机制复杂、代码耦合度高等问题。overtrue/easy-sms 组件通过统一抽象层,为PHP开发者提供了一套优雅的解决方案。
环境要求与安装
# 要求 PHP >= 8.4composer require "overtrue/easy-sms"核心配置详解
基础配置结构
<?php// config/sms.phpreturn [ // HTTP请求超时设置 'timeout' => 5.0, // 默认发送策略 'default' => [ // 策略配置:顺序、随机、轮询等 'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class, // 网关执行顺序 'gateways' => [ 'aliyun', // 主用通道 'yunpian', // 备用通道1 'qcloud', // 备用通道2 ], ], // 网关配置 'gateways' => [ // 调试网关配置 'errorlog' => [ 'file' => runtime_path('logs/sms.log'), ], // 阿里云配置 'aliyun' => [ 'access_key_id' => env('SMS_ALIYUN_KEY_ID'), 'access_key_secret' => env('SMS_ALIYUN_KEY_SECRET'), 'sign_name' => env('SMS_ALIYUN_SIGN_NAME'), 'template_code' => 'SMS_001', // 默认模板 ], // 云片网络配置 'yunpian' => [ 'api_key' => env('SMS_YUNPIAN_API_KEY'), ], // 腾讯云配置 'qcloud' => [ 'sdk_app_id' => env('SMS_QCLOUD_APP_ID'), 'secret_id' => env('SMS_QCLOUD_SECRET_ID'), 'secret_key' => env('SMS_QCLOUD_SECRET_KEY'), 'sign_name' => env('SMS_QCLOUD_SIGN_NAME'), ], // 华为云配置 'huawei' => [ 'endpoint' => 'https://api.rtc.huaweicloud.com', 'app_key' => env('SMS_HUAWEI_APP_KEY'), 'app_secret' => env('SMS_HUAWEI_APP_SECRET'), 'from' => [ 'channel' => '89******87', 'template_id' => '0000000' ], ], ],];环境变量配置
# .env 文件SMS_ALIYUN_KEY_ID=your_access_key_idSMS_ALIYUN_KEY_SECRET=your_access_key_secretSMS_ALIYUN_SIGN_NAME=您的签名SMS_YUNPIAN_API_KEY=your_yunpian_keySMS_QCLOUD_APP_ID=1400000000SMS_QCLOUD_SECRET_ID=your_secret_idSMS_QCLOUD_SECRET_KEY=your_secret_keySMS_QCLOUD_SIGN_NAME=腾讯云签名实战应用示例
基础短信发送服务类
<?phpnamespace app\service;use Overtrue\EasySms\EasySms;use Overtrue\EasySms\Exceptions\NoGatewayAvailableException;class SmsService{ private EasySms $easySms; public function __construct() { $config = config('sms'); $this->easySms = new EasySms($config); } public function sendVerificationCode(string $phone, string $code, string $template = null): array { $message = [ 'template' => $template ?? config('sms.gateways.aliyun.template_code'), 'data' => [ 'code' => $code, 'time' => '5分钟' ], 'content' => sprintf('您的验证码是%s,有效期5分钟。', $code) ]; return $this->send($phone, $message); } public function sendNotification(string $phone, string $content, array $data = []): array { $message = [ 'content' => $content, 'data' => $data ]; return $this->send($phone, $message); } private function send(string $phone, array $message): array { try { $result = $this->easySms->send($phone, $message); // 记录发送日志 $this->logSendResult($phone, $message, $result); return [ 'success' => true, 'data' => $result ]; } catch (NoGatewayAvailableException $e) { $errorInfo = $this->parseGatewayErrors($e); // 记录错误日志 $this->logSendError($phone, $message, $errorInfo); return [ 'success' => false, 'error' => '短信发送失败', 'details' => $errorInfo ]; } } private function parseGatewayErrors(NoGatewayAvailableException $e): array { $errors = []; foreach ($e->getResults() as $gateway => $result) { $errors[$gateway] = [ 'exception' => $result['exception']->getMessage(), 'code' => $result['exception']->getCode() ?? '未知错误' ]; } return $errors; } private function logSendResult(string $phone, array $message, array $result): void { logger()->info('短信发送成功', [ 'phone' => $phone, 'message' => $message, 'result' => $result, 'time' => date('Y-m-d H:i:s') ]); } private function logSendError(string $phone, array $message, array $errorInfo): void { logger()->error('短信发送失败', [ 'phone' => $phone, 'message' => $message, 'errors' => $errorInfo, 'time' => date('Y-m-d H:i:s') ]); }}控制器中使用示例
<?phpnamespace app\controller;use app\service\SmsService;use support\Request;use support\Response;class SmsController{ public function sendCode(Request $request): Response { $phone = $request->post('phone'); $code = mt_rand(100000, 999999); // 验证手机号格式 if (!$this->validatePhone($phone)) { return json(['success' => false, 'message' => '手机号格式错误']); } $smsService = new SmsService(); $result = $smsService->sendVerificationCode($phone, $code); if ($result['success']) { // 将验证码存入缓存,5分钟过期 cache('sms_code_' . $phone, $code, 300); return json(['success' => true, 'message' => '验证码发送成功']); } return json(['success' => false, 'message' => '验证码发送失败']); } public function batchSend(Request $request): Response { $phones = $request->post('phones', []); $content = $request->post('content'); $results = []; $smsService = new SmsService(); foreach ($phones as $phone) { if ($this->validatePhone($phone)) { $results[$phone] = $smsService->sendNotification($phone, $content); } } return json([ 'success' => true, 'data' => $results ]); } private function validatePhone(string $phone): bool { return preg_match('/^1[3-9]\d{9}$/', $phone) === 1; }}高级特性与最佳实践
自定义发送策略
<?php// 自定义权重策略class WeightStrategy implements \Overtrue\EasySms\Contracts\StrategyInterface{ public function apply(array $gateways): array { $weights = [ 'aliyun' => 60, // 60%流量 'qcloud' => 30, // 30%流量 'yunpian' => 10, // 10%流量 ]; $result = []; foreach ($weights as $gateway => $weight) { $result = array_merge($result, array_fill(0, $weight, $gateway)); } shuffle($result); return $result; }}// 在配置中使用自定义策略'default' => [ 'strategy' => \app\strategy\WeightStrategy::class, 'gateways' => ['aliyun', 'qcloud', 'yunpian'],];网关性能监控
<?phpclass MonitorableSmsService extends SmsService{ private array $gatewayStats = []; public function send(string $phone, array $message): array { $startTime = microtime(true); try { $result = parent::send($phone, $message); $this->recordSuccess($startTime); return $result; } catch (\Exception $e) { $this->recordFailure($startTime, $e); throw $e; } } private function recordSuccess(float $startTime): void { $responseTime = microtime(true) - $startTime; // 记录到监控系统 $this->updateGatewayStats('success', $responseTime); } private function recordFailure(float $startTime, \Exception $e): void { $responseTime = microtime(true) - $startTime; // 记录到监控系统 $this->updateGatewayStats('failure', $responseTime, $e->getMessage()); }}故障转移与灾备方案
自动降级配置
<?php// 根据时间段自动选择网关class TimebasedStrategy implements \Overtrue\EasySms\Contracts\StrategyInterface{ public function apply(array $gateways): array { $hour = date('H'); // 工作时间使用主通道,夜间使用备用通道 if ($hour >= 9 && $hour < 18) { return ['aliyun', 'qcloud', 'yunpian']; // 主从顺序 } else { return ['yunpian', 'aliyun', 'qcloud']; // 夜间备用顺序 } }}测试用例
单元测试示例
<?phpnamespace tests\service;use app\service\SmsService;use PHPUnit\framework\TestCase;class SmsServiceTest extends TestCase{ public function testSendVerificationCode() { $smsService = new SmsService(); // 使用模拟手机号 $testPhone = '13800138000'; $testCode = '123456'; $result = $smsService->sendVerificationCode($testPhone, $testCode); $this->assertIsArray($result); $this->assertArrayHasKey('success', $result); // 在实际项目中,可以验证日志记录等副作用 } public function testPhonevalidation() { $validPhones = ['13800138000', '13912345678']; $invalidPhones = ['123456', '1380013800a', '']; foreach ($validPhones as $phone) { $this->assertTrue($this->validatePhone($phone)); } foreach ($invalidPhones as $phone) { $this->assertFalse($this->validatePhone($phone)); } } private function validatePhone(string $phone): bool { return preg_match('/^1[3-9]\d{9}$/', $phone) === 1; }}部署与运维建议
Docker 部署配置
# DockerfileFROM php:8.4-fpm# 安装必要扩展RUN docker-php-ext-install bcmath# 安装ComposerCOPY --from=composer:latest /usr/bin/composer /usr/bin/composerWORKDIR /var/www/htmlCOPY . .RUN composer install --no-dev --optimize-autoloader总结
通过 overtrue/easy-sms 组件,我们实现了:
- 统一接入层:标准化多家短信服务商接入流程
- 自动故障转移:内置多网关轮询和降级机制
- 可观测性:完整的日志记录和监控支持
- 易于扩展:支持自定义策略和网关扩展
- 测试友好:便于编写单元测试和集成测试
该方案特别适合需要高可用性短信服务的生产环境,大幅降低了短信功能的技术复杂度和维护成本。
