diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9b3088e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.gitignore +vendor +data +docker-compose.yml +Dockerfile +README.MD +LICENSE \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4f21ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +config.json +*.log +.env +composer.phar +composer.lock +.DS_Store +Thumbs.db +/.idea +/.vscode +/vendor +/.settings +/.buildpath +/.project diff --git a/AliyunService.php b/AliyunService.php index 6bda5f6..1a3801d 100644 --- a/AliyunService.php +++ b/AliyunService.php @@ -1,97 +1,189 @@ getErrorCode(); + if (stripos($errorCode, 'Throttling') !== false) { + $lastException = $e; + // 流控触发时,等待时间稍长 + $this->backoff($attempt, true); + $attempt++; + continue; + } + throw $e; // 其他 4xx 错误直接抛出(如 AccessKey 错误) + } catch (ServerException $e) { + // 服务端错误(5xx)需要重试 + $lastException = $e; + } catch (\Exception $e) { + // 网络/cURL错误(超时、无法解析DNS等)需要重试 + $lastException = $e; + } + + $attempt++; + if ($attempt < $maxRetries) { + // 记录简短日志到标准输出(可选,方便调试 Docker logs) + // echo "Warning: Retrying $action (Attempt $attempt/$maxRetries)...\n"; + $this->backoff($attempt); + } + } + + throw $lastException; + } + + /** + * 指数退避策略 + * @param int $attempt 当前尝试次数 + * @param bool $isThrottling 是否因为流控 + */ + private function backoff($attempt, $isThrottling = false) + { + // 优化点2: 基础等待时间从 0.5s 提升至 1s + // 序列变为: 1s, 2s, 4s... 3次重试总耗时控制在合理范围内 + $base = 1000000 * pow(2, $attempt); + if ($isThrottling) { + $base *= 2; // 流控时等待时间翻倍 + } + // 增加随机抖动,避免多线程/多容器并发请求撞车 + $jitter = rand(0, 500000); + usleep($base + $jitter); + } + /** * 获取 CDT 流量 - * @throws \Exception|ClientException|ServerException + * @throws \Exception */ public function getTraffic($key, $secret) { - // 配置客户端 - AlibabaCloud::accessKeyClient($key, $secret)->regionId('cn-hongkong')->asDefaultClient(); - - // 发起 RPC 请求 - $result = AlibabaCloud::rpc() - ->product('CDT') - ->scheme('https') - ->version('2021-08-13') - ->action('ListCdtInternetTraffic') - ->method('POST') - ->host('cdt.aliyuncs.com') - ->request(); + return $this->executeWithRetry(function () use ($key, $secret) { + AlibabaCloud::accessKeyClient($key, $secret) + ->regionId('cn-hongkong') + ->asDefaultClient(); - if (isset($result['TrafficDetails'])) { - return array_sum(array_column($result['TrafficDetails'], 'Traffic')) / (1024 * 1024 * 1024); - } - - throw new \Exception("API 响应缺少 TrafficDetails 字段"); + $result = AlibabaCloud::rpc() + ->product('CDT') + ->scheme('https') + ->version('2021-08-13') + ->action('ListCdtInternetTraffic') + ->method('POST') + ->host('cdt.aliyuncs.com') + ->options([ + // 优化点3: 按要求缩短超时时间,提升响应速度 + 'connect_timeout' => 5.0, // 连接超时 10s -> 5s + 'timeout' => 10.0 // 读取超时 30s -> 10s + ]) + ->request(); + + if (isset($result['TrafficDetails'])) { + return array_sum(array_column($result['TrafficDetails'], 'Traffic')) / (1024 * 1024 * 1024); + } + + throw new \Exception("API 响应缺少 TrafficDetails 字段"); + }, 'getTraffic'); } /** * 获取实例状态 - * @throws \Exception|ClientException|ServerException + * @throws \Exception */ public function getInstanceStatus($account) { - AlibabaCloud::accessKeyClient($account['access_key_id'], $account['access_key_secret']) - ->regionId($account['region_id']) - ->asDefaultClient(); + return $this->executeWithRetry(function () use ($account) { + AlibabaCloud::accessKeyClient($account['access_key_id'], $account['access_key_secret']) + ->regionId($account['region_id']) + ->asDefaultClient(); - $options = ['query' => ['RegionId' => $account['region_id']]]; - if (!empty($account['instance_id'])) { - $options['query']['InstanceId'] = $account['instance_id']; - } - - $result = AlibabaCloud::rpc() - ->product('Ecs') - ->scheme('https') - ->version('2014-05-26') - ->action('DescribeInstanceStatus') - ->method('POST') - ->host("ecs.{$account['region_id']}.aliyuncs.com") - ->options($options) - ->request(); + $options = [ + 'query' => ['RegionId' => $account['region_id']], + // 优化点3: 同样缩短实例状态查询的超时 + 'connect_timeout' => 5.0, + 'timeout' => 10.0 + ]; - if (isset($result['InstanceStatuses']['InstanceStatus'][0]['Status'])) { - return $result['InstanceStatuses']['InstanceStatus'][0]['Status']; - } - - throw new \Exception("API 响应未找到实例状态 (请检查 Instance ID)"); + if (!empty($account['instance_id'])) { + $options['query']['InstanceId'] = $account['instance_id']; + } + + $result = AlibabaCloud::rpc() + ->product('Ecs') + ->scheme('https') + ->version('2014-05-26') + ->action('DescribeInstanceStatus') + ->method('POST') + ->host("ecs.{$account['region_id']}.aliyuncs.com") + ->options($options) + ->request(); + + if (isset($result['InstanceStatuses']['InstanceStatus'][0]['Status'])) { + return $result['InstanceStatuses']['InstanceStatus'][0]['Status']; + } + + throw new \Exception("API 响应未找到实例状态 (请检查 Instance ID)"); + }, 'getInstanceStatus'); } /** * 控制实例开关机 - * @throws \Exception|ClientException|ServerException + * @throws \Exception */ public function controlInstance($account, $action, $shutdownMode = 'KeepCharging') { - AlibabaCloud::accessKeyClient($account['access_key_id'], $account['access_key_secret']) - ->regionId($account['region_id']) - ->asDefaultClient(); + return $this->executeWithRetry(function () use ($account, $action, $shutdownMode) { + AlibabaCloud::accessKeyClient($account['access_key_id'], $account['access_key_secret']) + ->regionId($account['region_id']) + ->asDefaultClient(); - if (empty($account['instance_id'])) { - throw new \Exception("未配置 Instance ID"); - } - - $options = ['query' => ['RegionId' => $account['region_id'], 'InstanceId' => $account['instance_id']]]; - if ($action === 'stop') { - $options['query']['StoppedMode'] = $shutdownMode; - } - - AlibabaCloud::rpc() - ->product('Ecs') - ->scheme('https') - ->version('2014-05-26') - ->action($action === 'stop' ? 'StopInstance' : 'StartInstance') - ->method('POST') - ->host("ecs.{$account['region_id']}.aliyuncs.com") - ->options($options) - ->request(); + if (empty($account['instance_id'])) { + throw new \Exception("未配置 Instance ID"); + } - return true; + $options = [ + 'query' => [ + 'RegionId' => $account['region_id'], + 'InstanceId' => $account['instance_id'] + ], + // 优化点4: 控制操作保持一致,确保用户操作不卡死 + 'connect_timeout' => 5.0, + 'timeout' => 10.0 + ]; + + if ($action === 'stop') { + $options['query']['StoppedMode'] = $shutdownMode; + } + + AlibabaCloud::rpc() + ->product('Ecs') + ->scheme('https') + ->version('2014-05-26') + ->action($action === 'stop' ? 'StopInstance' : 'StartInstance') + ->method('POST') + ->host("ecs.{$account['region_id']}.aliyuncs.com") + ->options($options) + ->request(); + + return true; + }, 'controlInstance'); } } \ No newline at end of file diff --git a/Database.php b/Database.php index 816f3ff..09211bb 100644 --- a/Database.php +++ b/Database.php @@ -222,6 +222,35 @@ class Database $data = $stmt->fetchAll(PDO::FETCH_ASSOC); // 按时间正序排列返回 return array_reverse($data); +<<<<<<< Updated upstream +======= + } + + /** + * 获取最近 30 天的数据 + */ + public function getDailyStats($accessKeyId) + { + $stmt = $this->pdo->prepare("SELECT traffic, recorded_at FROM traffic_daily WHERE access_key_id = ? ORDER BY recorded_at DESC LIMIT 31"); + $stmt->execute([$accessKeyId]); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + return array_reverse($data); + } + + /** + * 清理过期统计数据 + */ + public function pruneStats() + { + // 1. 清理小时表:保留最近 24+2 小时以外的数据 + // 既然我们只取 Limit 24,其实可以删掉 48 小时前的 + $hourLimit = time() - (48 * 3600); + $this->pdo->exec("DELETE FROM traffic_hourly WHERE recorded_at < $hourLimit"); + + // 2. 清理天表:保留最近 60 天以外的 (留点余量) + $dayLimit = time() - (60 * 86400); + $this->pdo->exec("DELETE FROM traffic_daily WHERE recorded_at < $dayLimit"); +>>>>>>> Stashed changes } /**