Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88f7fc7141 | |||
| 914208aa70 | |||
| ce3cf309f9 | |||
| 743aa16428 | |||
| cea593c8f9 |
@ -1,6 +1,7 @@
|
||||
# VPS Hub - 多平台VPS监控与管理系统
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
**一个强大的多平台VPS监控、管理和自动重启系统**
|
||||
@ -24,7 +25,6 @@
|
||||
- [监控服务](#-监控服务)
|
||||
- [安全说明](#-安全说明)
|
||||
- [常见问题](#-常见问题)
|
||||
- [更新日志](#-更新日志)
|
||||
- [许可证](#-许可证)
|
||||
|
||||
---
|
||||
|
||||
@ -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)');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
102
app/logger.php
Normal 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
0
app/logs/php.log
Normal 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']
|
||||
})
|
||||
|
||||
# 批量添加到数据库
|
||||
|
||||
@ -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>
|
||||
|
||||
98
index.php
98
index.php
@ -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['api_label'] = $configMap[$configId] ?? 'Unknown';
|
||||
}
|
||||
$vps['site_url'] = $siteUrl;
|
||||
} else {
|
||||
$vps['site_type'] = 'unknown';
|
||||
$vps['site_url'] = '';
|
||||
}
|
||||
}
|
||||
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">
|
||||
|
||||
279
mofangidc.php
279
mofangidc.php
@ -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
351
view_log.php
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user