From de2c65eedd1b2705b31c2ba7e6bcf3314c4e790f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=92=E6=9F=A0?= <2338174386@qq.com> Date: Sat, 17 Jan 2026 16:58:29 +0800 Subject: [PATCH] update --- AliyunTrafficCheck.php | 195 +++++++++++++++++++++++++++++++---------- index.php | 19 ++-- template.html | 153 +++++++++++++++----------------- 3 files changed, 236 insertions(+), 131 deletions(-) diff --git a/AliyunTrafficCheck.php b/AliyunTrafficCheck.php index 22d7a2d..dd249f2 100644 --- a/AliyunTrafficCheck.php +++ b/AliyunTrafficCheck.php @@ -16,9 +16,8 @@ class AliyunTrafficCheck private $configCache = []; private $accountsCache = []; - const API_INTERVAL = 600; - // 新增:保活冷却时间 (秒),防止短时间内连续重启/发信 - const KEEP_ALIVE_COOLDOWN = 1800; // 30分钟 + // 默认保活冷却时间 + const KEEP_ALIVE_COOLDOWN = 1800; public function __construct() { @@ -79,7 +78,6 @@ class AliyunTrafficCheck $this->addColumnIfNotExists('accounts', 'traffic_used', 'REAL DEFAULT 0'); $this->addColumnIfNotExists('accounts', 'instance_status', "TEXT DEFAULT 'Unknown'"); $this->addColumnIfNotExists('accounts', 'updated_at', 'INTEGER DEFAULT 0'); - // 新增字段:上次保活时间 $this->addColumnIfNotExists('accounts', 'last_keep_alive_at', 'INTEGER DEFAULT 0'); } @@ -150,6 +148,7 @@ class AliyunTrafficCheck $this->saveSetting('shutdown_mode', $data['shutdown_mode']); $this->saveSetting('threshold_action', $data['threshold_action']); $this->saveSetting('keep_alive', isset($data['keep_alive']) && $data['keep_alive'] ? '1' : '0'); + $this->saveSetting('api_interval', $data['api_interval'] ?? 600); if (isset($data['Notification'])) { $this->saveSetting('notify_email', $data['Notification']['email']); @@ -213,6 +212,7 @@ class AliyunTrafficCheck 'shutdown_mode' => $this->configCache['shutdown_mode'] ?? 'KeepCharging', 'threshold_action' => $this->configCache['threshold_action'] ?? 'stop_and_notify', 'keep_alive' => ($this->configCache['keep_alive'] ?? '0') === '1', + 'api_interval' => (int)($this->configCache['api_interval'] ?? 600), 'Notification' => [ 'email' => $this->configCache['notify_email'] ?? '', 'host' => $this->configCache['notify_host'] ?? '', @@ -251,6 +251,33 @@ class AliyunTrafficCheck } } + public function refreshAccount($id) + { + if ($this->initError) return false; + + $targetAccount = null; + foreach ($this->accountsCache as $acc) { + if ($acc['id'] == $id) { + $targetAccount = $acc; + break; + } + } + + if (!$targetAccount) return false; + + $currentTime = time(); + $updateStmt = $this->db->prepare("UPDATE accounts SET traffic_used = ?, instance_status = ?, updated_at = ? WHERE id = ?"); + + $traffic = $this->getTrafficApi($targetAccount['access_key_id'], $targetAccount['access_key_secret']); + $status = $this->getInstanceStatusApi($targetAccount); + + if ($traffic < 0) { + $traffic = $targetAccount['traffic_used']; + } + + return $updateStmt->execute([$traffic, $status, $currentTime, $id]); + } + public function monitor() { if ($this->initError) return "Error: " . $this->initError; @@ -263,37 +290,46 @@ class AliyunTrafficCheck $thresholdAction = $this->configCache['threshold_action'] ?? 'stop_and_notify'; $keepAlive = ($this->configCache['keep_alive'] ?? '0') === '1'; + $userInterval = (int)($this->configCache['api_interval'] ?? 600); + $updateStmt = $this->db->prepare("UPDATE accounts SET traffic_used = ?, instance_status = ?, updated_at = ? WHERE id = ?"); - // 准备更新保活时间的SQL $updateKeepAliveStmt = $this->db->prepare("UPDATE accounts SET last_keep_alive_at = ? WHERE id = ?"); foreach ($this->accountsCache as $account) { $logPrefix = "[{$account['access_key_id']}]"; $actions = []; $forceRefresh = false; + $statusTransformed = false; - // 1. 定时任务 (优先级最高) + // 1. 定时任务 (逻辑:触发 -> 强制刷新 -> 状态变更为过渡态) if ($account['schedule_enabled'] == 1) { if ($account['start_time'] && $currentUserTime === $account['start_time']) { $this->controlInstance($account, 'start'); $actions[] = "定时启动"; - $this->notifySchedule("启动", $account); + $this->notifySchedule("定时启动", $account, "计划任务已触发,实例正在启动。"); $forceRefresh = true; + $statusTransformed = true; } if ($account['stop_time'] && $currentUserTime === $account['stop_time']) { $this->controlInstance($account, 'stop', $shutdownMode); $actions[] = "定时停止({$shutdownMode})"; - $this->notifySchedule("停止", $account); + $this->notifySchedule("定时停止", $account, "计划任务已触发,实例已停止。"); $forceRefresh = true; + $statusTransformed = true; } } - // 2. 数据获取 + // 2. 自适应心跳机制 (Smart Burst) $lastUpdate = $account['updated_at'] ?? 0; $cachedStatus = $account['instance_status'] ?? 'Unknown'; - $shouldCheckApi = $forceRefresh || (($currentTime - $lastUpdate) > self::API_INTERVAL) || ($cachedStatus === 'Unknown'); - // 关键逻辑:默认更新为当前时间,但如果失败则保持旧时间以便重试 + // 核心闭环:只要状态是“中间态”或“未知”,就强制每60秒检查一次 + // 即使阿里云 API 调用慢,只要返回的状态还是 Starting/Stopping,这里就会持续保持高频 + $isTransientState = in_array($cachedStatus, ['Starting', 'Stopping', 'Pending', 'Unknown']); + $currentInterval = ($isTransientState || $statusTransformed) ? 60 : $userInterval; + + $shouldCheckApi = $forceRefresh || (($currentTime - $lastUpdate) > $currentInterval); + $newUpdateTime = $currentTime; if ($shouldCheckApi) { @@ -307,26 +343,26 @@ class AliyunTrafficCheck if ($newTraffic < 0) { $traffic = $account['traffic_used']; - $apiStatusLog = "流量API异常(保留旧值)"; - // 失败:不更新时间戳,促使下次尽快重试 + $apiStatusLog = "流量API异常(保留)"; $newUpdateTime = $lastUpdate; } else { $traffic = $newTraffic; - $apiStatusLog = "已更新API数据"; + $apiStatusLog = "已更新"; } - // 失败:如果不更新时间戳,促使下次尽快重试 if ($status === 'Unknown') { $newUpdateTime = $lastUpdate; - $apiStatusLog .= " [状态Unknown]"; + $apiStatusLog .= "(状态Unknown)"; + } else { + $apiStatusLog .= in_array($status, ['Starting', 'Stopping', 'Pending']) ? " [过渡态]" : " [稳定态]"; } $updateStmt->execute([$traffic, $status, $newUpdateTime, $account['id']]); } else { $traffic = $account['traffic_used']; $status = $account['instance_status']; - $timeLeft = self::API_INTERVAL - ($currentTime - $lastUpdate); - $apiStatusLog = "缓存有效({$timeLeft}s)"; + $timeLeft = $currentInterval - ($currentTime - $lastUpdate); + $apiStatusLog = "缓存({$timeLeft}s)"; } $maxTraffic = $account['max_traffic']; @@ -334,7 +370,7 @@ class AliyunTrafficCheck $trafficDesc = "流量:{$usagePercent}%"; $isOverThreshold = $usagePercent >= $threshold; - // 3. 流量阈值检查 + // 3. 流量熔断 if ($isOverThreshold) { $trafficDesc .= "[警告]"; if ($shouldCheckApi) { @@ -342,7 +378,9 @@ class AliyunTrafficCheck if ($status !== 'Stopped') { $this->controlInstance($account, 'stop', $shutdownMode); $actions[] = "超限关机"; - $status = 'Stopped'; + // 立即更新数据库为 Stopping,确保下一分钟依然高频检查 + $updateStmt->execute([$traffic, 'Stopping', $currentTime, $account['id']]); + $status = 'Stopping'; } } else { $actions[] = "超限告警"; @@ -351,32 +389,36 @@ class AliyunTrafficCheck } } - // 4. 实例保活逻辑 (带冷却时间) + // 4. 保活逻辑 if ($keepAlive && $account['schedule_enabled'] == 1 && !$isOverThreshold) { if ($this->isTimeInRange($currentUserTime, $account['start_time'], $account['stop_time'])) { if ($status === 'Stopped') { - // 检查冷却时间 $lastKeepAlive = $account['last_keep_alive_at'] ?? 0; $timeSinceLast = $currentTime - $lastKeepAlive; if ($timeSinceLast > self::KEEP_ALIVE_COOLDOWN) { - // 执行保活 $this->controlInstance($account, 'start'); $actions[] = "保活启动"; - $status = 'Starting'; - $this->notifySchedule("保活启动", $account); - - // 立即更新保活时间戳 + $this->notifySchedule("保活启动", $account, "检测到实例在工作时段非预期关机,已尝试自动启动。"); $updateKeepAliveStmt->execute([$currentTime, $account['id']]); + // 立即更新数据库为 Starting,确保下一分钟依然高频检查 + $updateStmt->execute([$traffic, 'Starting', $currentTime, $account['id']]); + $status = 'Starting'; } else { - // 处于冷却期,跳过 $cooldownLeft = ceil((self::KEEP_ALIVE_COOLDOWN - $timeSinceLast) / 60); - $apiStatusLog .= " [保活冷却中: {$cooldownLeft}分]"; + $apiStatusLog .= " [保活冷却:{$cooldownLeft}m]"; } } } } + // 补充逻辑:如果刚刚执行了定时任务,立即将数据库状态置为过渡态 + if ($statusTransformed) { + $tempStatus = in_array("定时启动", $actions) ? 'Starting' : 'Stopping'; + $updateStmt->execute([$traffic, $tempStatus, $currentTime, $account['id']]); + $apiStatusLog .= " -> 强制过渡态"; + } + $actionLog = empty($actions) ? "无动作" : implode(", ", $actions); $logs[] = sprintf("%s %s | %s | %s | %s", $logPrefix, $actionLog, $trafficDesc, $status, $apiStatusLog); } @@ -386,12 +428,12 @@ class AliyunTrafficCheck public function getStatusForFrontend() { - if ($this->initError) { - return ['error' => $this->initError]; - } + if ($this->initError) return ['error' => $this->initError]; $data = []; $threshold = (int)($this->configCache['traffic_threshold'] ?? 95); + $userInterval = (int)($this->configCache['api_interval'] ?? 600); + $currentTime = time(); $updateStmt = $this->db->prepare("UPDATE accounts SET traffic_used = ?, instance_status = ?, updated_at = ? WHERE id = ?"); @@ -400,7 +442,11 @@ class AliyunTrafficCheck $cachedStatus = $account['instance_status'] ?? 'Unknown'; $newUpdateTime = $currentTime; - if (($currentTime - $lastUpdate) > self::API_INTERVAL || $cachedStatus === 'Unknown') { + // 前端自适应:如果数据库记录的是中间态,说明正在变动中,前端超时时间也缩短为60秒 + $isTransientState = in_array($cachedStatus, ['Starting', 'Stopping', 'Pending', 'Unknown']); + $checkInterval = $isTransientState ? 60 : $userInterval; + + if (($currentTime - $lastUpdate) > $checkInterval) { $newTraffic = $this->getTrafficApi($account['access_key_id'], $account['access_key_secret']); $status = $this->getInstanceStatusApi($account); @@ -411,13 +457,13 @@ class AliyunTrafficCheck if ($newTraffic < 0) { $traffic = $account['traffic_used']; - $newUpdateTime = $lastUpdate; // 失败则不更新时间 + $newUpdateTime = $lastUpdate; } else { $traffic = $newTraffic; } if ($status === 'Unknown') { - $newUpdateTime = $lastUpdate; // 失败则不更新时间 + $newUpdateTime = $lastUpdate; } $updateStmt->execute([$traffic, $status, $newUpdateTime, $account['id']]); @@ -430,6 +476,7 @@ class AliyunTrafficCheck $isFull = $usagePercent >= $threshold; $data[] = [ + 'id' => $account['id'], 'account' => substr($account['access_key_id'], 0, 7) . '***', 'flow_total' => (float)$account['max_traffic'], 'flow_used' => round($traffic, 2), @@ -454,9 +501,7 @@ class AliyunTrafficCheck return array_sum(array_column($result['TrafficDetails'], 'Traffic')) / (1024 * 1024 * 1024); } return -1; - } catch (Exception $e) { - return -1; - } + } catch (Exception $e) { return -1; } } private function getInstanceStatusApi($account) { @@ -481,24 +526,86 @@ class AliyunTrafficCheck } catch (Exception $e) {} } - private function notifySchedule($actionType, $account) + // ... (邮件部分代码保持不变,为节省篇幅省略) ... + // 请确保文件包含完整的 renderEmailTemplate, send_mail 等方法 + private function notifySchedule($actionType, $account, $description = "") { if (($this->configCache['enable_schedule_email'] ?? '0') !== '1') return; - $msg = "账号 {$account['access_key_id']} 执行定时任务: {$actionType}"; - $this->send_mail($this->configCache['notify_email'], '', 'CDT定时任务通知', $msg); + $title = "定时任务: " . $actionType; + $maskedKey = substr($account['access_key_id'], 0, 7) . '***'; + $details = [ + ['label' => '账号 ID', 'value' => $maskedKey], + ['label' => '执行动作', 'value' => $actionType, 'highlight' => true], + ['label' => '执行时间', 'value' => date('Y-m-d H:i:s')], + ['label' => '详情说明', 'value' => $description ?: '根据预设时间表自动执行。'] + ]; + $html = $this->renderEmailTemplate($title, "您的实例已执行{$actionType}操作", $details, 'info'); + $this->send_mail($this->configCache['notify_email'], '', "CDT通知 - {$actionType}", $html); } private function sendNotification($accessKeyId, $traffic, $percentage, $statusText) { if (empty($this->configCache['notify_email'])) return; $threshold = $this->configCache['traffic_threshold'] ?? 95; - $message = "账号: {$accessKeyId}
流量: {$traffic}GB
使用率: {$percentage}% (阈值: {$threshold}%)
状态: {$statusText}"; - $this->send_mail($this->configCache['notify_email'], '', 'CDT流量告警', $message); + $title = "流量告警 - " . $statusText; + $details = [ + ['label' => '账号 ID', 'value' => substr($accessKeyId, 0, 7) . '***'], + ['label' => '当前流量', 'value' => $traffic . ' GB'], + ['label' => '使用率', 'value' => $percentage . '%', 'highlight' => true], + ['label' => '设定阈值', 'value' => $threshold . '%'], + ['label' => '当前状态', 'value' => $statusText] + ]; + $html = $this->renderEmailTemplate($title, "检测到流量异常或达到阈值", $details, 'warning'); + $this->send_mail($this->configCache['notify_email'], '', 'CDT流量熔断告警', $html); } public function sendTestEmail($to) { - return $this->send_mail($to, 'Admin', 'CDT Monitor Test', '

