Files
ArticleWeather/Plugin.php
2026-02-23 17:09:10 +08:00

628 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文章页发布日温度、天气、地点
*
* @package ArticleWeather
* @author 石头厝
* @version 22.1
* @link https://www.shitoucuo.com/
*/
// 防止直接访问
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
class ArticleWeather_Plugin implements Typecho_Plugin_Interface
{
private static $tableName = 'article_weather';
private static $pendingWeatherData = array();
public static function activate()
{
self::createTable();
// 文章编辑相关钩子
Typecho_Plugin::factory('Widget_Contents_Post_Edit')->write = array(__CLASS__, 'onWrite');
Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishPublish = array(__CLASS__, 'finishSaveWeatherData');
Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishSave = array(__CLASS__, 'finishSaveWeatherData');
// 删除文章时同步删除天气数据
Typecho_Plugin::factory('Widget_Contents_Post_Edit')->delete = array(__CLASS__, 'onDelete');
return '天气插件已激活!';
}
public static function deactivate()
{
return '天气插件已禁用';
}
public static function config(Typecho_Widget_Helper_Form $form)
{
echo '<h3>高德地图API配置</h3>';
$apiKey = new Typecho_Widget_Helper_Form_Element_Text('apiKey',
NULL, '', '高德地图API_KEY',
'');
$form->addInput($apiKey->addRule('required', '必须填写API Key'));
// 添加测试按钮
echo '<div style="margin: 15px 0;">
<button type="button" id="test-api-btn" class="btn">测试API连接</button>
<span id="test-result" style="margin-left: 10px;"></span>
</div>';
echo '<script>
document.getElementById("test-api-btn").addEventListener("click", function() {
var apiKey = document.querySelector("input[name=\'apiKey\']").value;
var testResult = document.getElementById("test-result");
if (!apiKey) {
testResult.innerHTML = "<span style=\'color: red;\'>请先填写API Key</span>";
return;
}
testResult.innerHTML = "<span style=\'color: blue;\'>测试中...</span>";
// 高德天气API测试
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://restapi.amap.com/v3/weather/weatherInfo?key=" + encodeURIComponent(apiKey) + "&city=110000&extensions=base", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.status === "1" && data.infocode === "10000") {
testResult.innerHTML = "<span style=\'color: green;\'>✓ API连接成功</span>";
} else {
var msg = data.info || "未知错误";
var code = data.infocode || "未知";
testResult.innerHTML = "<span style=\'color: red;\'>API错误: " + msg + " (" + code + ")</span>";
}
} catch(e) {
testResult.innerHTML = "<span style=\'color: red;\'>响应解析失败</span>";
}
} else {
testResult.innerHTML = "<span style=\'color: red;\'>网络请求失败 (HTTP " + xhr.status + ")</span>";
}
}
};
xhr.send();
});
</script>';
echo '<div style="padding: 12px; border-radius: 6px; margin-top: 15px; border-left: 4px solid #1890ff;">
<strong>📝 如何获取高德API Key</strong>
<ol style="margin: 8px 0; padding-left: 20px; font-size: 13px;">
<li>访问 <a href="https://lbs.amap.com/" target="_blank" style="color: #0066cc;">高德开放平台</a></li>
<li>注册并登录账号</li>
<li>进入"控制台" → "应用管理" → "我的应用"</li>
<li>创建新应用,选择"Web服务"类型</li>
<li>添加Key服务选择"Web服务"不要选Web端</li>
<li>免费版每日调用限制天气API 5000次/Key</li>
<li><strong>注意:</strong>需要实名认证后才能使用</li>
</ol>
</div>';
}
public static function personalConfig(Typecho_Widget_Helper_Form $form) {}
private static function createTable()
{
try {
$db = Typecho_Db::get();
$prefix = $db->getPrefix();
// 检查表是否存在
$result = $db->fetchRow($db->query("SHOW TABLES LIKE '{$prefix}article_weather'", Typecho_Db::READ));
if (!$result) {
// 表不存在,创建新表
$sql = "CREATE TABLE IF NOT EXISTS `{$prefix}article_weather` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cid` int(10) unsigned NOT NULL,
`location` varchar(100) DEFAULT '',
`display_location` varchar(100) DEFAULT '',
`weather` varchar(50) DEFAULT '',
`temperature` varchar(50) DEFAULT '',
`created` int(10) unsigned DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `cid` (`cid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8";
$db->query($sql);
} else {
// 表已存在检查是否已有display_location字段
$result = $db->fetchRow($db->query("SHOW COLUMNS FROM `{$prefix}article_weather` LIKE 'display_location'", Typecho_Db::READ));
if (!$result) {
// 添加display_location字段
$db->query("ALTER TABLE `{$prefix}article_weather` ADD COLUMN `display_location` varchar(100) DEFAULT '' AFTER `location`");
}
}
} catch (Exception $e) {
error_log("ArticleWeather创建表失败: " . $e->getMessage());
}
}
/**
* 提供主题调用的添加字段方法
*/
public static function addFieldToLayout($layout)
{
$cid = isset($_GET['cid']) ? intval($_GET['cid']) : 0;
$currentLocation = '';
$currentDisplayLocation = '';
if ($cid > 0) {
$weatherData = self::getWeatherData($cid);
if ($weatherData) {
$currentLocation = isset($weatherData['location']) ? $weatherData['location'] : '';
$currentDisplayLocation = isset($weatherData['display_location']) ? $weatherData['display_location'] : '';
}
}
// 天气地点字段
$locationField = new Typecho_Widget_Helper_Form_Element_Text('weather_location',
NULL, $currentLocation, '天气地点', '填写城市名称,获取发布当日天气、温度,留空则不使用本功能');
$locationField->input->setAttribute('style', 'width: 50%; max-width: 200px;');
$locationField->input->setAttribute('placeholder', '例如:北京市');
$locationField->input->setAttribute('name', 'fields[weather_location]');
$layout->addItem($locationField);
// 新增:发布地点字段
$displayLocationField = new Typecho_Widget_Helper_Form_Element_Text('display_location',
NULL, $currentDisplayLocation, '发布地点', '填写要显示在文章页的发布地点,留空则使用天气地点');
$displayLocationField->input->setAttribute('style', 'width: 50%; max-width: 200px;');
$displayLocationField->input->setAttribute('placeholder', '例如:上海外等');
$displayLocationField->input->setAttribute('name', 'fields[display_location]');
$layout->addItem($displayLocationField);
}
/**
* write 钩子 - 获取天气数据
*/
public static function onWrite($contents, $widget)
{
// 处理天气地点
if (isset($_POST['fields']) && isset($_POST['fields']['weather_location'])) {
$location = trim($_POST['fields']['weather_location']);
if (!empty($location)) {
$created = isset($contents['created']) && $contents['created'] ? $contents['created'] : time();
$publishDate = date('Y-m-d', $created);
try {
$weatherData = self::fetchWeatherFromAPI($location, $publishDate);
self::$pendingWeatherData = array(
'location' => $location,
'weatherData' => $weatherData
);
} catch (Exception $e) {
// 保存错误信息,但不中断流程
self::$pendingWeatherData = array(
'location' => $location,
'error' => $e->getMessage()
);
}
} else {
self::$pendingWeatherData = array();
}
}
return $contents;
}
/**
* 文章保存完成后的钩子
*/
public static function finishSaveWeatherData($contents, $widget)
{
if (!isset($widget->cid) || !$widget->cid) {
return $contents;
}
$cid = $widget->cid;
// 获取天气地点
$location = '';
if (isset($_POST['fields']) && isset($_POST['fields']['weather_location'])) {
$location = trim($_POST['fields']['weather_location']);
}
// 获取显示地点
$displayLocation = '';
if (isset($_POST['fields']) && isset($_POST['fields']['display_location'])) {
$displayLocation = trim($_POST['fields']['display_location']);
}
if (!empty(self::$pendingWeatherData) && self::$pendingWeatherData['location'] == $location) {
$pendingData = self::$pendingWeatherData;
if (isset($pendingData['error'])) {
error_log("ArticleWeather API错误: " . $pendingData['error']);
self::$pendingWeatherData = array();
return $contents;
}
$weatherData = $pendingData['weatherData'];
// 如果显示地点为空,使用天气地点
if (empty($displayLocation) && !empty($location)) {
$displayLocation = $location;
}
self::saveToDatabase($cid, $location, $displayLocation, $weatherData);
self::$pendingWeatherData = array();
} else {
if (!empty($location)) {
$created = isset($widget->created) && $widget->created ? $widget->created : time();
$publishDate = date('Y-m-d', $created);
try {
$weatherData = self::fetchWeatherFromAPI($location, $publishDate);
// 如果显示地点为空,使用天气地点
if (empty($displayLocation)) {
$displayLocation = $location;
}
self::saveToDatabase($cid, $location, $displayLocation, $weatherData);
} catch (Exception $e) {
error_log("ArticleWeather API错误: " . $e->getMessage());
}
} else {
self::removeFromDatabase($cid);
}
}
return $contents;
}
/**
* 删除文章时的钩子
*/
public static function onDelete($cid, $widget)
{
self::removeFromDatabase($cid);
return $cid;
}
/**
* 从API获取天气数据 - 使用高德地图天气API
*/
private static function fetchWeatherFromAPI($location, $date = null)
{
$options = Typecho_Widget::widget('Widget_Options')->plugin('ArticleWeather');
$apiKey = $options ? $options->apiKey : '';
if (empty($apiKey)) {
throw new Exception('请先设置高德地图API Key');
}
if (empty($date)) {
$date = date('Y-m-d');
}
// 使用高德地图天气API
try {
// 第一步获取城市编码adcode
$geocodeUrl = "https://restapi.amap.com/v3/geocode/geo?key=" . urlencode($apiKey) .
"&address=" . urlencode($location);
$geocodeResponse = self::httpRequest($geocodeUrl);
if (!$geocodeResponse) {
throw new Exception('地理位置编码请求失败,请检查网络连接');
}
$geocodeData = json_decode($geocodeResponse, true);
if (!$geocodeData || !isset($geocodeData['status'])) {
throw new Exception('地理位置API返回数据格式错误');
}
if ($geocodeData['status'] !== '1') {
$msg = isset($geocodeData['info']) ? $geocodeData['info'] : '未知错误';
$code = isset($geocodeData['infocode']) ? $geocodeData['infocode'] : '未知';
if ($code === '10001') {
throw new Exception('API Key无效或不存在请检查Key是否正确');
} elseif ($code === '10003') {
throw new Exception('API Key每日调用超限5000次/天)');
} elseif ($code === '10004') {
throw new Exception('API Key访问权限不足请确认已开通Web服务');
} elseif ($code === '10005') {
throw new Exception('请求频率超限');
} else {
throw new Exception("地理位置查询失败 [{$code}]: {$msg}");
}
}
if (empty($geocodeData['geocodes'])) {
throw new Exception('未找到该城市信息,请确认城市名称是否正确:' . $location);
}
// 获取城市编码adcode
$adcode = $geocodeData['geocodes'][0]['adcode'];
// 第二步:获取天气信息
// 高德天气API提供实时天气base和预报天气all
// 这里我们使用实时天气,因为免费版可能不支持历史天气
$weatherUrl = "https://restapi.amap.com/v3/weather/weatherInfo?key=" . urlencode($apiKey) .
"&city=" . urlencode($adcode) . "&extensions=base";
$weatherResponse = self::httpRequest($weatherUrl);
if (!$weatherResponse) {
throw new Exception('天气API请求失败');
}
$weatherData = json_decode($weatherResponse, true);
if (!$weatherData || !isset($weatherData['status'])) {
throw new Exception('天气API返回数据格式错误');
}
if ($weatherData['status'] !== '1') {
$msg = isset($weatherData['info']) ? $weatherData['info'] : '未知错误';
$code = isset($weatherData['infocode']) ? $weatherData['infocode'] : '未知';
throw new Exception("天气查询失败 [{$code}]: {$msg}");
}
if (empty($weatherData['lives']) || empty($weatherData['lives'][0])) {
throw new Exception('天气API返回数据为空');
}
$live = $weatherData['lives'][0];
// 检查是否是今天
$today = date('Y-m-d');
$reportTime = isset($live['reporttime']) ? substr($live['reporttime'], 0, 10) : $today;
// 如果是过去日期,尝试获取预报天气
if ($date != $today && strtotime($date) > strtotime($today)) {
// 尝试获取预报天气
$forecastUrl = "https://restapi.amap.com/v3/weather/weatherInfo?key=" . urlencode($apiKey) .
"&city=" . urlencode($adcode) . "&extensions=all";
$forecastResponse = self::httpRequest($forecastUrl);
if ($forecastResponse) {
$forecastData = json_decode($forecastResponse, true);
if ($forecastData && $forecastData['status'] === '1' && !empty($forecastData['forecasts'][0]['casts'])) {
$casts = $forecastData['forecasts'][0]['casts'];
// 查找指定日期的预报
foreach ($casts as $cast) {
if ($cast['date'] == $date) {
return array(
'weather' => $cast['dayweather'],
'temperature' => $cast['nighttemp'] . '~' . $cast['daytemp'] . '°C',
'date' => $date,
'note' => '预报天气'
);
}
}
// 使用第一天预报
$cast = $casts[0];
return array(
'weather' => $cast['dayweather'],
'temperature' => $cast['nighttemp'] . '~' . $cast['daytemp'] . '°C',
'date' => $cast['date'],
'note' => '预报天气'
);
}
}
}
// 使用实时天气数据
return array(
'weather' => $live['weather'],
'temperature' => $live['temperature'] . '°C',
'date' => $reportTime,
'note' => '实时天气'
);
} catch (Exception $e) {
throw $e;
}
}
/**
* 通用的HTTP请求方法
*/
private static function httpRequest($url)
{
// 优先使用cURL
if (function_exists('curl_init')) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'ArticleWeather Plugin/22.1');
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($httpCode == 200 && $response) {
return $response;
} else {
error_log("ArticleWeather cURL请求失败: URL: {$url}, HTTP Code: {$httpCode}, Error: {$error}");
return false;
}
}
// 备选方案使用file_get_contents
$context = stream_context_create([
'http' => [
'timeout' => 10,
'ignore_errors' => true,
'header' => "User-Agent: ArticleWeather Plugin/22.1\r\n"
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false
]
]);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
$error = error_get_last();
error_log("ArticleWeather HTTP请求失败: " . ($error['message'] ?? '未知错误'));
return false;
}
return $response;
}
/**
* 保存到数据库
*/
private static function saveToDatabase($cid, $location, $displayLocation, $weatherData)
{
try {
$db = Typecho_Db::get();
$data = array(
'cid' => $cid,
'location' => $location,
'display_location' => $displayLocation,
'weather' => isset($weatherData['weather']) ? $weatherData['weather'] : '',
'temperature' => isset($weatherData['temperature']) ? $weatherData['temperature'] : '',
'created' => time()
);
// 检查是否已存在
$exists = $db->fetchRow($db->select()->from('table.' . self::$tableName)->where('cid = ?', $cid));
if ($exists) {
$db->query($db->update('table.' . self::$tableName)->rows($data)->where('cid = ?', $cid));
} else {
$db->query($db->insert('table.' . self::$tableName)->rows($data));
}
return true;
} catch (Exception $e) {
error_log("ArticleWeather保存失败: " . $e->getMessage() . " CID: " . $cid);
return false;
}
}
/**
* 从数据库删除记录
*/
private static function removeFromDatabase($cid)
{
try {
$db = Typecho_Db::get();
$db->query($db->delete('table.' . self::$tableName)->where('cid = ?', $cid));
return true;
} catch (Exception $e) {
error_log("ArticleWeather删除失败: " . $e->getMessage() . " CID: " . $cid);
return false;
}
}
/**
* 获取天气数据
*/
private static function getWeatherData($cid, $field = '')
{
if (!$cid) return null;
try {
$db = Typecho_Db::get();
$row = $db->fetchRow($db->select()->from('table.' . self::$tableName)->where('cid = ?', $cid));
if ($row && !empty($row['location'])) {
if ($field && isset($row[$field])) {
return $row[$field];
}
return $row;
}
} catch (Exception $e) {
// 忽略错误
}
return null;
}
/**
* 生成天气HTML - 紧凑单行显示
*/
private static function generateWeatherHtml($location, $weather, $temperature)
{
$weatherIcon = self::getWeatherIcon($weather);
return <<<HTML
<div class="article-weather-compact" style="
background: #f15a22;
color: white;
padding: 12px 12px;
border-radius: 20px;
margin: 15px auto;
max-width:180px;
font-size: 14px;
line-height: 1;">
<!--<span style="font-size:12px;">📍</span>-->
<span>{$location}</span>
<span style="opacity: 0.8;">|</span>
<!--<span style="font-size:12px;">{$weatherIcon}</span>-->
<span>{$weather}</span>
<span style="opacity: 0.8;">|</span>
<!--<span style="font-size:12px;">🌡️</span>-->
<span>{$temperature}</span>
</div>
HTML;
}
/**
* 获取天气图标
*/
private static function getWeatherIcon($weather)
{
if (strpos($weather, '晴') !== false) return '☀️';
if (strpos($weather, '多云') !== false) return '⛅';
if (strpos($weather, '阴') !== false) return '☁️';
if (strpos($weather, '雨') !== false) return '🌧️';
if (strpos($weather, '雪') !== false) return '❄️';
if (strpos($weather, '雷') !== false) return '⛈️';
if (strpos($weather, '雾') !== false) return '🌫️';
if (strpos($weather, '沙') !== false) return '🌪️';
return '🌤️';
}
/**
* 手动调用方法 - 主要使用这个
*/
public static function show($cid = null)
{
if (!$cid) {
$widget = Typecho_Widget::widget('Widget_Archive');
if (!$widget->is('single')) return '';
$cid = $widget->cid;
}
$weatherData = self::getWeatherData($cid);
if ($weatherData && !empty($weatherData['location']) && !empty($weatherData['weather'])) {
// 优先使用显示地点,如果为空则使用天气地点
$displayLocation = !empty($weatherData['display_location'])
? $weatherData['display_location']
: $weatherData['location'];
return self::generateWeatherHtml(
$displayLocation,
$weatherData['weather'],
$weatherData['temperature']
);
}
return '';
}
}