本文目录导读:

我来为您设计一个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='供应商健康检查记录';
这个方案提供了完整的短信平台多供应商支持,包括:
- 统一的接口定义 - 所有供应商实现统一接口
- 多种选择策略 - 权重轮询、优先级、随机等
- 故障转移 - 自动切换到备用供应商
- 监控统计 - 记录发送日志和统计
- 限流控制 - 防止滥用
- 健康检查 - 自动检测供应商可用性
- 配置化管理 - 动态调整供应商配置
您可以根据实际需求调整供应商的具体实现代码。