前幾章已完成:插件啟用/停用、配置儲存、掛載發布文章鉤子、取得文章永久連結。本章實作核心邏輯:呼叫百度站長平台 API 推送 URL。
<?php
class BaiduSubmit_Plugin implements Typecho_Plugin_Interface {
// ... activate, deactivate, config 等方法 ...
/** 發送推送請求 */
public static function sendUrls(array $urls, array $options) {
$api = $options->plugin('BaiduSubmit')->api;
if (empty($api)) return false;
$ch = curl_init($api);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => implode("\n", $urls),
CURLOPT_HTTPHEADER => ['Content-Type: text/plain'],
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true,
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
curl_close($ch);
if ($errno) {
self::log("cURL 錯誤 [$errno]: $error");
return false;
}
self::log("API 回應: $response");
return json_decode($response, true);
}
private static function log($message) {
$logFile = __DIR__ . '/../logs/baidu_submit.log';
@file_put_contents($logFile, date('Y-m-d H:i:s') . " $message\n", FILE_APPEND);
}
}
?>
<?php
use Typecho\Http\Client;
class BaiduSubmit_Plugin implements Typecho_Plugin_Interface {
// ...
public static function sendUrls(array $urls, $options) {
$api = $options->plugin('BaiduSubmit')->api;
if (empty($api)) return false;
// 檢查 HTTP 客戶端可用性
if (false === Client::get()) {
throw new Typecho_Plugin_Exception(
'主機不支援 php-curl 且未開啟 allow_url_fopen,無法使用此功能'
);
}
$http = Client::get();
$http->setData(implode("\n", $urls));
$http->setHeader('Content-Type', 'text/plain');
try {
$response = $http->send($api);
self::log("API 回應: $response");
return json_decode($response, true);
} catch (Exception $e) {
self::log("請求異常: " . $e->getMessage());
return false;
}
}
private static function log($message) {
// 建議使用 Typecho 內建日誌或資料庫記錄
$logFile = Typecho_Common::config()->rootDir . '/usr/logs/baidu_submit.log';
@file_put_contents($logFile, date('c') . " $message\n", FILE_APPEND);
}
}
?>
<?php
// 在 Plugin.php 中的 finishPublish 鉤子回調
public static function onPublish($contents, $widget) {
// 1. 檢查文章狀態:僅發布狀態且非預約發文
if ($contents['visibility'] !== 'publish' || $contents['created'] > time()) {
return;
}
// 2. 取得系統配置
$options = Helper::options();
$pluginConfig = $options->plugin('BaiduSubmit');
if (empty($pluginConfig->api)) {
return; // 未配置 API
}
// 3. 產生文章永久連結
$type = $contents['type']; // 'post' 或 'page'
$routeExists = (NULL !== Typecho_Router::get($type));
$pathInfo = $routeExists ? Typecho_Router::url($type, $contents) : '#';
$permalink = Typecho_Common::url($pathInfo, $options->index);
// 4. 非同步發送(避免阻塞發布流程)
self::asyncSend([$permalink], $pluginConfig);
}
private static function asyncSend(array $urls, $config) {
// 方案 A: fastcgi_finish_request() 後執行 (PHP-FPM)
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request(); // 先回應使用者
self::sendUrls($urls, $config); // 背景執行
return;
}
// 方案 B: 背景進程 (需 shell_exec 權限)
$script = __DIR__ . '/sender.php';
$data = escapeshellarg(json_encode(['urls' => $urls, 'api' => $config->api]));
shell_exec("php $script $data > /dev/null 2>&1 &");
// 方案 C: 佇列系統 (Redis/RabbitMQ/Database) - 生產環境推薦
// Queue::push(new BaiduSubmitJob($urls, $config->api));
}
?>
<?php
// sender.php - 獨立執行,不載入 Typecho 核心
$data = json_decode($argv[1], true);
$urls = $data['urls'];
$api = $data['api'];
$ch = curl_init($api);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => implode("\n", $urls),
CURLOPT_HTTPHEADER => ['Content-Type: text/plain'],
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$errno = curl_errno($ch);
curl_close($ch);
// 記錄結果到資料庫或檔案
$log = [
'time' => date('c'),
'urls' => $urls,
'status' => $errno ? 'failed' : 'success',
'response' => $errno ? curl_error($ch) : $response,
];
// 寫入 SQLite/MySQL/檔案...
?>
private static function sendWithRetry(array $urls, $api, $maxRetries = 3) {
$delay = 1; // 秒
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
$result = self::sendUrls($urls, $api);
if ($result && isset($result['success']) && $result['success'] > 0) {
return $result; // 成功
}
self::log("第 $attempt 次嘗試失敗,{$delay}秒後重試...");
sleep($delay);
$delay *= 2; // 指數退避
}
self::log("重試 $maxRetries 次均失敗,記錄待人工處理");
// 寫入失敗佇列供後台重試
return false;
}
{
"remain": 4999, // 當日剩餘配額
"success": 1, // 成功推送數量
"error": 0, // 失敗數量
"message": "success" // 狀態訊息
}
建議記錄 remain 監控配額用量,接近限制時發出警示。