From 9854f478c0ae050ee7c46206a7e7b425ab138d8e Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Thu, 9 Apr 2026 13:55:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=87=AA=E6=A3=80=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db-schema-check.js | 225 ++++++++++++++++++++++++++++++++++++++ server/index.js | 2 +- server/init-db.js | 12 ++ 3 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 server/db-schema-check.js diff --git a/server/db-schema-check.js b/server/db-schema-check.js new file mode 100644 index 0000000..30a7796 --- /dev/null +++ b/server/db-schema-check.js @@ -0,0 +1,225 @@ +/** + * Database schema check + * Ensures required tables and columns exist at startup. + */ +require('dotenv').config(); +const db = require('./db'); +const path = require('path'); +const fs = require('fs'); + +const SCHEMA = { + users: { + createSql: ` + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + salt VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `, + columns: [] + }, + prometheus_sources: { + createSql: ` + CREATE TABLE IF NOT EXISTS prometheus_sources ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + url VARCHAR(500) NOT NULL, + description TEXT, + is_server_source TINYINT(1) DEFAULT 1, + type VARCHAR(50) DEFAULT 'prometheus', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `, + columns: [ + { + name: 'is_server_source', + sql: "ALTER TABLE prometheus_sources ADD COLUMN is_server_source TINYINT(1) DEFAULT 1 AFTER description" + }, + { + name: 'type', + sql: "ALTER TABLE prometheus_sources ADD COLUMN type VARCHAR(50) DEFAULT 'prometheus' AFTER is_server_source" + } + ] + }, + site_settings: { + createSql: ` + CREATE TABLE IF NOT EXISTS site_settings ( + id INT PRIMARY KEY DEFAULT 1, + page_name VARCHAR(255) DEFAULT 'Data Visualization Display Wall', + show_page_name TINYINT(1) DEFAULT 1, + title VARCHAR(255) DEFAULT 'Data Visualization Display Wall', + logo_url TEXT, + logo_url_dark TEXT, + favicon_url TEXT, + default_theme VARCHAR(20) DEFAULT 'dark', + show_95_bandwidth TINYINT(1) DEFAULT 0, + p95_type VARCHAR(20) DEFAULT 'tx', + require_login_for_server_details TINYINT(1) DEFAULT 1, + blackbox_source_id INT, + latency_source VARCHAR(100), + latency_dest VARCHAR(100), + latency_target VARCHAR(255), + icp_filing VARCHAR(255), + ps_filing VARCHAR(255), + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `, + seedSql: ` + INSERT IGNORE INTO site_settings ( + id, page_name, show_page_name, title, default_theme, show_95_bandwidth, p95_type, require_login_for_server_details + ) VALUES ( + 1, 'Data Visualization Display Wall', 1, 'Data Visualization Display Wall', 'dark', 0, 'tx', 1 + ) + `, + columns: [ + { + name: 'show_page_name', + sql: "ALTER TABLE site_settings ADD COLUMN show_page_name TINYINT(1) DEFAULT 1 AFTER page_name" + }, + { + name: 'logo_url_dark', + sql: "ALTER TABLE site_settings ADD COLUMN logo_url_dark TEXT AFTER logo_url" + }, + { + name: 'favicon_url', + sql: "ALTER TABLE site_settings ADD COLUMN favicon_url TEXT AFTER logo_url_dark" + }, + { + name: 'show_95_bandwidth', + sql: "ALTER TABLE site_settings ADD COLUMN show_95_bandwidth TINYINT(1) DEFAULT 0 AFTER default_theme" + }, + { + name: 'p95_type', + sql: "ALTER TABLE site_settings ADD COLUMN p95_type VARCHAR(20) DEFAULT 'tx' AFTER show_95_bandwidth" + }, + { + name: 'require_login_for_server_details', + sql: "ALTER TABLE site_settings ADD COLUMN require_login_for_server_details TINYINT(1) DEFAULT 1 AFTER p95_type" + }, + { + name: 'blackbox_source_id', + sql: "ALTER TABLE site_settings ADD COLUMN blackbox_source_id INT AFTER require_login_for_server_details" + }, + { + name: 'latency_source', + sql: "ALTER TABLE site_settings ADD COLUMN latency_source VARCHAR(100) AFTER blackbox_source_id" + }, + { + name: 'latency_dest', + sql: "ALTER TABLE site_settings ADD COLUMN latency_dest VARCHAR(100) AFTER latency_source" + }, + { + name: 'latency_target', + sql: "ALTER TABLE site_settings ADD COLUMN latency_target VARCHAR(255) AFTER latency_dest" + }, + { + name: 'icp_filing', + sql: "ALTER TABLE site_settings ADD COLUMN icp_filing VARCHAR(255) AFTER latency_target" + }, + { + name: 'ps_filing', + sql: "ALTER TABLE site_settings ADD COLUMN ps_filing VARCHAR(255) AFTER icp_filing" + } + ] + }, + traffic_stats: { + createSql: ` + CREATE TABLE IF NOT EXISTS traffic_stats ( + id INT AUTO_INCREMENT PRIMARY KEY, + rx_bytes BIGINT UNSIGNED DEFAULT 0, + tx_bytes BIGINT UNSIGNED DEFAULT 0, + rx_bandwidth DOUBLE DEFAULT 0, + tx_bandwidth DOUBLE DEFAULT 0, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE INDEX (timestamp) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `, + columns: [] + }, + server_locations: { + createSql: ` + CREATE TABLE IF NOT EXISTS server_locations ( + id INT AUTO_INCREMENT PRIMARY KEY, + ip VARCHAR(255) NOT NULL UNIQUE, + country CHAR(2), + country_name VARCHAR(100), + region VARCHAR(100), + city VARCHAR(100), + latitude DOUBLE, + longitude DOUBLE, + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `, + columns: [] + }, + latency_routes: { + createSql: ` + CREATE TABLE IF NOT EXISTS latency_routes ( + id INT AUTO_INCREMENT PRIMARY KEY, + source_id INT NOT NULL, + latency_source VARCHAR(100) NOT NULL, + latency_dest VARCHAR(100) NOT NULL, + latency_target VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `, + columns: [] + } +}; + +async function addColumnIfMissing(tableName, existingColumns, column) { + if (existingColumns.has(column.name)) { + return; + } + + try { + console.log(`[Database Integrity] Missing column '${column.name}' in '${tableName}'. Adding it...`); + await db.query(column.sql); + console.log(`[Database Integrity] Column '${column.name}' added to '${tableName}'.`); + } catch (err) { + console.error(`[Database Integrity] Failed to add '${tableName}.${column.name}':`, err.message); + + if (column.sql.includes(' AFTER ')) { + try { + const fallbackSql = column.sql.split(' AFTER ')[0]; + await db.query(fallbackSql); + console.log(`[Database Integrity] Column '${column.name}' added to '${tableName}' via fallback.`); + } catch (fallbackErr) { + console.error(`[Database Integrity] Fallback failed for '${tableName}.${column.name}':`, fallbackErr.message); + } + } + } +} + +async function ensureTable(tableName, tableSchema) { + await db.query(tableSchema.createSql); + + const [columns] = await db.query(`SHOW COLUMNS FROM \`${tableName}\``); + const existingColumns = new Set(columns.map((column) => column.Field)); + + for (const column of tableSchema.columns || []) { + await addColumnIfMissing(tableName, existingColumns, column); + } + + if (tableSchema.seedSql) { + await db.query(tableSchema.seedSql); + } +} + +async function checkAndFixDatabase() { + const envPath = path.join(__dirname, '..', '.env'); + if (!fs.existsSync(envPath)) return; + + try { + for (const [tableName, tableSchema] of Object.entries(SCHEMA)) { + await ensureTable(tableName, tableSchema); + } + } catch (err) { + console.error('[Database Integrity] Startup schema check failed:', err.message); + } +} + +module.exports = checkAndFixDatabase; diff --git a/server/index.js b/server/index.js index 39c0f24..9bc374e 100644 --- a/server/index.js +++ b/server/index.js @@ -7,7 +7,7 @@ const prometheusService = require('./prometheus-service'); const cache = require('./cache'); const geoService = require('./geo-service'); const latencyService = require('./latency-service'); -const checkAndFixDatabase = require('./db-integrity-check'); +const checkAndFixDatabase = require('./db-schema-check'); const http = require('http'); const WebSocket = require('ws'); const net = require('net'); diff --git a/server/init-db.js b/server/init-db.js index 2e8d631..eeeb410 100644 --- a/server/init-db.js +++ b/server/init-db.js @@ -57,6 +57,18 @@ async function initDatabase() { title VARCHAR(255) DEFAULT '数据可视化展示大屏', logo_url TEXT, default_theme VARCHAR(20) DEFAULT 'dark', + show_page_name TINYINT(1) DEFAULT 1, + logo_url_dark TEXT, + favicon_url TEXT, + show_95_bandwidth TINYINT(1) DEFAULT 0, + p95_type VARCHAR(20) DEFAULT 'tx', + require_login_for_server_details TINYINT(1) DEFAULT 1, + blackbox_source_id INT, + latency_source VARCHAR(100), + latency_dest VARCHAR(100), + latency_target VARCHAR(255), + icp_filing VARCHAR(255), + ps_filing VARCHAR(255), updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `);