微信公眾號**群發介面**有嚴格限制:服務號月 4 次、認證訂閱號月 1 次、需審核。但**客服介面**允許 48 小時內無限制主動推送訊息給任意關注用戶。本文實作「無限群發」:批次獲取 OpenID → 逐個調用客服介面發送文本/圖文/圖片等訊息。
appId = $appId;
$this->appSecret = $appSecret;
$this->tokenCacheFile = $cacheDir . '/access_token.json';
if (!is_dir($cacheDir)) mkdir($cacheDir, 0755, true);
$this->accessToken = $this->getAccessToken();
}
// ===== HTTP 工具 =====
public function getData(string $url): string {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; PHP Wechat Bot)',
CURLOPT_ENCODING => 'gzip',
CURLOPT_TIMEOUT => 15,
]);
$data = curl_exec($ch);
if (curl_errno($ch)) throw new RuntimeException("cURL GET error: " . curl_error($ch));
curl_close($ch);
return $data;
}
public function postData(string $url, string $data): string {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; PHP Wechat Bot)',
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_AUTOREFERER => true,
CURLOPT_TIMEOUT => 15,
]);
$result = curl_exec($ch);
if (curl_errno($ch)) throw new RuntimeException("cURL POST error: " . curl_error($ch));
curl_close($ch);
return $result;
}
// ===== Access Token(快取 7000 秒)=====
public function getAccessToken(): string {
if (is_file($this->tokenCacheFile)) {
$cache = json_decode(file_get_contents($this->tokenCacheFile), true);
if ($cache && time() < $cache['expire']) return $cache['access_token'];
}
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->appId}&secret={$this->appSecret}";
$res = $this->getData($url);
$data = json_decode($res, true);
if (empty($data['access_token'])) {
throw new RuntimeException("Get access_token failed: {$res}");
}
$data['expire'] = time() + 7000;
file_put_contents($this->tokenCacheFile, json_encode($data));
return $data['access_token'];
}
// ===== 獲取所有關注者 OpenID(支援分頁)=====
private function getAllOpenIds(): array {
$openids = [];
$nextOpenid = '';
do {
$url = "https://api.weixin.qq.com/cgi-bin/user/get?access_token={$this->accessToken}" .
($nextOpenid ? "&next_openid={$nextOpenid}" : '');
$res = $this->getData($url);
$data = json_decode($res, true);
if (isset($data['errcode'])) {
throw new RuntimeException("Get user list failed: {$res}");
}
$openids = array_merge($openids, $data['data']['openid'] ?? []);
$nextOpenid = $data['next_openid'] ?? '';
} while ($nextOpenid);
return $openids;
}
// ===== 發送客服訊息 =====
private function sendCustomMessage(string $openid, array $msgData): bool {
$url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={$this->accessToken}";
$payload = array_merge(['touser' => $openid], $msgData);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
$res = $this->postData($url, $json);
$data = json_decode($res, true);
return empty($data['errcode']);
}
// ===== 公開介面:無限群發 =====
public function sendMsgToAll(): void {
$openids = $this->getAllOpenIds();
echo "總共 {$openids} 位關注用戶\n";
$success = $failed = 0;
foreach ($openids as $openid) {
// 示範:發送文本訊息
$msg = [
'msgtype' => 'text',
'text' => ['content' => '測試群發訊息,抱歉打擾']
];
// 可擴展:圖文、圖片、語音、視頻、卡券等
// $msg = ['msgtype' => 'news', 'news' => ['articles' => [...]]];
if ($this->sendCustomMessage($openid, $msg)) {
$success++;
} else {
$failed++;
}
// 避免頻率限制:每秒最多 ~100 次(官方未公開確切值,建議 usleep)
usleep(20000); // 20ms = 50/sec
}
echo "完成:成功 {$success},失敗 {$failed}\n";
}
}
// 使用範例
$sender = new SendAllMsg('YOUR_APPID', 'YOUR_APPSECRET');
$sender->sendMsgToAll();
| 類型 | msgtype | payload 結構 |
|---|---|---|
| 文本 | text | ['text' => ['content' => '內容']] |
| 圖片 | image | ['image' => ['media_id' => 'MEDIA_ID']] |
| 語音 | voice | ['voice' => ['media_id' => 'MEDIA_ID']] |
| 視頻 | video | ['video' => ['media_id' => 'MEDIA_ID', 'title' => '', 'description' => '']] |
| 圖文 | news | ['news' => ['articles' => [['title'=>'', 'description'=>'', 'url'=>'', 'picurl'=>''], ...]]] |
| 卡券 | wxcard | ['wxcard' => ['card_id' => 'CARD_ID']] |
| 限制 | 官方規定 | 應對策略 |
|---|---|---|
| 48 小時規則 | 用戶 48 小時內有互動才能發送 | 引導用戶點擊選單/回覆關鍵字/掃碼,重置 48 小時窗口 |
| 頻率限制 | 未公開確切值,約 100-500 次/秒 | usleep(20000) 控制 50/sec,分批次執行 |
| 用戶量 > 1 萬 | user/get 需分頁 | 已實作 next_openid 分頁迴圈 |
| Access Token 過期 | 2 小時(7200 秒) | 檔案快取 + 7000 秒提前刷新 |
| 僵尸粉/封號 | 發送失敗 errcode 40003/43004 | 記錄失敗 OpenID,定期清理、標記異常 |
// 1. 任務佇列化(Redis + Worker) // 生產者:API 接收群發請求 -> 推入 Redis List // 消費者:多進程 Worker 消費 -> 呼叫 sendCustomMessage -> 記錄結果 // 2. 失敗重試機制 // 失敗 OpenID 寫入 retry queue,指數退避重試 3 次 // 3. 統計儀表板 // 記錄每次群發:總數、成功、失敗、耗時、失敗原因分布 // 4. 內容審核 // 敏感詞過濾、圖片安全檢測、合規性審核
「無限群發」本質是**客服介面批次推送**,核心流程:
message/custom/send配合任務佇列、多進程 Worker、監控儀表板,即可構建企業級微信群發系統,突破官方群發介面的次數與審核限制。