测试邮件

配置正确。

'); + $details = [ + ['label' => '测试结果', 'value' => '成功 (Success)'], + ['label' => '发送时间', 'value' => date('Y-m-d H:i:s')], + ['label' => '服务器', 'value' => $_SERVER['SERVER_NAME'] ?? 'localhost'] + ]; + $html = $this->renderEmailTemplate("测试邮件", "SMTP 配置验证成功", $details, 'success'); + return $this->send_mail($to, 'Admin', 'CDT Monitor Test', $html); + } + + private function renderEmailTemplate($title, $summary, $details, $type = 'info') + { + $color = '#007AFF'; + if ($type === 'warning') $color = '#FF3B30'; + if ($type === 'success') $color = '#34C759'; + + $rows = ''; + foreach ($details as $item) { + $valColor = isset($item['highlight']) && $item['highlight'] ? $color : '#1C1C1E'; + $rows .= " + + {$item['label']} + {$item['value']} + "; + } + + return " + + + + + + +
+ + + + +
+
CDT MONITOR
+

{$title}

+

{$summary}

+ {$rows}
+
© " . date('Y') . " CDT Monitor
+
+ "; } private function send_mail($to, $name, $subject, $body) diff --git a/index.php b/index.php index af7c1c0..0548e2f 100644 --- a/index.php +++ b/index.php @@ -1,7 +1,6 @@ getInitError(); @@ -26,7 +24,6 @@ if ($action === 'check_init') { exit; } -// 2. 初始化系统 if ($action === 'setup') { header('Content-Type: application/json'); if ($app->isInitialized()) { @@ -49,7 +46,6 @@ if ($action === 'setup') { exit; } -// 3. 登录 if ($action === 'login') { $data = json_decode(file_get_contents('php://input'), true); if ($app->login($data['password'] ?? '')) { @@ -61,13 +57,11 @@ if ($action === 'login') { exit; } -// 4. 检查登录状态 if ($action === 'check_login') { echo json_encode(['logged_in' => isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true]); exit; } -// 5. 获取状态数据 if ($action === 'get_status') { header('Content-Type: application/json; charset=utf-8'); $initError = $app->getInitError(); @@ -109,11 +103,22 @@ if ($action === 'send_test_email') { exit; } +// 新增:手动刷新单个账号状态 +if ($action === 'refresh_account') { + $data = json_decode(file_get_contents('php://input'), true); + $id = $data['id'] ?? 0; + if ($app->refreshAccount($id)) { + echo json_encode(['success' => true]); + } else { + echo json_encode(['success' => false, 'message' => 'Refresh failed']); + } + exit; +} + if ($action === 'logout') { session_destroy(); echo json_encode(['success' => true]); exit; } -// 渲染页面 echo $app->renderTemplate(); \ No newline at end of file diff --git a/template.html b/template.html index ead9bba..62f03e7 100644 --- a/template.html +++ b/template.html @@ -25,15 +25,16 @@ }, backdropBlur: { xs: '2px', + }, + animation: { + 'spin-slow': 'spin 1.5s linear infinite', } } } } @@ -108,7 +88,6 @@
-

