核心業務邏輯
- 手機號格式驗證(正則
/^1\d{10}$/)
- 驗證碼生成(6 位數字)
- 發送頻率限制:60 秒/次
- 單日單手機號發送上限:5 次
- 單日單手機號設備(IMEI)上限:3 台
- 驗證碼有效期:30 分鐘
- Redis 存儲狀態:驗證碼、發送記錄、限流計數器
完整實現代碼
<?php
class Api_Sms {
const EXPIRE_SEC = 1800; // 30分鐘
const RESEND_SEC = 60; // 1分鐘
const ONE_DAY_FREQ = 5; // 日發送上限
const ONE_DAY_IMEI = 3; // 日設備上限
public $error = [];
public function sendVerifyCode(string $mobile, string $imei): bool {
// 1. 手機號驗證
if (!$this->isMobile($mobile)) {
$this->error = ['code'=>-1, 'msg'=>'手機號格式錯誤'];
return false;
}
$redis = Api_Common::redis();
$vcKey = "VC_{$mobile}";
$limitKey = "VC_LIMIT_{$mobile}";
// 2. 重發頻率限制
$data = json_decode($redis->get($vcKey), true);
if ($data && time() < $data['resend_expire']) {
$this->error = ['code'=>-1, 'msg'=> '短信已發送,請 60 秒後重試'];
return false;
}
// 3. 單設備日發送上限
$sendCnt = $redis->zScore($limitKey, $imei);
if ($sendCnt && $sendCnt >= self::ONE_DAY_FREQ) {
$this->error = ['code'=>-1, 'msg'=> '該設備今日發送次數超限'];
return false;
}
// 4. 單手機號日設備數上限
$imeiCnt = $redis->zCard($limitKey);
if ($imeiCnt >= self::ONE_DAY_IMEI && !$sendCnt) {
$this->error = ['code'=>-1, 'msg'=> '驗證碼發送設備數超限'];
return false;
}
// 5. 生成/復用驗證碼
if (!$data) {
$vc = (string) random_int(100000, 999999); // PHP 7+ 加密安全隨機
$data = ['vc' => $vc, 'resend_expire' => 0];
$redis->set($vcKey, json_encode($data));
$redis->expire($vcKey, self::EXPIRE_SEC);
}
$vc = $data['vc'];
// 6. 調用服務商發送
$result = $this->send($mobile, "驗證碼:{$vc}");
if ($result) {
// 更新重發時間窗
$data['resend_expire'] = time() + self::RESEND_SEC;
$ttl = $redis->ttl($vcKey);
$redis->set($vcKey, json_encode($data));
$redis->expire($vcKey, $ttl);
// 記錄發送計數(有序集合:member=IMEI, score=次數)
$redis->zIncrBy($limitKey, 1, $imei);
// 設置當日 23:59:59 過期
$redis->expireAt($limitKey, strtotime('tomorrow'));
}
return $result;
}
// 服務商適配介面
protected function send(string $mobile, string $content): bool {
// TODO: 對接阿里云/騰訊雲/網關 HTTP API
// 返回 true/false
return true;
}
private function isMobile(string $mobile): bool {
return (bool) preg_match('/^1\d{10}$/', $mobile);
}
public function checkVerifyCode(string $mobile, string $vc): bool {
$data = json_decode(Api_Common::redis()->get("VC_{$mobile}"), true);
return $data && $data['vc'] === $vc;
}
public function cleanVerifyCode(string $mobile): void {
$redis = Api_Common::redis();
$redis->del("VC_{$mobile}", "VC_LIMIT_{$mobile}");
}
}
Redis 數據結構設計
| Key | 類型 | 結構 | TTL |
VC_{mobile} | String (JSON) | {"vc":"123456","resend_expire":1699999999} | 1800s |
VC_LIMIT_{mobile} | Sorted Set | member=IMEI, score=發送次數 | 當日 23:59:59 |
關鍵設計決策解析
| 決策點 | 方案 | 理由 |
| 隨機數生成 | random_int() | CSPRNG,避免 rand() 可預測性 |
| 限流計數器 | Redis Sorted Set | 原子增減、自動按分數排序、支援 TTL |
| 重發窗口 | 嵌入驗證碼 JSON | 單 Key 存儲,減少網絡往返 |
| 錯誤碼規範 | 統一 ['code','msg'] | 前端統一處理,國際化友好 |
| 服務商解耦 | protected send() | 繼承覆蓋適配不同通道 |
生產環境強化清單
- 服務商 API 簽名驗證、IP 白名單、HTTPS 強制
- 發送失敗重試策略(指數退避 + 死信隊列)
- 驗證碼加密存儲(AES-GCM),防 Redis 洩露
- 圖形/滑塊驗證碼前置,防機器人刷接口
- 監控大盤:發送成功率、到達率、延遲 P99、限流觸發率
- 備用通道自動切換(主通道失效 → 備用通道)
- 黑名單機制:高投訴/退訂號碼自動攔截
替代實現參考:HTTP 直連網關
function sendSMS($uid, $pwd, $mobile, $content) {
$url = 'https://api.sms.cn/send/';
$data = [
'uid' => $uid,
'pwd' => md5($pwd),
'mobile' => $mobile,
'content' => $content,
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_TIMEOUT => 10,
]);
$resp = curl_exec($ch);
curl_close($ch);
return $resp === '100'; // 100=成功,其他見文檔
}
短信驗證碼是帳號體系的關鍵基礎設施,**安全性、可用性、可觀測性**三位一體,容不得半點馬虎。