PHP项目短信平台多供应商

wen PHP项目 1

本文目录导读:

PHP项目短信平台多供应商

  1. 整体架构设计
  2. 高级功能实现

我来为您设计一个PHP项目中的短信平台多供应商架构方案。

整体架构设计

核心接口定义

<?php
// interfaces/SmsProviderInterface.php
interface SmsProviderInterface
{
    /**
     * 发送短信
     * @param string $phone 手机号
     * @param string $content 短信内容
     * @param array $options 附加选项
     * @return SmsResult
     */
    public function send(string $phone, string $content, array $options = []): SmsResult;
    /**
     * 发送批量短信
     * @param array $messages 消息数组 [['phone'=>'', 'content'=>'']]
     * @return array SmsResult[]
     */
    public function sendBatch(array $messages): array;
    /**
     * 查询余额
     * @return float
     */
    public function queryBalance(): float;
    /**
     * 获取供应商名称
     * @return string
     */
    public function getProviderName(): string;
    /**
     * 检查供应商健康状态
     * @return bool
     */
    public function healthCheck(): bool;
}
// Result/SmsResult.php
class SmsResult
{
    public bool $success;
    public string $messageId;
    public string $provider;
    public string $errorMessage;
    public array $rawResponse;
    public float $cost;
    public int $timestamp;
}

供应商实现示例

<?php
// Providers/AliyunSmsProvider.php
class AliyunSmsProvider implements SmsProviderInterface
{
    private $accessKeyId;
    private $accessKeySecret;
    private $signName;
    public function __construct(array $config)
    {
        $this->accessKeyId = $config['access_key_id'];
        $this->accessKeySecret = $config['access_key_secret'];
        $this->signName = $config['sign_name'];
    }
    public function send(string $phone, string $content, array $options = []): SmsResult
    {
        // 阿里云短信发送逻辑
        // 使用阿里云SDK
        $result = new SmsResult();
        try {
            // 实际API调用
            $response = $this->callApi($phone, $content);
            $result->success = true;
            $result->messageId = $response['MessageId'];
            $result->provider = $this->getProviderName();
            $result->timestamp = time();
        } catch (Exception $e) {
            $result->success = false;
            $result->errorMessage = $e->getMessage();
        }
        return $result;
    }
    public function getProviderName(): string
    {
        return 'aliyun';
    }
    // ... 其他方法实现
}
// Providers/TencentSmsProvider.php
class TencentSmsProvider implements SmsProviderInterface
{
    // 腾讯云短信实现
}
// Providers/SubmailSmsProvider.php
class SubmailSmsProvider implements SmsProviderInterface
{
    // SUBMAIL短信实现
}

策略管理器

