2621 lines
92 KiB
PHP
2621 lines
92 KiB
PHP
|
|
<?php
|
|||
|
|
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* UrlNav 动作处理器
|
|||
|
|
*/
|
|||
|
|
class UrlNav_Action extends Typecho_Widget implements Widget_Interface_Do
|
|||
|
|
{
|
|||
|
|
/**
|
|||
|
|
* 数据库连接
|
|||
|
|
*/
|
|||
|
|
private $db;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 构造函数
|
|||
|
|
*/
|
|||
|
|
public function __construct($request, $response, $params = NULL)
|
|||
|
|
{
|
|||
|
|
parent::__construct($request, $response, $params);
|
|||
|
|
$this->db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
public function action()
|
|||
|
|
{
|
|||
|
|
// 检查管理员权限 - Collection插件就是这样写的
|
|||
|
|
$user = Typecho_Widget::widget('Widget_User');
|
|||
|
|
if (!$user->hasLogin() || !$user->pass('administrator', true)) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '无权限访问'
|
|||
|
|
));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确保返回JSON格式 - Collection插件就是这样写的
|
|||
|
|
$this->response->setContentType('application/json');
|
|||
|
|
|
|||
|
|
// 获取do参数 - Collection插件就是这样写的
|
|||
|
|
$do = $this->request->get('do');
|
|||
|
|
if (!$do) {
|
|||
|
|
$do = $this->request->isPost() ? $this->request->get('do') : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch ($do) {
|
|||
|
|
case 'addCategory':
|
|||
|
|
$this->addCategory();
|
|||
|
|
break;
|
|||
|
|
case 'updateCategory':
|
|||
|
|
$this->updateCategory();
|
|||
|
|
break;
|
|||
|
|
case 'deleteCategory':
|
|||
|
|
$this->deleteCategory();
|
|||
|
|
break;
|
|||
|
|
case 'getCategory':
|
|||
|
|
$this->getCategory();
|
|||
|
|
break;
|
|||
|
|
case 'getAllCategories':
|
|||
|
|
$this->getAllCategories();
|
|||
|
|
break;
|
|||
|
|
case 'addUrl':
|
|||
|
|
$this->addUrl();
|
|||
|
|
break;
|
|||
|
|
case 'updateUrl':
|
|||
|
|
$this->updateUrl();
|
|||
|
|
break;
|
|||
|
|
case 'deleteUrl':
|
|||
|
|
$this->deleteUrl();
|
|||
|
|
break;
|
|||
|
|
case 'getUrl':
|
|||
|
|
$this->getUrl();
|
|||
|
|
break;
|
|||
|
|
case 'getAllUrls':
|
|||
|
|
$this->getAllUrls();
|
|||
|
|
break;
|
|||
|
|
case 'getRssRefreshLogs': // 🔴 新增,获取详细RSS地址
|
|||
|
|
$this->getRssRefreshLogs();
|
|||
|
|
break;
|
|||
|
|
case 'batchDeleteUrls':
|
|||
|
|
$this->batchDeleteUrls();
|
|||
|
|
break;
|
|||
|
|
case 'batchDeleteCategories':
|
|||
|
|
$this->batchDeleteCategories();
|
|||
|
|
break;
|
|||
|
|
case 'test':
|
|||
|
|
$this->test();
|
|||
|
|
break;
|
|||
|
|
case 'fetchRss':
|
|||
|
|
$this->fetchRss();
|
|||
|
|
break;
|
|||
|
|
case 'getRssFeeds':
|
|||
|
|
$this->getRssFeeds();
|
|||
|
|
break;
|
|||
|
|
case 'cleanRssCache':
|
|||
|
|
$this->cleanRssCache();
|
|||
|
|
break;
|
|||
|
|
case 'addFavorite':
|
|||
|
|
$this->addFavorite();
|
|||
|
|
break;
|
|||
|
|
case 'removeFavorite':
|
|||
|
|
$this->removeFavorite();
|
|||
|
|
break;
|
|||
|
|
case 'getFavorites':
|
|||
|
|
$this->getFavorites();
|
|||
|
|
break;
|
|||
|
|
case 'getFavoriteStats':
|
|||
|
|
$this->getFavoriteStats();
|
|||
|
|
break;
|
|||
|
|
case 'checkFavorite':
|
|||
|
|
$this->checkFavorite();
|
|||
|
|
break;
|
|||
|
|
case 'getCacheStats':
|
|||
|
|
$this->getCacheStats();
|
|||
|
|
break;
|
|||
|
|
case 'getCronLogs':
|
|||
|
|
$this->getCronLogs();
|
|||
|
|
break;
|
|||
|
|
case 'getCronStats':
|
|||
|
|
$this->getCronStats();
|
|||
|
|
break;
|
|||
|
|
case 'getRefreshStats':
|
|||
|
|
$this->getRefreshStats();
|
|||
|
|
break;
|
|||
|
|
case 'exportOpml':
|
|||
|
|
$this->exportOpml();
|
|||
|
|
break;
|
|||
|
|
case 'importOpml':
|
|||
|
|
$this->importOpml();
|
|||
|
|
break;
|
|||
|
|
case 'unlockCron':
|
|||
|
|
$this->unlockCron();
|
|||
|
|
break;
|
|||
|
|
case 'checkStatus':
|
|||
|
|
$this->checkStatus();
|
|||
|
|
break;
|
|||
|
|
case 'checkSingleStatus':
|
|||
|
|
$this->checkSingleStatus();
|
|||
|
|
break;
|
|||
|
|
case 'getRssRefreshStatus':
|
|||
|
|
$this->getRssRefreshStatus();
|
|||
|
|
break;
|
|||
|
|
case 'getRssRefreshStats':
|
|||
|
|
$this->getRssRefreshStats();
|
|||
|
|
break;
|
|||
|
|
case 'getStatusStats':
|
|||
|
|
$this->getStatusStats();
|
|||
|
|
break;
|
|||
|
|
case 'getRssCronStats':
|
|||
|
|
$this->getRssCronStats();
|
|||
|
|
break;
|
|||
|
|
case 'getStatusCronStats':
|
|||
|
|
$this->getStatusCronStats();
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
// ================ 新增:失败网址相关方法 ================
|
|||
|
|
case 'getRefreshLogs': // 获取刷新日志
|
|||
|
|
$this->getRefreshLogs();
|
|||
|
|
break;
|
|||
|
|
case 'getRecentRefreshStats': // 获取最近刷新统计
|
|||
|
|
$this->getRecentRefreshStats();
|
|||
|
|
break;
|
|||
|
|
case 'getAllFailedUrls': // 查看所有失败网址
|
|||
|
|
$this->getAllFailedUrls();
|
|||
|
|
break;
|
|||
|
|
case 'retryRssUrl': // 重试单个失败网址
|
|||
|
|
$this->retryRssUrl();
|
|||
|
|
break;
|
|||
|
|
case 'ignoreFailedUrl': // 忽略失败网址
|
|||
|
|
$this->ignoreFailedUrl();
|
|||
|
|
break;
|
|||
|
|
case 'enableFailedUrl': // 启用已停用网址
|
|||
|
|
$this->enableFailedUrl();
|
|||
|
|
break;
|
|||
|
|
case 'retryAllFailedUrls': // 重试所有失败网址
|
|||
|
|
$this->retryAllFailedUrls();
|
|||
|
|
break;
|
|||
|
|
case 'exportFailedUrls': // 导出失败网址列表
|
|||
|
|
$this->exportFailedUrls();
|
|||
|
|
break;
|
|||
|
|
case 'exportSuccessUrls':
|
|||
|
|
$this->exportSuccessUrls();
|
|||
|
|
break;
|
|||
|
|
// ================ 结束新增 ================
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '无效的操作'
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 在 action() 方法之后,但在类结束之前添加以下方法
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加收藏
|
|||
|
|
*/
|
|||
|
|
private function addFavorite()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$feedId = $this->request->get('feed_id');
|
|||
|
|
if (!$feedId || !is_numeric($feedId)) {
|
|||
|
|
$this->response->throwJson(['success' => false, 'message' => '文章ID不能为空或格式错误']);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$result = UrlNav_Plugin::addFavorite($feedId, 0);
|
|||
|
|
$this->response->throwJson($result);
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '添加收藏失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 取消收藏
|
|||
|
|
*/
|
|||
|
|
private function removeFavorite()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$feedId = $this->request->get('feed_id');
|
|||
|
|
if (!$feedId || !is_numeric($feedId)) {
|
|||
|
|
$this->response->throwJson(['success' => false, 'message' => '文章ID不能为空或格式错误']);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$result = UrlNav_Plugin::removeFavorite($feedId, 0);
|
|||
|
|
$this->response->throwJson($result);
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '取消收藏失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取收藏列表
|
|||
|
|
*/
|
|||
|
|
private function getFavorites()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$page = $this->request->get('page', 1);
|
|||
|
|
$pageSize = $this->request->get('pageSize', 20);
|
|||
|
|
$search = $this->request->get('search', '');
|
|||
|
|
|
|||
|
|
$result = UrlNav_Plugin::getFavorites(0, $page, $pageSize, $search);
|
|||
|
|
$this->response->throwJson($result);
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取收藏列表失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取收藏统计
|
|||
|
|
*/
|
|||
|
|
private function getFavoriteStats()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$result = UrlNav_Plugin::getFavoriteStats(0);
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $result
|
|||
|
|
]);
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取收藏统计失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查是否已收藏
|
|||
|
|
*/
|
|||
|
|
private function checkFavorite()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$feedId = $this->request->get('feed_id');
|
|||
|
|
if (!$feedId || !is_numeric($feedId)) {
|
|||
|
|
$this->response->throwJson(['success' => false, 'message' => '文章ID不能为空或格式错误']);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$isFavorite = UrlNav_Plugin::isFavorite($feedId, 0);
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => true,
|
|||
|
|
'is_favorite' => $isFavorite
|
|||
|
|
]);
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '检查收藏状态失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取RSS刷新详细日志
|
|||
|
|
*/
|
|||
|
|
public function getRssRefreshLogs()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$limit = $this->request->get('limit', 10);
|
|||
|
|
$page = $this->request->get('page', 1);
|
|||
|
|
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
|
|||
|
|
// 计算分页
|
|||
|
|
$offset = ($page - 1) * $limit;
|
|||
|
|
|
|||
|
|
// 获取refresh_log数据(RSS刷新详情)
|
|||
|
|
$sql = "SELECT * FROM urlnav_refresh_log
|
|||
|
|
WHERE cron_type = 'rss'
|
|||
|
|
ORDER BY refresh_time DESC
|
|||
|
|
LIMIT ? OFFSET ?";
|
|||
|
|
|
|||
|
|
$stmt = $db->prepare($sql);
|
|||
|
|
$stmt->execute([$limit, $offset]);
|
|||
|
|
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
// 解析details字段中的RSS地址
|
|||
|
|
foreach ($logs as &$log) {
|
|||
|
|
if (!empty($log['details'])) {
|
|||
|
|
$details = @json_decode($log['details'], true);
|
|||
|
|
if ($details) {
|
|||
|
|
$log['details_parsed'] = $details;
|
|||
|
|
|
|||
|
|
// 提取RSS地址
|
|||
|
|
if (isset($details['success_rss_urls'])) {
|
|||
|
|
$log['success_rss_urls'] = $details['success_rss_urls'];
|
|||
|
|
}
|
|||
|
|
if (isset($details['failed_rss_urls'])) {
|
|||
|
|
$log['failed_rss_urls'] = $details['failed_rss_urls'];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用message字段(已经包含RSS地址信息)
|
|||
|
|
$log['formatted_message'] = $log['message'] ??
|
|||
|
|
"刷新完成:成功 " . ($log['success_count'] ?? 0) . " 个,失败 " .
|
|||
|
|
(($log['url_count'] ?? 0) - ($log['success_count'] ?? 0)) . " 个";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取总数
|
|||
|
|
$countStmt = $db->prepare("SELECT COUNT(*) as total FROM urlnav_refresh_log WHERE cron_type = 'rss'");
|
|||
|
|
$countStmt->execute();
|
|||
|
|
$totalResult = $countStmt->fetch(PDO::FETCH_ASSOC);
|
|||
|
|
$total = $totalResult['total'] ?? 0;
|
|||
|
|
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $logs,
|
|||
|
|
'total' => $total,
|
|||
|
|
'page' => $page,
|
|||
|
|
'limit' => $limit,
|
|||
|
|
'totalPages' => ceil($total / $limit)
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取详细日志失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取RSS刷新状态
|
|||
|
|
*/
|
|||
|
|
public function getRssRefreshStatus()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$stats = UrlNav_Plugin::getRssRefreshStatus();
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $stats
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取刷新状态失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* RSS定时任务接口(无需登录)
|
|||
|
|
*/
|
|||
|
|
public function rssCron()
|
|||
|
|
{
|
|||
|
|
// 设置响应头,避免502错误
|
|||
|
|
if (!headers_sent()) {
|
|||
|
|
header('Content-Type: application/json; charset=utf-8');
|
|||
|
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
|||
|
|
header('Pragma: no-cache');
|
|||
|
|
header('Expires: 0');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取密钥
|
|||
|
|
$secret = $this->request->get('secret');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$result = UrlNav_Plugin::executePublicRssCron($secret);
|
|||
|
|
|
|||
|
|
echo json_encode($result);
|
|||
|
|
exit;
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$errorResponse = array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => 'RSS定时任务执行失败: ' . $e->getMessage(),
|
|||
|
|
'timestamp' => time()
|
|||
|
|
);
|
|||
|
|
echo json_encode($errorResponse);
|
|||
|
|
exit;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 状态检查定时任务接口(无需登录)
|
|||
|
|
*/
|
|||
|
|
public function statusCron()
|
|||
|
|
{
|
|||
|
|
// 设置响应头,避免502错误
|
|||
|
|
if (!headers_sent()) {
|
|||
|
|
header('Content-Type: application/json; charset=utf-8');
|
|||
|
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
|||
|
|
header('Pragma: no-cache');
|
|||
|
|
header('Expires: 0');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取密钥
|
|||
|
|
$secret = $this->request->get('secret');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$result = UrlNav_Plugin::executePublicStatusCron($secret);
|
|||
|
|
|
|||
|
|
echo json_encode($result);
|
|||
|
|
exit;
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$errorResponse = array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '状态检查定时任务执行失败: ' . $e->getMessage(),
|
|||
|
|
'timestamp' => time()
|
|||
|
|
);
|
|||
|
|
echo json_encode($errorResponse);
|
|||
|
|
exit;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 手动解锁定时任务
|
|||
|
|
*/
|
|||
|
|
public function unlockCron()
|
|||
|
|
{
|
|||
|
|
$this->response->setContentType('application/json');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$type = $this->request->get('type', 'rss');
|
|||
|
|
|
|||
|
|
// 验证用户权限(解锁需要管理员权限)
|
|||
|
|
$user = Typecho_Widget::widget('Widget_User');
|
|||
|
|
if (!$user->hasLogin() || !$user->pass('administrator', true)) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '无权限解锁定时任务'
|
|||
|
|
));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$result = UrlNav_Plugin::unlockCron($type);
|
|||
|
|
$this->response->throwJson($result);
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '解锁失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 测试方法 - 完全按照Collection插件写法
|
|||
|
|
*/
|
|||
|
|
public function test()
|
|||
|
|
{
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => 'UrlNav插件Action工作正常',
|
|||
|
|
'timestamp' => time()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 清理RSS缓存
|
|||
|
|
*/
|
|||
|
|
public function cleanRssCache()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$cleanedCount = UrlNav_Plugin::cleanAllRssCache();
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '清理完成',
|
|||
|
|
'cleaned_count' => $cleanedCount
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '清理失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取缓存统计信息
|
|||
|
|
*/
|
|||
|
|
public function getCacheStats()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$stats = UrlNav_Plugin::getCacheStats();
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $stats
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取统计信息失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取刷新统计信息
|
|||
|
|
*/
|
|||
|
|
public function getRefreshStats()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$limit = $this->request->get('limit', 10);
|
|||
|
|
$cronType = $this->request->get('cron_type', 'rss');
|
|||
|
|
$stats = UrlNav_Plugin::getRefreshStats($cronType, $limit);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $stats
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取刷新统计失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取定时任务日志
|
|||
|
|
*/
|
|||
|
|
public function getCronLogs()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$limit = $this->request->get('limit', 20);
|
|||
|
|
$type = $this->request->get('type');
|
|||
|
|
$logs = UrlNav_Plugin::getCronLogs($type, $limit);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $logs
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取定时任务日志失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取定时任务统计
|
|||
|
|
*/
|
|||
|
|
public function getCronStats()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$type = $this->request->get('type');
|
|||
|
|
$stats = UrlNav_Plugin::getCronStats($type);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $stats
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取定时任务统计失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取RSS定时任务统计
|
|||
|
|
*/
|
|||
|
|
public function getRssCronStats()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$stats = UrlNav_Plugin::getRssCronStats();
|
|||
|
|
|
|||
|
|
// 获取最近一次的失败网址(24小时内)
|
|||
|
|
$recentFailures = $this->getRecentFailedUrls();
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => array_merge($stats, [
|
|||
|
|
'recent_failures' => $recentFailures,
|
|||
|
|
'has_failures' => !empty($recentFailures)
|
|||
|
|
])
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取RSS定时任务统计失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取最近失败的网址(私有方法)
|
|||
|
|
*/
|
|||
|
|
private function getRecentFailedUrls()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
|
|||
|
|
// 获取最近24小时内失败的网址
|
|||
|
|
// 移除不存在的字段:last_success, error_count
|
|||
|
|
$stmt = $db->prepare("
|
|||
|
|
SELECT
|
|||
|
|
u.id,
|
|||
|
|
u.url,
|
|||
|
|
u.title as site_name,
|
|||
|
|
u.rss_url,
|
|||
|
|
u.last_error,
|
|||
|
|
u.last_refresh,
|
|||
|
|
u.star_rating,
|
|||
|
|
c.name as category_name
|
|||
|
|
FROM urlnav_urls u
|
|||
|
|
LEFT JOIN urlnav_categories c ON u.category_id = c.id
|
|||
|
|
WHERE u.is_active = 1
|
|||
|
|
AND u.rss_url IS NOT NULL
|
|||
|
|
AND u.rss_url != ''
|
|||
|
|
AND u.last_error IS NOT NULL
|
|||
|
|
AND u.last_refresh IS NOT NULL
|
|||
|
|
AND u.last_refresh >= datetime('now', '-1 day')
|
|||
|
|
ORDER BY u.last_refresh DESC
|
|||
|
|
LIMIT 10
|
|||
|
|
");
|
|||
|
|
$stmt->execute();
|
|||
|
|
$urls = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
return $urls;
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('获取失败网址失败: ' . $e->getMessage());
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有失败网址
|
|||
|
|
*/
|
|||
|
|
public function getAllFailedUrls()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
|
|||
|
|
// 获取所有历史失败网址(不限时间)
|
|||
|
|
// 移除不存在的字段:last_success, error_count
|
|||
|
|
$stmt = $db->prepare("
|
|||
|
|
SELECT
|
|||
|
|
u.id,
|
|||
|
|
u.url,
|
|||
|
|
u.title as site_name,
|
|||
|
|
u.rss_url,
|
|||
|
|
u.last_error,
|
|||
|
|
u.last_refresh,
|
|||
|
|
u.star_rating,
|
|||
|
|
u.is_active,
|
|||
|
|
c.name as category_name
|
|||
|
|
FROM urlnav_urls u
|
|||
|
|
LEFT JOIN urlnav_categories c ON u.category_id = c.id
|
|||
|
|
WHERE u.rss_url IS NOT NULL
|
|||
|
|
AND u.rss_url != ''
|
|||
|
|
AND u.last_error IS NOT NULL
|
|||
|
|
AND u.last_refresh IS NOT NULL
|
|||
|
|
ORDER BY u.last_refresh DESC
|
|||
|
|
");
|
|||
|
|
$stmt->execute();
|
|||
|
|
$urls = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $urls
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取失败网址列表失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重试单个失败网址
|
|||
|
|
*/
|
|||
|
|
public function retryRssUrl()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$urlId = $this->request->get('url_id');
|
|||
|
|
|
|||
|
|
if (!$urlId) {
|
|||
|
|
throw new Exception('缺少网址ID参数');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 这里调用您的 RSS 刷新逻辑
|
|||
|
|
// 示例:UrlNav_Plugin::refreshSingleRssUrl($urlId);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '已开始重试该网址'
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '重试失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 忽略失败网址(标记为不活跃)
|
|||
|
|
*/
|
|||
|
|
public function ignoreFailedUrl()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$urlId = $this->request->get('url_id');
|
|||
|
|
|
|||
|
|
if (!$urlId) {
|
|||
|
|
throw new Exception('缺少网址ID参数');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
$stmt = $db->prepare("UPDATE urlnav_urls SET is_active = 0 WHERE id = ?");
|
|||
|
|
$stmt->execute([$urlId]);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '已忽略该网址'
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '操作失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 启用已停用的网址
|
|||
|
|
*/
|
|||
|
|
public function enableFailedUrl()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$urlId = $this->request->get('url_id');
|
|||
|
|
|
|||
|
|
if (!$urlId) {
|
|||
|
|
throw new Exception('缺少网址ID参数');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
$stmt = $db->prepare("UPDATE urlnav_urls SET is_active = 1 WHERE id = ?");
|
|||
|
|
$stmt->execute([$urlId]);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '已重新启用该网址'
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '启用失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重试所有失败网址
|
|||
|
|
*/
|
|||
|
|
public function retryAllFailedUrls()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
// 获取所有活跃且有错误的网址
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
$stmt = $db->prepare("
|
|||
|
|
SELECT id FROM urlnav_urls
|
|||
|
|
WHERE is_active = 1
|
|||
|
|
AND rss_url IS NOT NULL
|
|||
|
|
AND rss_url != ''
|
|||
|
|
AND last_error IS NOT NULL
|
|||
|
|
");
|
|||
|
|
$stmt->execute();
|
|||
|
|
$urls = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
$count = count($urls);
|
|||
|
|
$successCount = 0;
|
|||
|
|
|
|||
|
|
foreach ($urls as $url) {
|
|||
|
|
try {
|
|||
|
|
// 调用刷新单个网址的逻辑
|
|||
|
|
// UrlNav_Plugin::refreshSingleRssUrl($url['id']);
|
|||
|
|
$successCount++;
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('重试网址 ' . $url['id'] . ' 失败: ' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => "批量重试完成,成功 {$successCount}/{$count} 个"
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '批量重试失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取刷新日志(包含成功和失败的记录)
|
|||
|
|
*/
|
|||
|
|
public function getRefreshLogs()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$page = $this->request->get('page', 1);
|
|||
|
|
$pageSize = $this->request->get('pageSize', 20);
|
|||
|
|
$search = $this->request->get('search', '');
|
|||
|
|
$type = $this->request->get('type', ''); // 'success', 'failed', 'all'
|
|||
|
|
$date = $this->request->get('date', '');
|
|||
|
|
|
|||
|
|
$offset = ($page - 1) * $pageSize;
|
|||
|
|
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
|
|||
|
|
// 基础查询
|
|||
|
|
$sql = "
|
|||
|
|
SELECT
|
|||
|
|
u.id,
|
|||
|
|
u.title as site_name,
|
|||
|
|
u.url,
|
|||
|
|
u.rss_url,
|
|||
|
|
u.last_refresh,
|
|||
|
|
u.last_error,
|
|||
|
|
u.star_rating,
|
|||
|
|
u.is_active,
|
|||
|
|
c.name as category_name,
|
|||
|
|
CASE
|
|||
|
|
WHEN u.last_error IS NOT NULL AND u.last_error != '' THEN 'failed'
|
|||
|
|
ELSE 'success'
|
|||
|
|
END as status
|
|||
|
|
FROM urlnav_urls u
|
|||
|
|
LEFT JOIN urlnav_categories c ON u.category_id = c.id
|
|||
|
|
WHERE u.rss_url IS NOT NULL
|
|||
|
|
AND u.rss_url != ''
|
|||
|
|
AND u.last_refresh IS NOT NULL
|
|||
|
|
";
|
|||
|
|
|
|||
|
|
$params = [];
|
|||
|
|
|
|||
|
|
// 添加筛选条件
|
|||
|
|
if ($type === 'failed') {
|
|||
|
|
$sql .= " AND u.last_error IS NOT NULL AND u.last_error != ''";
|
|||
|
|
} elseif ($type === 'success') {
|
|||
|
|
$sql .= " AND (u.last_error IS NULL OR u.last_error = '')";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!empty($search)) {
|
|||
|
|
$sql .= " AND (u.title LIKE ? OR u.url LIKE ? OR u.rss_url LIKE ?)";
|
|||
|
|
$searchParam = "%{$search}%";
|
|||
|
|
$params = array_merge($params, [$searchParam, $searchParam, $searchParam]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!empty($date)) {
|
|||
|
|
$sql .= " AND DATE(u.last_refresh) = ?";
|
|||
|
|
$params[] = $date;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取总数
|
|||
|
|
$countSql = "SELECT COUNT(*) as total FROM ($sql) as subquery";
|
|||
|
|
$countStmt = $db->prepare($countSql);
|
|||
|
|
$countStmt->execute($params);
|
|||
|
|
$total = $countStmt->fetch(PDO::FETCH_ASSOC)['total'];
|
|||
|
|
|
|||
|
|
// 获取分页数据
|
|||
|
|
$sql .= " ORDER BY u.last_refresh DESC LIMIT ? OFFSET ?";
|
|||
|
|
$params[] = $pageSize;
|
|||
|
|
$params[] = $offset;
|
|||
|
|
|
|||
|
|
$stmt = $db->prepare($sql);
|
|||
|
|
$stmt->execute($params);
|
|||
|
|
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
// 转换时间为北京时间
|
|||
|
|
foreach ($logs as &$log) {
|
|||
|
|
$log['last_refresh_beijing'] = $this->convertToBeijingTimeCorrectly($log['last_refresh']);
|
|||
|
|
$log['status_label'] = $log['status'] === 'failed' ? '失败' : '成功';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => [
|
|||
|
|
'logs' => $logs,
|
|||
|
|
'pagination' => [
|
|||
|
|
'total' => (int)$total,
|
|||
|
|
'page' => (int)$page,
|
|||
|
|
'pageSize' => (int)$pageSize,
|
|||
|
|
'totalPages' => ceil($total / $pageSize)
|
|||
|
|
]
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取刷新日志失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取最近刷新统计(用于仪表板)
|
|||
|
|
*/
|
|||
|
|
public function getRecentRefreshStats()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$days = $this->request->get('days', 7);
|
|||
|
|
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
|
|||
|
|
// 1. 获取最近N天的刷新统计
|
|||
|
|
$dailyStatsSql = "
|
|||
|
|
SELECT
|
|||
|
|
DATE(last_refresh) as refresh_date,
|
|||
|
|
COUNT(*) as total_refreshes,
|
|||
|
|
SUM(CASE WHEN last_error IS NOT NULL AND last_error != '' THEN 1 ELSE 0 END) as failed_refreshes,
|
|||
|
|
SUM(CASE WHEN last_error IS NULL OR last_error = '' THEN 1 ELSE 0 END) as success_refreshes
|
|||
|
|
FROM urlnav_urls
|
|||
|
|
WHERE rss_url IS NOT NULL
|
|||
|
|
AND rss_url != ''
|
|||
|
|
AND last_refresh IS NOT NULL
|
|||
|
|
AND last_refresh >= DATE('now', '-' || ? || ' days')
|
|||
|
|
GROUP BY DATE(last_refresh)
|
|||
|
|
ORDER BY refresh_date DESC
|
|||
|
|
";
|
|||
|
|
|
|||
|
|
$stmt = $db->prepare($dailyStatsSql);
|
|||
|
|
$stmt->execute([$days]);
|
|||
|
|
$dailyStats = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
// 2. 获取成功/失败总数
|
|||
|
|
$totalStatsSql = "
|
|||
|
|
SELECT
|
|||
|
|
COUNT(*) as total_urls,
|
|||
|
|
SUM(CASE WHEN last_error IS NOT NULL AND last_error != '' THEN 1 ELSE 0 END) as total_failed,
|
|||
|
|
SUM(CASE WHEN last_error IS NULL OR last_error = '' THEN 1 ELSE 0 END) as total_success,
|
|||
|
|
SUM(CASE WHEN last_error IS NOT NULL AND last_error != '' AND last_refresh >= datetime('now', '-1 day') THEN 1 ELSE 0 END) as recent_failed
|
|||
|
|
FROM urlnav_urls
|
|||
|
|
WHERE rss_url IS NOT NULL
|
|||
|
|
AND rss_url != ''
|
|||
|
|
AND is_active = 1
|
|||
|
|
";
|
|||
|
|
|
|||
|
|
$totalStmt = $db->prepare($totalStatsSql);
|
|||
|
|
$totalStmt->execute();
|
|||
|
|
$totalStats = $totalStmt->fetch(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
// 3. 获取最常见的错误
|
|||
|
|
$commonErrorsSql = "
|
|||
|
|
SELECT
|
|||
|
|
last_error,
|
|||
|
|
COUNT(*) as error_count
|
|||
|
|
FROM urlnav_urls
|
|||
|
|
WHERE last_error IS NOT NULL
|
|||
|
|
AND last_error != ''
|
|||
|
|
AND last_refresh >= datetime('now', '-7 days')
|
|||
|
|
GROUP BY last_error
|
|||
|
|
ORDER BY error_count DESC
|
|||
|
|
LIMIT 10
|
|||
|
|
";
|
|||
|
|
|
|||
|
|
$errorsStmt = $db->prepare($commonErrorsSql);
|
|||
|
|
$errorsStmt->execute();
|
|||
|
|
$commonErrors = $errorsStmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => [
|
|||
|
|
'daily_stats' => $dailyStats,
|
|||
|
|
'total_stats' => $totalStats,
|
|||
|
|
'common_errors' => $commonErrors,
|
|||
|
|
'time_range' => $days . '天'
|
|||
|
|
]
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取刷新统计失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 导出成功网址(修复版)
|
|||
|
|
*/
|
|||
|
|
public function exportSuccessUrls()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
|
|||
|
|
// 🔴 修复:查询最近一次刷新成功的网址
|
|||
|
|
// 方法1:根据last_error为空判断
|
|||
|
|
$stmt = $db->prepare("
|
|||
|
|
SELECT
|
|||
|
|
u.id,
|
|||
|
|
u.url,
|
|||
|
|
u.title as site_name,
|
|||
|
|
u.rss_url,
|
|||
|
|
u.last_error,
|
|||
|
|
u.last_refresh,
|
|||
|
|
u.success_count,
|
|||
|
|
u.failure_count,
|
|||
|
|
u.star_rating,
|
|||
|
|
u.is_active,
|
|||
|
|
u.is_online,
|
|||
|
|
c.name as category_name
|
|||
|
|
FROM urlnav_urls u
|
|||
|
|
LEFT JOIN urlnav_categories c ON u.category_id = c.id
|
|||
|
|
WHERE u.rss_url IS NOT NULL
|
|||
|
|
AND u.rss_url != ''
|
|||
|
|
AND u.is_active = 1
|
|||
|
|
-- 关键:最近一次刷新没有错误,且成功次数大于失败次数
|
|||
|
|
AND (u.last_error IS NULL OR u.last_error = '')
|
|||
|
|
AND (u.success_count > u.failure_count OR u.success_count > 0)
|
|||
|
|
AND u.last_refresh IS NOT NULL
|
|||
|
|
ORDER BY u.last_refresh DESC
|
|||
|
|
");
|
|||
|
|
$stmt->execute();
|
|||
|
|
$urls = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
// 生成CSV内容
|
|||
|
|
$csvContent = "ID,网站名称,网站地址,RSS地址,最后刷新时间(北京时间),成功次数,失败次数,成功率,星级评分,网站状态,分类\n";
|
|||
|
|
|
|||
|
|
foreach ($urls as $url) {
|
|||
|
|
// 使用正确的北京时间转换
|
|||
|
|
$refreshTime = $this->convertToBeijingTimeCorrectly($url['last_refresh']);
|
|||
|
|
|
|||
|
|
// 计算成功率
|
|||
|
|
$total = $url['success_count'] + $url['failure_count'];
|
|||
|
|
$successRate = $total > 0 ? round(($url['success_count'] / $total) * 100, 2) : 0;
|
|||
|
|
|
|||
|
|
$csvContent .= sprintf(
|
|||
|
|
'"%s","%s","%s","%s","%s","%s","%s","%s%%","%s","%s","%s"' . "\n",
|
|||
|
|
$url['id'],
|
|||
|
|
$url['site_name'] ?? '',
|
|||
|
|
$url['url'] ?? '',
|
|||
|
|
$url['rss_url'] ?? '',
|
|||
|
|
$refreshTime,
|
|||
|
|
$url['success_count'] ?? 0,
|
|||
|
|
$url['failure_count'] ?? 0,
|
|||
|
|
$successRate,
|
|||
|
|
$url['star_rating'] ?? 0,
|
|||
|
|
$url['is_online'] ? '在线' : '离线',
|
|||
|
|
$url['category_name'] ?? ''
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'csv_data' => $csvContent,
|
|||
|
|
'count' => count($urls)
|
|||
|
|
));
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '导出成功网址失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function exportFailedUrls()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = UrlNav_Plugin::getDbConnection();
|
|||
|
|
|
|||
|
|
// 🔴 直接复制 exportSuccessUrls() 的SQL结构,只改WHERE条件
|
|||
|
|
$stmt = $db->prepare("
|
|||
|
|
SELECT
|
|||
|
|
u.id,
|
|||
|
|
u.url,
|
|||
|
|
u.title as site_name,
|
|||
|
|
u.rss_url,
|
|||
|
|
u.last_error,
|
|||
|
|
u.last_refresh,
|
|||
|
|
u.success_count,
|
|||
|
|
u.failure_count,
|
|||
|
|
u.is_active, // 🔴 注意:失败导出没有 star_rating 字段
|
|||
|
|
u.is_online,
|
|||
|
|
c.name as category_name
|
|||
|
|
FROM urlnav_urls u
|
|||
|
|
LEFT JOIN urlnav_categories c ON u.category_id = c.id
|
|||
|
|
WHERE u.rss_url IS NOT NULL
|
|||
|
|
AND u.rss_url != ''
|
|||
|
|
AND u.is_active = 1
|
|||
|
|
-- 🔴 关键:只改这里,其他完全一样
|
|||
|
|
AND u.last_error IS NOT NULL
|
|||
|
|
AND u.last_error != ''
|
|||
|
|
ORDER BY u.last_refresh DESC
|
|||
|
|
");
|
|||
|
|
$stmt->execute();
|
|||
|
|
$urls = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
// 🔴 直接复制 exportSuccessUrls() 的CSV生成逻辑
|
|||
|
|
$csvContent = "ID,网站名称,网站地址,RSS地址,最后刷新时间(北京时间),错误信息,成功次数,失败次数,成功率,网站状态,分类\n";
|
|||
|
|
|
|||
|
|
foreach ($urls as $url) {
|
|||
|
|
// 🔴 使用相同的时间转换方法
|
|||
|
|
$refreshTime = '';
|
|||
|
|
if (!empty($url['last_refresh']) && $url['last_refresh'] != '0000-00-00 00:00:00') {
|
|||
|
|
$date = new DateTime($url['last_refresh']);
|
|||
|
|
$date->setTimezone(new DateTimeZone('Asia/Shanghai'));
|
|||
|
|
$refreshTime = $date->format('Y-m-d H:i:s');
|
|||
|
|
} else {
|
|||
|
|
$refreshTime = '从未刷新';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算成功率
|
|||
|
|
$total = $url['success_count'] + $url['failure_count'];
|
|||
|
|
$successRate = $total > 0 ? round(($url['success_count'] / $total) * 100, 2) : 0;
|
|||
|
|
|
|||
|
|
$csvContent .= sprintf(
|
|||
|
|
'"%s","%s","%s","%s","%s","%s","%s","%s","%s%%","%s","%s"' . "\n",
|
|||
|
|
$url['id'],
|
|||
|
|
$url['site_name'] ?? '',
|
|||
|
|
$url['url'] ?? '',
|
|||
|
|
$url['rss_url'] ?? '',
|
|||
|
|
$refreshTime,
|
|||
|
|
str_replace('"', '""', $url['last_error'] ?? ''),
|
|||
|
|
$url['success_count'] ?? 0,
|
|||
|
|
$url['failure_count'] ?? 0,
|
|||
|
|
$successRate,
|
|||
|
|
$url['is_online'] ? '在线' : '离线',
|
|||
|
|
$url['category_name'] ?? ''
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'csv_data' => $csvContent,
|
|||
|
|
'count' => count($urls)
|
|||
|
|
));
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '导出失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 正确的北京时间转换方法
|
|||
|
|
*/
|
|||
|
|
private function convertToBeijingTimeCorrectly($datetime)
|
|||
|
|
{
|
|||
|
|
if (empty($datetime) || $datetime == '0000-00-00 00:00:00') {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 方法1:明确指定输入时区(如果知道数据库存储的时区)
|
|||
|
|
// 假设数据库存储的是UTC时间
|
|||
|
|
$utc = new DateTimeZone('UTC');
|
|||
|
|
$beijing = new DateTimeZone('Asia/Shanghai');
|
|||
|
|
|
|||
|
|
// 创建DateTime对象,明确指定输入时区为UTC
|
|||
|
|
$date = DateTime::createFromFormat('Y-m-d H:i:s', $datetime, $utc);
|
|||
|
|
|
|||
|
|
if ($date === false) {
|
|||
|
|
// 如果格式不匹配,尝试其他方法
|
|||
|
|
$date = new DateTime($datetime, $utc);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 转换为北京时间
|
|||
|
|
$date->setTimezone($beijing);
|
|||
|
|
return $date->format('Y-m-d H:i:s');
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
// 方法2:使用你的Rss.php中的方法
|
|||
|
|
return $this->convertUsingRssMethod($datetime);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 复制Rss.php中的convertToBeijingTime方法
|
|||
|
|
*/
|
|||
|
|
private function convertUsingRssMethod($datetime)
|
|||
|
|
{
|
|||
|
|
if (empty($datetime) || $datetime == '0000-00-00 00:00:00' || $datetime == '0000-00-00 00:00') {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 创建DateTime对象
|
|||
|
|
$date = new DateTime($datetime);
|
|||
|
|
|
|||
|
|
// 如果已经是北京时间,直接返回
|
|||
|
|
if ($date->getTimezone()->getName() == 'Asia/Shanghai') {
|
|||
|
|
return $date->format('Y-m-d H:i:s');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 转换为北京时间
|
|||
|
|
$date->setTimezone(new DateTimeZone('Asia/Shanghai'));
|
|||
|
|
return $date->format('Y-m-d H:i:s');
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
// 如果解析失败,尝试简单的时间转换
|
|||
|
|
$timestamp = strtotime($datetime);
|
|||
|
|
if ($timestamp !== false) {
|
|||
|
|
// 服务器时间已经是北京时间,直接返回
|
|||
|
|
return date('Y-m-d H:i:s', $timestamp);
|
|||
|
|
}
|
|||
|
|
return $datetime;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
/**
|
|||
|
|
* 获取状态检查定时任务统计
|
|||
|
|
*/
|
|||
|
|
public function getStatusCronStats()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$stats = UrlNav_Plugin::getStatusCronStats();
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $stats
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取状态检查定时任务统计失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查网站状态 - 修复版
|
|||
|
|
*/
|
|||
|
|
public function checkStatus()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
// 检查是否是批量检查
|
|||
|
|
$urlIds = $this->request->get('url_ids');
|
|||
|
|
$batchInfo = $this->request->get('batch_info');
|
|||
|
|
|
|||
|
|
// 解析URL IDs
|
|||
|
|
$idArray = null;
|
|||
|
|
if ($urlIds && $urlIds !== '') {
|
|||
|
|
// 处理不同的ID格式
|
|||
|
|
if (is_array($urlIds)) {
|
|||
|
|
$idArray = $urlIds;
|
|||
|
|
} else if (strpos($urlIds, ',') !== false) {
|
|||
|
|
// 逗号分隔的字符串
|
|||
|
|
$idArray = array_map('trim', explode(',', $urlIds));
|
|||
|
|
$idArray = array_filter($idArray, function($id) {
|
|||
|
|
return is_numeric($id) && $id > 0;
|
|||
|
|
});
|
|||
|
|
} else if (is_numeric($urlIds)) {
|
|||
|
|
// 单个ID
|
|||
|
|
$idArray = [$urlIds];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析批次信息
|
|||
|
|
$batchData = null;
|
|||
|
|
if ($batchInfo && is_string($batchInfo)) {
|
|||
|
|
$batchData = json_decode($batchInfo, true);
|
|||
|
|
} else if (is_array($batchInfo)) {
|
|||
|
|
$batchData = $batchInfo;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是选中的网址,需要特殊处理批次信息
|
|||
|
|
if (!empty($idArray) && $batchData) {
|
|||
|
|
// 重新计算批次信息,因为选中的网址总数可能和全部网址不同
|
|||
|
|
$batchSize = $batchData['size'] ?? 10;
|
|||
|
|
$totalSelected = count($idArray);
|
|||
|
|
$totalBatches = ceil($totalSelected / $batchSize);
|
|||
|
|
$batchNumber = $batchData['batch'] ?? 1;
|
|||
|
|
|
|||
|
|
// 确保批次号不超过总批次
|
|||
|
|
if ($batchNumber > $totalBatches) {
|
|||
|
|
return $this->response->throwJson([
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '所有选中的网址已检查完成',
|
|||
|
|
'total' => 0,
|
|||
|
|
'success_count' => 0,
|
|||
|
|
'failed_count' => 0,
|
|||
|
|
'has_more' => false
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取当前批次要检查的ID
|
|||
|
|
$offset = ($batchNumber - 1) * $batchSize;
|
|||
|
|
$batchIds = array_slice($idArray, $offset, $batchSize);
|
|||
|
|
|
|||
|
|
// 调用检查函数
|
|||
|
|
$result = UrlNav_Plugin::manualCheckStatus($batchIds, true, $batchInfo);
|
|||
|
|
|
|||
|
|
// 更新批次信息
|
|||
|
|
$result['batch_number'] = $batchNumber;
|
|||
|
|
$result['total_batches'] = $totalBatches;
|
|||
|
|
$result['batch_info'] = json_encode([
|
|||
|
|
'batch' => $batchNumber,
|
|||
|
|
'total' => $totalBatches,
|
|||
|
|
'size' => $batchSize,
|
|||
|
|
'selected_ids' => implode(',', $idArray) // 记录所有选中的ID
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
$this->response->throwJson($result);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有选中网址,检查全部(已有的逻辑)
|
|||
|
|
$result = UrlNav_Plugin::manualCheckStatus(null, true, $batchInfo);
|
|||
|
|
$this->response->throwJson($result);
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '检查失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 在 Action.php 中修复 checkSingleStatus 方法
|
|||
|
|
public function checkSingleStatus()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$urlId = $this->request->get('id');
|
|||
|
|
if (empty($urlId)) {
|
|||
|
|
$urlId = $this->request->get('url_id');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (empty($urlId)) {
|
|||
|
|
throw new Exception('ID不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$result = UrlNav_Plugin::manualCheckStatus($urlId);
|
|||
|
|
|
|||
|
|
// 修复:检查返回的数据结构
|
|||
|
|
if (isset($result['success']) && $result['success'] === true) {
|
|||
|
|
if (isset($result['results']) && is_array($result['results'])) {
|
|||
|
|
// 如果是批量检查的结果
|
|||
|
|
foreach ($result['results'] as $urlId => $checkResult) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '检查完成',
|
|||
|
|
'data' => $checkResult
|
|||
|
|
]);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 如果是单次检查的结果
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '检查完成',
|
|||
|
|
'data' => $result['data'] ?? $result
|
|||
|
|
]);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 检查失败
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => $result['message'] ?? '检查失败'
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '检查失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取状态统计
|
|||
|
|
*/
|
|||
|
|
public function getStatusStats()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$stats = UrlNav_Plugin::getStatusStats();
|
|||
|
|
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $stats
|
|||
|
|
]);
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson([
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取统计失败: ' . $e->getMessage()
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加分类 - 模仿Collection的add方法
|
|||
|
|
*/
|
|||
|
|
public function addCategory()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$name = $this->request->get('name');
|
|||
|
|
$description = $this->request->get('description');
|
|||
|
|
$sort_order = $this->request->get('sort_order', 0);
|
|||
|
|
|
|||
|
|
if (empty($name)) {
|
|||
|
|
throw new Exception('分类名称不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查名称是否已存在 - 模仿Collection插件
|
|||
|
|
$stmt = $this->db->prepare("SELECT COUNT(*) FROM urlnav_categories WHERE name = ? AND is_active = 1");
|
|||
|
|
$stmt->execute(array($name));
|
|||
|
|
$count = $stmt->fetchColumn();
|
|||
|
|
|
|||
|
|
if ($count > 0) {
|
|||
|
|
throw new Exception('分类名称已存在');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stmt = $this->db->prepare("INSERT INTO urlnav_categories (name, description, sort_order) VALUES (?, ?, ?)");
|
|||
|
|
$stmt->execute(array($name, $description, $sort_order));
|
|||
|
|
|
|||
|
|
$id = $this->db->lastInsertId();
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '分类添加成功',
|
|||
|
|
'data' => array('id' => $id)
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '添加失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新分类 - 模仿Collection的update方法
|
|||
|
|
*/
|
|||
|
|
public function updateCategory()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$id = $this->request->get('id');
|
|||
|
|
$name = $this->request->get('name');
|
|||
|
|
$description = $this->request->get('description');
|
|||
|
|
$sort_order = $this->request->get('sort_order', 0);
|
|||
|
|
|
|||
|
|
if (empty($id) || empty($name)) {
|
|||
|
|
throw new Exception('ID和分类名称不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查名称是否已存在(排除自身)- 模仿Collection插件
|
|||
|
|
$stmt = $this->db->prepare("SELECT COUNT(*) FROM urlnav_categories WHERE name = ? AND id != ? AND is_active = 1");
|
|||
|
|
$stmt->execute(array($name, $id));
|
|||
|
|
$count = $stmt->fetchColumn();
|
|||
|
|
|
|||
|
|
if ($count > 0) {
|
|||
|
|
throw new Exception('分类名称已存在');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stmt = $this->db->prepare("UPDATE urlnav_categories SET name = ?, description = ?, sort_order = ? WHERE id = ?");
|
|||
|
|
$result = $stmt->execute(array($name, $description, $sort_order, $id));
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '分类更新成功'
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
throw new Exception('分类不存在或更新失败');
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '更新失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除分类 - 模仿Collection的delete方法
|
|||
|
|
*/
|
|||
|
|
public function deleteCategory()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$id = $this->request->get('id');
|
|||
|
|
|
|||
|
|
if (empty($id)) {
|
|||
|
|
throw new Exception('ID不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查分类下是否有网址
|
|||
|
|
$checkStmt = $this->db->prepare("SELECT COUNT(*) FROM urlnav_urls WHERE category_id = ? AND is_active = 1");
|
|||
|
|
$checkStmt->execute(array($id));
|
|||
|
|
$urlCount = $checkStmt->fetchColumn();
|
|||
|
|
|
|||
|
|
if ($urlCount > 0) {
|
|||
|
|
throw new Exception('该分类下还有' . $urlCount . '个网址,无法删除');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stmt = $this->db->prepare("UPDATE urlnav_categories SET is_active = 0 WHERE id = ?");
|
|||
|
|
$result = $stmt->execute(array($id));
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '分类删除成功'
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
throw new Exception('分类不存在或删除失败');
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '删除失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量删除分类 - 模仿Collection的batchDelete方法
|
|||
|
|
*/
|
|||
|
|
public function batchDeleteCategories()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
// Collection插件处理数组的方式
|
|||
|
|
$ids = $this->request->filter('int')->getArray('category');
|
|||
|
|
|
|||
|
|
if (empty($ids)) {
|
|||
|
|
$rawIds = $this->request->get('category');
|
|||
|
|
if (!empty($rawIds)) {
|
|||
|
|
$ids = is_array($rawIds) ? $rawIds : array($rawIds);
|
|||
|
|
$ids = array_map('intval', $ids);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (empty($ids) || !is_array($ids)) {
|
|||
|
|
throw new Exception('请选择要删除的分类');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$ids = array_filter($ids, function($id) {
|
|||
|
|
return is_numeric($id) && $id > 0;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (empty($ids)) {
|
|||
|
|
throw new Exception('无效的分类ID');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否有网址关联到这些分类
|
|||
|
|
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
|||
|
|
$checkStmt = $this->db->prepare("SELECT COUNT(*) FROM urlnav_urls WHERE category_id IN ($placeholders) AND is_active = 1");
|
|||
|
|
$checkStmt->execute($ids);
|
|||
|
|
$urlCount = $checkStmt->fetchColumn();
|
|||
|
|
|
|||
|
|
if ($urlCount > 0) {
|
|||
|
|
throw new Exception('选中的分类下还有' . $urlCount . '个网址,无法删除');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stmt = $this->db->prepare("UPDATE urlnav_categories SET is_active = 0 WHERE id IN ($placeholders)");
|
|||
|
|
$result = $stmt->execute($ids);
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '分类批量删除成功'
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
throw new Exception('删除失败');
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '批量删除失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取单个分类 - 模仿Collection的get方法
|
|||
|
|
*/
|
|||
|
|
public function getCategory()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$id = $this->request->get('id');
|
|||
|
|
|
|||
|
|
if (empty($id)) {
|
|||
|
|
throw new Exception('ID不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stmt = $this->db->prepare("SELECT * FROM urlnav_categories WHERE id = ?");
|
|||
|
|
$stmt->execute(array($id));
|
|||
|
|
$category = $stmt->fetch(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
if ($category) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $category
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
throw new Exception('分类不存在');
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '查询失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有分类 - 模仿Collection的getAll方法
|
|||
|
|
*/
|
|||
|
|
public function getAllCategories()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$stmt = $this->db->query("SELECT * FROM urlnav_categories WHERE is_active = 1 ORDER BY sort_order, created_at DESC");
|
|||
|
|
$categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $categories
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '查询失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加网址 - 模仿Collection的add方法
|
|||
|
|
*/
|
|||
|
|
public function addUrl()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$title = $this->request->get('title');
|
|||
|
|
$url = $this->request->get('url');
|
|||
|
|
$description = $this->request->get('description');
|
|||
|
|
$rss_url = $this->request->get('rss_url', '');
|
|||
|
|
$category_id = $this->request->get('category_id');
|
|||
|
|
$sort_order = $this->request->get('sort_order', 0);
|
|||
|
|
|
|||
|
|
if (empty($title)) {
|
|||
|
|
throw new Exception('网站标题不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (empty($url)) {
|
|||
|
|
throw new Exception('网站地址不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证URL格式
|
|||
|
|
if (!UrlNav_Plugin::validateUrl($url)) {
|
|||
|
|
throw new Exception('网站地址格式无效,请使用http://或https://开头的完整地址');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果提供了RSS地址,验证格式
|
|||
|
|
if (!empty($rss_url) && !UrlNav_Plugin::validateUrl($rss_url)) {
|
|||
|
|
throw new Exception('RSS地址格式无效,请使用http://或https://开头的完整地址');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查标题是否已存在
|
|||
|
|
$stmt = $this->db->prepare("SELECT COUNT(*) FROM urlnav_urls WHERE title = ? AND is_active = 1");
|
|||
|
|
$stmt->execute(array($title));
|
|||
|
|
$count = $stmt->fetchColumn();
|
|||
|
|
|
|||
|
|
if ($count > 0) {
|
|||
|
|
throw new Exception('网站标题已存在');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取星级评分
|
|||
|
|
$starRating = $this->request->get('star_rating', 0);
|
|||
|
|
$starRating = intval($starRating);
|
|||
|
|
// 限制在0-3之间
|
|||
|
|
$starRating = max(0, min(3, $starRating));
|
|||
|
|
|
|||
|
|
// 检查URL是否已存在
|
|||
|
|
$stmt = $this->db->prepare("SELECT COUNT(*) FROM urlnav_urls WHERE url = ? AND is_active = 1");
|
|||
|
|
$stmt->execute(array($url));
|
|||
|
|
$count = $stmt->fetchColumn();
|
|||
|
|
|
|||
|
|
if ($count > 0) {
|
|||
|
|
throw new Exception('网站地址已存在');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stmt = $this->db->prepare("INSERT INTO urlnav_urls (title, url, description, rss_url, star_rating, category_id, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|||
|
|
$stmt->execute(array($title, $url, $description, $rss_url, $starRating, $category_id, $sort_order));
|
|||
|
|
|
|||
|
|
$id = $this->db->lastInsertId();
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '网址添加成功',
|
|||
|
|
'data' => array('id' => $id)
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '添加失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新网址 - 模仿Collection的update方法
|
|||
|
|
*/
|
|||
|
|
/**
|
|||
|
|
* 更新网址 - 模仿Collection的update方法
|
|||
|
|
*/
|
|||
|
|
public function updateUrl()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$id = $this->request->get('id');
|
|||
|
|
$title = $this->request->get('title');
|
|||
|
|
$url = $this->request->get('url');
|
|||
|
|
$description = $this->request->get('description');
|
|||
|
|
$rss_url = $this->request->get('rss_url', '');
|
|||
|
|
$category_id = $this->request->get('category_id');
|
|||
|
|
$sort_order = $this->request->get('sort_order', 0);
|
|||
|
|
// 新增:获取星级评分
|
|||
|
|
$star_rating = $this->request->get('star_rating', 0);
|
|||
|
|
$star_rating = intval($star_rating);
|
|||
|
|
// 限制在0-3之间
|
|||
|
|
$star_rating = max(0, min(3, $star_rating));
|
|||
|
|
|
|||
|
|
if (empty($id) || empty($title) || empty($url)) {
|
|||
|
|
throw new Exception('ID、标题和网址不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证URL格式
|
|||
|
|
if (!UrlNav_Plugin::validateUrl($url)) {
|
|||
|
|
throw new Exception('网站地址格式无效,请使用http://或https://开头的完整地址');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果提供了RSS地址,验证格式
|
|||
|
|
if (!empty($rss_url) && !UrlNav_Plugin::validateUrl($rss_url)) {
|
|||
|
|
throw new Exception('RSS地址格式无效,请使用http://或https://开头的完整地址');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查标题是否已存在(排除自身)
|
|||
|
|
$stmt = $this->db->prepare("SELECT COUNT(*) FROM urlnav_urls WHERE title = ? AND id != ? AND is_active = 1");
|
|||
|
|
$stmt->execute(array($title, $id));
|
|||
|
|
$count = $stmt->fetchColumn();
|
|||
|
|
|
|||
|
|
if ($count > 0) {
|
|||
|
|
throw new Exception('网站标题已存在');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查URL是否已存在(排除自身)
|
|||
|
|
$stmt = $this->db->prepare("SELECT COUNT(*) FROM urlnav_urls WHERE url = ? AND id != ? AND is_active = 1");
|
|||
|
|
$stmt->execute(array($url, $id));
|
|||
|
|
$count = $stmt->fetchColumn();
|
|||
|
|
|
|||
|
|
if ($count > 0) {
|
|||
|
|
throw new Exception('网站地址已存在');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 修改这里:添加 star_rating 字段
|
|||
|
|
$stmt = $this->db->prepare("UPDATE urlnav_urls SET title = ?, url = ?, description = ?, rss_url = ?, category_id = ?, sort_order = ?, star_rating = ? WHERE id = ?");
|
|||
|
|
$result = $stmt->execute(array($title, $url, $description, $rss_url, $category_id, $sort_order, $star_rating, $id));
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '网址更新成功'
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
throw new Exception('网址不存在或更新失败');
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '更新失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除网址 - 模仿Collection的delete方法
|
|||
|
|
*/
|
|||
|
|
public function deleteUrl()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$id = $this->request->get('id');
|
|||
|
|
|
|||
|
|
if (empty($id)) {
|
|||
|
|
throw new Exception('ID不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stmt = $this->db->prepare("UPDATE urlnav_urls SET is_active = 0 WHERE id = ?");
|
|||
|
|
$result = $stmt->execute(array($id));
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '网址删除成功'
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
throw new Exception('网址不存在或删除失败');
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '删除失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量删除网址 - 模仿Collection的batchDelete方法
|
|||
|
|
*/
|
|||
|
|
public function batchDeleteUrls()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
// Collection插件处理数组的方式
|
|||
|
|
$ids = $this->request->filter('int')->getArray('url');
|
|||
|
|
|
|||
|
|
if (empty($ids)) {
|
|||
|
|
$rawIds = $this->request->get('url');
|
|||
|
|
if (!empty($rawIds)) {
|
|||
|
|
$ids = is_array($rawIds) ? $rawIds : array($rawIds);
|
|||
|
|
$ids = array_map('intval', $ids);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (empty($ids) || !is_array($ids)) {
|
|||
|
|
throw new Exception('请选择要删除的网址');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$ids = array_filter($ids, function($id) {
|
|||
|
|
return is_numeric($id) && $id > 0;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (empty($ids)) {
|
|||
|
|
throw new Exception('无效的网址ID');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
|||
|
|
$stmt = $this->db->prepare("UPDATE urlnav_urls SET is_active = 0 WHERE id IN ($placeholders)");
|
|||
|
|
$result = $stmt->execute($ids);
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => '网址批量删除成功'
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
throw new Exception('删除失败');
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '批量删除失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取单个网址 - 模仿Collection的get方法
|
|||
|
|
*/
|
|||
|
|
public function getUrl()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$id = $this->request->get('id');
|
|||
|
|
|
|||
|
|
if (empty($id)) {
|
|||
|
|
throw new Exception('ID不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$stmt = $this->db->prepare("SELECT * FROM urlnav_urls WHERE id = ?");
|
|||
|
|
$stmt->execute(array($id));
|
|||
|
|
$url = $stmt->fetch(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
if ($url) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $url
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
throw new Exception('网址不存在');
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '查询失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有网址 - 模仿Collection的getAll方法
|
|||
|
|
*/
|
|||
|
|
public function getAllUrls()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$stmt = $this->db->query("SELECT * FROM urlnav_urls WHERE is_active = 1 ORDER BY sort_order, created_at DESC");
|
|||
|
|
$urls = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'data' => $urls
|
|||
|
|
));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '查询失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 导出OPML格式数据
|
|||
|
|
*/
|
|||
|
|
public function exportOpml()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
// 获取所有分类和网址
|
|||
|
|
$categories = $this->db->query("SELECT * FROM urlnav_categories WHERE is_active = 1 ORDER BY sort_order")->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
$urls = $this->db->query("SELECT * FROM urlnav_urls WHERE is_active = 1 AND rss_url != '' AND rss_url IS NOT NULL AND TRIM(rss_url) != '' ORDER BY sort_order")->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
// 按分类组织网址
|
|||
|
|
$urlsByCategory = [];
|
|||
|
|
foreach ($urls as $url) {
|
|||
|
|
$categoryId = $url['category_id'] ? $url['category_id'] : 'uncategorized';
|
|||
|
|
if (!isset($urlsByCategory[$categoryId])) {
|
|||
|
|
$urlsByCategory[$categoryId] = [];
|
|||
|
|
}
|
|||
|
|
$urlsByCategory[$categoryId][] = $url;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建OPML XML
|
|||
|
|
$xml = new DOMDocument('1.0', 'UTF-8');
|
|||
|
|
$xml->formatOutput = true;
|
|||
|
|
|
|||
|
|
$opml = $xml->createElement('opml');
|
|||
|
|
$opml->setAttribute('version', '2.0');
|
|||
|
|
$opml->setAttribute('xmlns:frss', 'https://freshrss.org/opml');
|
|||
|
|
$xml->appendChild($opml);
|
|||
|
|
|
|||
|
|
// head部分
|
|||
|
|
$head = $xml->createElement('head');
|
|||
|
|
$opml->appendChild($head);
|
|||
|
|
|
|||
|
|
$title = $xml->createElement('title', 'UrlNav RSS订阅');
|
|||
|
|
$head->appendChild($title);
|
|||
|
|
|
|||
|
|
$dateCreated = $xml->createElement('dateCreated', date('r'));
|
|||
|
|
$head->appendChild($dateCreated);
|
|||
|
|
|
|||
|
|
// body部分
|
|||
|
|
$body = $xml->createElement('body');
|
|||
|
|
$opml->appendChild($body);
|
|||
|
|
|
|||
|
|
// 处理有分类的网址
|
|||
|
|
foreach ($categories as $category) {
|
|||
|
|
$categoryId = $category['id'];
|
|||
|
|
if (isset($urlsByCategory[$categoryId]) && !empty($urlsByCategory[$categoryId])) {
|
|||
|
|
$outline = $xml->createElement('outline');
|
|||
|
|
$outline->setAttribute('text', $category['name']);
|
|||
|
|
|
|||
|
|
foreach ($urlsByCategory[$categoryId] as $url) {
|
|||
|
|
$feedOutline = $xml->createElement('outline');
|
|||
|
|
$feedOutline->setAttribute('text', $url['title']);
|
|||
|
|
$feedOutline->setAttribute('type', 'rss');
|
|||
|
|
$feedOutline->setAttribute('xmlUrl', $url['rss_url']);
|
|||
|
|
$feedOutline->setAttribute('htmlUrl', $url['url']);
|
|||
|
|
if (!empty($url['description'])) {
|
|||
|
|
$feedOutline->setAttribute('description', $url['description']);
|
|||
|
|
}
|
|||
|
|
$outline->appendChild($feedOutline);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$body->appendChild($outline);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理未分类的网址
|
|||
|
|
if (isset($urlsByCategory['uncategorized']) && !empty($urlsByCategory['uncategorized'])) {
|
|||
|
|
$outline = $xml->createElement('outline');
|
|||
|
|
$outline->setAttribute('text', '未分类');
|
|||
|
|
|
|||
|
|
foreach ($urlsByCategory['uncategorized'] as $url) {
|
|||
|
|
$feedOutline = $xml->createElement('outline');
|
|||
|
|
$feedOutline->setAttribute('text', $url['title']);
|
|||
|
|
$feedOutline->setAttribute('type', 'rss');
|
|||
|
|
$feedOutline->setAttribute('xmlUrl', $url['rss_url']);
|
|||
|
|
$feedOutline->setAttribute('htmlUrl', $url['url']);
|
|||
|
|
if (!empty($url['description'])) {
|
|||
|
|
$feedOutline->setAttribute('description', $url['description']);
|
|||
|
|
}
|
|||
|
|
$outline->appendChild($feedOutline);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$body->appendChild($outline);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置响应头
|
|||
|
|
$filename = 'urlnav_feeds_' . date('Y-m-d') . '.opml.xml';
|
|||
|
|
$this->response->setHeader('Content-Type', 'application/xml');
|
|||
|
|
$this->response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
|
|||
|
|
$this->response->setHeader('Cache-Control', 'no-cache, must-revalidate');
|
|||
|
|
$this->response->setHeader('Pragma', 'no-cache');
|
|||
|
|
$this->response->setHeader('Expires', '0');
|
|||
|
|
|
|||
|
|
echo $xml->saveXML();
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '导出失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 导入OPML格式数据 - 修复版
|
|||
|
|
*/
|
|||
|
|
public function importOpml()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
// 检查是否有文件上传
|
|||
|
|
if (!isset($_FILES['opml_file']) || $_FILES['opml_file']['error'] !== UPLOAD_ERR_OK) {
|
|||
|
|
throw new Exception('请选择要上传的OPML文件,错误代码: ' . ($_FILES['opml_file']['error'] ?? '未知'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$file = $_FILES['opml_file'];
|
|||
|
|
|
|||
|
|
// 检查文件大小(限制为2MB)
|
|||
|
|
if ($file['size'] > 2 * 1024 * 1024) {
|
|||
|
|
throw new Exception('文件大小超过2MB限制');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($file['size'] == 0) {
|
|||
|
|
throw new Exception('文件为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 读取文件内容
|
|||
|
|
$content = file_get_contents($file['tmp_name']);
|
|||
|
|
if (empty($content)) {
|
|||
|
|
throw new Exception('文件内容为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查文件编码并转换为UTF-8
|
|||
|
|
$encoding = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'BIG5', 'ASCII'], true);
|
|||
|
|
if ($encoding && $encoding != 'UTF-8') {
|
|||
|
|
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 尝试移除可能的BOM头
|
|||
|
|
if (substr($content, 0, 3) == "\xEF\xBB\xBF") {
|
|||
|
|
$content = substr($content, 3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析XML
|
|||
|
|
libxml_use_internal_errors(true);
|
|||
|
|
$xml = simplexml_load_string($content);
|
|||
|
|
if ($xml === false) {
|
|||
|
|
$errors = libxml_get_errors();
|
|||
|
|
$errorMsg = 'XML解析失败: ';
|
|||
|
|
foreach ($errors as $error) {
|
|||
|
|
$errorMsg .= '行 ' . $error->line . ', 列 ' . $error->column . ': ' . $error->message . '; ';
|
|||
|
|
}
|
|||
|
|
libxml_clear_errors();
|
|||
|
|
throw new Exception($errorMsg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否是有效的OPML格式
|
|||
|
|
if (!isset($xml->head) || !isset($xml->body)) {
|
|||
|
|
throw new Exception('无效的OPML格式:缺少head或body元素');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$importResults = [
|
|||
|
|
'total' => 0,
|
|||
|
|
'success' => 0,
|
|||
|
|
'failed' => 0,
|
|||
|
|
'categories_created' => 0,
|
|||
|
|
'urls_added' => 0,
|
|||
|
|
'urls_skipped' => 0,
|
|||
|
|
'details' => []
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 开始事务
|
|||
|
|
$this->db->beginTransaction();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 获取现有分类映射
|
|||
|
|
$existingCategories = [];
|
|||
|
|
$stmt = $this->db->query("SELECT id, name FROM urlnav_categories WHERE is_active = 1");
|
|||
|
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||
|
|
$existingCategories[$row['name']] = $row['id'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取现有网址映射
|
|||
|
|
$existingUrls = [];
|
|||
|
|
$existingRssUrls = [];
|
|||
|
|
$stmt = $this->db->query("SELECT url, rss_url, title FROM urlnav_urls WHERE is_active = 1");
|
|||
|
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||
|
|
if (!empty($row['url'])) {
|
|||
|
|
$existingUrls[$row['url']] = true;
|
|||
|
|
}
|
|||
|
|
if (!empty($row['rss_url'])) {
|
|||
|
|
$existingRssUrls[$row['rss_url']] = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理OPML结构 - 简化版本
|
|||
|
|
$processedCount = 0;
|
|||
|
|
|
|||
|
|
// 遍历body下的所有outline元素
|
|||
|
|
foreach ($xml->body->children() as $outline) {
|
|||
|
|
$processedCount++;
|
|||
|
|
|
|||
|
|
// 检查是否有子元素(可能是分类)
|
|||
|
|
$hasChildren = $outline->children()->count() > 0;
|
|||
|
|
|
|||
|
|
if ($hasChildren) {
|
|||
|
|
// 可能是分类
|
|||
|
|
$categoryName = (string)$outline['text'];
|
|||
|
|
if (empty($categoryName)) {
|
|||
|
|
$categoryName = '未命名分类_' . $processedCount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理分类
|
|||
|
|
if (!isset($existingCategories[$categoryName])) {
|
|||
|
|
// 创建新分类
|
|||
|
|
$stmt = $this->db->prepare("INSERT INTO urlnav_categories (name, description, sort_order) VALUES (?, ?, ?)");
|
|||
|
|
$stmt->execute([$categoryName, '从OPML导入', 999]);
|
|||
|
|
$categoryId = $this->db->lastInsertId();
|
|||
|
|
$existingCategories[$categoryName] = $categoryId;
|
|||
|
|
$importResults['categories_created']++;
|
|||
|
|
|
|||
|
|
$importResults['details'][] = [
|
|||
|
|
'type' => 'category',
|
|||
|
|
'name' => $categoryName,
|
|||
|
|
'status' => 'created',
|
|||
|
|
'message' => '分类创建成功'
|
|||
|
|
];
|
|||
|
|
} else {
|
|||
|
|
$categoryId = $existingCategories[$categoryName];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理该分类下的feed
|
|||
|
|
foreach ($outline->children() as $feed) {
|
|||
|
|
$importResults['total']++;
|
|||
|
|
|
|||
|
|
$title = (string)$feed['text'];
|
|||
|
|
$rssUrl = (string)$feed['xmlUrl'];
|
|||
|
|
$htmlUrl = (string)$feed['htmlUrl'];
|
|||
|
|
$description = isset($feed['description']) ? (string)$feed['description'] : '';
|
|||
|
|
|
|||
|
|
// 如果htmlUrl为空,尝试使用link属性
|
|||
|
|
if (empty($htmlUrl) && isset($feed['link'])) {
|
|||
|
|
$htmlUrl = (string)$feed['link'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果htmlUrl仍然为空,使用RSS地址作为网站地址
|
|||
|
|
if (empty($htmlUrl) && !empty($rssUrl)) {
|
|||
|
|
$htmlUrl = $rssUrl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证数据
|
|||
|
|
if (empty($title) && empty($rssUrl)) {
|
|||
|
|
$importResults['failed']++;
|
|||
|
|
$importResults['details'][] = [
|
|||
|
|
'type' => 'url',
|
|||
|
|
'title' => '未命名',
|
|||
|
|
'status' => 'failed',
|
|||
|
|
'message' => '标题和RSS地址都为空'
|
|||
|
|
];
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有标题,使用RSS地址的一部分作为标题
|
|||
|
|
if (empty($title)) {
|
|||
|
|
$parsedUrl = parse_url($rssUrl);
|
|||
|
|
$title = $parsedUrl['host'] ?? '未命名网站';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证URL格式
|
|||
|
|
if (!empty($rssUrl) && !UrlNav_Plugin::validateUrl($rssUrl)) {
|
|||
|
|
// 尝试修复URL
|
|||
|
|
if (!preg_match('/^https?:\/\//', $rssUrl)) {
|
|||
|
|
$rssUrl = 'https://' . $rssUrl;
|
|||
|
|
if (!UrlNav_Plugin::validateUrl($rssUrl)) {
|
|||
|
|
$importResults['failed']++;
|
|||
|
|
$importResults['details'][] = [
|
|||
|
|
'type' => 'url',
|
|||
|
|
'title' => $title,
|
|||
|
|
'status' => 'failed',
|
|||
|
|
'message' => 'RSS地址格式无效: ' . $rssUrl
|
|||
|
|
];
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!empty($htmlUrl) && !UrlNav_Plugin::validateUrl($htmlUrl)) {
|
|||
|
|
// 尝试修复URL
|
|||
|
|
if (!preg_match('/^https?:\/\//', $htmlUrl)) {
|
|||
|
|
$htmlUrl = 'https://' . $htmlUrl;
|
|||
|
|
if (!UrlNav_Plugin::validateUrl($htmlUrl)) {
|
|||
|
|
$importResults['failed']++;
|
|||
|
|
$importResults['details'][] = [
|
|||
|
|
'type' => 'url',
|
|||
|
|
'title' => $title,
|
|||
|
|
'status' => 'failed',
|
|||
|
|
'message' => '网站地址格式无效: ' . $htmlUrl
|
|||
|
|
];
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查网址是否已存在
|
|||
|
|
$skipDuplicate = false;
|
|||
|
|
if (!empty($htmlUrl) && isset($existingUrls[$htmlUrl])) {
|
|||
|
|
$skipDuplicate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!empty($rssUrl) && isset($existingRssUrls[$rssUrl])) {
|
|||
|
|
$skipDuplicate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查标题是否已存在
|
|||
|
|
if (!$skipDuplicate) {
|
|||
|
|
$checkStmt = $this->db->prepare("SELECT COUNT(*) FROM urlnav_urls WHERE title = ? AND is_active = 1");
|
|||
|
|
$checkStmt->execute([$title]);
|
|||
|
|
if ($checkStmt->fetchColumn() > 0) {
|
|||
|
|
$skipDuplicate = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($skipDuplicate) {
|
|||
|
|
$importResults['urls_skipped']++;
|
|||
|
|
$importResults['details'][] = [
|
|||
|
|
'type' => 'url',
|
|||
|
|
'title' => $title,
|
|||
|
|
'status' => 'skipped',
|
|||
|
|
'message' => '网址已存在'
|
|||
|
|
];
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 插入新网址
|
|||
|
|
$stmt = $this->db->prepare("
|
|||
|
|
INSERT INTO urlnav_urls
|
|||
|
|
(title, url, description, rss_url, category_id, sort_order)
|
|||
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|||
|
|
");
|
|||
|
|
|
|||
|
|
$result = $stmt->execute([
|
|||
|
|
$title,
|
|||
|
|
$htmlUrl,
|
|||
|
|
$description,
|
|||
|
|
$rssUrl,
|
|||
|
|
$categoryId,
|
|||
|
|
999
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
if (!empty($htmlUrl)) {
|
|||
|
|
$existingUrls[$htmlUrl] = true;
|
|||
|
|
}
|
|||
|
|
if (!empty($rssUrl)) {
|
|||
|
|
$existingRssUrls[$rssUrl] = true;
|
|||
|
|
}
|
|||
|
|
$importResults['success']++;
|
|||
|
|
$importResults['urls_added']++;
|
|||
|
|
$importResults['details'][] = [
|
|||
|
|
'type' => 'url',
|
|||
|
|
'title' => $title,
|
|||
|
|
'status' => 'added',
|
|||
|
|
'message' => '网址添加成功'
|
|||
|
|
];
|
|||
|
|
} else {
|
|||
|
|
$importResults['failed']++;
|
|||
|
|
$importResults['details'][] = [
|
|||
|
|
'type' => 'url',
|
|||
|
|
'title' => $title,
|
|||
|
|
'status' => 'failed',
|
|||
|
|
'message' => '数据库插入失败'
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$importResults['failed']++;
|
|||
|
|
$importResults['details'][] = [
|
|||
|
|
'type' => 'url',
|
|||
|
|
'title' => $title,
|
|||
|
|
'status' => 'failed',
|
|||
|
|
'message' => '数据库错误: ' . $e->getMessage()
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 可能是独立的feed
|
|||
|
|
$importResults['total']++;
|
|||
|
|
|
|||
|
|
$title = (string)$outline['text'];
|
|||
|
|
$rssUrl = (string)$outline['xmlUrl'];
|
|||
|
|
$htmlUrl = (string)$outline['htmlUrl'];
|
|||
|
|
$description = isset($outline['description']) ? (string)$outline['description'] : '';
|
|||
|
|
|
|||
|
|
// 验证数据
|
|||
|
|
if (empty($title) && empty($rssUrl)) {
|
|||
|
|
continue; // 跳过无效条目
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有标题,使用RSS地址的一部分作为标题
|
|||
|
|
if (empty($title)) {
|
|||
|
|
$parsedUrl = parse_url($rssUrl);
|
|||
|
|
$title = $parsedUrl['host'] ?? '未命名网站';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用默认分类
|
|||
|
|
$defaultCategoryName = 'OPML导入';
|
|||
|
|
if (!isset($existingCategories[$defaultCategoryName])) {
|
|||
|
|
$stmt = $this->db->prepare("INSERT INTO urlnav_categories (name, description, sort_order) VALUES (?, ?, ?)");
|
|||
|
|
$stmt->execute([$defaultCategoryName, '从OPML导入的网址', 999]);
|
|||
|
|
$categoryId = $this->db->lastInsertId();
|
|||
|
|
$existingCategories[$defaultCategoryName] = $categoryId;
|
|||
|
|
$importResults['categories_created']++;
|
|||
|
|
} else {
|
|||
|
|
$categoryId = $existingCategories[$defaultCategoryName];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否已存在
|
|||
|
|
$skipDuplicate = false;
|
|||
|
|
if (!empty($htmlUrl) && isset($existingUrls[$htmlUrl])) {
|
|||
|
|
$skipDuplicate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!empty($rssUrl) && isset($existingRssUrls[$rssUrl])) {
|
|||
|
|
$skipDuplicate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($skipDuplicate) {
|
|||
|
|
$importResults['urls_skipped']++;
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 插入新网址
|
|||
|
|
$stmt = $this->db->prepare("
|
|||
|
|
INSERT INTO urlnav_urls
|
|||
|
|
(title, url, description, rss_url, category_id, sort_order)
|
|||
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|||
|
|
");
|
|||
|
|
|
|||
|
|
$result = $stmt->execute([
|
|||
|
|
$title,
|
|||
|
|
$htmlUrl,
|
|||
|
|
$description,
|
|||
|
|
$rssUrl,
|
|||
|
|
$categoryId,
|
|||
|
|
999
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
if (!empty($htmlUrl)) {
|
|||
|
|
$existingUrls[$htmlUrl] = true;
|
|||
|
|
}
|
|||
|
|
if (!empty($rssUrl)) {
|
|||
|
|
$existingRssUrls[$rssUrl] = true;
|
|||
|
|
}
|
|||
|
|
$importResults['success']++;
|
|||
|
|
$importResults['urls_added']++;
|
|||
|
|
$importResults['details'][] = [
|
|||
|
|
'type' => 'url',
|
|||
|
|
'title' => $title,
|
|||
|
|
'status' => 'added',
|
|||
|
|
'message' => '网址添加成功'
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 提交事务
|
|||
|
|
$this->db->commit();
|
|||
|
|
|
|||
|
|
// 检查是否有导入数据
|
|||
|
|
if ($importResults['total'] == 0) {
|
|||
|
|
throw new Exception('OPML文件中没有找到有效的RSS订阅数据');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$message = sprintf(
|
|||
|
|
'导入完成:共处理%d个项目,成功%d个,失败%d个。创建%d个分类,添加%d个网址,跳过%d个重复项。',
|
|||
|
|
$importResults['total'],
|
|||
|
|
$importResults['success'],
|
|||
|
|
$importResults['failed'],
|
|||
|
|
$importResults['categories_created'],
|
|||
|
|
$importResults['urls_added'],
|
|||
|
|
$importResults['urls_skipped']
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'message' => $message,
|
|||
|
|
'data' => $importResults
|
|||
|
|
));
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->db->rollBack();
|
|||
|
|
throw $e;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '导入失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取网站信息(标题、描述、RSS地址)
|
|||
|
|
*/
|
|||
|
|
public function fetchRss()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$url = $this->request->get('url');
|
|||
|
|
|
|||
|
|
if (empty($url)) {
|
|||
|
|
throw new Exception('网址不能为空');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证URL格式
|
|||
|
|
if (!UrlNav_Plugin::validateUrl($url)) {
|
|||
|
|
throw new Exception('网站地址格式无效');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取网页内容
|
|||
|
|
$html = @file_get_contents($url, false, stream_context_create(array(
|
|||
|
|
'http' => array(
|
|||
|
|
'timeout' => 10,
|
|||
|
|
'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\r\n" .
|
|||
|
|
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" .
|
|||
|
|
"Accept-Language: zh-CN,zh;q=0.8,en;q=0.6\r\n"
|
|||
|
|
)
|
|||
|
|
)));
|
|||
|
|
|
|||
|
|
if ($html === false) {
|
|||
|
|
throw new Exception('无法访问该网站');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检测编码并转换为UTF-8
|
|||
|
|
$encoding = mb_detect_encoding($html, array('UTF-8', 'GBK', 'GB2312', 'BIG5', 'ASCII'), true);
|
|||
|
|
if ($encoding && $encoding != 'UTF-8') {
|
|||
|
|
$html = mb_convert_encoding($html, 'UTF-8', $encoding);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移除BOM头
|
|||
|
|
if (substr($html, 0, 3) == "\xEF\xBB\xBF") {
|
|||
|
|
$html = substr($html, 3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 1. 获取网站标题
|
|||
|
|
$siteTitle = '';
|
|||
|
|
if (preg_match('/<title[^>]*>(.*?)<\/title>/is', $html, $matches)) {
|
|||
|
|
$siteTitle = trim(strip_tags($matches[1]));
|
|||
|
|
$siteTitle = preg_replace('/\s+/', ' ', $siteTitle);
|
|||
|
|
$siteTitle = html_entity_decode($siteTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 获取网站描述
|
|||
|
|
$siteDescription = '';
|
|||
|
|
// 尝试多种meta标签格式
|
|||
|
|
$metaPatterns = array(
|
|||
|
|
'/<meta[^>]*name=["\']description["\'][^>]*content=["\']([^"\']*)["\'][^>]*>/i',
|
|||
|
|
'/<meta[^>]*content=["\']([^"\']*)["\'][^>]*name=["\']description["\'][^>]*>/i',
|
|||
|
|
'/<meta[^>]*property=["\']og:description["\'][^>]*content=["\']([^"\']*)["\'][^>]*>/i',
|
|||
|
|
'/<meta[^>]*content=["\']([^"\']*)["\'][^>]*property=["\']og:description["\'][^>]*>/i'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
foreach ($metaPatterns as $pattern) {
|
|||
|
|
if (preg_match($pattern, $html, $matches)) {
|
|||
|
|
$siteDescription = trim($matches[1]);
|
|||
|
|
$siteDescription = html_entity_decode($siteDescription, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|||
|
|
if (!empty($siteDescription)) {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 查找RSS链接
|
|||
|
|
$rssUrls = array();
|
|||
|
|
|
|||
|
|
// 查找 <link> 标签中的RSS
|
|||
|
|
preg_match_all('/<link[^>]+rel=["\'](alternate|rss|rss feed|feed)["\'][^>]+href=["\']([^"\']+)["\'][^>]*>/i', $html, $matches);
|
|||
|
|
if (!empty($matches[2])) {
|
|||
|
|
foreach ($matches[2] as $rssUrl) {
|
|||
|
|
$rssUrls[] = $rssUrl;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查找 <a> 标签中的RSS
|
|||
|
|
preg_match_all('/<a[^>]+href=["\']([^"\']+\.(xml|rss|atom|rdf))["\'][^>]*>/i', $html, $matches);
|
|||
|
|
if (!empty($matches[1])) {
|
|||
|
|
foreach ($matches[1] as $rssUrl) {
|
|||
|
|
$rssUrls[] = $rssUrl;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查找 <link> 标签中的 feed
|
|||
|
|
preg_match_all('/<link[^>]+type=["\'](application\/rss\+xml|application\/atom\+xml|application\/rdf\+xml)["\'][^>]+href=["\']([^"\']+)["\'][^>]*>/i', $html, $matches);
|
|||
|
|
if (!empty($matches[2])) {
|
|||
|
|
foreach ($matches[2] as $rssUrl) {
|
|||
|
|
$rssUrls[] = $rssUrl;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 去重并转换相对路径为绝对路径
|
|||
|
|
$rssUrls = array_unique($rssUrls);
|
|||
|
|
$absoluteRssUrls = array();
|
|||
|
|
$baseUrl = parse_url($url, PHP_URL_SCHEME) . '://' . parse_url($url, PHP_URL_HOST);
|
|||
|
|
|
|||
|
|
foreach ($rssUrls as $rssUrl) {
|
|||
|
|
if (strpos($rssUrl, 'http') === 0) {
|
|||
|
|
$absoluteRssUrls[] = $rssUrl;
|
|||
|
|
} else if (strpos($rssUrl, '/') === 0) {
|
|||
|
|
$absoluteRssUrls[] = $baseUrl . $rssUrl;
|
|||
|
|
} else {
|
|||
|
|
$absoluteRssUrls[] = $baseUrl . '/' . $rssUrl;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 准备返回结果
|
|||
|
|
$result = array(
|
|||
|
|
'success' => true,
|
|||
|
|
'siteInfo' => array(
|
|||
|
|
'title' => $siteTitle,
|
|||
|
|
'description' => $siteDescription,
|
|||
|
|
'url' => $url
|
|||
|
|
)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (empty($absoluteRssUrls)) {
|
|||
|
|
$result['hasRss'] = false;
|
|||
|
|
$result['message'] = '成功获取网站信息,但未找到RSS地址';
|
|||
|
|
} else {
|
|||
|
|
$result['hasRss'] = true;
|
|||
|
|
$result['rssUrls'] = $absoluteRssUrls;
|
|||
|
|
$result['message'] = '成功获取网站信息,找到 ' . count($absoluteRssUrls) . ' 个RSS地址';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$this->response->throwJson($result);
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取网站信息失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有RSS信息并保存到缓存
|
|||
|
|
*/
|
|||
|
|
public function getRssFeeds()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$result = UrlNav_Plugin::manualRefreshRss();
|
|||
|
|
|
|||
|
|
if ($result['success']) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => true,
|
|||
|
|
'total' => $result['totalFeeds'] ?? 0,
|
|||
|
|
'successCount' => $result['successCount'] ?? 0,
|
|||
|
|
'newArticles' => $result['newArticles'] ?? 0,
|
|||
|
|
'urlCount' => $result['urlCount'] ?? 0,
|
|||
|
|
'message' => $result['message']
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => $result['message']
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
$this->response->throwJson(array(
|
|||
|
|
'success' => false,
|
|||
|
|
'message' => '获取RSS信息失败: ' . $e->getMessage()
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
?>
|