Files
ArticleWeather/Plugin.php

628 lines
25 KiB
PHP
Raw Normal View History

2026-02-23 17:09:10 +08:00
<?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 '';
}
}