CDT Monitor

@@ -122,14 +101,23 @@
-
-
-
+ + +
+
{{ item.regionName }} @@ -163,7 +151,6 @@
-

暂无监控账号,请点击右上角登录管理员添加。

@@ -173,7 +160,7 @@
- +
@@ -186,13 +173,9 @@

{{ criticalError }}

-

- 请检查服务器目录权限,确保 Web 用户 (如 www) 对当前目录有写入权限以创建数据库文件。 -

+

请检查服务器目录权限,确保 Web 用户对当前目录有写入权限。

- +
@@ -205,28 +188,23 @@

管理员验证

- +
- +
+

欢迎使用 CDT Monitor

初次使用,请先配置核心参数。

-
-
- -
@@ -240,12 +218,7 @@
- - - +

点击后将创建本地数据库并自动登录。

@@ -274,37 +247,43 @@
+ +
+ + +

控制更新状态和流量的频率。开关机操作触发时会自动加速至 1 分钟。

+
+
-
- -

普通停机模式 (KeepCharging)

停止实例后保留实例的资源并继续收费。

-

建议场景:更换操作系统、重新初始化云盘、更改实例规格、修改私网IP等操作。可避免启动失败。

节省停机模式 (StopCharging)

计算资源(CPU/内存)、固定公网IP带宽暂停计费。

-

注意:计算资源被回收,重新启动时可能因库存不足失败。固定公网IP可能会变,弹性公网IP(EIP)不会变。

-
- -

达到流量阈值时触发的操作。

@@ -329,12 +307,8 @@ - -

防止抢占式实例在非关机时间段被意外释放。

-
-

开启后,如果在“开机时间段”内检测到实例状态为“关机”且流量未超标,程序将尝试自动执行开机操作。

@@ -430,7 +404,6 @@ placeholder="输入地域名称或代码..." class="w-full glass-input rounded-xl px-4 py-2 text-sm"> -