首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 發送簡訊 (SMS) 完整方案:API 整合、佇列異步、簽名驗證與失敗重試

     編輯:PHP 綜合知識

主流簡訊服務商對比

服務商類型特色適用場景
Twilio國際 API全球覆蓋、支援 WhatsApp、豐富 SDK出海業務
阿里云/騰訊云/華為云國內雲廠商價格低、模板審核嚴、到達率高國內電商、驗證碼
Submail / 容聯云 / 梦网專業簡訊平台通道穩定、支援營銷/通知分類大規模營銷
自建網關 (SMPP)運營商直連成本最低、完全可控超大規模、特殊合規

通用 HTTP API 封裝類 (PSR-18 相容)

<?php
namespace App\Sms;

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;

class SmsSender {
    public function __construct(
        private ClientInterface $httpClient,
        private RequestFactoryInterface $requestFactory,
        private string $apiUrl,
        private string $accessKeyId,
        private string $accessKeySecret,
        private string $signName,
        private string $templateCode
    ) {}

    /** 發送單條驗證碼/通知
     * @param string $phone 手機號 (E.164 格式 +86138xxxxxxxx)
     * @param array  $params 模板變數 ['code'=>'123456', 'product'=>'iPhone']
     * @return array ['success'=>bool, 'request_id'=>string, 'message'=>string]
     */
    public function send(string $phone, array $params): array {
        $timestamp = date('Y-m-d\TH:i:s\Z');
        $paramsJson = json_encode($params, JSON_UNESCAPED_UNICODE);

        $query = http_build_query([
            'AccessKeyId'     => $this->accessKeyId,
            'Timestamp'       => $timestamp,
            'SignatureMethod' => 'HMAC-SHA1',
            'SignatureVersion'=> '1.0',
            'SignatureNonce'  => uniqid('', true),
            'Action'          => 'SendSms',
            'Version'         => '2017-05-25',
            'RegionId'        => 'cn-hangzhou',
            'PhoneNumbers'    => $phone,
            'SignName'        => $this->signName,
            'TemplateCode'    => $this->templateCode,
            'TemplateParam'   => $paramsJson,
        ]);

        // 簽名 (阿里云風格)
        $stringToSign = "GET&%2F&" . rawurlencode($query);
        $signature = base64_encode(hash_hmac('sha1', $stringToSign, $this->accessKeySecret . '&', true));
        $url = $this->apiUrl . '?' . $query . '&Signature=' . rawurlencode($signature);

        $request = $this->requestFactory->createRequest('GET', $url);
        $response = $this->httpClient->sendRequest($request);
        $body = json_decode((string)$response->getBody(), true);

        return [
            'success'    => ($body['Code'] ?? '') === 'OK',
            'request_id' => $body['RequestId'] ?? '',
            'message'    => $body['Message'] ?? 'Unknown error',
            'biz_id'     => $body['BizId'] ?? null,
        ];
    }

    /** 批量發送 (同一模板、不同手機/參數) */
    public function batchSend(array $items): array {
        // 多數服務商支援單次最多 1000 號碼,這裡分批
        $results = [];
        foreach (array_chunk($items, 500) as $chunk) {
            // 實作略:組裝 PhoneNumbers=1,2,3... TemplateParam=[{...},{...}]
            $results[] = $this->sendChunk($chunk);
        }
        return $results;
    }
}
?>

異步佇列架構 (Laravel / Symfony Messenger / 原生)

<?php
// Job 類別
class SendSmsJob implements ShouldQueue {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public string $phone,
        public array  $params,
        public int    $tries = 3,
        public int    $backoff = 60 // 秒
    ) {}

    public function handle(SmsSender $sender): void {
        $result = $sender->send($this->phone, $this->params);
        if (!$result['success']) {
            // 記錄失敗原因、觸發重試
            throw new \RuntimeException("SMS failed: {$result['message']}");
        }
        // 成功可寫入發送日誌表
        SmsLog::create([
            'phone'      => $this->phone,
            'template'   => $this->params,
            'request_id' => $result['request_id'],
            'biz_id'     => $result['biz_id'],
            'status'     => 'sent',
        ]);
    }
}

// 控制器調用
public function sendVerifyCode(Request $request) {
    $code = random_int(100000, 999999);
    Cache::put("sms_code:{$request->phone}", $code, 300); // 5 分鐘有效
    SendSmsJob::dispatch($request->phone, ['code' => $code])->onQueue('sms');
    return response()->json(['message' => '已發送']);
}
?>

安全與合規要點

  1. 簽名驗證:回調通知 (SmsReport) 必須驗證簽名,防偽造
  2. 頻率限制:同一手機號/同一 IP 每分鐘 ≤ 1 條、每天 ≤ 10 條
  3. 模板審核:變數內容不可包含敏感詞、連結需備案
  4. 退訂機制:營銷類必須支援回 T 退訂,記錄退訂名單永不發送
  5. 資料加密:手機號入庫掩碼 (138****5678)、傳輸全程 HTTPS

失敗重試與熔斷策略

<?php
class SmsCircuitBreaker {
    private int $failures = 0;
    private int $lastFailure = 0;
    private const THRESHOLD = 5;
    private const TIMEOUT = 60; // 秒

    public function call(callable $action): mixed {
        if ($this->isOpen()) throw new \RuntimeException('SMS circuit open');
        try {
            $result = $action();
            $this->onSuccess();
            return $result;
        } catch (\Throwable $e) {
            $this->onFailure();
            throw $e;
        }
    }

    private function isOpen(): bool {
        if ($this->failures < self::THRESHOLD) return false;
        if (time() - $this->lastFailure > self::TIMEOUT) {
            $this->failures = 0; // 半開狀態,允許嘗試
            return false;
        }
        return true;
    }
    private function onSuccess(): void { $this->failures = 0; }
    private function onFailure(): void { $this->failures++; $this->lastFailure = time(); }
}
?>

測試與除錯技巧

  • 使用 php -S localhost:8000 本地接收回調 (配合 ngrok 穿透)
  • 模擬服務商回調:POST /sms/callback 測試簽名驗證、入庫邏輯
  • 開發環境用 Mock 發送器:只記錄日誌、不真正發送、回傳固定 request_id
  • 監控指標:發送量、到達率、延遲 P99、失敗率、餘額預警

結語

簡訊發送看似簡單,實則涉及簽名安全、頻控合規、異步解耦、重試熔斷、可觀測性等工程細節。建議:封裝統一 Sender 介面、接入佇列異步、接入熔斷器、完善日誌與告警,才能支撐業務高增長期的穩定送達。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved