From 00617621f9755acaa4deb6f01f8c09f644e3767f Mon Sep 17 00:00:00 2001 From: MasonLiu <2857911564@qq.com> Date: Tue, 30 Jun 2026 23:56:57 +0800 Subject: [PATCH] =?UTF-8?q?BUG=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=88=86=E7=A6=BB=E5=BC=8F=E5=88=B7=E6=96=B0=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aliyun.php | 9 +- app/db_helper.php | 55 ++++++++- app/db_manager.py | 278 ++++++++++++++++++++++++---------------------- index.php | 86 +++++++++++++- 4 files changed, 287 insertions(+), 141 deletions(-) diff --git a/aliyun.php b/aliyun.php index 7b64f7e..fd4371a 100644 --- a/aliyun.php +++ b/aliyun.php @@ -195,13 +195,18 @@ function getAliyunRegions($configId) { */ function getAliyunSwasRegions($configId) { return [ - 'cn-hangzhou', // 华东1(杭州) - 'cn-shanghai', // 华东2(上海) 'cn-qingdao', // 华北1(青岛) 'cn-beijing', // 华北2(北京) 'cn-zhangjiakou', // 华北3(张家口) + 'cn-huhehaote', // 华北5(呼和浩特) + 'cn-wulanchabu', // 华北6(乌兰察布) + 'cn-hangzhou', // 华东1(杭州) + 'cn-shanghai', // 华东2(上海) + 'cn-nanjing', // 华东5(南京) + 'cn-fuzhou', // 华东6(福州) 'cn-shenzhen', // 华南1(深圳) 'cn-heyuan', // 华南2(河源) + 'cn-guangzhou', // 华南3(广州) 'cn-chengdu', // 西南1(成都) 'cn-hongkong', // 中国香港 'ap-southeast-1', // 新加坡 diff --git a/app/db_helper.php b/app/db_helper.php index 734e10a..c2c41c2 100644 --- a/app/db_helper.php +++ b/app/db_helper.php @@ -141,10 +141,61 @@ 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, - UNIQUE(site_url, account) + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) "); + + // 迁移:如果存在旧的UNIQUE约束,需要重建表 + migrateConfigsTable($db); +} + +/** + * 迁移configs表,删除UNIQUE(site_url, account)约束 + * 允许同一个源添加多个不同的AccessKey + */ +function migrateConfigsTable($db) { + try { + // 检查是否存在旧的唯一约束 + $result = $db->query("PRAGMA index_list(configs)"); + $hasOldConstraint = false; + + foreach ($result as $index) { + if (isset($index['name']) && strpos($index['name'], 'site_url') !== false) { + $hasOldConstraint = true; + break; + } + } + + if ($hasOldConstraint) { + // 需要迁移:创建新表 -> 复制数据 -> 删除旧表 -> 重命名 + $db->execute("ALTER TABLE configs RENAME TO configs_old"); + + $db->execute(" + CREATE TABLE configs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + api_label TEXT NOT NULL UNIQUE, + site_type TEXT NOT NULL, + site_url TEXT, + account TEXT NOT NULL, + api_key TEXT NOT NULL, + auto_monitor BOOLEAN DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + "); + + $db->execute(" + INSERT INTO configs (id, api_label, site_type, site_url, account, api_key, auto_monitor, created_at, updated_at) + SELECT id, api_label, site_type, site_url, account, api_key, auto_monitor, created_at, updated_at + FROM configs_old + "); + + $db->execute("DROP TABLE configs_old"); + } + } catch (Exception $e) { + // 迁移失败不影响正常使用,记录日志即可 + error_log("[迁移警告] configs表迁移失败: " . $e->getMessage()); + } } /** diff --git a/app/db_manager.py b/app/db_manager.py index aadb753..52f06d5 100644 --- a/app/db_manager.py +++ b/app/db_manager.py @@ -9,33 +9,34 @@ from datetime import datetime class DatabaseManager: """数据库管理器""" - + def __init__(self, db_dir=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 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): """获取数据库连接""" conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row return conn - + def init_vps_db(self): """初始化vps.db - VPS配置表""" conn = self.get_connection(self.vps_db) cursor = conn.cursor() - + cursor.execute(''' CREATE TABLE IF NOT EXISTS configs ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -46,19 +47,18 @@ class DatabaseManager: api_key TEXT NOT NULL, auto_monitor BOOLEAN DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(site_url, account) + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') - + conn.commit() conn.close() - + def init_vpslist_db(self): """初始化vpslist.db - VPS列表缓存表""" conn = self.get_connection(self.vpslist_db) cursor = conn.cursor() - + cursor.execute(''' CREATE TABLE IF NOT EXISTS vps_list ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -80,19 +80,22 @@ class DatabaseManager: 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)') - + + 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() - + def init_status_db(self): """初始化status.db - Ping状态记录和摘要统计表""" conn = self.get_connection(self.status_db) cursor = conn.cursor() - + cursor.execute(''' CREATE TABLE IF NOT EXISTS ping_status ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -103,7 +106,7 @@ class DatabaseManager: check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') - + cursor.execute(''' CREATE TABLE IF NOT EXISTS vps_summary ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -120,36 +123,40 @@ class DatabaseManager: UNIQUE(vps_id, date) ) ''') - - 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)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_summary_date ON vps_summary(date)') - + + 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)') + cursor.execute( + 'CREATE INDEX IF NOT EXISTS idx_summary_date ON vps_summary(date)') + conn.commit() conn.close() - + # ==================== vps.db 操作 ==================== - + def add_config(self, api_label, site_type, account, api_key, site_url=None, auto_monitor=True): """添加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 (?, ?, ?, ?, ?, ?) ''', (api_label, site_type, site_url, account, api_key, auto_monitor)) - + config_id = cursor.lastrowid conn.commit() return config_id @@ -158,82 +165,83 @@ class DatabaseManager: raise e finally: conn.close() - + def get_all_configs(self): """获取所有配置""" conn = self.get_connection(self.vps_db) cursor = conn.cursor() - + cursor.execute('SELECT * FROM configs ORDER BY id') configs = [dict(row) for row in cursor.fetchall()] - + conn.close() return configs - + def get_config_by_id(self, config_id): """根据ID获取配置""" conn = self.get_connection(self.vps_db) cursor = conn.cursor() - + cursor.execute('SELECT * FROM configs WHERE id = ?', (config_id,)) row = cursor.fetchone() - + conn.close() return dict(row) if row else None - + def update_config(self, config_id, **kwargs): """更新配置""" if not kwargs: return False - + conn = self.get_connection(self.vps_db) cursor = conn.cursor() - + fields = ', '.join([f"{key} = ?" for key in kwargs.keys()]) values = list(kwargs.values()) values.append(config_id) - - cursor.execute(f'UPDATE configs SET {fields}, updated_at = CURRENT_TIMESTAMP WHERE id = ?', values) - + + cursor.execute( + f'UPDATE configs SET {fields}, updated_at = CURRENT_TIMESTAMP WHERE id = ?', values) + affected = cursor.rowcount conn.commit() conn.close() - + return affected > 0 - + def delete_config(self, config_id): """删除配置""" conn = self.get_connection(self.vps_db) cursor = conn.cursor() - + cursor.execute('DELETE FROM configs WHERE id = ?', (config_id,)) - + affected = cursor.rowcount conn.commit() conn.close() - + 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) @@ -248,7 +256,7 @@ class DatabaseManager: config['created_at'], config['updated_at'] )) - + cursor.execute('DROP TABLE IF EXISTS configs_backup') conn.commit() return True @@ -257,35 +265,35 @@ class DatabaseManager: raise e finally: conn.close() - + # ==================== vpslist.db 操作 ==================== - + 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) @@ -296,29 +304,29 @@ class DatabaseManager: 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 conn.commit() conn.close() - + return record_id - + def batch_add_vps(self, vps_list): """批量添加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 INTO vps_list @@ -360,87 +368,90 @@ class DatabaseManager: vps.get('nextduedate'), vps.get('section', False) )) - + conn.commit() conn.close() - + def get_all_vps(self): """获取所有VPS""" conn = self.get_connection(self.vpslist_db) cursor = conn.cursor() - + cursor.execute('SELECT * FROM vps_list ORDER BY config_id, vps_id') vps_list = [dict(row) for row in cursor.fetchall()] - + conn.close() return vps_list - + def get_vps_by_config(self, config_id): """根据配置ID获取VPS列表""" conn = self.get_connection(self.vpslist_db) cursor = conn.cursor() - - cursor.execute('SELECT * FROM vps_list WHERE config_id = ? ORDER BY vps_id', (config_id,)) + + cursor.execute( + 'SELECT * FROM vps_list WHERE config_id = ? ORDER BY vps_id', (config_id,)) vps_list = [dict(row) for row in cursor.fetchall()] - + conn.close() return vps_list - + def update_vps_details(self, vps_id, **kwargs): """更新VPS详细信息""" if not kwargs: return False - + conn = self.get_connection(self.vpslist_db) cursor = conn.cursor() - + fields = ', '.join([f"{key} = ?" for key in kwargs.keys()]) values = list(kwargs.values()) values.append(vps_id) - - cursor.execute(f'UPDATE vps_list SET {fields}, last_check = CURRENT_TIMESTAMP WHERE vps_id = ?', values) - + + cursor.execute( + f'UPDATE vps_list SET {fields}, last_check = CURRENT_TIMESTAMP WHERE vps_id = ?', values) + affected = cursor.rowcount conn.commit() conn.close() - + return affected > 0 - + def update_vps_status(self, vps_id, status): """更新VPS状态""" return self.update_vps_details(vps_id, status=status) - + def get_monitored_vps(self): """获取所有标记为需要监控的VPS""" conn = self.get_connection(self.vpslist_db) cursor = conn.cursor() - - cursor.execute('SELECT * FROM vps_list WHERE section = 1 ORDER BY config_id, vps_id') + + cursor.execute( + 'SELECT * FROM vps_list WHERE section = 1 ORDER BY config_id, vps_id') vps_list = [dict(row) for row in cursor.fetchall()] - + 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) @@ -462,7 +473,7 @@ class DatabaseManager: vps.get('section', 0), vps.get('last_check') )) - + cursor.execute('DROP TABLE IF EXISTS vps_list_backup') conn.commit() return True @@ -471,27 +482,27 @@ class DatabaseManager: raise e finally: conn.close() - + # ==================== status.db 操作 ==================== - + def save_ping_status(self, vps_id, target, status, latency_ms=None): """保存Ping状态记录""" conn = self.get_connection(self.status_db) cursor = conn.cursor() - + cursor.execute(''' INSERT INTO ping_status (vps_id, target, status, latency_ms) VALUES (?, ?, ?, ?) ''', (vps_id, target, status, latency_ms)) - + conn.commit() conn.close() - + def get_ping_records(self, vps_id, date=None): """获取Ping记录""" conn = self.get_connection(self.status_db) cursor = conn.cursor() - + if date: cursor.execute(''' SELECT * FROM ping_status @@ -504,35 +515,37 @@ class DatabaseManager: WHERE vps_id = ? ORDER BY check_time DESC ''', (vps_id,)) - + records = [dict(row) for row in cursor.fetchall()] conn.close() - + return records - + def cleanup_old_ping_records(self, days=30): """清理旧的Ping记录""" from datetime import timedelta - + conn = self.get_connection(self.status_db) cursor = conn.cursor() - - cutoff_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S') - cursor.execute('DELETE FROM ping_status WHERE check_time < ?', (cutoff_date,)) - + + cutoff_date = (datetime.now() - timedelta(days=days) + ).strftime('%Y-%m-%d %H:%M:%S') + cursor.execute( + 'DELETE FROM ping_status WHERE check_time < ?', (cutoff_date,)) + deleted = cursor.rowcount conn.commit() conn.close() - + return deleted - + 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): + count_under_100, count_100_to_300, count_300_to_500, + count_abnormal, availability): """保存VPS摘要统计""" conn = self.get_connection(self.status_db) cursor = conn.cursor() - + cursor.execute(''' INSERT OR REPLACE INTO vps_summary (vps_id, date, avg_latency_ms, max_latency_ms, min_latency_ms, @@ -541,32 +554,35 @@ class DatabaseManager: ''', (vps_id, date, avg_latency, max_latency, min_latency, count_under_100, count_100_to_300, count_300_to_500, count_abnormal, availability)) - + conn.commit() conn.close() - + def get_vps_summary(self, vps_id, date=None): """获取VPS摘要统计""" conn = self.get_connection(self.status_db) cursor = conn.cursor() - + if date: - cursor.execute('SELECT * FROM vps_summary WHERE vps_id = ? AND date = ?', (vps_id, date)) + cursor.execute( + 'SELECT * FROM vps_summary WHERE vps_id = ? AND date = ?', (vps_id, date)) else: - cursor.execute('SELECT * FROM vps_summary WHERE vps_id = ? ORDER BY date DESC LIMIT 1', (vps_id,)) - + cursor.execute( + 'SELECT * FROM vps_summary WHERE vps_id = ? ORDER BY date DESC LIMIT 1', (vps_id,)) + row = cursor.fetchone() conn.close() - + return dict(row) if row else None - + def get_all_summaries(self, date=None): """获取所有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,)) + cursor.execute( + 'SELECT * FROM vps_summary WHERE date = ? ORDER BY vps_id', (date,)) else: cursor.execute(''' SELECT vs.* FROM vps_summary vs @@ -577,10 +593,10 @@ class DatabaseManager: ) latest ON vs.vps_id = latest.vps_id AND vs.date = latest.max_date ORDER BY vs.vps_id ''') - + summaries = [dict(row) for row in cursor.fetchall()] conn.close() - + return summaries diff --git a/index.php b/index.php index 13aed5f..7337bb8 100644 --- a/index.php +++ b/index.php @@ -87,8 +87,52 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $errorMsg = $result['error'] ?? $result['msg'] ?? '未知错误'; $message = '
'; } + } elseif ($action === 'refresh_single_config') { + // 单独刷新某个配置 + $configId = intval($_POST['config_id'] ?? 0); + + if ($configId > 0) { + $configDb = getVpsDB(); + $config = $configDb->queryOne('SELECT * FROM configs WHERE id = ?', [$configId]); + + if ($config) { + $success = false; + $typeName = ''; + + if ($config['site_type'] === 'mofang') { + require_once __DIR__ . '/mofangidc.php'; + $success = refreshVpsListForConfig($configId); + $typeName = '魔方平台'; + } elseif ($config['site_type'] === 'tencent') { + require_once __DIR__ . '/tencentcloud.php'; + // 同时刷新CVM和轻量 + $cvmSuccess = refreshTencentCvmListForConfig($configId); + $lhSuccess = refreshTencentLighthouseListForConfig($configId); + $success = $cvmSuccess || $lhSuccess; + $typeName = '腾讯云'; + } elseif ($config['site_type'] === 'aliyun') { + require_once __DIR__ . '/aliyun.php'; + $success = refreshAliyunEcsListForConfig($configId); + $typeName = '阿里云ECS'; + } elseif ($config['site_type'] === 'aliyun_swas') { + require_once __DIR__ . '/aliyun.php'; + $success = refreshAliyunSwasListForConfig($configId); + $typeName = '阿里云轻量'; + } + + if ($success) { + $message = ''; + } else { + $message = ''; + } + } else { + $message = ''; + } + } else { + $message = ''; + } } elseif ($action === 'refresh_vps_list') { - // 手动刷新VPS列表 + // 手动刷新VPS列表(全部) $count = 0; // 刷新魔方平台的VPS列表 @@ -201,6 +245,12 @@ foreach ($vpsList as $vps) { $vpsByApiLabel[$apiLabel][] = $vps; } +// 构建api_label到config_id的映射(用于单独刷新) +$apiLabelToConfigId = []; +foreach ($configs as $config) { + $apiLabelToConfigId[$config['api_label']] = $config['id']; +} + // 统计信息 $totalCount = count($vpsList); $runningCount = 0; @@ -245,6 +295,11 @@ foreach ($vpsList as $vps) { .btn-refresh:hover { background-color: #0056b3; } + .config-title { + display: flex; + justify-content: space-between; + align-items: center; + } /* 消息自动消失动画 */ .success-message, .error-message { transition: opacity 0.5s ease-out; @@ -417,9 +472,9 @@ foreach ($vpsList as $vps) {