<?php
// Services/SmsManager.php
class SmsManager
{
    private array $providers = [];
    private array $config;
    public function __construct(array $config = [])
    {
        $this->config = $config;
        $this->initProviders();
    }
    private function initProviders(): void
    {
        foreach ($this->config['providers'] as $name => $providerConfig) {
            if ($providerConfig['enabled']) {
                $className = $providerConfig['class'];
                $this->providers[$name] = new $className($providerConfig['config']);
            }
        }
    }
    /**
     * 智能发送 - 自动选择供应商
     */
    public function smartSend(string $phone, string $content, string $strategy = 'weighted'): SmsResult
    {
        // 1. 获取可用供应商列表
        $availableProviders = $this->getAvailableProviders();
        if (empty($availableProviders)) {
            throw new RuntimeException('No available SMS providers');
        }
        // 2. 根据策略选择供应商
        $selectedProvider = $this->selectProvider($availableProviders, $strategy);
        // 3. 发送短信
        $result = $selectedProvider->send($phone, $content);
        // 4. 如果失败且需要容错
        if (!$result->success && $this->config['failover_enabled']) {
            $result = $this->failoverSend($phone, $content, $selectedProvider->getProviderName());
        }
        return $result;
    }
    /**
     * 故障转移发送
     */
    private function failoverSend(string $phone, string $content, string $excludeProvider): SmsResult
    {
        $backupProviders = array_filter(
            $this->providers,
            fn($p) => $p->getProviderName() !== $excludeProvider
        );
        foreach ($backupProviders as $provider) {
            $result = $provider->send($phone, $content);
            if ($result->success) {
                return $result;
            }
        }
        throw new RuntimeException('All SMS providers failed');
    }
    /**
     * 获取可用供应商
     */
    private function getAvailableProviders(): array
    {
        $available = [];
        foreach ($this->providers as $name => $provider) {
            if ($provider->healthCheck()) {
                $available[$name] = $provider;
            }
        }
        return $available;
    }
    /**
     * 选择供应商策略
     */
    private function selectProvider(array $providers, string $strategy): SmsProviderInterface
    {
        switch ($strategy) {
            case 'random':
                return $providers[array_rand($providers)];
            case 'priority':
                // 按配置优先级选择
                uasort($providers, function($a, $b) {
                    return $this->getProviderPriority($a) <=> $this->getProviderPriority($b);
                });
                return array_shift($providers);
            case 'weighted':
                // 加权轮询
                return $this->weightedRoundRobin($providers);
            default:
                return array_shift($providers);
        }
    }
    /**
     * 加权轮询算法
     */
    private function weightedRoundRobin(array $providers): SmsProviderInterface
    {
        static $currentIndex = 0;
        static $currentWeight = 0;
        $maxWeight = $this->getMaxWeight($providers);
        $gcd = $this->getGcdWeights($providers);
        while (true) {
            $currentIndex = ($currentIndex + 1) % count($providers);
            if ($currentIndex == 0) {
                $currentWeight = $currentWeight - $gcd;
                if ($currentWeight <= 0) {
                    $currentWeight = $maxWeight;
                    if ($currentWeight == 0) {
                        return reset($providers);
                    }
                }
            }
            $provider = array_values($providers)[$currentIndex];
            $weight = $this->getProviderWeight($provider);
            if ($weight >= $currentWeight) {
                return $provider;
            }
        }
    }
}

配置管理

<?php
// config/sms.php
return [
    'default_provider' => 'aliyun',
    'failover_enabled' => true,
    'failover_threshold' => 3,  // 失败次数阈值
    'providers' => [
        'aliyun' => [
            'enabled' => true,
            'class' => AliyunSmsProvider::class,
            'priority' => 1,
            'weight' => 5,
            'config' => [
                'access_key_id' => env('ALIYUN_SMS_ACCESS_KEY_ID'),
                'access_key_secret' => env('ALIYUN_SMS_ACCESS_KEY_SECRET'),
                'sign_name' => env('ALIYUN_SMS_SIGN_NAME'),
                'template_code' => 'SMS_123456789',
                'region_id' => 'cn-hangzhou',
            ]
        ],
        'tencent' => [
            'enabled' => true,
            'class' => TencentSmsProvider::class,
            'priority' => 2,
            'weight' => 3,
            'config' => [
                'secret_id' => env('TENCENT_SMS_SECRET_ID'),
                'secret_key' => env('TENCENT_SMS_SECRET_KEY'),
                'app_id' => env('TENCENT_SMS_APP_ID'),
                'sign_name' => env('TENCENT_SMS_SIGN_NAME'),
                'template_id' => '123456',
            ]
        ],
        'submail' => [
            'enabled' => true,
            'class' => SubmailSmsProvider::class,
            'priority' => 3,
            'weight' => 2,
            'config' => [
                'app_id' => env('SUBMAIL_APP_ID'),
                'app_key' => env('SUBMAIL_APP_KEY'),
                'sign_type' => 'normal',
            ]
        ],
    ],
    'strategies' => [
        'default' => 'weighted',
        'high_priority' => 'priority',
        'load_balance' => 'random',
    ],
    'rate_limits' => [
        'per_minute' => 20,
        'per_hour' => 500,
        'per_day' => 3000,
    ],
    'templates' => [
        'verification_code' => [
            'aliyun' => 'SMS_123456789',
            'tencent' => '123456',
            'submail' => '12345',
        ],
        'notification' => [
            'aliyun' => 'SMS_987654321',
            'tencent' => '654321',
        ],
    ],
];

