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

View File

@ -1,21 +1,13 @@
<?php <?php
/** require_once __DIR__ . '/logger.php';
* VPS Hub 数据库辅助类
* 提供SQLite数据库的统一访问接口
*/
class DBHelper { class DBHelper {
private $db; private $db;
private $dbPath; private $dbPath;
/**
* 构造函数
* @param string $dbPath 数据库文件路径
*/
public function __construct($dbPath) { public function __construct($dbPath) {
$this->dbPath = $dbPath; $this->dbPath = $dbPath;
// 确保目录存在
$dir = dirname($dbPath); $dir = dirname($dbPath);
if (!is_dir($dir)) { if (!is_dir($dir)) {
mkdir($dir, 0755, true); mkdir($dir, 0755, true);
@ -32,9 +24,6 @@ class DBHelper {
/** /**
* 执行查询并返回所有结果 * 执行查询并返回所有结果
* @param string $sql SQL语句
* @param array $params 参数数组
* @return array 结果数组
*/ */
public function query($sql, $params = []) { public function query($sql, $params = []) {
try { try {
@ -42,16 +31,13 @@ class DBHelper {
$stmt->execute($params); $stmt->execute($params);
return $stmt->fetchAll(); return $stmt->fetchAll();
} catch (PDOException $e) { } catch (PDOException $e) {
error_log("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql); Logger::error("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::query');
return []; return [];
} }
} }
/** /**
* 执行查询并返回单条结果 * 执行查询并返回单条结果
* @param string $sql SQL语句
* @param array $params 参数数组
* @return array|false 结果数组或false
*/ */
public function queryOne($sql, $params = []) { public function queryOne($sql, $params = []) {
try { try {
@ -59,32 +45,26 @@ class DBHelper {
$stmt->execute($params); $stmt->execute($params);
return $stmt->fetch(); return $stmt->fetch();
} catch (PDOException $e) { } catch (PDOException $e) {
error_log("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql); Logger::error("数据库查询错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::queryOne');
return false; return false;
} }
} }
/** /**
* 执行插入、更新、删除操作 * 执行插入、更新、删除操作
* @param string $sql SQL语句
* @param array $params 参数数组
* @return bool 是否成功
*/ */
public function execute($sql, $params = []) { public function execute($sql, $params = []) {
try { try {
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
return $stmt->execute($params); return $stmt->execute($params);
} catch (PDOException $e) { } catch (PDOException $e) {
error_log("数据库执行错误: " . $e->getMessage() . " | SQL: " . $sql); Logger::error("数据库执行错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::execute');
return false; return false;
} }
} }
/** /**
* 插入数据并返回最后插入的ID * 插入数据并返回最后插入的ID
* @param string $sql SQL语句
* @param array $params 参数数组
* @return int|false 最后插入的ID或false
*/ */
public function insert($sql, $params = []) { public function insert($sql, $params = []) {
try { try {
@ -92,36 +72,23 @@ class DBHelper {
$stmt->execute($params); $stmt->execute($params);
return $this->db->lastInsertId(); return $this->db->lastInsertId();
} catch (PDOException $e) { } catch (PDOException $e) {
error_log("数据库插入错误: " . $e->getMessage() . " | SQL: " . $sql); Logger::error("数据库插入错误: " . $e->getMessage() . " | SQL: " . $sql, 'DBHelper::insert');
return false; return false;
} }
} }
/**
* 开始事务
*/
public function beginTransaction() { public function beginTransaction() {
return $this->db->beginTransaction(); return $this->db->beginTransaction();
} }
/**
* 提交事务
*/
public function commit() { public function commit() {
return $this->db->commit(); return $this->db->commit();
} }
/**
* 回滚事务
*/
public function rollBack() { public function rollBack() {
return $this->db->rollBack(); return $this->db->rollBack();
} }
/**
* 获取数据库连接对象(用于高级操作)
* @return PDO
*/
public function getConnection() { public function getConnection() {
return $this->db; return $this->db;
} }
@ -133,7 +100,6 @@ function getVpsDB() {
if ($db === null) { if ($db === null) {
$dbPath = __DIR__ . '/db/vps.db'; $dbPath = __DIR__ . '/db/vps.db';
$db = new DBHelper($dbPath); $db = new DBHelper($dbPath);
// 初始化表结构
initVpsTables($db); initVpsTables($db);
} }
return $db; return $db;
@ -156,7 +122,6 @@ function getStatusDB() {
if ($db === null) { if ($db === null) {
$dbPath = __DIR__ . '/db/status.db'; $dbPath = __DIR__ . '/db/status.db';
$db = new DBHelper($dbPath); $db = new DBHelper($dbPath);
// 初始化表结构
initStatusTables($db); initStatusTables($db);
} }
return $db; return $db;
@ -176,7 +141,8 @@ function initVpsTables($db) {
api_key TEXT NOT NULL, api_key TEXT NOT NULL,
auto_monitor BOOLEAN DEFAULT 1, auto_monitor BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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, bandwidth TEXT,
os_type TEXT, os_type TEXT,
status TEXT, status TEXT,
amount TEXT,
nextduedate INTEGER,
section BOOLEAN DEFAULT 0, section BOOLEAN DEFAULT 0,
last_check TIMESTAMP, last_check TIMESTAMP,
FOREIGN KEY (config_id) REFERENCES configs(id), FOREIGN KEY (config_id) REFERENCES configs(id)
UNIQUE(config_id, vps_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_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_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 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """VPS Hub 数据库管理模块"""
VPS Hub 数据库管理模块
负责初始化和管理三个SQLite数据库: vps.db, vpslist.db, status.db
"""
import os import os
import sqlite3 import sqlite3
@ -14,38 +11,24 @@ class DatabaseManager:
"""数据库管理器""" """数据库管理器"""
def __init__(self, db_dir=None): def __init__(self, db_dir=None):
"""初始化数据库管理器
Args:
db_dir: 数据库文件目录,默认为app/db/
"""
if db_dir is None: if db_dir is None:
db_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'db') db_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'db')
self.db_dir = db_dir self.db_dir = db_dir
os.makedirs(db_dir, exist_ok=True) os.makedirs(db_dir, exist_ok=True)
# 数据库文件路径
self.vps_db = os.path.join(db_dir, 'vps.db') self.vps_db = os.path.join(db_dir, 'vps.db')
self.vpslist_db = os.path.join(db_dir, 'vpslist.db') self.vpslist_db = os.path.join(db_dir, 'vpslist.db')
self.status_db = os.path.join(db_dir, 'status.db') self.status_db = os.path.join(db_dir, 'status.db')
# 初始化所有数据库
self.init_vps_db() self.init_vps_db()
self.init_vpslist_db() self.init_vpslist_db()
self.init_status_db() self.init_status_db()
def get_connection(self, db_path): def get_connection(self, db_path):
"""获取数据库连接 """获取数据库连接"""
Args:
db_path: 数据库文件路径
Returns:
SQLite连接对象
"""
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row # 使结果可以通过列名访问 conn.row_factory = sqlite3.Row
return conn return conn
def init_vps_db(self): def init_vps_db(self):
@ -63,7 +46,8 @@ class DatabaseManager:
api_key TEXT NOT NULL, api_key TEXT NOT NULL,
auto_monitor BOOLEAN DEFAULT 1, auto_monitor BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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, bandwidth TEXT,
os_type TEXT, os_type TEXT,
status TEXT, status TEXT,
amount TEXT,
nextduedate INTEGER,
section BOOLEAN DEFAULT 0, section BOOLEAN DEFAULT 0,
last_check TIMESTAMP, last_check TIMESTAMP,
FOREIGN KEY (config_id) REFERENCES configs(id), FOREIGN KEY (config_id) REFERENCES configs(id)
UNIQUE(config_id, vps_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_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_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.commit()
conn.close() conn.close()
@ -108,7 +93,6 @@ class DatabaseManager:
conn = self.get_connection(self.status_db) conn = self.get_connection(self.status_db)
cursor = conn.cursor() cursor = conn.cursor()
# Ping状态记录表
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS ping_status ( CREATE TABLE IF NOT EXISTS ping_status (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -120,7 +104,6 @@ class DatabaseManager:
) )
''') ''')
# VPS摘要统计表
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS vps_summary ( CREATE TABLE IF NOT EXISTS vps_summary (
id INTEGER PRIMARY KEY AUTOINCREMENT, 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_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_ping_time ON ping_status(check_time)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_vps ON vps_summary(vps_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_vps ON vps_summary(vps_id)')
@ -150,23 +132,19 @@ class DatabaseManager:
# ==================== vps.db 操作 ==================== # ==================== vps.db 操作 ====================
def add_config(self, api_label, site_type, account, api_key, site_url=None, auto_monitor=True): def add_config(self, api_label, site_type, account, api_key, site_url=None, auto_monitor=True):
"""添加VPS配置 """添加VPS配置基于site_url和account去重"""
Args:
api_label: API标识必填唯一
site_type: 网站类型 (mofang/aliyun/tencent)
account: 账户
api_key: API密钥
site_url: 网站链接
auto_monitor: 是否开启自动监控
Returns:
新配置的ID
"""
conn = self.get_connection(self.vps_db) conn = self.get_connection(self.vps_db)
cursor = conn.cursor() cursor = conn.cursor()
try: try:
existing = cursor.execute(
'SELECT id FROM configs WHERE site_url = ? AND account = ?',
(site_url, account)
).fetchone()
if existing:
return existing['id']
cursor.execute(''' cursor.execute('''
INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor) INSERT INTO configs (api_label, site_type, site_url, account, api_key, auto_monitor)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
@ -182,11 +160,7 @@ class DatabaseManager:
conn.close() conn.close()
def get_all_configs(self): def get_all_configs(self):
"""获取所有配置 """获取所有配置"""
Returns:
配置列表
"""
conn = self.get_connection(self.vps_db) conn = self.get_connection(self.vps_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -197,14 +171,7 @@ class DatabaseManager:
return configs return configs
def get_config_by_id(self, config_id): def get_config_by_id(self, config_id):
"""根据ID获取配置 """根据ID获取配置"""
Args:
config_id: 配置ID
Returns:
配置字典或None
"""
conn = self.get_connection(self.vps_db) conn = self.get_connection(self.vps_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -215,22 +182,13 @@ class DatabaseManager:
return dict(row) if row else None return dict(row) if row else None
def update_config(self, config_id, **kwargs): def update_config(self, config_id, **kwargs):
"""更新配置 """更新配置"""
Args:
config_id: 配置ID
**kwargs: 要更新的字段
Returns:
是否成功
"""
if not kwargs: if not kwargs:
return False return False
conn = self.get_connection(self.vps_db) conn = self.get_connection(self.vps_db)
cursor = conn.cursor() cursor = conn.cursor()
# 构建UPDATE语句
fields = ', '.join([f"{key} = ?" for key in kwargs.keys()]) fields = ', '.join([f"{key} = ?" for key in kwargs.keys()])
values = list(kwargs.values()) values = list(kwargs.values())
values.append(config_id) values.append(config_id)
@ -244,14 +202,7 @@ class DatabaseManager:
return affected > 0 return affected > 0
def delete_config(self, config_id): def delete_config(self, config_id):
"""删除配置 """删除配置"""
Args:
config_id: 配置ID
Returns:
是否成功
"""
conn = self.get_connection(self.vps_db) conn = self.get_connection(self.vps_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -263,30 +214,88 @@ class DatabaseManager:
return affected > 0 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 操作 ==================== # ==================== vpslist.db 操作 ====================
def add_vps(self, config_id, vps_id, domain=None, ip_address=None, def get_next_available_id(self):
product_name=None, section=False): """获取下一个可用的ID填补空缺"""
"""添加VPS到列表
Args:
config_id: 配置ID
vps_id: VPS在平台的ID
domain: 域名
ip_address: IP地址
product_name: 产品名称
section: 是否标记为需要监控
Returns:
新记录的ID
"""
conn = self.get_connection(self.vpslist_db) conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('SELECT id FROM vps_list ORDER BY id')
INSERT INTO vps_list (config_id, vps_id, domain, ip_address, product_name, section, last_check) ids = [row['id'] for row in cursor.fetchall()]
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) conn.close()
''', (config_id, vps_id, domain, ip_address, product_name, section))
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)
''', (config_id, vps_id, domain, ip_address, product_name, section))
record_id = cursor.lastrowid record_id = cursor.lastrowid
conn.commit() conn.commit()
@ -295,42 +304,68 @@ class DatabaseManager:
return record_id return record_id
def batch_add_vps(self, vps_list): def batch_add_vps(self, vps_list):
"""批量添加VPS """批量添加VPS基于vps_id和ip_address去重"""
Args:
vps_list: VPS信息列表,每个元素是字典
"""
conn = self.get_connection(self.vpslist_db) conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor() cursor = conn.cursor()
for vps in vps_list: for vps in vps_list:
cursor.execute(''' existing = cursor.execute(
INSERT OR REPLACE INTO vps_list 'SELECT id FROM vps_list WHERE vps_id = ? AND ip_address = ?',
(config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, section, last_check) (vps['vps_id'], vps.get('ip_address'))
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ).fetchone()
''', (
vps['config_id'], if existing:
vps['vps_id'], continue
vps.get('domain'),
vps.get('ip_address'), custom_id = self.get_next_available_id()
vps.get('product_name'),
vps.get('cpu_cores'), if custom_id:
vps.get('memory_size'), cursor.execute('''
vps.get('disk_size'), INSERT INTO vps_list
vps.get('bandwidth'), (id, config_id, vps_id, domain, ip_address, product_name, cpu_cores, memory_size, disk_size, bandwidth, os_type, amount, nextduedate, section, last_check)
vps.get('os_type'), VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
vps.get('section', False) ''', (
)) 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'],
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)
))
conn.commit() conn.commit()
conn.close() conn.close()
def get_all_vps(self): def get_all_vps(self):
"""获取所有VPS """获取所有VPS"""
Returns:
VPS列表
"""
conn = self.get_connection(self.vpslist_db) conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -341,14 +376,7 @@ class DatabaseManager:
return vps_list return vps_list
def get_vps_by_config(self, config_id): def get_vps_by_config(self, config_id):
"""根据配置ID获取VPS列表 """根据配置ID获取VPS列表"""
Args:
config_id: 配置ID
Returns:
VPS列表
"""
conn = self.get_connection(self.vpslist_db) conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -359,15 +387,7 @@ class DatabaseManager:
return vps_list return vps_list
def update_vps_details(self, vps_id, **kwargs): def update_vps_details(self, vps_id, **kwargs):
"""更新VPS详细信息 """更新VPS详细信息"""
Args:
vps_id: VPS ID
**kwargs: 要更新的字段
Returns:
是否成功
"""
if not kwargs: if not kwargs:
return False return False
@ -387,23 +407,11 @@ class DatabaseManager:
return affected > 0 return affected > 0
def update_vps_status(self, vps_id, status): def update_vps_status(self, vps_id, status):
"""更新VPS状态 """更新VPS状态"""
Args:
vps_id: VPS ID
status: 状态 (on/off/unknown)
Returns:
是否成功
"""
return self.update_vps_details(vps_id, status=status) return self.update_vps_details(vps_id, status=status)
def get_monitored_vps(self): def get_monitored_vps(self):
"""获取所有标记为需要监控的VPS """获取所有标记为需要监控的VPS"""
Returns:
VPS列表
"""
conn = self.get_connection(self.vpslist_db) conn = self.get_connection(self.vpslist_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -413,17 +421,61 @@ class DatabaseManager:
conn.close() conn.close()
return vps_list 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 操作 ==================== # ==================== status.db 操作 ====================
def save_ping_status(self, vps_id, target, status, latency_ms=None): def save_ping_status(self, vps_id, target, status, latency_ms=None):
"""保存Ping状态记录 """保存Ping状态记录"""
Args:
vps_id: VPS ID
target: 目标(IP或域名)
status: 状态 (normal/abnormal)
latency_ms: 延迟(ms)
"""
conn = self.get_connection(self.status_db) conn = self.get_connection(self.status_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -436,15 +488,7 @@ class DatabaseManager:
conn.close() conn.close()
def get_ping_records(self, vps_id, date=None): def get_ping_records(self, vps_id, date=None):
"""获取Ping记录 """获取Ping记录"""
Args:
vps_id: VPS ID
date: 日期 (YYYY-MM-DD),为空则获取所有记录
Returns:
Ping记录列表
"""
conn = self.get_connection(self.status_db) conn = self.get_connection(self.status_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -467,11 +511,7 @@ class DatabaseManager:
return records return records
def cleanup_old_ping_records(self, days=30): def cleanup_old_ping_records(self, days=30):
"""清理旧的Ping记录 """清理旧的Ping记录"""
Args:
days: 保留天数,默认30天
"""
from datetime import timedelta from datetime import timedelta
conn = self.get_connection(self.status_db) 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, 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_under_100, count_100_to_300, count_300_to_500,
count_abnormal, availability): count_abnormal, availability):
"""保存VPS摘要统计 """保存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: 可用性评分
"""
conn = self.get_connection(self.status_db) conn = self.get_connection(self.status_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -519,15 +546,7 @@ class DatabaseManager:
conn.close() conn.close()
def get_vps_summary(self, vps_id, date=None): def get_vps_summary(self, vps_id, date=None):
"""获取VPS摘要统计 """获取VPS摘要统计"""
Args:
vps_id: VPS ID
date: 日期,为空则获取最新一条
Returns:
摘要统计字典或None
"""
conn = self.get_connection(self.status_db) conn = self.get_connection(self.status_db)
cursor = conn.cursor() cursor = conn.cursor()
@ -542,21 +561,13 @@ class DatabaseManager:
return dict(row) if row else None return dict(row) if row else None
def get_all_summaries(self, date=None): def get_all_summaries(self, date=None):
"""获取所有VPS的摘要统计 """获取所有VPS的摘要统计"""
Args:
date: 日期,为空则获取最新
Returns:
摘要统计列表
"""
conn = self.get_connection(self.status_db) conn = self.get_connection(self.status_db)
cursor = conn.cursor() cursor = conn.cursor()
if date: if date:
cursor.execute('SELECT * FROM vps_summary WHERE date = ? ORDER BY vps_id', (date,)) cursor.execute('SELECT * FROM vps_summary WHERE date = ? ORDER BY vps_id', (date,))
else: else:
# 获取每个VPS的最新摘要
cursor.execute(''' cursor.execute('''
SELECT vs.* FROM vps_summary vs SELECT vs.* FROM vps_summary vs
INNER JOIN ( 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 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """VPS Hub 多平台监控程序"""
VPS Hub 多平台监控程序
功能支持多平台(魔方/阿里云/腾讯云)的VPS监控自动开机和可用性统计
"""
import os import os
import sys import sys
@ -16,7 +13,6 @@ import schedule
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
# 导入数据库管理器
from db_manager import DatabaseManager from db_manager import DatabaseManager
@ -28,42 +24,13 @@ class PlatformAdapter:
self.account = account self.account = account
self.api_key = api_key self.api_key = api_key
self.jwt_token = None 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): class MofangAdapter(PlatformAdapter):
"""魔方平台适配器(核云IDC等使用此适配器)""" """魔方平台适配器"""
def __init__(self, site_url, account, api_key): def __init__(self, site_url, account, api_key):
super().__init__(site_url, account, api_key) super().__init__(site_url, account, api_key)
# 魔方平台的API路径统一为 /v1
if not self.site_url: if not self.site_url:
raise ValueError("魔方平台必须提供网站链接(API地址)") raise ValueError("魔方平台必须提供网站链接(API地址)")
@ -79,7 +46,6 @@ class MofangAdapter(PlatformAdapter):
def login(self): def login(self):
try: try:
# 魔方平台API统一在 /v1 路径下
url = f"{self.site_url}/v1/login_api" url = f"{self.site_url}/v1/login_api"
data = { data = {
'account': self.account, 'account': self.account,
@ -729,7 +695,9 @@ class MonitorService:
'disk_size': disk_size, 'disk_size': disk_size,
'bandwidth': bandwidth, 'bandwidth': bandwidth,
'os_type': os_type, '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'; require_once __DIR__ . '/mofangidc.php';
// Token文件路径(存储API_PASS) // Token文件路径(存储API_PASS)
$tokenFile = __DIR__ . '/app/token_pass.php'; $tokenFile = __DIR__ . '/app/pass.php'; # 更新路径为app/pass.php避免与其他配置文件混淆
// 检查是否首次访问(需要设置API_PASS) // 检查是否首次访问(需要设置API_PASS)
$needSetup = !file_exists($tokenFile); $needSetup = !file_exists($tokenFile);
@ -43,7 +43,7 @@ if ($needSetup && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['setup_p
// 如果需要设置密码,显示设置页面 // 如果需要设置密码,显示设置页面
if ($needSetup) { if ($needSetup) {
?> ?>n
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>

100
index.php
View File

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

View File

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

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>