// config/database.js const Database = require('better-sqlite3'); class AppDatabase { constructor(dbPath) { if (!dbPath) throw new Error("Database path must be provided."); try { this.db = new Database(dbPath); console.log(`成功连接到数据库: ${dbPath}`); this.initTables(); } catch (err) { console.error('数据库连接错误:', err.message); } } run(sql, params = []) { return this.db.prepare(sql).run(params); } get(sql, params = []) { return this.db.prepare(sql).get(params); } all(sql, params = []) { return this.db.prepare(sql).all(params); } initTables() { // 日志表 this.run(`CREATE TABLE IF NOT EXISTS logs (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, action TEXT NOT NULL, category TEXT)`); // 基础配置表 this.run(`CREATE TABLE IF NOT EXISTS app_config (key TEXT PRIMARY KEY, value TEXT)`); // 流量统计表 this.run(`CREATE TABLE IF NOT EXISTS traffic_log (log_date TEXT PRIMARY KEY, bytes_used INTEGER NOT NULL)`); // 远程配置存储表 this.run(`CREATE TABLE IF NOT EXISTS remote_configs (id TEXT PRIMARY KEY, data TEXT, updated_at TEXT)`); // [重构] 接口健康度校验表 this.run(`CREATE TABLE IF NOT EXISTS api_health ( id TEXT PRIMARY KEY, api_id TEXT NOT NULL, status TEXT NOT NULL, response_time INTEGER, error_message TEXT, checked_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) )`); // [重构] 接口预留位置表 this.run(`CREATE TABLE IF NOT EXISTS api_reservations ( id TEXT PRIMARY KEY, api_id TEXT NOT NULL UNIQUE, category TEXT, description TEXT, priority INTEGER DEFAULT 0, enabled INTEGER DEFAULT 1, config TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) )`); // [重构] 接口调用记录表 this.run(`CREATE TABLE IF NOT EXISTS api_call_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, api_id TEXT NOT NULL, method TEXT, url TEXT, status_code INTEGER, response_time INTEGER, success INTEGER DEFAULT 0, error_message TEXT, called_at TEXT NOT NULL DEFAULT (datetime('now')) )`); // [重构] 工具健康检查结果表(5天强制检查) this.run(`CREATE TABLE IF NOT EXISTS tool_health_check_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, tool_id TEXT NOT NULL, check_date TEXT NOT NULL, status TEXT NOT NULL, response_time INTEGER, error_message TEXT, http_status INTEGER, attempt INTEGER, reason TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) )`); // [重构] 工具健康检查汇总表(用于5天强制检查判断) this.run(`CREATE TABLE IF NOT EXISTS tool_health_check_summary ( id INTEGER PRIMARY KEY AUTOINCREMENT, check_date TEXT NOT NULL UNIQUE, total_tools INTEGER NOT NULL, success_count INTEGER NOT NULL, failed_count INTEGER NOT NULL, check_duration INTEGER, created_at TEXT NOT NULL DEFAULT (datetime('now')) )`); // 创建索引以提高查询性能 this.run(`CREATE INDEX IF NOT EXISTS idx_api_health_api_id ON api_health(api_id)`); this.run(`CREATE INDEX IF NOT EXISTS idx_api_health_checked_at ON api_health(checked_at)`); this.run(`CREATE INDEX IF NOT EXISTS idx_api_call_logs_api_id ON api_call_logs(api_id)`); this.run(`CREATE INDEX IF NOT EXISTS idx_api_call_logs_called_at ON api_call_logs(called_at)`); this.run(`CREATE INDEX IF NOT EXISTS idx_tool_health_check_results_tool_id ON tool_health_check_results(tool_id)`); this.run(`CREATE INDEX IF NOT EXISTS idx_tool_health_check_results_check_date ON tool_health_check_results(check_date)`); this.run(`CREATE INDEX IF NOT EXISTS idx_tool_health_check_summary_check_date ON tool_health_check_summary(check_date)`); // 初始化默认配置 const initialConfigs = { 'theme': 'dark', 'global_volume': '0.5', 'total_downloaded_bytes': '0', 'background_image': '', 'background_opacity': '1.0', 'card_opacity': '0.7', 'window_width': '1200', 'window_height': '800', 'window_x': 'null', 'window_y': 'null', 'secret_code_attempts': '0', 'secret_code_lockout_until': '0', 'app_version': '0.0.0', // [新增] 用户协议状态 (默认为 false) 'user_agreement_accepted': 'false' }; const stmt = this.db.prepare(`INSERT OR IGNORE INTO app_config (key, value) VALUES (?, ?)`); this.db.transaction(() => { for (const [key, value] of Object.entries(initialConfigs)) { stmt.run(key, value); } })(); } // --- 远程配置操作 --- saveRemoteConfig(id, data) { const jsonStr = JSON.stringify(data); const time = new Date().toISOString(); const sql = `INSERT OR REPLACE INTO remote_configs (id, data, updated_at) VALUES (?, ?, ?)`; return this.run(sql, [id, jsonStr, time]); } getRemoteConfig(id) { const result = this.get(`SELECT data FROM remote_configs WHERE id = ?`, [id]); return result ? JSON.parse(result.data) : null; } // --- 版本升级检查 --- checkAndRecordVersion(currentVersion) { const oldVersion = this.getConfig('app_version') || '0.0.0'; if (oldVersion !== currentVersion) { this.setConfig('app_version', currentVersion); return { isUpgrade: true, oldVersion: oldVersion }; } return { isUpgrade: false, oldVersion: oldVersion }; } logAction(logData) { const { timestamp, action, category = 'general' } = logData; this.run(`INSERT INTO logs (timestamp, action, category) VALUES (?, ?, ?)`, [timestamp, action, category]); } getLogs(filterDate = null, limit = 500) { if (filterDate) return this.all(`SELECT * FROM logs WHERE date(timestamp) = ? ORDER BY timestamp DESC`, [filterDate]); return this.all(`SELECT * FROM logs ORDER BY timestamp DESC LIMIT ?`, [limit]); } clearLogs() { return this.run(`DELETE FROM logs`); } getConfig(key) { const result = this.get(`SELECT value FROM app_config WHERE key = ?`, [key]); return result ? result.value : null; } setConfig(key, value) { return this.run(`INSERT OR REPLACE INTO app_config (key, value) VALUES (?, ?)`, [key, value]); } getTrafficStats() { const result = this.get(`SELECT value FROM app_config WHERE key = 'total_downloaded_bytes'`); return result ? parseInt(result.value, 10) : 0; } getTrafficHistory() { return this.all(`SELECT log_date, bytes_used FROM traffic_log ORDER BY log_date ASC`); } addTraffic(bytes) { if (typeof bytes !== 'number' || bytes <= 0) return; const currentTotalBytes = this.getTrafficStats(); this.setConfig('total_downloaded_bytes', (currentTotalBytes + bytes).toString()); const today = new Date().toISOString().split('T')[0]; this.run(`INSERT INTO traffic_log (log_date, bytes_used) VALUES (?, ?) ON CONFLICT(log_date) DO UPDATE SET bytes_used = bytes_used + excluded.bytes_used;`, [today, bytes]); } // --- [重构] 接口健康度校验操作 --- /** * 记录接口健康度 * @param {Object} healthData - 健康度数据 */ recordApiHealth(healthData) { const { apiId, status, responseTime, errorMessage } = healthData; const checkedAt = new Date().toISOString(); // 删除旧记录(只保留最近100条) this.run(`DELETE FROM api_health WHERE api_id = ? AND id NOT IN ( SELECT id FROM api_health WHERE api_id = ? ORDER BY checked_at DESC LIMIT 99 )`, [apiId, apiId]); // 插入新记录 const id = `${apiId}_${Date.now()}`; this.run(`INSERT INTO api_health (id, api_id, status, response_time, error_message, checked_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, apiId, status, responseTime || null, errorMessage || null, checkedAt]); } /** * 获取接口健康度历史 * @param {string} apiId - 接口ID * @param {number} limit - 限制数量 * @returns {Array} 健康度历史记录 */ getApiHealthHistory(apiId, limit = 100) { return this.all(`SELECT * FROM api_health WHERE api_id = ? ORDER BY checked_at DESC LIMIT ?`, [apiId, limit]); } /** * 获取接口最新健康度 * @param {string} apiId - 接口ID * @returns {Object|null} 最新健康度记录 */ getLatestApiHealth(apiId) { return this.get(`SELECT * FROM api_health WHERE api_id = ? ORDER BY checked_at DESC LIMIT 1`, [apiId]); } // --- [重构] 接口预留位置操作 --- /** * 注册接口预留位置 * @param {Object} reservation - 预留位置数据 */ registerApiReservation(reservation) { const { apiId, category, description, priority, enabled, config } = reservation; const updatedAt = new Date().toISOString(); this.run(`INSERT OR REPLACE INTO api_reservations (id, api_id, category, description, priority, enabled, config, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [apiId, apiId, category || null, description || null, priority || 0, enabled ? 1 : 0, config ? JSON.stringify(config) : null, updatedAt]); } /** * 获取接口预留位置 * @param {string} apiId - 接口ID * @returns {Object|null} 预留位置数据 */ getApiReservation(apiId) { const result = this.get(`SELECT * FROM api_reservations WHERE api_id = ?`, [apiId]); if (result && result.config) { try { result.config = JSON.parse(result.config); } catch (e) { result.config = null; } } return result; } /** * 获取所有接口预留位置 * @param {string} category - 分类过滤(可选) * @returns {Array} 预留位置列表 */ getAllApiReservations(category = null) { if (category) { return this.all(`SELECT * FROM api_reservations WHERE category = ? ORDER BY priority DESC, created_at ASC`, [category]); } return this.all(`SELECT * FROM api_reservations ORDER BY priority DESC, created_at ASC`); } /** * 更新接口预留位置 * @param {string} apiId - 接口ID * @param {Object} updates - 更新数据 */ updateApiReservation(apiId, updates) { const fields = []; const values = []; if (updates.category !== undefined) { fields.push('category = ?'); values.push(updates.category); } if (updates.description !== undefined) { fields.push('description = ?'); values.push(updates.description); } if (updates.priority !== undefined) { fields.push('priority = ?'); values.push(updates.priority); } if (updates.enabled !== undefined) { fields.push('enabled = ?'); values.push(updates.enabled ? 1 : 0); } if (updates.config !== undefined) { fields.push('config = ?'); values.push(updates.config ? JSON.stringify(updates.config) : null); } if (fields.length === 0) return; fields.push('updated_at = ?'); values.push(new Date().toISOString()); values.push(apiId); this.run(`UPDATE api_reservations SET ${fields.join(', ')} WHERE api_id = ?`, values); } /** * 删除接口预留位置 * @param {string} apiId - 接口ID */ deleteApiReservation(apiId) { this.run(`DELETE FROM api_reservations WHERE api_id = ?`, [apiId]); } // --- [重构] 接口调用记录操作 --- /** * 记录接口调用 * @param {Object} callData - 调用数据 */ recordApiCall(callData) { const { apiId, method, url, statusCode, responseTime, success, errorMessage } = callData; this.run(`INSERT INTO api_call_logs (api_id, method, url, status_code, response_time, success, error_message) VALUES (?, ?, ?, ?, ?, ?, ?)`, [apiId, method || null, url || null, statusCode || null, responseTime || null, success ? 1 : 0, errorMessage || null]); // 自动清理旧记录(只保留最近10000条) this.run(`DELETE FROM api_call_logs WHERE id NOT IN ( SELECT id FROM api_call_logs ORDER BY called_at DESC LIMIT 10000 )`); } /** * 获取接口调用统计 * @param {string} apiId - 接口ID * @param {string} startDate - 开始日期(可选) * @param {string} endDate - 结束日期(可选) * @returns {Object} 调用统计 */ getApiCallStats(apiId, startDate = null, endDate = null) { let query = `SELECT COUNT(*) as total_calls, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_calls, SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed_calls, AVG(response_time) as avg_response_time, MIN(response_time) as min_response_time, MAX(response_time) as max_response_time FROM api_call_logs WHERE api_id = ?`; const params = [apiId]; if (startDate) { query += ` AND called_at >= ?`; params.push(startDate); } if (endDate) { query += ` AND called_at <= ?`; params.push(endDate); } return this.get(query, params); } /** * 获取接口调用历史 * @param {string} apiId - 接口ID * @param {number} limit - 限制数量 * @returns {Array} 调用历史记录 */ getApiCallHistory(apiId, limit = 100) { return this.all(`SELECT * FROM api_call_logs WHERE api_id = ? ORDER BY called_at DESC LIMIT ?`, [apiId, limit]); } // --- [重构] 工具健康检查结果操作 --- /** * 记录工具健康检查结果 * @param {Object} resultData - 检查结果数据 */ recordToolHealthCheckResult(resultData) { const { toolId, checkDate, status, responseTime, errorMessage, httpStatus, attempt, reason } = resultData; const createdAt = new Date().toISOString(); this.run(`INSERT INTO tool_health_check_results (tool_id, check_date, status, response_time, error_message, http_status, attempt, reason, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [toolId, checkDate, status, responseTime || null, errorMessage || null, httpStatus || null, attempt || 1, reason || null, createdAt]); } /** * 获取工具的最新健康检查结果 * @param {string} toolId - 工具ID * @returns {Object|null} 最新检查结果 */ getLatestToolHealthCheckResult(toolId) { return this.get(`SELECT * FROM tool_health_check_results WHERE tool_id = ? ORDER BY check_date DESC, created_at DESC LIMIT 1`, [toolId]); } /** * 获取工具的健康检查历史 * @param {string} toolId - 工具ID * @param {number} limit - 限制数量 * @returns {Array} 检查历史记录 */ getToolHealthCheckHistory(toolId, limit = 100) { return this.all(`SELECT * FROM tool_health_check_results WHERE tool_id = ? ORDER BY check_date DESC, created_at DESC LIMIT ?`, [toolId, limit]); } /** * 获取指定日期的所有工具健康检查结果 * @param {string} checkDate - 检查日期 (YYYY-MM-DD) * @returns {Array} 检查结果列表 */ getToolHealthCheckResultsByDate(checkDate) { return this.all(`SELECT * FROM tool_health_check_results WHERE check_date = ? ORDER BY tool_id`, [checkDate]); } /** * 获取最后一次健康检查的日期 * @returns {string|null} 最后一次检查的日期 */ getLastHealthCheckDate() { const result = this.get(`SELECT check_date FROM tool_health_check_summary ORDER BY check_date DESC LIMIT 1`); return result ? result.check_date : null; } /** * 检查距离上次检查是否超过指定天数 * @param {number} days - 天数(默认5天) * @returns {boolean} true 表示超过指定天数 */ shouldForceHealthCheck(days = 5) { const lastDate = this.getLastHealthCheckDate(); if (!lastDate) { return true; // 从未检查过 } const lastCheck = new Date(lastDate); const now = new Date(); const daysDiff = Math.floor((now - lastCheck) / (1000 * 60 * 60 * 24)); return daysDiff >= days; } /** * 保存工具健康检查汇总 * @param {Object} summaryData - 汇总数据 */ saveToolHealthCheckSummary(summaryData) { const { checkDate, totalTools, successCount, failedCount, checkDuration } = summaryData; // 删除同一天的旧记录 this.run(`DELETE FROM tool_health_check_summary WHERE check_date = ?`, [checkDate]); // 插入新记录 this.run(`INSERT INTO tool_health_check_summary (check_date, total_tools, success_count, failed_count, check_duration, created_at) VALUES (?, ?, ?, ?, ?, ?)`, [checkDate, totalTools, successCount, failedCount, checkDuration || null, new Date().toISOString()]); } /** * 获取工具健康检查汇总 * @param {string} checkDate - 检查日期(可选,不提供则返回最新的) * @returns {Object|null} 汇总数据 */ getToolHealthCheckSummary(checkDate = null) { if (checkDate) { return this.get(`SELECT * FROM tool_health_check_summary WHERE check_date = ?`, [checkDate]); } else { return this.get(`SELECT * FROM tool_health_check_summary ORDER BY check_date DESC LIMIT 1`); } } /** * 获取所有健康检查汇总(按日期排序) * @param {number} limit - 限制数量 * @returns {Array} 汇总列表 */ getAllToolHealthCheckSummaries(limit = 100) { return this.all(`SELECT * FROM tool_health_check_summary ORDER BY check_date DESC LIMIT ?`, [limit]); } close() { if (this.db) { this.db.close(); console.log('数据库连接已关闭。'); } } } module.exports = AppDatabase;