高级功能实现

监控与统计

<?php
// Services/SmsMonitor.php
class SmsMonitor
{
    private $metrics = [];
    private $db;
    /**
     * 记录发送日志
     */
    public function logSend(SmsResult $result): void
    {
        $logData = [
            'phone' => $result->phone,
            'content' => $result->content,
            'provider' => $result->provider,
            'success' => $result->success,
            'message_id' => $result->messageId,
            'error_message' => $result->errorMessage,
            'cost' => $result->cost,
            'created_at' => date('Y-m-d H:i:s'),
            'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
        ];
        // 存储到数据库
        $this->db->table('sms_logs')->insert($logData);
        // 更新统计指标
        $this->updateMetrics($result);
    }
    /**
     * 获取供应商成功率统计
     */
    public function getProviderStats(string $provider, string $startDate, string $endDate): array
    {
        return $this->db->table('sms_logs')
            ->where('provider', $provider)
            ->whereBetween('created_at', [$startDate, $endDate])
            ->selectRaw('
                COUNT(*) as total,
                SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count,
                AVG(CAST(cost AS DECIMAL(10,4))) as avg_cost
            ')
            ->first();
    }
    /**
     * 告警检查
     */
    public function checkAlerts(): void
    {
        // 检查各供应商成功率
        foreach ($this->getAllProviders() as $provider) {
            $stats = $this->getProviderStats(
                $provider, 
                date('Y-m-d H:i:s', strtotime('-5 minutes')),
                date('Y-m-d H:i:s')
            );
            // 如果成功率低于阈值,发送告警
            if ($stats['total'] > 10 && 
                ($stats['success_count'] / $stats['total']) < 0.9) {
                $this->sendAlert("Provider {$provider} success rate below 90%");
            }
        }
    }
}

缓存与限流

<?php
// Services/SmsRateLimiter.php
class SmsRateLimiter
{
    private $cache;
    private $config;
    public function __construct($cache, array $config)
    {
        $this->cache = $cache;
        $this->config = $config;
    }
    /**
     * 检查是否可以发送
     */
    public function canSend(string $phone): bool
    {
        $key = "sms_rate_limit:{$phone}";
        $minuteKey = "{$key}:minute";
        $hourKey = "{$key}:hour";
        $dayKey = "{$key}:day";
        $minuteCount = $this->cache->get($minuteKey, 0);
        $hourCount = $this->cache->get($hourKey, 0);
        $dayCount = $this->cache->get($dayKey, 0);
        if ($minuteCount >= $this->config['per_minute']) {
            return false;
        }
        if ($hourCount >= $this->config['per_hour']) {
            return false;
        }
        if ($dayCount >= $this->config['per_day']) {
            return false;
        }
        return true;
    }
    /**
     * 记录发送
     */
    public function recordSend(string $phone): void
    {
        $key = "sms_rate_limit:{$phone}";
        $this->cache->increment("{$key}:minute");
        $this->cache->expire("{$key}:minute", 60);
        $this->cache->increment("{$key}:hour");
        $this->cache->expire("{$key}:hour", 3600);
        $this->cache->increment("{$key}:day");
        $this->cache->expire("{$key}:day", 86400);
    }
}

使用示例

<?php
// 使用示例
class SmsService
{
    private SmsManager $smsManager;
    private SmsMonitor $monitor;
    private SmsRateLimiter $rateLimiter;
    public function __construct()
    {
        $config = require 'config/sms.php';
        $this->smsManager = new SmsManager($config);
        $this->monitor = new SmsMonitor();
        $this->rateLimiter = new SmsRateLimiter(new Redis(), $config['rate_limits']);
    }
    /**
     * 发送验证码
     */
    public function sendVerificationCode(string $phone): array
    {
        // 检查限流
        if (!$this->rateLimiter->canSend($phone)) {
            throw new RuntimeException('SMS rate limit exceeded');
        }
        // 生成验证码
        $code = random_int(100000, 999999);
        $content = "您的验证码是:{$code},5分钟内有效。";
        // 智能发送
        $result = $this->smsManager->smartSend($phone, $content, 'weighted');
        // 记录日志
        $result->content = $content;
        $result->phone = $phone;
        $this->monitor->logSend($result);
        // 记录限流
        $this->rateLimiter->recordSend($phone);
        // 缓存验证码
        Cache::put("sms_code:{$phone}", $code, 300);
        return [
            'success' => $result->success,
            'message_id' => $result->messageId,
            'provider' => $result->provider,
        ];
    }
    /**
     * 批量发送通知
     */
    public function batchSendNotification(array $phones, string $template): array
    {
        $messages = [];
        foreach ($phones as $phone) {
            $messages[] = [
                'phone' => $phone,
                'content' => $this->buildTemplate($template, ['name' => 'User']),
            ];
        }
        // 获取最佳供应商
        $provider = $this->smsManager->getOptimalProvider('notification');
        // 发送
        $results = $provider->sendBatch($messages);
        // 记录所有结果
        foreach ($results as $result) {
            $this->monitor->logSend($result);
        }
        return [
            'total' => count($results),
            'success' => count(array_filter($results, fn($r) => $r->success)),
            'failed' => count(array_filter($results, fn($r) => !$r->success)),
            'details' => $results,
        ];
    }
}

数据库表设计

-- 短信日志表
CREATE TABLE `sms_logs` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `phone` varchar(20) NOT NULL COMMENT '手机号',
    `content` text NOT NULL COMMENT '短信内容',
    `provider` varchar(50) NOT NULL COMMENT '供应商',
    `success` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否成功',
    `message_id` varchar(100) DEFAULT NULL COMMENT '消息ID',
    `error_message` text COMMENT '错误信息',
    `cost` decimal(10,4) DEFAULT '0.0000' COMMENT '费用',
    `ip` varchar(45) DEFAULT NULL COMMENT '请求IP',
    `created_at` datetime DEFAULT NULL,
    `updated_at` datetime DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `idx_phone` (`phone`),
    KEY `idx_provider` (`provider`),
    KEY `idx_created_at` (`created_at`),
    KEY `idx_success` (`success`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短信发送日志';
-- 供应商配置表
CREATE TABLE `sms_provider_configs` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(50) NOT NULL COMMENT '供应商名称',
    `config` json NOT NULL COMMENT '配置信息',
    `enabled` tinyint(1) NOT NULL DEFAULT '1',
    `priority` int(11) NOT NULL DEFAULT '0' COMMENT '优先级',
    `weight` int(11) NOT NULL DEFAULT '1' COMMENT '权重',
    `created_at` datetime DEFAULT NULL,
    `updated_at` datetime DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短信供应商配置';
-- 供应商健康检查表
CREATE TABLE `sms_health_checks` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `provider` varchar(50) NOT NULL,
    `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1:健康 0:不健康',
    `response_time` int(11) DEFAULT NULL COMMENT '响应时间(ms)',
    `error_message` text COMMENT '错误信息',
    `checked_at` datetime DEFAULT NULL COMMENT '检查时间',
    PRIMARY KEY (`id`),
    KEY `idx_provider_checked` (`provider`, `checked_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商健康检查记录';

这个方案提供了完整的短信平台多供应商支持,包括:

  1. 统一的接口定义 - 所有供应商实现统一接口
  2. 多种选择策略 - 权重轮询、优先级、随机等
  3. 故障转移 - 自动切换到备用供应商
  4. 监控统计 - 记录发送日志和统计
  5. 限流控制 - 防止滥用
  6. 健康检查 - 自动检测供应商可用性
  7. 配置化管理 - 动态调整供应商配置

您可以根据实际需求调整供应商的具体实现代码。

抱歉,评论功能暂时关闭!