Compare commits

...

5 Commits

Author SHA1 Message Date
88f7fc7141 bug修复 2026-05-30 15:06:24 +08:00
914208aa70 bug修复 2026-05-30 09:34:47 +08:00
ce3cf309f9 bug修复 2026-05-30 08:55:19 +08:00
743aa16428 小更新 2026-05-29 23:41:45 +08:00
cea593c8f9 图片调整 2026-05-29 23:23:29 +08:00
10 changed files with 952 additions and 459 deletions

View File

@ -1,6 +1,7 @@
# VPS Hub - 多平台VPS监控与管理系统
<div align="center">
![License](https://img.shields.io/badge/license-MIT-red)
**一个强大的多平台VPS监控、管理和自动重启系统**
@ -24,7 +25,6 @@
- [监控服务](#-监控服务)
- [安全说明](#-安全说明)
- [常见问题](#-常见问题)
- [更新日志](#-更新日志)
- [许可证](#-许可证)
---

View File

@ -1,21 +1,13 @@
<?php
/**
* VPS Hub 数据库辅助类
* 提供SQLite数据库的统一访问接口
*/
require_once __DIR__ . '/logger.php';
class DBHelper {
private $db;
private $dbPath;
/**
* 构造函数
* @param string $dbPath 数据库文件路径
*/
public function __construct($dbPath) {
$this->dbPath = $dbPath;
// 确保目录存在
$dir = dirname($dbPath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
@ -32,9 +24,6 @@ class DBHelper {
/**
* 执行查询并返回所有结果
* @param string $sql SQL语句
* @param array $params 参数数组
* @return array 结果数组
*/
public function query($sql, $params = []) {
try {
@ -42,16 +31,13 @@ class DBHelper {
$stmt->execute($params);
return $stmt->fetchAll();
} catch (PDOException $e) {
error_log("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql);
Logger::error("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::query');
return [];
}
}
/**
* 执行查询并返回单条结果
* @param string $sql SQL语句
* @param array $params 参数数组
* @return array|false 结果数组或false
*/
public function queryOne($sql, $params = []) {
try {
@ -59,32 +45,26 @@ class DBHelper {
$stmt->execute($params);
return $stmt->fetch();
} catch (PDOException $e) {
error_log("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql);
Logger::error("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::queryOne');
return false;
}
}
/**
* 执行插入、更新、删除操作
* @param string $sql SQL语句
* @param array $params 参数数组
* @return bool 是否成功
*/
public function execute($sql, $params = []) {
try {
$stmt = $this->db->prepare($sql);
return $stmt->execute($params);
} catch (PDOException $e) {
error_log("数据库执行错误: " . $e->getMessage() . " | SQL: " . $sql);
Logger::error("数据库执行错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::execute');
return false;
}
}
/**
* 插入数据并返回最后插入的ID
* @param string $sql SQL语句
* @param array $params 参数数组
* @return int|false 最后插入的ID或false
*/
public function insert($sql, $params = []) {
try {
@ -92,36 +72,23 @@ class DBHelper {
$stmt->execute($params);
return $this->db->lastInsertId();
} catch (PDOException $e) {
error_log("数据库插入错误: " . $e->getMessage() . " | SQL: " . $sql);
Logger::error("数据库插入错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::insert');
return false;
}
}
/**
* 开始事务
*/
public function beginTransaction() {
return $this->db->beginTransaction();
}
/**
* 提交事务
*/
public function commit() {
return $this->db->commit();
}
/**
* 回滚事务
*/
public function rollBack() {
return $this->db->rollBack();
}
/**
* 获取数据库连接对象(用于高级操作)
* @return PDO
*/
public function getConnection() {
return $this->db;
}
@ -133,7 +100,6 @@ function getVpsDB() {
if ($db === null) {
$dbPath = __DIR__ . '/db/vps.db';
$db = new DBHelper($dbPath);
// 初始化表结构
initVpsTables($db);
}
return $db;
@ -156,7 +122,6 @@ function getStatusDB() {
if ($db === null) {
$dbPath = __DIR__ . '/db/status.db';
$db = new DBHelper($dbPath);
// 初始化表结构
initStatusTables($db);
}
return $db;
@ -176,7 +141,8 @@ function initVpsTables($db) {
api_key TEXT NOT NULL,
auto_monitor BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(site_url, account)
)
");
}
@ -199,15 +165,17 @@ function initVpsListTables($db) {
bandwidth TEXT,
os_type TEXT,
status TEXT,
amount TEXT,
nextduedate INTEGER,
section BOOLEAN DEFAULT 0,
last_check TIMESTAMP,
FOREIGN KEY (config_id) REFERENCES configs(id),
UNIQUE(config_id, vps_id)
FOREIGN KEY (config_id) REFERENCES configs(id)
)
");
$db->execute('CREATE INDEX IF NOT EXISTS idx_vps_config ON vps_list(config_id)');
$db->execute('CREATE INDEX IF NOT EXISTS idx_vps_vps_id ON vps_list(vps_id)');
$db->execute('CREATE INDEX IF NOT EXISTS idx_vps_unique ON vps_list(vps_id, ip_address)');
}
/**

View File

@ -1,9 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VPS Hub 数据库管理模块
负责初始化和管理三个SQLite数据库: vps.db, vpslist.db, status.db
"""
"""VPS Hub 数据库管理模块"""
import os
import sqlite3
@ -14,38 +11,24 @@ class DatabaseManager:
"""数据库管理器"""
def __init__(self, db_dir=None):
"""初始化数据库管理器
Args:
db_dir: 数据库文件目录,默认为app/db/
"""
if db_dir is None:
db_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'db')
self.db_dir = db_dir
os.makedirs(db_dir, exist_ok=True)
# 数据库文件路径
self.vps_db = os.path.join(db_dir, 'vps.db')
self.vpslist_db = os.path.join(db_dir, 'vpslist.db')
self.status_db = os.path.join(db_dir, 'status.db')
# 初始化所有数据库
self.init_vps_db()
self.init_vpslist_db()
self.init_status_db()
def get_connection(self, db_path):
"""获取数据库连接
Args:
db_path: 数据库文件路径
Returns:
SQLite连接对象
"""
"""获取数据库连接"""
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row # 使结果可以通过列名访问
conn.row_factory = sqlite3.Row
return conn
def init_vps_db(self):
@ -63,7 +46,8 @@ class DatabaseManager:
api_key TEXT NOT NULL,
auto_monitor BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(site_url, account)
)
''')
@ -89,16 +73,17 @@ class DatabaseManager:
bandwidth TEXT,
os_type TEXT,
status TEXT,
amount TEXT,
nextduedate INTEGER,
section BOOLEAN DEFAULT 0,
last_check TIMESTAMP,
FOREIGN KEY (config_id) REFERENCES configs(id),
UNIQUE(config_id, vps_id)
FOREIGN KEY (config_id) REFERENCES configs(id)
)
''')
# 创建索引以提高查询性能
cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_config ON vps_list(config_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_vps_id ON vps_list(vps_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_vps_unique ON vps_list(vps_id, ip_address)')
conn.commit()
conn.close()
@ -108,7 +93,6 @@ class DatabaseManager:
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
# Ping状态记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS ping_status (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -120,7 +104,6 @@ class DatabaseManager:
)
''')
# VPS摘要统计表
cursor.execute('''
CREATE TABLE IF NOT EXISTS vps_summary (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -138,7 +121,6 @@ class DatabaseManager:
)
''')
# 创建索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ping_vps ON ping_status(vps_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ping_time ON ping_status(check_time)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_vps ON vps_summary(vps_id)')
@ -150,23 +132,19 @@ class DatabaseManager:
# ==================== vps.db 操作 ====================
def add_config(self, api_label, site_type, account, api_key, site_url=None, auto_monitor=True):
"""添加VPS配置
Args:
api_label: API标识必填唯一
site_type: 网站类型 (mofang/aliyun/tencent)
account: 账户
api_key: API密钥
site_url: 网站链接
auto_monitor: 是否开启自动监控
Returns:
新配置的ID
"""
"""添加VPS配置基于site_url和account去重"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
try:
existing = cursor.execute(
'SELECT id FROM configs WHERE site_url = ? AND account = ?',
(site_url, account)
).fetchone()
if existing:
return existing['id']
cursor.execute('''
INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor)
VALUES (?, ?, ?, ?, ?, ?)
@ -182,11 +160,7 @@ class DatabaseManager:
conn.close()
def get_all_configs(self):
"""获取所有配置
Returns:
配置列表
"""
"""获取所有配置"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
@ -197,14 +171,7 @@ class DatabaseManager:
return configs
def get_config_by_id(self, config_id):
"""根据ID获取配置
Args:
config_id: 配置ID
Returns:
配置字典或None
"""
"""根据ID获取配置"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
@ -215,22 +182,13 @@ class DatabaseManager:
return dict(row) if row else None
def update_config(self, config_id, **kwargs):
"""更新配置
Args:
config_id: 配置ID
**kwargs: 要更新的字段
Returns:
是否成功
"""
"""更新配置"""
if not kwargs:
return False
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
# 构建UPDATE语句
fields = ', '.join([f"{key} = ?" for key in kwargs.keys()])
values = list(kwargs.values())
values.append(config_id)
@ -244,14 +202,7 @@ class DatabaseManager:
return affected > 0
def delete_config(self, config_id):
"""删除配置
Args:
config_id: 配置ID
Returns:
是否成功
"""
"""删除配置"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
@ -263,26 +214,84 @@ class DatabaseManager:
return affected > 0
def reset_config_ids(self):
"""重置configs表的ID序列从1开始连续编号"""
conn = self.get_connection(self.vps_db)
cursor = conn.cursor()
try:
cursor.execute('SELECT * FROM configs ORDER BY id')
configs = [dict(row) for row in cursor.fetchall()]
if not configs:
return True
cursor.execute('DROP TABLE IF EXISTS configs_backup')
cursor.execute('''
CREATE TABLE configs_backup AS SELECT * FROM configs
''')
cursor.execute('DELETE FROM configs')
cursor.execute("DELETE FROM sqlite_sequence WHERE name='configs'")
for config in configs:
cursor.execute('''
INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
config['api_label'],
config['site_type'],
config['site_url'],
config['account'],
config['api_key'],
config['auto_monitor'],
config['created_at'],
config['updated_at']
))
cursor.execute('DROP TABLE IF EXISTS configs_backup')
conn.commit()
return True
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
# ==================== vpslist.db 操作 ====================
def add_vps(self, config_id, vps_id, domain=None, ip_address=None,
product_name=None, section=False):
"""添加VPS到列表
Args:
config_id: 配置ID
vps_id: VPS在平台的ID
domain: 域名
ip_address: IP地址
product_name: 产品名称
section: 是否标记为需要监控
Returns:
新记录的ID
"""
def get_next_available_id(self):
"""获取下一个可用的ID填补空缺"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
cursor.execute('SELECT id FROM vps_list ORDER BY id')
ids = [row['id'] for row in cursor.fetchall()]
conn.close()
if not ids:
return 1
for i in range(1, max(ids) + 1):
if i not in ids:
return i
return None
def add_vps(self, config_id, vps_id, domain=None, ip_address=None, product_name=None, section=False, custom_id=None):
"""添加VPS到列表支持指定ID以填补空缺"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
if custom_id is None:
custom_id = self.get_next_available_id()
if custom_id:
cursor.execute('''
INSERT INTO vps_list (id, config_id, vps_id, domain, ip_address, product_name, section, last_check)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (custom_id, config_id, vps_id, domain, ip_address, product_name, section))
else:
cursor.execute('''
INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, section, last_check)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
@ -295,19 +304,47 @@ class DatabaseManager:
return record_id
def batch_add_vps(self, vps_list):
"""批量添加VPS
Args:
vps_list: VPS信息列表,每个元素是字典
"""
"""批量添加VPS基于vps_id和ip_address去重"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
for vps in vps_list:
existing = cursor.execute(
'SELECT id FROM vps_list WHERE vps_id = ? AND ip_address = ?',
(vps['vps_id'], vps.get('ip_address'))
).fetchone()
if existing:
continue
custom_id = self.get_next_available_id()
if custom_id:
cursor.execute('''
INSERT OR REPLACE INTO vps_list
(config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, section, last_check)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
INSERT INTO vps_list
(id, config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, section, last_check)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (
custom_id,
vps['config_id'],
vps['vps_id'],
vps.get('domain'),
vps.get('ip_address'),
vps.get('product_name'),
vps.get('cpu_cores'),
vps.get('memory_size'),
vps.get('disk_size'),
vps.get('bandwidth'),
vps.get('os_type'),
vps.get('amount'),
vps.get('nextduedate'),
vps.get('section', False)
))
else:
cursor.execute('''
INSERT INTO vps_list
(config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, section, last_check)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
''', (
vps['config_id'],
vps['vps_id'],
@ -319,6 +356,8 @@ class DatabaseManager:
vps.get('disk_size'),
vps.get('bandwidth'),
vps.get('os_type'),
vps.get('amount'),
vps.get('nextduedate'),
vps.get('section', False)
))
@ -326,11 +365,7 @@ class DatabaseManager:
conn.close()
def get_all_vps(self):
"""获取所有VPS
Returns:
VPS列表
"""
"""获取所有VPS"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
@ -341,14 +376,7 @@ class DatabaseManager:
return vps_list
def get_vps_by_config(self, config_id):
"""根据配置ID获取VPS列表
Args:
config_id: 配置ID
Returns:
VPS列表
"""
"""根据配置ID获取VPS列表"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
@ -359,15 +387,7 @@ class DatabaseManager:
return vps_list
def update_vps_details(self, vps_id, **kwargs):
"""更新VPS详细信息
Args:
vps_id: VPS ID
**kwargs: 要更新的字段
Returns:
是否成功
"""
"""更新VPS详细信息"""
if not kwargs:
return False
@ -387,23 +407,11 @@ class DatabaseManager:
return affected > 0
def update_vps_status(self, vps_id, status):
"""更新VPS状态
Args:
vps_id: VPS ID
status: 状态 (on/off/unknown)
Returns:
是否成功
"""
"""更新VPS状态"""
return self.update_vps_details(vps_id, status=status)
def get_monitored_vps(self):
"""获取所有标记为需要监控的VPS
Returns:
VPS列表
"""
"""获取所有标记为需要监控的VPS"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
@ -413,17 +421,61 @@ class DatabaseManager:
conn.close()
return vps_list
def reset_vps_list_ids(self):
"""重置vps_list表的ID序列从1开始连续编号"""
conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor()
try:
cursor.execute('SELECT * FROM vps_list ORDER BY id')
vps_items = [dict(row) for row in cursor.fetchall()]
if not vps_items:
return True
cursor.execute('DROP TABLE IF EXISTS vps_list_backup')
cursor.execute('''
CREATE TABLE vps_list_backup AS SELECT * FROM vps_list
''')
cursor.execute('DELETE FROM vps_list')
cursor.execute("DELETE FROM sqlite_sequence WHERE name='vps_list'")
for vps in vps_items:
cursor.execute('''
INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, status, section, last_check)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
vps['config_id'],
vps['vps_id'],
vps.get('domain'),
vps.get('ip_address'),
vps.get('product_name'),
vps.get('cpu_cores'),
vps.get('memory_size'),
vps.get('disk_size'),
vps.get('bandwidth'),
vps.get('os_type'),
vps.get('amount'),
vps.get('nextduedate'),
vps.get('status'),
vps.get('section', 0),
vps.get('last_check')
))
cursor.execute('DROP TABLE IF EXISTS vps_list_backup')
conn.commit()
return True
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
# ==================== status.db 操作 ====================
def save_ping_status(self, vps_id, target, status, latency_ms=None):
"""保存Ping状态记录
Args:
vps_id: VPS ID
target: 目标(IP或域名)
status: 状态 (normal/abnormal)
latency_ms: 延迟(ms)
"""
"""保存Ping状态记录"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
@ -436,15 +488,7 @@ class DatabaseManager:
conn.close()
def get_ping_records(self, vps_id, date=None):
"""获取Ping记录
Args:
vps_id: VPS ID
date: 日期 (YYYY-MM-DD),为空则获取所有记录
Returns:
Ping记录列表
"""
"""获取Ping记录"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
@ -467,11 +511,7 @@ class DatabaseManager:
return records
def cleanup_old_ping_records(self, days=30):
"""清理旧的Ping记录
Args:
days: 保留天数,默认30天
"""
"""清理旧的Ping记录"""
from datetime import timedelta
conn = self.get_connection(self.status_db)
@ -489,20 +529,7 @@ class DatabaseManager:
def save_vps_summary(self, vps_id, date, avg_latency, max_latency, min_latency,
count_under_100, count_100_to_300, count_300_to_500,
count_abnormal, availability):
"""保存VPS摘要统计
Args:
vps_id: VPS ID
date: 日期 (YYYY-MM-DD)
avg_latency: 平均延迟
max_latency: 最大延迟
min_latency: 最小延迟
count_under_100: <100ms次数
count_100_to_300: 100-300ms次数
count_300_to_500: 300-500ms次数
count_abnormal: 异常次数
availability: 可用性评分
"""
"""保存VPS摘要统计"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
@ -519,15 +546,7 @@ class DatabaseManager:
conn.close()
def get_vps_summary(self, vps_id, date=None):
"""获取VPS摘要统计
Args:
vps_id: VPS ID
date: 日期,为空则获取最新一条
Returns:
摘要统计字典或None
"""
"""获取VPS摘要统计"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
@ -542,21 +561,13 @@ class DatabaseManager:
return dict(row) if row else None
def get_all_summaries(self, date=None):
"""获取所有VPS的摘要统计
Args:
date: 日期,为空则获取最新
Returns:
摘要统计列表
"""
"""获取所有VPS的摘要统计"""
conn = self.get_connection(self.status_db)
cursor = conn.cursor()
if date:
cursor.execute('SELECT * FROM vps_summary WHERE date = ? ORDER BY vps_id', (date,))
else:
# 获取每个VPS的最新摘要
cursor.execute('''
SELECT vs.* FROM vps_summary vs
INNER JOIN (

102
app/logger.php Normal file
View File

@ -0,0 +1,102 @@
<?php
/**
* PHP日志管理类
* 统一处理所有PHP文件的日志记录
*/
class Logger {
private static $logFile = __DIR__ . '/logs/php.log';
private static $initialized = false;
/**
* 初始化日志系统
*/
public static function init() {
if (self::$initialized) {
return;
}
// 确保日志目录存在
$logDir = dirname(self::$logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
// 创建日志文件(如果不存在)
if (!file_exists(self::$logFile)) {
touch(self::$logFile);
chmod(self::$logFile, 0666);
}
// 设置PHP错误日志路径
ini_set('error_log', self::$logFile);
ini_set('log_errors', 1);
ini_set('display_errors', 0); // 生产环境不显示错误
self::$initialized = true;
}
/**
* 记录日志
* @param string $message 日志消息
* @param string $level 日志级别 (INFO, WARNING, ERROR, DEBUG)
* @param string $source 来源文件/函数
*/
public static function log($message, $level = 'INFO', $source = '') {
self::init();
$timestamp = date('Y-m-d H:i:s');
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
$caller = $source ?: (isset($backtrace[1]) ? basename($backtrace[1]['file']) . ':' . $backtrace[1]['line'] : 'unknown');
$logEntry = "[{$timestamp}] [{$level}] [{$caller}] {$message}" . PHP_EOL;
file_put_contents(self::$logFile, $logEntry, FILE_APPEND | LOCK_EX);
}
/**
* 记录信息日志
*/
public static function info($message, $source = '') {
self::log($message, 'INFO', $source);
}
/**
* 记录警告日志
*/
public static function warning($message, $source = '') {
self::log($message, 'WARNING', $source);
}
/**
* 记录错误日志
*/
public static function error($message, $source = '') {
self::log($message, 'ERROR', $source);
}
/**
* 记录调试日志
*/
public static function debug($message, $source = '') {
self::log($message, 'DEBUG', $source);
}
/**
* 获取日志文件路径
*/
public static function getLogFile() {
return self::$logFile;
}
/**
* 清空日志文件
*/
public static function clear() {
file_put_contents(self::$logFile, '');
}
}
// 自动初始化
Logger::init();
?>

0
app/logs/php.log Normal file
View File

View File

@ -1,9 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VPS Hub 多平台监控程序
功能支持多平台(魔方/阿里云/腾讯云)的VPS监控自动开机和可用性统计
"""
"""VPS Hub 多平台监控程序"""
import os
import sys
@ -16,7 +13,6 @@ import schedule
from datetime import datetime, timedelta
from pathlib import Path
# 导入数据库管理器
from db_manager import DatabaseManager
@ -29,41 +25,12 @@ class PlatformAdapter:
self.api_key = api_key
self.jwt_token = None
def login(self):
"""登录获取Token - 需要子类实现"""
raise NotImplementedError
def get_vps_list(self):
"""获取VPS列表 - 需要子类实现"""
raise NotImplementedError
def get_vps_status(self, vps_id):
"""获取VPS状态 - 需要子类实现"""
raise NotImplementedError
def get_vps_details(self, vps_id):
"""获取VPS详细信息 - 需要子类实现"""
raise NotImplementedError
def power_on(self, vps_id):
"""开机 - 需要子类实现"""
raise NotImplementedError
def power_off(self, vps_id):
"""关机 - 需要子类实现"""
raise NotImplementedError
def hard_reboot(self, vps_id):
"""硬重启 - 需要子类实现"""
raise NotImplementedError
class MofangAdapter(PlatformAdapter):
"""魔方平台适配器(核云IDC等使用此适配器)"""
"""魔方平台适配器"""
def __init__(self, site_url, account, api_key):
super().__init__(site_url, account, api_key)
# 魔方平台的API路径统一为 /v1
if not self.site_url:
raise ValueError("魔方平台必须提供网站链接(API地址)")
@ -79,7 +46,6 @@ class MofangAdapter(PlatformAdapter):
def login(self):
try:
# 魔方平台API统一在 /v1 路径下
url = f"{self.site_url}/v1/login_api"
data = {
'account': self.account,
@ -729,7 +695,9 @@ class MonitorService:
'disk_size': disk_size,
'bandwidth': bandwidth,
'os_type': os_type,
'section': config['auto_monitor'] # 根据配置的auto_monitor设置
'amount': host.get('amount'),
'nextduedate': host.get('nextduedate'),
'section': config['auto_monitor']
})
# 批量添加到数据库

View File

@ -8,7 +8,7 @@ require_once __DIR__ . '/app/db_helper.php';
require_once __DIR__ . '/mofangidc.php';
// Token文件路径(存储API_PASS)
$tokenFile = __DIR__ . '/app/token_pass.php';
$tokenFile = __DIR__ . '/app/pass.php'; # 更新路径为app/pass.php避免与其他配置文件混淆
// 检查是否首次访问(需要设置API_PASS)
$needSetup = !file_exists($tokenFile);
@ -43,7 +43,7 @@ if ($needSetup && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['setup_p
// 如果需要设置密码,显示设置页面
if ($needSetup) {
?>
?>n
<!DOCTYPE html>
<html lang="zh-CN">
<head>

View File

@ -1,5 +1,6 @@
<?php
require_once __DIR__ . '/app/db_helper.php';
require_once __DIR__ . '/app/logger.php';
require_once __DIR__ . '/mofangidc.php';
// 存储API_PASS
@ -110,50 +111,36 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db = getVpsDB();
$configs = $db->query('SELECT * FROM configs ORDER BY id');
// 构建配置ID到配置的映射
// 构建配置ID到api_label的映射
$configMap = [];
foreach ($configs as $config) {
$configMap[$config['id']] = $config;
$configMap[$config['id']] = $config['api_label'];
}
// 获取所有VPS列表从vpslist.db查询
// 获取所有VPS列表从vpslist.db查询
$listDb = getVpsListDB();
$vpsList = $listDb->query('SELECT * FROM vps_list ORDER BY config_id, vps_id');
// 为每个VPS添加site_type和site_url信息
// 为每个VPS添加api_label
foreach ($vpsList as &$vps) {
$configId = $vps['config_id'];
if (isset($configMap[$configId])) {
$vps['site_type'] = $configMap[$configId]['site_type'];
// 修正site_url移除末尾的/v1或/v1/
$siteUrl = $configMap[$configId]['site_url'];
$siteUrl = rtrim($siteUrl, '/'); // 移除末尾的 /
if (substr($siteUrl, -3) === '/v1') {
$siteUrl = substr($siteUrl, 0, -3); // 移除 /v1
}
$vps['site_url'] = $siteUrl;
} else {
$vps['site_type'] = 'unknown';
$vps['site_url'] = '';
}
$vps['api_label'] = $configMap[$configId] ?? 'Unknown';
}
unset($vps); // 解除引用
unset($vps);
// 调试信息(临时)
error_log("Configs count: " . count($configs));
error_log("VPS List count: " . count($vpsList));
Logger::info("Total VPS count: " . count($vpsList), 'index.php');
if (!empty($vpsList)) {
error_log("First VPS: " . json_encode($vpsList[0]));
Logger::debug("First VPS: " . json_encode($vpsList[0]), 'index.php');
}
// 按配置分组
$vpsByConfig = [];
// 按api_label分组
$vpsByApiLabel = [];
foreach ($vpsList as $vps) {
$configId = $vps['config_id'];
if (!isset($vpsByConfig[$configId])) {
$vpsByConfig[$configId] = [];
$apiLabel = $vps['api_label'];
if (!isset($vpsByApiLabel[$apiLabel])) {
$vpsByApiLabel[$apiLabel] = [];
}
$vpsByConfig[$configId][] = $vps;
$vpsByApiLabel[$apiLabel][] = $vps;
}
// 统计信息
@ -296,6 +283,15 @@ foreach ($vpsList as $vps) {
.power-off {
background-color: #dc3545;
}
.power-process {
background-color: #ffc107;
box-shadow: 0 0 8px #ffc107;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.power-unknown {
background-color: #6c757d;
}
@ -395,43 +391,37 @@ foreach ($vpsList as $vps) {
</a>
</div>
<?php else: ?>
<!-- 按配置分组显示VPS -->
<?php foreach ($configs as $config):
$configVps = $vpsByConfig[$config['id']] ?? [];
$typeMap = [
'mofang' => '魔方平台',
'aliyun' => '阿里云',
'tencent' => '腾讯云'
];
$typeName = $typeMap[$config['site_type']] ?? $config['site_type'];
?>
<!-- 按api_label分组显示VPS -->
<?php foreach ($vpsByApiLabel as $apiLabel => $labelVps): ?>
<div class="config-section">
<div class="config-title">
<?php echo htmlspecialchars($config['api_label']); ?>
(<?php echo htmlspecialchars($typeName); ?>)
- <?php echo count($configVps); ?> 台VPS
<?php echo htmlspecialchars($apiLabel); ?>
- <?php echo count($labelVps); ?> 台VPS
</div>
<?php if (empty($configVps)): ?>
<?php if (empty($labelVps)): ?>
<div class="empty-state" style="background: white; padding: 40px;">
<p>此配置下暂无VPS请点击"手动刷新VPS列表"获取</p>
<p>此配置下暂无VPS请点击“手动刷新VPS列表”获取</p>
</div>
<?php else: ?>
<div class="vps-grid">
<?php foreach ($configVps as $vps):
<?php foreach ($labelVps as $vps):
$statusClass = 'power-unknown';
$statusText = '未知';
$statusColor = '#999';
if ($vps['status'] === 'on') {
if ($vps['status'] === 'on' || $vps['status'] === 'running') {
$statusClass = 'power-on';
$statusText = '运行中';
$statusColor = '#28a745';
} elseif ($vps['status'] === 'off') {
} elseif ($vps['status'] === 'off' || $vps['status'] === 'stopped') {
$statusClass = 'power-off';
$statusText = '已关机';
$statusColor = '#dc3545';
} elseif ($vps['status'] === 'process' || $vps['status'] === 'pending' || $vps['status'] === 'rebooting') {
$statusClass = 'power-process';
$statusText = '处理中';
$statusColor = '#ffc107';
}
?>
<div class="vps-card">
@ -482,6 +472,20 @@ foreach ($vpsList as $vps) {
<span class="info-value"><?php echo htmlspecialchars($vps['disk_size']) . ' / ' . htmlspecialchars($vps['bandwidth']) . ' / ' . htmlspecialchars($vps['os_type']); ?></span>
</div>
<?php endif; ?>
<?php if ($vps['amount']): ?>
<div class="info-row">
<span class="info-label">价格</span>
<span class="info-value"><?php echo htmlspecialchars($vps['amount']); ?></span>
</div>
<?php endif; ?>
<?php if ($vps['nextduedate']): ?>
<div class="info-row">
<span class="info-label">到期时间</span>
<span class="info-value"><?php echo date('Y-m-d', $vps['nextduedate']); ?></span>
</div>
<?php endif; ?>
</div>
<div class="power-status">

View File

@ -1,19 +1,12 @@
<?php
/**
* 魔方平台API接口封装
* 所有智简魔方的操作API以及相关函数存储在此文件中
*/
require_once __DIR__ . '/app/db_helper.php';
require_once __DIR__ . '/app/logger.php';
// Token缓存文件路径
define('TOKEN_CACHE_FILE', __DIR__ . '/app/token.php');
define('TOKEN_EXPIRE_TIME', 7200); // Token过期时间2小时
define('TOKEN_EXPIRE_TIME', 7200);
/**
* 获取缓存的Token
* @param int $configId 配置ID
* @return string|null Token字符串或null
*/
function getCachedToken($configId) {
if (!file_exists(TOKEN_CACHE_FILE)) {
@ -43,8 +36,6 @@ function getCachedToken($configId) {
/**
* 保存Token到缓存文件
* @param int $configId 配置ID
* @param string $token Token字符串
*/
function saveToken($configId, $token) {
$cached_tokens = [];
@ -77,10 +68,6 @@ function saveTokensFile($cached_tokens) {
/**
* 魔方平台登录获取JWT Token
* @param string $siteUrl API基础URL
* @param string $account 账户
* @param string $apiKey API密钥
* @return string|null JWT Token或null
*/
function mofangLogin($siteUrl, $account, $apiKey) {
try {
@ -104,7 +91,7 @@ function mofangLogin($siteUrl, $account, $apiKey) {
curl_close($ch);
if ($httpCode !== 200) {
error_log("魔方登录请求失败HTTP状态码: {$httpCode}");
Logger::error("魔方登录请求失败HTTP状态码: {$httpCode}", 'mofangLogin');
return null;
}
@ -113,20 +100,18 @@ function mofangLogin($siteUrl, $account, $apiKey) {
if (isset($result['status']) && $result['status'] === 200 && isset($result['jwt'])) {
return $result['jwt'];
} else {
error_log("魔方登录失败: " . ($result['msg'] ?? '未知错误'));
Logger::error("魔方登录失败: " . ($result['msg'] ?? '未知错误'), 'mofangLogin');
return null;
}
} catch (Exception $e) {
error_log("魔方登录异常: " . $e->getMessage());
Logger::error("魔方登录异常: " . $e->getMessage(), 'mofangLogin');
return null;
}
}
/**
* 获取有效的Token(先查缓存,没有则重新登录)
* @param int $configId 配置ID
* @return string|null Token或null
*/
function getValidToken($configId) {
// 先尝试从缓存获取
@ -140,7 +125,7 @@ function getValidToken($configId) {
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
error_log("配置ID {$configId} 不存在");
Logger::error("配置ID {$configId} 不存在", 'getValidToken');
return null;
}
@ -155,12 +140,6 @@ function getValidToken($configId) {
/**
* 发送魔方API请求
* @param string $siteUrl API基础URL
* @param string $endpoint API端点
* @param string $method HTTP方法 (GET/POST/PUT)
* @param array $data 请求数据
* @param int $configId 配置ID
* @return array|null 响应数据或null
*/
function mofangApiRequest($siteUrl, $endpoint, $method = 'GET', $data = [], $configId = null) {
// 获取Token
@ -233,10 +212,6 @@ function mofangApiRequest($siteUrl, $endpoint, $method = 'GET', $data = [], $con
/**
* 获取VPS列表
* @param int $configId 配置ID
* @param int $page 页码
* @param int $limit 每页数量
* @return array|null VPS列表数据或null
*/
function mofangGetVpsList($configId, $page = 1, $limit = 100) {
$db = getVpsDB();
@ -252,10 +227,6 @@ function mofangGetVpsList($configId, $page = 1, $limit = 100) {
/**
* 获取VPS状态
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @param bool $updateDb 是否更新数据库默认true
* @return array|null 状态数据或null
*/
function mofangGetVpsStatus($configId, $vpsId, $updateDb = true) {
$db = getVpsDB();
@ -271,43 +242,64 @@ function mofangGetVpsStatus($configId, $vpsId, $updateDb = true) {
// 如果获取成功且需要更新数据库
if ($updateDb && $result && isset($result['status']) && $result['status'] === 200 && isset($result['data'])) {
$statusData = $result['data'];
$status = $statusData['status'] ?? 'unknown';
$rawStatus = $statusData['status'] ?? 'unknown';
// 状态映射:将魔方平台的原始状态转换为标准状态
$statusMap = [
'on' => 'on', // 开机
'off' => 'off', // 关机
'running' => 'on', // 运行中
'stopped' => 'off', // 已停止
'process' => 'process', // 处理中(开机/关机/重启过程中)
'pending' => 'process', // 等待中
'installing' => 'process', // 安装中
'rebooting' => 'process', // 重启中
'unknown' => 'unknown' // 未知
];
$status = $statusMap[$rawStatus] ?? $rawStatus;
// 只更新status字段不影响其他字段
$listDb = getVpsListDB();
// 先检查记录是否存在
$existing = $listDb->queryOne(
'SELECT id FROM vps_list WHERE config_id = ? AND vps_id = ?',
// 先获取该VPS的IP地址用于精确匹配
$vpsInfo = $listDb->queryOne(
'SELECT id, ip_address FROM vps_list WHERE config_id = ? AND vps_id = ?',
[$configId, $vpsId]
);
if ($existing) {
if ($vpsInfo) {
// 记录存在执行UPDATE
$listDb->execute(
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE config_id = ? AND vps_id = ?',
[$status, $configId, $vpsId]
$success = $listDb->execute(
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE id = ?',
[$status, $vpsInfo['id']]
);
error_log("[mofangGetVpsStatus] VPS {$vpsId} 状态已更新为: {$status}");
if ($success) {
Logger::info("[mofangGetVpsStatus] VPS {$vpsId} (IP: {$vpsInfo['ip_address']}) 状态已更新为: {$status}", 'mofangGetVpsStatus');
} else {
Logger::error("[mofangGetVpsStatus] VPS {$vpsId} 状态更新失败", 'mofangGetVpsStatus');
}
} else {
// 记录不存在,插入新记录
$listDb->execute(
$success = $listDb->execute(
'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
[$configId, $vpsId, $status]
);
error_log("[mofangGetVpsStatus] VPS {$vpsId} 新记录已插入,状态: {$status}");
if ($success) {
Logger::info("[mofangGetVpsStatus] VPS {$vpsId} 新记录已插入,状态: {$status}", 'mofangGetVpsStatus');
} else {
Logger::error("[mofangGetVpsStatus] VPS {$vpsId} 新记录插入失败", 'mofangGetVpsStatus');
}
}
} elseif ($updateDb && (!$result || !isset($result['status']) || $result['status'] !== 200)) {
// API调用失败不更新数据库保留原有状态
Logger::warning("[mofangGetVpsStatus] VPS {$vpsId} API调用失败保留原有状态", 'mofangGetVpsStatus');
}
return $result;
}
/**
* 获取VPS详细信息
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @param bool $updateDb 是否更新数据库默认true
* @return array|null 详细信息或null
*/
function mofangGetVpsDetails($configId, $vpsId, $updateDb = true) {
$db = getVpsDB();
@ -328,6 +320,28 @@ function mofangGetVpsDetails($configId, $vpsId, $updateDb = true) {
$updates = []; // 存储要更新的字段
$values = [];
// 提取开关机状态(如果有)
if (isset($host['status'])) {
$rawStatus = $host['status'];
// 状态映射:将魔方平台的原始状态转换为标准状态
$statusMap = [
'on' => 'on', // 开机
'off' => 'off', // 关机
'running' => 'on', // 运行中
'stopped' => 'off', // 已停止
'process' => 'process', // 处理中(开机/关机/重启过程中)
'pending' => 'process', // 等待中
'installing' => 'process', // 安装中
'rebooting' => 'process', // 重启中
'unknown' => 'unknown' // 未知
];
$status = $statusMap[$rawStatus] ?? $rawStatus;
$updates[] = 'status = ?';
$values[] = $status;
}
if (isset($host['config_option']) && is_array($host['config_option'])) {
foreach ($host['config_option'] as $option) {
switch ($option['key']) {
@ -392,7 +406,7 @@ function mofangGetVpsDetails($configId, $vpsId, $updateDb = true) {
"UPDATE vps_list SET {$setClause} WHERE config_id = ? AND vps_id = ?",
$values
);
error_log("[mofangGetVpsDetails] VPS {$vpsId} 详细信息已更新");
Logger::info("[mofangGetVpsDetails] VPS {$vpsId} 详细信息已更新" . (isset($status) ? ",状态: {$status}" : ""), 'mofangGetVpsDetails');
} else {
// 记录不存在,插入新记录(只插入获取到的字段)
$columns = [];
@ -422,7 +436,7 @@ function mofangGetVpsDetails($configId, $vpsId, $updateDb = true) {
"INSERT INTO vps_list ({$columnStr}) VALUES ({$placeholderStr})",
$insertValues
);
error_log("[mofangGetVpsDetails] VPS {$vpsId} 新记录已插入");
Logger::info("[mofangGetVpsDetails] VPS {$vpsId} 新记录已插入", 'mofangGetVpsDetails');
}
}
}
@ -433,9 +447,6 @@ function mofangGetVpsDetails($configId, $vpsId, $updateDb = true) {
/**
* VPS开机
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @return array|null 响应数据或null
*/
function mofangPowerOn($configId, $vpsId) {
$db = getVpsDB();
@ -446,14 +457,19 @@ function mofangPowerOn($configId, $vpsId) {
}
$endpoint = "/v1/hosts/{$vpsId}/module/on";
return mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
$result = mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
// 开机操作后立即获取状态并更新数据库
if ($result && isset($result['status']) && $result['status'] === 200) {
sleep(2); // 等待2秒让状态生效
mofangGetVpsStatus($configId, $vpsId, true);
}
return $result;
}
/**
* VPS关机
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @return array|null 响应数据或null
*/
function mofangPowerOff($configId, $vpsId) {
$db = getVpsDB();
@ -464,14 +480,19 @@ function mofangPowerOff($configId, $vpsId) {
}
$endpoint = "/v1/hosts/{$vpsId}/module/off";
return mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
$result = mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
// 关机操作后立即获取状态并更新数据库
if ($result && isset($result['status']) && $result['status'] === 200) {
sleep(2); // 等待2秒让状态生效
mofangGetVpsStatus($configId, $vpsId, true);
}
return $result;
}
/**
* VPS硬重启
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @return array|null 响应数据或null
*/
function mofangHardReboot($configId, $vpsId) {
$db = getVpsDB();
@ -482,13 +503,19 @@ function mofangHardReboot($configId, $vpsId) {
}
$endpoint = "/v1/hosts/{$vpsId}/module/hard_reboot";
return mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
$result = mofangApiRequest($config['site_url'], $endpoint, 'PUT', [], $configId);
// 硬重启操作后立即获取状态并更新数据库
if ($result && isset($result['status']) && $result['status'] === 200) {
sleep(3); // 等待3秒让重启生效
mofangGetVpsStatus($configId, $vpsId, true);
}
return $result;
}
/**
* 刷新指定配置的VPS列表并保存到数据库
* @param int $configId 配置ID
* @return bool 是否成功
*/
function refreshVpsListForConfig($configId) {
$db = getVpsDB();
@ -502,7 +529,7 @@ function refreshVpsListForConfig($configId) {
$result = mofangGetVpsList($configId);
if (!$result || !isset($result['status']) || $result['status'] !== 200) {
error_log("获取VPS列表失败: " . ($result['msg'] ?? '未知错误'));
Logger::error("获取VPS列表失败: " . ($result['msg'] ?? '未知错误'), 'refreshVpsListForConfig');
return false;
}
@ -559,17 +586,34 @@ function refreshVpsListForConfig($configId) {
}
}
// 使用INSERT OR REPLACE防止重复数据
// 注意需要保留原有的status字段避免被覆盖为NULL
// 基于vps_id和ip_address去重
$existing = $listDb->queryOne(
'SELECT status FROM vps_list WHERE config_id = ? AND vps_id = ?',
[$configId, $host['id']]
'SELECT id, status FROM vps_list WHERE vps_id = ? AND ip_address = ?',
[$host['id'], $host['dedicatedip'] ?? null]
);
$currentStatus = $existing ? $existing['status'] : null;
if ($existing) {
// 记录已存在只更新非status字段保留原有status
$listDb->execute(
'INSERT OR REPLACE INTO vps_list (config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, status, section, last_check) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)',
'UPDATE vps_list SET domain = ?, product_name = ?, cpu_cores = ?, memory_size = ?, disk_size = ?, bandwidth = ?, os_type = ?, amount = ?, nextduedate = ?, section = ?, last_check = CURRENT_TIMESTAMP WHERE id = ?',
[
$host['domain'] ?? null,
$host['product_name'] ?? null,
$cpuCores,
$memorySize,
$diskSize,
$bandwidth,
$osType,
$host['amount'] ?? null,
$host['nextduedate'] ?? null,
$config['auto_monitor'] ? 1 : 0,
$existing['id']
]
);
} else {
// 新记录插入所有字段status设为null稍后通过状态接口获取
$listDb->execute(
'INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, status, section, last_check) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, CURRENT_TIMESTAMP)',
[
$configId,
$host['id'],
@ -581,18 +625,32 @@ function refreshVpsListForConfig($configId) {
$diskSize,
$bandwidth,
$osType,
$currentStatus, // 保留原有状态
$host['amount'] ?? null,
$host['nextduedate'] ?? null,
$config['auto_monitor'] ? 1 : 0
]
);
}
}
// 刷新列表后批量获取每个VPS的状态
Logger::info("[refreshVpsListForConfig] 开始批量获取 {$configId} 配置下 " . count($hosts) . " 台VPS的状态", 'refreshVpsListForConfig');
foreach ($hosts as $host) {
// 调用专门的状态接口获取开关机状态
mofangGetVpsStatus($configId, $host['id'], true);
// 避免频繁请求每次请求间隔0.5秒
usleep(500000); // 500毫秒
}
Logger::info("[refreshVpsListForConfig] 配置 {$configId} 的VPS列表和状态刷新完成", 'refreshVpsListForConfig');
return true;
}
/**
* 刷新所有配置的VPS列表
* @return int 成功刷新的配置数量
*/
function refreshAllVpsLists() {
$db = getVpsDB();
@ -611,16 +669,13 @@ function refreshAllVpsLists() {
/**
* 获取单个VPS的状态并更新到数据库
* @param int $configId 配置ID
* @param int $vpsId VPS ID
* @return array|null 状态信息或null
*/
function updateVpsStatusToDb($configId, $vpsId) {
$db = getVpsDB();
$config = $db->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]);
if (!$config) {
error_log("配置ID {$configId} 不存在");
Logger::error("配置ID {$configId} 不存在", 'updateVpsStatusToDb');
return null;
}
@ -628,35 +683,60 @@ function updateVpsStatusToDb($configId, $vpsId) {
$result = mofangGetVpsStatus($configId, $vpsId, false);
if (!$result || !isset($result['status']) || $result['status'] !== 200) {
error_log("获取VPS {$vpsId} 状态失败: " . ($result['msg'] ?? '未知错误'));
Logger::error("获取VPS {$vpsId} 状态失败,保留原有状态: " . ($result['msg'] ?? '未知错误'), 'updateVpsStatusToDb');
return null;
}
$statusData = $result['data'];
$status = $statusData['status'] ?? 'unknown';
$rawStatus = $statusData['status'] ?? 'unknown';
$des = $statusData['des'] ?? '未知';
// 状态映射:将魔方平台的原始状态转换为标准状态
$statusMap = [
'on' => 'on', // 开机
'off' => 'off', // 关机
'running' => 'on', // 运行中
'stopped' => 'off', // 已停止
'process' => 'process', // 处理中(开机/关机/重启过程中)
'pending' => 'process', // 等待中
'installing' => 'process', // 安装中
'rebooting' => 'process', // 重启中
'unknown' => 'unknown' // 未知
];
$status = $statusMap[$rawStatus] ?? $rawStatus;
// 检查记录是否存在
$listDb = getVpsListDB();
$existing = $listDb->queryOne(
'SELECT id FROM vps_list WHERE config_id = ? AND vps_id = ?',
$vpsInfo = $listDb->queryOne(
'SELECT id, ip_address FROM vps_list WHERE config_id = ? AND vps_id = ?',
[$configId, $vpsId]
);
if ($existing) {
if ($vpsInfo) {
// 记录存在执行UPDATE
$listDb->execute(
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE config_id = ? AND vps_id = ?',
[$status, $configId, $vpsId]
$success = $listDb->execute(
'UPDATE vps_list SET status = ?, last_check = CURRENT_TIMESTAMP WHERE id = ?',
[$status, $vpsInfo['id']]
);
error_log("[updateVpsStatusToDb] VPS {$vpsId} 状态已更新为: {$status}");
if ($success) {
Logger::info("[updateVpsStatusToDb] VPS {$vpsId} (IP: {$vpsInfo['ip_address']}) 状态已更新为: {$status}", 'updateVpsStatusToDb');
} else {
Logger::error("[updateVpsStatusToDb] VPS {$vpsId} 状态更新失败,保留原有状态", 'updateVpsStatusToDb');
return null;
}
} else {
// 记录不存在,插入新记录
$listDb->execute(
$success = $listDb->execute(
'INSERT INTO vps_list (config_id, vps_id, status, last_check) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
[$configId, $vpsId, $status]
);
error_log("[updateVpsStatusToDb] VPS {$vpsId} 新记录已插入,状态: {$status}");
if ($success) {
Logger::info("[updateVpsStatusToDb] VPS {$vpsId} 新记录已插入,状态: {$status}", 'updateVpsStatusToDb');
} else {
Logger::error("[updateVpsStatusToDb] VPS {$vpsId} 新记录插入失败", 'updateVpsStatusToDb');
return null;
}
}
return [
@ -669,9 +749,6 @@ function updateVpsStatusToDb($configId, $vpsId) {
/**
* 批量获取VPS状态并更新到数据库
* @param int $configId 配置ID
* @param array $vpsIds VPS ID数组为空则更新该配置下所有VPS
* @return array 更新结果统计
*/
function batchUpdateVpsStatus($configId, $vpsIds = []) {
$db = getVpsDB();
@ -696,6 +773,10 @@ function batchUpdateVpsStatus($configId, $vpsIds = []) {
$failedCount = 0;
$results = [];
$listDb = getVpsListDB();
$listDb->getConnection()->beginTransaction();
try {
foreach ($vpsIds as $vpsId) {
$result = updateVpsStatusToDb($configId, $vpsId);
@ -713,6 +794,14 @@ function batchUpdateVpsStatus($configId, $vpsIds = []) {
}
}
$listDb->getConnection()->commit();
Logger::info("[batchUpdateVpsStatus] 批量更新完成: 成功{$successCount}, 失败{$failedCount}", 'batchUpdateVpsStatus');
} catch (Exception $e) {
$listDb->getConnection()->rollBack();
Logger::error("[batchUpdateVpsStatus] 批量更新失败,已回滚: " . $e->getMessage(), 'batchUpdateVpsStatus');
return ['success' => 0, 'failed' => count($vpsIds), 'error' => '批量更新失败: ' . $e->getMessage()];
}
return [
'success' => $successCount,
'failed' => $failedCount,

351
view_log.php Normal file
View File

@ -0,0 +1,351 @@
<?php
require_once __DIR__ . '/app/logger.php';
require_once __DIR__ . '/app/pass.php';
// 检查是否已登录(简单验证)
session_start();
if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) {
// 如果没有登录,显示简单的密码验证
if (isset($_POST['password'])) {
// 使用pass.php中定义的API_PASS作为密码
if ($_POST['password'] === API_PASS) {
$_SESSION['admin_logged_in'] = true;
} else {
$error = '密码错误';
}
}
if (!isset($_SESSION['admin_logged_in'])) {
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>VPSHUB - 日志查看</title>
<style>
body { font-family: Arial, sans-serif; margin: 50px; background: #f5f5f5; }
.login-box { max-width: 400px; margin: 100px auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h2 { margin-top: 0; color: #333; }
input[type="password"] { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0056b3; }
.error { color: red; margin-bottom: 10px; }
</style>
</head>
<body>
<div class="login-box">
<h2>🔐 日志查看权限验证</h2>
<?php if (isset($error)): ?>
<div class="error"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<form method="POST">
<input type="password" name="password" placeholder="请输入管理员密码" required>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
<?php
exit;
}
}
// 处理操作
$action = $_GET['action'] ?? '';
if ($action === 'clear' && $_SERVER['REQUEST_METHOD'] === 'POST') {
Logger::clear();
header('Location: view_log.php?cleared=1');
exit;
}
// 读取日志文件
$logFile = Logger::getLogFile();
$logContent = '';
$logLines = [];
if (file_exists($logFile)) {
$logContent = file_get_contents($logFile);
$logLines = array_filter(explode("\n", trim($logContent)));
// 反转数组,最新的在前面
$logLines = array_reverse($logLines);
}
// 过滤级别
$filterLevel = $_GET['level'] ?? 'ALL';
$filteredLines = $logLines;
if ($filterLevel !== 'ALL') {
$filteredLines = array_filter($logLines, function($line) use ($filterLevel) {
return strpos($line, "[{$filterLevel}]") !== false;
});
}
// 搜索关键词
$search = $_GET['search'] ?? '';
if ($search) {
$filteredLines = array_filter($filteredLines, function($line) use ($search) {
return stripos($line, $search) !== false;
});
}
// 限制显示行数
$limit = intval($_GET['limit'] ?? 100);
$filteredLines = array_slice($filteredLines, 0, $limit);
$cleared = isset($_GET['cleared']);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPSHUB - 日志查看器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f0f2f5;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 24px;
}
.controls {
padding: 20px 30px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.controls select,
.controls input,
.controls button {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.controls input {
flex: 1;
min-width: 200px;
}
.controls button {
background: #007bff;
color: white;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.controls button:hover {
background: #0056b3;
}
.controls button.danger {
background: #dc3545;
}
.controls button.danger:hover {
background: #c82333;
}
.stats {
padding: 15px 30px;
background: #fff3cd;
border-bottom: 1px solid #ffc107;
display: flex;
gap: 20px;
font-size: 14px;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.log-container {
padding: 20px 30px;
max-height: 70vh;
overflow-y: auto;
}
.log-line {
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
word-break: break-all;
}
.log-line.INFO {
background: #d1ecf1;
border-left: 3px solid #17a2b8;
}
.log-line.WARNING {
background: #fff3cd;
border-left: 3px solid #ffc107;
}
.log-line.ERROR {
background: #f8d7da;
border-left: 3px solid #dc3545;
}
.log-line.DEBUG {
background: #e2e3e5;
border-left: 3px solid #6c757d;
}
.timestamp {
color: #6c757d;
font-weight: bold;
}
.level {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
margin: 0 8px;
}
.level.INFO { background: #17a2b8; color: white; }
.level.WARNING { background: #ffc107; color: black; }
.level.ERROR { background: #dc3545; color: white; }
.level.DEBUG { background: #6c757d; color: white; }
.source {
color: #495057;
font-style: italic;
}
.message {
color: #212529;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6c757d;
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 20px;
opacity: 0.3;
}
.alert {
padding: 12px 20px;
margin: 20px 30px 0;
border-radius: 4px;
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📋 VPSHUB 日志查看器</h1>
<div>
<span style="font-size: 14px; opacity: 0.9;">
<?php echo date('Y-m-d H:i:s'); ?>
</span>
</div>
</div>
<?php if ($cleared): ?>
<div class="alert"> 日志已清空</div>
<?php endif; ?>
<div class="controls">
<select name="level" onchange="window.location.href='view_log.php?level=' + this.value + '&search=<?php echo urlencode($search); ?>&limit=<?php echo $limit; ?>'">
<option value="ALL" <?php echo $filterLevel === 'ALL' ? 'selected' : ''; ?>>全部级别</option>
<option value="INFO" <?php echo $filterLevel === 'INFO' ? 'selected' : ''; ?>>INFO</option>
<option value="WARNING" <?php echo $filterLevel === 'WARNING' ? 'selected' : ''; ?>>WARNING</option>
<option value="ERROR" <?php echo $filterLevel === 'ERROR' ? 'selected' : ''; ?>>ERROR</option>
<option value="DEBUG" <?php echo $filterLevel === 'DEBUG' ? 'selected' : ''; ?>>DEBUG</option>
</select>
<form method="GET" style="flex: 1; display: flex; gap: 10px;">
<input type="hidden" name="level" value="<?php echo htmlspecialchars($filterLevel); ?>">
<input type="text" name="search" placeholder="搜索日志..." value="<?php echo htmlspecialchars($search); ?>">
<button type="submit">🔍 搜索</button>
</form>
<select onchange="window.location.href='view_log.php?level=<?php echo urlencode($filterLevel); ?>&search=<?php echo urlencode($search); ?>&limit=' + this.value">
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50行</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100行</option>
<option value="200" <?php echo $limit === 200 ? 'selected' : ''; ?>>200行</option>
<option value="500" <?php echo $limit === 500 ? 'selected' : ''; ?>>500行</option>
<option value="1000" <?php echo $limit === 1000 ? 'selected' : ''; ?>>1000行</option>
</select>
<form method="POST" action="view_log.php?action=clear" onsubmit="return confirm('确定要清空所有日志吗?此操作不可恢复!');">
<button type="submit" class="danger">🗑️ 清空日志</button>
</form>
<button onclick="location.reload()">🔄 刷新</button>
</div>
<div class="stats">
<div class="stat-item">
<strong>总行数:</strong> <?php echo count($logLines); ?>
</div>
<div class="stat-item">
<strong>显示:</strong> <?php echo count($filteredLines); ?>
</div>
<div class="stat-item">
<strong>文件大小:</strong> <?php echo number_format(filesize($logFile) / 1024, 2); ?> KB
</div>
</div>
<div class="log-container">
<?php if (empty($filteredLines)): ?>
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>
</svg>
<h3>暂无日志记录</h3>
<p>当系统运行时,日志将显示在这里</p>
</div>
<?php else: ?>
<?php foreach ($filteredLines as $line): ?>
<?php
// 解析日志行
preg_match('/\[(.*?)\] \[(.*?)\] \[(.*?)\] (.*)/', $line, $matches);
if ($matches) {
$timestamp = $matches[1];
$level = $matches[2];
$source = $matches[3];
$message = $matches[4];
} else {
$timestamp = '';
$level = 'INFO';
$source = '';
$message = $line;
}
?>
<div class="log-line <?php echo htmlspecialchars($level); ?>">
<?php if ($timestamp): ?>
<span class="timestamp">[<?php echo htmlspecialchars($timestamp); ?>]</span>
<?php endif; ?>
<span class="level <?php echo htmlspecialchars($level); ?>"><?php echo htmlspecialchars($level); ?></span>
<?php if ($source): ?>
<span class="source">[<?php echo htmlspecialchars($source); ?>]</span>
<?php endif; ?>
<span class="message"><?php echo htmlspecialchars($message); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</body>
</html>