chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+972
View File
@@ -0,0 +1,972 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
/**
* 插件健康检查命令(合并版)
*
* 融合原 addon:health(人工排查 + --fix 自动修复)与 addon:health-check
* (系统环境 / 缓存系统检查 + JSON/HTML 输出 + 告警 + 报告存档)的能力。
*
* php think addon:health # 系统级 + 全部插件健康检查
* php think addon:health <name> # 单独检查某个插件(含详情)
* php think addon:health --detail # 显示详细信息
* php think addon:health --fix # 尝试自动修复问题(清缓存/修权限)
* php think addon:health --output=json # 输出 JSON(机器消费)
* php think addon:health --output=html # 输出 HTML 报告
* php think addon:health --send-alert # 异常时发送告警
* php think addon:health --webhook=URL # 告警 Webhook 地址
* php think addon:health --email=ADDR # 告警邮箱
* php think addon:health --save-report # 存档检查报告(runtime 保留 30 天)
*/
declare(strict_types=1);
namespace ywxapp\command;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Log;
use think\facade\Config;
use think\facade\Cache;
use ywxapp\service\AddonPerformanceMonitor;
/**
* AddonHealth 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class AddonHealth extends Command
{
/**
* 配置命令
*/
protected function configure()
{
$this->setName('addon:health')
->addArgument('addon', Argument::OPTIONAL, '插件名称(留空则检查全部插件 + 系统环境)')
->addOption('detail', 'd', Option::VALUE_NONE, '显示详细信息')
->addOption('fix', 'f', Option::VALUE_NONE, '尝试自动修复问题(清缓存 / 修权限)')
->addOption('output', 'o', Option::VALUE_OPTIONAL, '输出格式 (text, json, html)', 'text')
->addOption('send-alert', 'a', Option::VALUE_NONE, '异常时发送告警通知')
->addOption('webhook', 'w', Option::VALUE_OPTIONAL, '告警 Webhook URL')
->addOption('email', 'e', Option::VALUE_OPTIONAL, '告警邮箱')
->addOption('save-report', 's', Option::VALUE_NONE, '保存检查报告')
->setDescription('插件健康检查(含系统环境 / 缓存 / 文件 / 性能 / 依赖 / 告警)');
}
/**
* 执行命令
*
* @param Input $input 输入
* @param Output $output 输出
* @return int 退出码(0 健康 / 1 异常)
*/
protected function execute(Input $input, Output $output): int
{
$addon = $input->getArgument('addon');
$detail = $input->getOption('detail');
$fix = $input->getOption('fix');
$outputFormat = $input->getOption('output') ?: 'text';
$sendAlert = $input->getOption('send-alert');
$webhookUrl = $input->getOption('webhook');
$email = $input->getOption('email');
$saveReport = $input->getOption('save-report');
// 仅文本模式打印标题横幅,避免污染 JSON/HTML 输出(JSON/HTML 模式下保持纯输出)
if ($outputFormat === 'text') {
$output->writeln('<info>=== 插件健康检查 ===</info>');
$output->writeln('');
}
// 构建检查报告(始终含系统环境 + 缓存系统检查)
$report = $this->buildReport($addon, $detail);
// 按格式输出
switch ($outputFormat) {
case 'json':
$this->outputJson($report, $output);
break;
case 'html':
$this->outputHtml($report, $output);
break;
default:
$this->outputText($report, $output);
// 单插件 + --detail 时,补充版本 / 作者 / 依赖等详细信息
if ($addon && $detail) {
$this->outputSingleDetail($addon, $report['addon'][$addon] ?? [], $output);
}
break;
}
// 自动修复
if ($fix) {
$this->runAutoFix($report, $output);
}
// 发送告警
if ($sendAlert && !$report['healthy']) {
$this->sendAlert($report, $webhookUrl, $email, $output);
}
// 保存报告
if ($saveReport) {
$this->saveReport($report, $output);
}
return $report['healthy'] ? 0 : 1;
}
/**
* 构建健康检查报告
*/
private function buildReport(?string $addon, bool $detail): array
{
$report = [
'healthy' => true,
'timestamp' => time(),
'checks' => [],
'issues' => [],
'warnings' => [],
'addon' => []
];
// 1/2. 系统环境 + 缓存系统(始终检查,属全局健康)
$report['checks']['system'] = $this->checkSystemEnvironment();
$report['checks']['cache'] = $this->checkCacheSystem();
if ($addon) {
// 单插件深检
$report['addon'][$addon] = $this->getAddonHealth($addon);
// --detail 时把性能采样并入报告,使 JSON/HTML 输出也包含详情
// version/author/dependencies 等已在 getAddonHealth 返回中,此处补性能)
if ($detail && !empty($report['addon'][$addon]['enabled'])) {
$stats = AddonPerformanceMonitor::getPerformanceStats($addon);
if ($stats['total_calls'] > 0) {
$report['addon'][$addon]['performance'] = $stats;
}
}
} else {
// 全量:补充文件 / 性能 / 依赖 / 状态聚合检查
$report['checks']['files'] = $this->checkAddonFiles();
$report['checks']['performance'] = $this->checkAddonPerformance();
$report['checks']['dependencies'] = $this->checkDependencies();
$report['checks']['status'] = $this->checkaddontatus();
$addon = Config::get('addon', []);
foreach ($addon as $a) {
if (!isset($a['name'])) {
continue;
}
$report['addon'][$a['name']] = $this->getAddonHealth($a['name']);
}
}
// 聚合 checks 中的问题 / 警告
foreach ($report['checks'] as $checkResult) {
if (!$checkResult['healthy']) {
$report['healthy'] = false;
$report['issues'] = array_merge($report['issues'], $checkResult['issues']);
}
if (!empty($checkResult['warnings'])) {
$report['warnings'] = array_merge($report['warnings'], $checkResult['warnings']);
}
}
// 插件个体健康状态影响整体
foreach ($report['addon'] as $health) {
if (!$health['healthy']) {
$report['healthy'] = false;
}
}
return $report;
}
/**
* 获取单个插件健康状态(丰富信息:目录 / 文件 / info / 依赖 / 扩展 / 主类 / 性能)
*/
private function getAddonHealth(string $addon): array
{
$health = [
'healthy' => true,
'issues' => [],
'warnings' => [],
'enabled' => false,
'install_time' => null
];
$addonPath = ADDON_PATH . $addon;
// 检查插件目录
if (!is_dir($addonPath)) {
$health['healthy'] = false;
$health['issues'][] = "插件目录不存在: {$addonPath}";
return $health;
}
// 检查必需文件
$requiredFiles = ['info.php', 'Addon.php'];
foreach ($requiredFiles as $file) {
if (!is_file($addonPath . DIRECTORY_SEPARATOR . $file)) {
$health['healthy'] = false;
$health['issues'][] = "缺少必需文件: {$file}";
}
}
// 检查 info.php
$infoFile = $addonPath . DIRECTORY_SEPARATOR . 'info.php';
if (is_file($infoFile)) {
$info = include $infoFile;
$health['enabled'] = ($info['state'] ?? false) == 1;
$health['install_time'] = $info['install_time'] ?? null;
$health['version'] = $info['version'] ?? null;
$health['author'] = $info['author'] ?? null;
$health['description'] = $info['intro'] ?? null;
// 检查依赖插件
if (isset($info['dependencies']) && !empty($info['dependencies'])) {
$health['dependencies'] = $info['dependencies'];
foreach ($info['dependencies'] as $dep) {
if (!$this->isAddonInstalled($dep)) {
$health['healthy'] = false;
$health['issues'][] = "依赖插件 {$dep} 未安装";
} elseif (!$this->isAddonEnabled($dep)) {
$health['healthy'] = false;
$health['issues'][] = "依赖插件 {$dep} 未启用";
}
}
}
// 检查 PHP 扩展
if (isset($info['extensions']) && !empty($info['extensions'])) {
$health['extensions'] = $info['extensions'];
foreach ($info['extensions'] as $ext) {
if (!extension_loaded($ext)) {
$health['healthy'] = false;
$health['issues'][] = "缺少PHP扩展: {$ext}";
}
}
}
}
// 检查插件主类与必需方法
$addonClass = '\\addon\\' . $addon . '\\Addon';
if (class_exists($addonClass)) {
try {
$instance = app()->make($addonClass);
foreach (['install', 'uninstall'] as $method) {
if (!method_exists($instance, $method)) {
$health['healthy'] = false;
$health['issues'][] = "缺少必需方法: {$method}()";
}
}
} catch (\Exception $e) {
$health['healthy'] = false;
$health['issues'][] = "插件类初始化失败: " . $e->getMessage();
}
}
// 性能检查
if ($health['enabled']) {
$stats = AddonPerformanceMonitor::getPerformanceStats($addon);
if ($stats['total_calls'] > 10) {
if ($stats['avg_execution_time'] > AddonPerformanceMonitor::PERFORMANCE_THRESHOLD) {
$health['warnings'][] = "执行时间过长: " . number_format($stats['avg_execution_time'], 2) . "";
}
if ($stats['avg_memory_usage'] > AddonPerformanceMonitor::MEMORY_THRESHOLD) {
$health['warnings'][] = "内存使用过多: " . number_format($stats['avg_memory_usage'] / 1024 / 1024, 2) . "MB";
}
if ($stats['success_rate'] < 90) {
$health['healthy'] = false;
$health['issues'][] = "成功率过低: " . number_format($stats['success_rate'], 2) . "%";
}
}
}
return $health;
}
/**
* 检查系统环境(PHP 版本 / 必需扩展 / runtime 可写 / 磁盘空间)
*/
private function checkSystemEnvironment(): array
{
$result = [
'healthy' => true,
'issues' => [],
'warnings' => [],
'details' => []
];
// PHP 版本
$phpVersion = PHP_VERSION;
$result['details']['php_version'] = $phpVersion;
if (version_compare($phpVersion, '8.0.0', '<')) {
$result['healthy'] = false;
$result['issues'][] = "PHP版本过低 ({$phpVersion}),要求 >= 8.0.0";
}
// 必需扩展
foreach (['json', 'mbstring', 'curl', 'zip'] as $ext) {
$loaded = extension_loaded($ext);
$result['details'][$ext] = $loaded;
if (!$loaded) {
$result['healthy'] = false;
$result['issues'][] = "缺少PHP扩展: {$ext}";
}
}
// runtime 目录可写
$runtimePath = root_path() . 'runtime';
if (!is_writable($runtimePath)) {
$result['healthy'] = false;
$result['issues'][] = "runtime目录不可写: {$runtimePath}";
}
// 磁盘空间
$freeSpace = disk_free_space(root_path());
$result['details']['disk_free_space'] = $this->formatBytes((int) $freeSpace);
if ($freeSpace < 104857600) { // 小于 100MB
$result['warnings'][] = "磁盘空间不足,剩余: " . $this->formatBytes((int) $freeSpace);
}
return $result;
}
/**
* 检查缓存系统
*/
private function checkCacheSystem(): array
{
$result = [
'healthy' => true,
'issues' => [],
'warnings' => [],
'details' => []
];
try {
$testKey = 'health_check_test_' . time();
$testValue = ['test' => true, 'timestamp' => time()];
Cache::set($testKey, $testValue, 60);
$retrieved = Cache::get($testKey);
if ($retrieved === false) {
$result['healthy'] = false;
$result['issues'][] = "缓存写入/读取失败";
} else {
$result['details']['cache_test'] = 'passed';
}
Cache::delete($testKey);
// 插件缓存命中情况(仅警告,不影响健康)
$addon = Config::get('addon', []);
foreach ($addon as $addon) {
if (!isset($addon['name']) || !($addon['state'] ?? false)) {
continue;
}
if (Cache::get('addon_config_' . $addon['name']) === false) {
$result['warnings'][] = "插件 {$addon['name']} 缓存未命中,可能影响性能";
}
}
} catch (\Exception $e) {
$result['healthy'] = false;
$result['issues'][] = "缓存系统异常: " . $e->getMessage();
}
return $result;
}
/**
* 检查插件文件完整性(全量)
*/
private function checkAddonFiles(): array
{
$result = [
'healthy' => true,
'issues' => [],
'warnings' => [],
'details' => []
];
$addon = Config::get('addon', []);
foreach ($addon as $addon) {
if (!isset($addon['name'])) {
continue;
}
$addonPath = ADDON_PATH . $addon['name'];
$result['details'][$addon['name']] = [];
if (!is_dir($addonPath)) {
$result['healthy'] = false;
$result['issues'][] = "插件 {$addon['name']} 目录不存在";
continue;
}
foreach (['info.php', 'Addon.php'] as $file) {
$filePath = $addonPath . DIRECTORY_SEPARATOR . $file;
$exists = is_file($filePath);
$result['details'][$addon['name']][$file] = $exists;
if (!$exists) {
$result['healthy'] = false;
$result['issues'][] = "插件 {$addon['name']} 缺少必需文件: {$file}";
}
}
// 文件权限(不可读即警告)
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($addonPath),
\RecursiveIteratorIterator::SELF_FIRST
);
$permissionIssues = [];
foreach ($iterator as $item) {
if ($item->isFile() && ($item->getPerms() & 0x0004) === 0) {
$permissionIssues[] = $item->getPathname();
}
}
if (!empty($permissionIssues)) {
$result['warnings'][] = "插件 {$addon['name']} 有文件权限问题";
$result['details'][$addon['name']]['permission_issues'] = count($permissionIssues);
}
}
return $result;
}
/**
* 检查插件性能(全量)
*/
private function checkAddonPerformance(): array
{
$result = [
'healthy' => true,
'issues' => [],
'warnings' => [],
'details' => []
];
$problematicaddon = AddonPerformanceMonitor::getProblematicaddon();
if (!empty($problematicaddon)) {
$result['healthy'] = false;
foreach ($problematicaddon as $addon => $info) {
$result['issues'][] = "插件 {$addon} 性能异常: " . implode(', ', $info['issues']);
$result['details'][$addon] = $info['issues'];
}
}
$overview = AddonPerformanceMonitor::getAllPerformanceOverview();
$totalCalls = $totalTime = $totalSuccess = 0;
foreach ($overview as $stats) {
$totalCalls += $stats['total_calls'];
$totalTime += $stats['avg_execution_time'] * $stats['total_calls'];
$totalSuccess += ($stats['success_rate'] / 100) * $stats['total_calls'];
}
if ($totalCalls > 0) {
$avgTime = $totalTime / $totalCalls;
$successRate = ($totalSuccess / $totalCalls) * 100;
$result['details']['total_calls'] = $totalCalls;
$result['details']['avg_execution_time'] = $avgTime;
$result['details']['overall_success_rate'] = $successRate;
if ($avgTime > 1.0) {
$result['warnings'][] = "总体平均执行时间过长: " . number_format($avgTime * 1000, 2) . " ms";
}
if ($successRate < 95) {
$result['healthy'] = false;
$result['issues'][] = "总体成功率过低: " . number_format($successRate, 2) . "%";
}
}
return $result;
}
/**
* 检查依赖关系(全量)
*/
private function checkDependencies(): array
{
$result = [
'healthy' => true,
'issues' => [],
'warnings' => [],
'details' => []
];
$addon = Config::get('addon', []);
$installedaddon = [];
foreach ($addon as $addon) {
if (isset($addon['name'])) {
$installedaddon[$addon['name']] = $addon['state'] ?? false;
}
}
foreach ($addon as $addon) {
if (!isset($addon['name'])) {
continue;
}
$addonName = $addon['name'];
$result['details'][$addonName] = [];
$addonClass = '\\addon\\' . $addonName . '\\Addon';
if (!class_exists($addonClass)) {
continue;
}
try {
$instance = app()->make($addonClass);
if (method_exists($instance, 'getDependencies')) {
$dependencies = $instance->getDependencies();
$result['details'][$addonName]['dependencies'] = $dependencies;
foreach ($dependencies as $dep) {
if (!isset($installedaddon[$dep])) {
$result['healthy'] = false;
$result['issues'][] = "插件 {$addonName} 依赖插件 {$dep} 未安装";
} elseif (!$installedaddon[$dep]) {
$result['healthy'] = false;
$result['issues'][] = "插件 {$addonName} 依赖插件 {$dep} 未启用";
}
}
}
if (method_exists($instance, 'getExtensions')) {
$extensions = $instance->getExtensions();
$result['details'][$addonName]['extensions'] = $extensions;
foreach ($extensions as $ext) {
if (!extension_loaded($ext)) {
$result['healthy'] = false;
$result['issues'][] = "插件 {$addonName} 需要PHP扩展: {$ext}";
}
}
}
} catch (\Exception $e) {
$result['warnings'][] = "无法检查插件 {$addonName} 的依赖: " . $e->getMessage();
}
}
return $result;
}
/**
* 检查插件状态(全量)
*/
private function checkaddontatus(): array
{
$result = [
'healthy' => true,
'issues' => [],
'warnings' => [],
'details' => []
];
$addon = Config::get('addon', []);
$enabledCount = $disabledCount = $errorCount = 0;
foreach ($addon as $addon) {
if (!isset($addon['name'])) {
continue;
}
$addonName = $addon['name'];
$isEnabled = ($addon['state'] ?? false) == 1;
if ($isEnabled) {
$enabledCount++;
$addonClass = '\\addon\\' . $addonName . '\\Addon';
if (class_exists($addonClass)) {
try {
app()->make($addonClass);
$result['details'][$addonName] = 'healthy';
} catch (\Exception $e) {
$errorCount++;
$result['healthy'] = false;
$result['issues'][] = "插件 {$addonName} 加载失败: " . $e->getMessage();
$result['details'][$addonName] = 'error';
}
} else {
$errorCount++;
$result['healthy'] = false;
$result['issues'][] = "插件 {$addonName} 主类不存在";
$result['details'][$addonName] = 'missing';
}
} else {
$disabledCount++;
$result['details'][$addonName] = 'disabled';
}
}
$result['details']['enabled_count'] = $enabledCount;
$result['details']['disabled_count'] = $disabledCount;
$result['details']['error_count'] = $errorCount;
return $result;
}
/**
* 输出文本格式
*/
private function outputText(array $report, Output $output): void
{
$output->writeln('检查时间: ' . date('Y-m-d H:i:s', $report['timestamp']));
$output->writeln('整体状态: ' . ($report['healthy'] ? '<info>健康</info>' : '<error>异常</error>'));
$output->writeln('');
// 检查项
foreach ($report['checks'] as $checkName => $checkResult) {
$status = $checkResult['healthy'] ? '<info>✓</info>' : '<error>✗</error>';
$output->writeln("{$status} {$this->getCheckName($checkName)}");
foreach ($checkResult['issues'] ?? [] as $issue) {
$output->writeln(" <error>✗</error> {$issue}");
}
foreach ($checkResult['warnings'] ?? [] as $warning) {
$output->writeln(" <comment>⚠</comment> {$warning}");
}
}
// 插件状态
if (!empty($report['addon'])) {
$output->writeln('');
$output->writeln('<comment>插件状态:</comment>');
foreach ($report['addon'] as $addon => $health) {
$status = $health['healthy'] ? '<info>✓</info>' : '<error>✗</error>';
$output->writeln("{$status} {$addon}" .
($health['enabled'] ? ' <comment>(已启用)</comment>' : ' <comment>(已禁用)</comment>'));
foreach ($health['issues'] ?? [] as $issue) {
$output->writeln(" <error>✗</error> {$issue}");
}
foreach ($health['warnings'] ?? [] as $warning) {
$output->writeln(" <comment>⚠</comment> {$warning}");
}
}
}
// 汇总
$output->writeln('');
$output->writeln('<info>=== 检查汇总 ===</info>');
$output->writeln('总问题数: ' . count($report['issues']));
$output->writeln('总警告数: ' . count($report['warnings']));
$output->writeln('插件总数: ' . count($report['addon']));
$output->writeln('健康插件: ' . count(array_filter($report['addon'], fn($a) => $a['healthy'])));
}
/**
* 单插件详细信息(--detail 时补充)
*/
private function outputSingleDetail(string $addon, array $health, Output $output): void
{
if (empty($health)) {
return;
}
$output->writeln('');
$output->writeln('<info>详细信息:</info>');
$output->writeln("版本: " . ($health['version'] ?? '未知'));
$output->writeln("作者: " . ($health['author'] ?? '未知'));
$output->writeln("描述: " . ($health['description'] ?? '无'));
$output->writeln("启用状态: " . ($health['enabled'] ? '已启用' : '已禁用'));
$output->writeln("安装时间: " . (isset($health['install_time']) ? date('Y-m-d H:i:s', $health['install_time']) : '未知'));
if (isset($health['dependencies'])) {
$output->writeln("依赖插件: " . implode(', ', $health['dependencies']));
}
if (isset($health['extensions'])) {
$output->writeln("PHP扩展: " . implode(', ', $health['extensions']));
}
if ($health['enabled']) {
$stats = $health['performance'] ?? AddonPerformanceMonitor::getPerformanceStats($addon);
if (!empty($stats) && $stats['total_calls'] > 0) {
$output->writeln('');
$output->writeln('<info>性能状态:</info>');
$output->writeln(" 总调用次数: {$stats['total_calls']}");
$output->writeln(" 平均执行时间: " . number_format($stats['avg_execution_time'] * 1000, 2) . " ms");
$output->writeln(" 成功率: " . number_format($stats['success_rate'], 2) . "%");
if ($stats['avg_execution_time'] > AddonPerformanceMonitor::PERFORMANCE_THRESHOLD) {
$output->writeln(' <error>⚠ 执行时间过长</error>');
}
if ($stats['success_rate'] < 90) {
$output->writeln(' <error>⚠ 成功率过低</error>');
}
}
}
}
/**
* 输出 JSON 格式
*/
private function outputJson(array $report, Output $output): void
{
echo json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
/**
* 输出 HTML 格式
*/
private function outputHtml(array $report, Output $output): void
{
$html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>插件健康检查报告</title>';
$html .= '<style>body{font-family:Arial,sans-serif;margin:20px;}';
$html .= '.healthy{color:#5fb878;}.error{color:#ff5722;}.warning{color:#ffb800;}';
$html .= '.section{margin:20px 0;padding:15px;background:#f9f9f9;border-radius:4px;}';
$html .= '.check{margin:10px 0;padding:10px;background:#fff;border-radius:4px;}';
$html .= '</style></head><body>';
$html .= '<h1>插件健康检查报告</h1>';
$html .= '<p>检查时间: ' . date('Y-m-d H:i:s', $report['timestamp']) . '</p>';
$html .= '<p>整体状态: <span class="' . ($report['healthy'] ? 'healthy' : 'error') . '">' . ($report['healthy'] ? '健康' : '异常') . '</span></p>';
foreach ($report['checks'] as $checkName => $checkResult) {
$html .= '<div class="section">';
$html .= '<h2>' . $this->getCheckName($checkName) . '</h2>';
foreach ($checkResult['issues'] ?? [] as $issue) {
$html .= '<div class="check error">✗ ' . htmlspecialchars($issue) . '</div>';
}
foreach ($checkResult['warnings'] ?? [] as $warning) {
$html .= '<div class="check warning">⚠ ' . htmlspecialchars($warning) . '</div>';
}
$html .= '</div>';
}
$html .= '</body></html>';
echo $html;
}
/**
* 获取检查项中文名
*/
private function getCheckName(string $checkKey): string
{
$names = [
'system' => '系统环境检查',
'cache' => '缓存系统检查',
'files' => '插件文件检查',
'performance' => '插件性能检查',
'dependencies' => '依赖关系检查',
'status' => '插件状态检查'
];
return $names[$checkKey] ?? $checkKey;
}
/**
* 尝试自动修复所有问题(清缓存 / 修权限)
*/
private function runAutoFix(array $report, Output $output): void
{
// 遍历插件问题(缓存测试键已由 checkCacheSystem 自身清理,无需此处重复)
foreach ($report['addon'] as $addon => $health) {
$this->tryAutoFix($addon, $health['issues'] ?? [], $output);
}
}
/**
* 尝试自动修复单个插件问题
*/
private function tryAutoFix(string $addon, array $issues, Output $output): void
{
$fixedCount = 0;
foreach ($issues as $issue) {
// 缓存类问题
if (strpos($issue, '缓存') !== false) {
try {
Cache::delete('addon_config_' . $addon);
Cache::delete('addon_config_version_' . $addon);
$output->writeln("<info> ✓ 已清除插件 {$addon} 缓存</info>");
$fixedCount++;
} catch (\Exception $e) {
$output->writeln("<comment> ⚠ 清除缓存失败: {$e->getMessage()}</comment>");
}
}
// 权限类问题(含 runtime 不可写)
if (strpos($issue, '权限') !== false || strpos($issue, 'runtime') !== false) {
try {
if (strpos($issue, 'runtime') !== false) {
$this->fixPermissions(root_path() . 'runtime');
}
$this->fixPermissions(ADDON_PATH . $addon);
$output->writeln("<info> ✓ 已修复文件权限 ({$addon})</info>");
$fixedCount++;
} catch (\Exception $e) {
$output->writeln("<comment> ⚠ 修复权限失败: {$e->getMessage()}</comment>");
}
}
}
if ($fixedCount > 0) {
$output->writeln("<info>已自动修复 {$fixedCount} 个问题({$addon}</info>");
} else {
$output->writeln("<comment>插件 {$addon} 无可通过 --fix 自动修复的问题</comment>");
}
}
/**
* 修复文件权限(递归 chmod)
*/
private function fixPermissions(string $path): void
{
if (!is_dir($path)) {
return;
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
chmod($item->getPathname(), $item->isDir() ? 0755 : 0644);
}
chmod($path, 0755);
}
/**
* 发送告警
*/
private function sendAlert(array $report, ?string $webhookUrl, ?string $email, Output $output): void
{
$message = "插件系统健康检查发现异常!\n\n";
$message .= "检查时间: " . date('Y-m-d H:i:s', $report['timestamp']) . "\n";
$message .= "总问题数: " . count($report['issues']) . "\n";
$message .= "总警告数: " . count($report['warnings']) . "\n\n";
if (!empty($report['issues'])) {
$message .= "问题列表:\n";
foreach ($report['issues'] as $issue) {
$message .= "- {$issue}\n";
}
$message .= "\n";
}
Log::error('插件健康检查异常', $report);
if ($webhookUrl) {
try {
$this->sendWebhook($webhookUrl, $report);
$output->writeln('<info>已发送Webhook告警</info>');
} catch (\Exception $e) {
$output->writeln('<error>Webhook发送失败: ' . $e->getMessage() . '</error>');
}
}
if ($email) {
try {
$this->sendEmail($email, '插件系统健康检查告警', $message);
$output->writeln('<info>已发送邮件告警</info>');
} catch (\Exception $e) {
$output->writeln('<error>邮件发送失败: ' . $e->getMessage() . '</error>');
}
}
$output->writeln('<error>告警消息: ' . str_replace("\n", " ", $message) . '</error>');
}
/**
* 发送 Webhook
*/
private function sendWebhook(string $url, array $data): void
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
throw new \Exception("Webhook返回错误: HTTP {$httpCode}");
}
}
/**
* 发送邮件
*/
private function sendEmail(string $to, string $subject, string $message): void
{
$headers = "From: noreply@yourdomain.com\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
if (!mail($to, $subject, $message, $headers)) {
throw new \Exception("邮件发送失败");
}
}
/**
* 保存检查报告(runtime/addon_health_reports,保留 30 天)
*/
private function saveReport(array $report, Output $output): void
{
$reportDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'addon_health_reports';
if (!is_dir($reportDir)) {
mkdir($reportDir, 0755, true);
}
$reportFile = $reportDir . DIRECTORY_SEPARATOR . 'health_report_' . date('YmdHis') . '.json';
file_put_contents($reportFile, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
$output->writeln("<info>检查报告已保存: {$reportFile}</info>");
$this->cleanupOldReports($reportDir, 30);
}
/**
* 清理旧报告
*/
private function cleanupOldReports(string $dir, int $days): void
{
$files = glob($dir . DIRECTORY_SEPARATOR . 'health_report_*.json');
$cutoffTime = time() - ($days * 86400);
foreach ($files as $file) {
if (filemtime($file) < $cutoffTime) {
unlink($file);
}
}
}
/**
* 格式化字节
*/
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
/**
* 检查插件是否已安装
*/
private function isAddonInstalled(string $addon): bool
{
return is_dir(ADDON_PATH . $addon) && is_file(ADDON_PATH . $addon . DIRECTORY_SEPARATOR . 'info.php');
}
/**
* 检查插件是否已启用
*/
private function isAddonEnabled(string $addon): bool
{
$infoFile = ADDON_PATH . $addon . DIRECTORY_SEPARATOR . 'info.php';
if (!is_file($infoFile)) {
return false;
}
$info = include $infoFile;
return isset($info['state']) && $info['state'] == 1;
}
}