From b7f6e9fcf6f4abe806725c84c513d9c470b9da84 Mon Sep 17 00:00:00 2001 From: Ethanfly Date: Fri, 26 Dec 2025 20:07:30 +0800 Subject: [PATCH] Add MySQL configuration management and smart sync functionality - Implemented configuration storage for MySQL settings using electron-store. - Added automatic MySQL connection on app startup if a saved configuration exists. - Enhanced the shutdown process to sync data to the remote database before closing. - Introduced smart sync functionality to handle bidirectional synchronization based on the latest updated_at timestamps. - Updated IPC methods for saving and retrieving MySQL configurations. - Modified the Settings component to load and save MySQL configurations, and trigger host list refresh after connection. --- main.js | 52 ++++++++- preload.js | 3 + src/App.js | 7 +- src/components/Settings.js | 32 +++++- src/services/database.js | 216 ++++++++++++++++++++++++++++--------- 5 files changed, 253 insertions(+), 57 deletions(-) diff --git a/main.js b/main.js index c2b0ed0..af53e19 100644 --- a/main.js +++ b/main.js @@ -3,12 +3,21 @@ */ const { app, BrowserWindow, ipcMain, Menu } = require('electron'); const path = require('path'); +const Store = require('electron-store'); const databaseService = require('./src/services/database'); const sshService = require('./src/services/ssh'); let mainWindow; const isDev = process.env.NODE_ENV !== 'production' || !app.isPackaged; +// 配置存储 +const configStore = new Store({ + name: 'easyshell-config', + defaults: { + mysqlConfig: null, + }, +}); + // 活动的SSH连接 const activeConnections = new Map(); @@ -49,6 +58,21 @@ app.whenReady().then(async () => { // 初始化本地数据库 (异步) await databaseService.initLocalDatabase(); + // 尝试自动连接 MySQL(如果有保存的配置) + const savedConfig = configStore.get('mysqlConfig'); + if (savedConfig && savedConfig.host) { + try { + const result = await databaseService.connectMySQL(savedConfig); + if (result.success) { + console.log('✅ 自动连接 MySQL 成功'); + // 自动同步 + await databaseService.syncFromRemote(); + } + } catch (err) { + console.log('⚠️ 自动连接 MySQL 失败:', err.message); + } + } + createWindow(); app.on('activate', () => { @@ -58,9 +82,21 @@ app.whenReady().then(async () => { }); }); -app.on('window-all-closed', () => { +app.on('window-all-closed', async () => { // 关闭所有SSH连接 sshService.disconnectAll(); + + // 关闭前自动同步到远程 + if (databaseService.isRemoteConnected) { + try { + console.log('📤 正在同步数据到远程...'); + await databaseService.syncToRemote(); + console.log('✅ 数据同步完成'); + } catch (err) { + console.error('❌ 关闭前同步失败:', err.message); + } + } + // 关闭数据库 databaseService.close(); @@ -93,6 +129,16 @@ ipcMain.handle('window:isMaximized', () => { // ========== 数据库 IPC ========== +// 配置管理 +ipcMain.handle('db:saveConfig', (event, config) => { + configStore.set('mysqlConfig', config); + return { success: true }; +}); + +ipcMain.handle('db:getConfig', () => { + return configStore.get('mysqlConfig'); +}); + // MySQL连接 ipcMain.handle('db:connectMySQL', async (event, config) => { return await databaseService.connectMySQL(config); @@ -115,6 +161,10 @@ ipcMain.handle('db:syncFromRemote', async () => { return await databaseService.syncFromRemote(); }); +ipcMain.handle('db:smartSync', async () => { + return await databaseService.smartSync(); +}); + // 主机管理 ipcMain.handle('hosts:getAll', () => { return databaseService.getAllHosts(); diff --git a/preload.js b/preload.js index 155ea82..53bf37c 100644 --- a/preload.js +++ b/preload.js @@ -15,11 +15,14 @@ contextBridge.exposeInMainWorld('electronAPI', { // 数据库操作 db: { + saveConfig: (config) => ipcRenderer.invoke('db:saveConfig', config), + getConfig: () => ipcRenderer.invoke('db:getConfig'), connectMySQL: (config) => ipcRenderer.invoke('db:connectMySQL', config), disconnectMySQL: () => ipcRenderer.invoke('db:disconnectMySQL'), isRemoteConnected: () => ipcRenderer.invoke('db:isRemoteConnected'), syncToRemote: () => ipcRenderer.invoke('db:syncToRemote'), syncFromRemote: () => ipcRenderer.invoke('db:syncFromRemote'), + smartSync: () => ipcRenderer.invoke('db:smartSync'), }, // 主机管理 diff --git a/src/App.js b/src/App.js index 29a26e9..162db47 100644 --- a/src/App.js +++ b/src/App.js @@ -31,8 +31,12 @@ function App() { if (window.electronAPI) { const connected = await window.electronAPI.db.isRemoteConnected(); setIsRemoteConnected(connected); + // 如果已连接,刷新主机列表(因为启动时可能已自动同步) + if (connected) { + loadHosts(); + } } - }, []); + }, [loadHosts]); useEffect(() => { loadHosts(); @@ -254,6 +258,7 @@ function App() { setIsRemoteConnected(connected); if (connected) loadHosts(); }} + onHostsUpdate={loadHosts} /> )} diff --git a/src/components/Settings.js b/src/components/Settings.js index a2971bb..b38dd9b 100644 --- a/src/components/Settings.js +++ b/src/components/Settings.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { FiX, @@ -13,7 +13,7 @@ import { FiAlertCircle, } from 'react-icons/fi'; -function Settings({ onClose, isRemoteConnected, onConnectionChange }) { +function Settings({ onClose, isRemoteConnected, onConnectionChange, onHostsUpdate }) { const [activeTab, setActiveTab] = useState('database'); const [connecting, setConnecting] = useState(false); const [syncing, setSyncing] = useState(false); @@ -26,6 +26,19 @@ function Settings({ onClose, isRemoteConnected, onConnectionChange }) { database: 'easyshell', }); + // 加载保存的配置 + useEffect(() => { + const loadConfig = async () => { + if (window.electronAPI) { + const savedConfig = await window.electronAPI.db.getConfig(); + if (savedConfig) { + setMysqlConfig(savedConfig); + } + } + }; + loadConfig(); + }, []); + const handleConnect = async () => { if (!window.electronAPI) return; @@ -33,10 +46,20 @@ function Settings({ onClose, isRemoteConnected, onConnectionChange }) { setMessage(null); try { + // 保存配置 + await window.electronAPI.db.saveConfig(mysqlConfig); + const result = await window.electronAPI.db.connectMySQL(mysqlConfig); if (result.success) { - setMessage({ type: 'success', text: '数据库连接成功!已自动创建数据库和表结构' }); + setMessage({ type: 'success', text: '数据库连接成功!正在同步数据...' }); onConnectionChange(true); + + // 连接成功后自动同步 + const syncResult = await window.electronAPI.db.syncFromRemote(); + if (syncResult.success) { + setMessage({ type: 'success', text: `连接成功!已同步 ${syncResult.hosts} 条主机信息` }); + onHostsUpdate?.(); // 刷新主机列表 + } } else { setMessage({ type: 'error', text: `连接失败: ${result.error}` }); onConnectionChange(false); @@ -87,8 +110,9 @@ function Settings({ onClose, isRemoteConnected, onConnectionChange }) { if (result.success) { setMessage({ type: 'success', - text: `同步成功!已下载 ${result.hosts} 条主机信息和 ${result.commands} 条命令` + text: `同步成功!已同步 ${result.hosts} 条主机信息` }); + onHostsUpdate?.(); // 刷新主机列表 } else { setMessage({ type: 'error', text: `同步失败: ${result.error}` }); } diff --git a/src/services/database.js b/src/services/database.js index 04cc51b..3c71ac9 100644 --- a/src/services/database.js +++ b/src/services/database.js @@ -223,7 +223,8 @@ class DatabaseService { description TEXT, last_connected_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uk_host (host) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 `); @@ -274,7 +275,102 @@ class DatabaseService { } /** - * 同步数据到远程 + * 智能双向同步 - 以 host 为唯一标识,比较 updated_at 取最新记录 + */ + async smartSync() { + if (!this.isRemoteConnected) { + return { success: false, error: '未连接到远程数据库' }; + } + + try { + let uploaded = 0; + let downloaded = 0; + + // 获取所有本地主机 + const localHosts = this.runQuery('SELECT * FROM hosts'); + // 获取所有远程主机 + const [remoteHosts] = await this.mysqlConnection.execute('SELECT * FROM hosts'); + + // 创建 host 地址到记录的映射 + const localMap = new Map(); + for (const h of localHosts) { + localMap.set(h.host, h); + } + + const remoteMap = new Map(); + for (const h of remoteHosts) { + remoteMap.set(h.host, h); + } + + // 1. 处理本地有的记录 + for (const local of localHosts) { + const remote = remoteMap.get(local.host); + + if (!remote) { + // 远程没有,上传到远程 + await this.mysqlConnection.execute(` + INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [local.name, local.host, local.port, local.username, local.password, + local.private_key, local.group_name, local.color, local.description, + local.updated_at || new Date().toISOString()]); + uploaded++; + } else { + // 远程有,比较时间 + const localTime = new Date(local.updated_at || 0).getTime(); + const remoteTime = new Date(remote.updated_at || 0).getTime(); + + if (localTime > remoteTime) { + // 本地更新,上传到远程 + await this.mysqlConnection.execute(` + UPDATE hosts SET name=?, port=?, username=?, password=?, private_key=?, + group_name=?, color=?, description=?, updated_at=? + WHERE host=? + `, [local.name, local.port, local.username, local.password, local.private_key, + local.group_name, local.color, local.description, local.updated_at, local.host]); + uploaded++; + } else if (remoteTime > localTime) { + // 远程更新,下载到本地 + this.sqliteDb.run(` + UPDATE hosts SET name=?, port=?, username=?, password=?, private_key=?, + group_name=?, color=?, description=?, updated_at=?, is_synced=1 + WHERE host=? + `, [remote.name, remote.port, remote.username, remote.password, remote.private_key, + remote.group_name, remote.color, remote.description, + remote.updated_at?.toISOString() || new Date().toISOString(), local.host]); + downloaded++; + } + } + + // 标记为已同步 + this.sqliteDb.run('UPDATE hosts SET is_synced = 1 WHERE id = ?', [local.id]); + } + + // 2. 处理远程有但本地没有的记录 + for (const remote of remoteHosts) { + if (!localMap.has(remote.host)) { + // 本地没有,下载到本地 + this.sqliteDb.run(` + INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description, updated_at, is_synced) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) + `, [remote.name, remote.host, remote.port, remote.username, remote.password, + remote.private_key, remote.group_name, remote.color, remote.description, + remote.updated_at?.toISOString() || new Date().toISOString()]); + downloaded++; + } + } + + this.saveDatabase(); + console.log(`✅ 智能同步完成: 上传 ${uploaded}, 下载 ${downloaded}`); + return { success: true, uploaded, downloaded }; + } catch (error) { + console.error('❌ 智能同步失败:', error); + return { success: false, error: error.message }; + } + } + + /** + * 同步数据到远程 - 只上传本地更新的 */ async syncToRemote() { if (!this.isRemoteConnected) { @@ -282,38 +378,48 @@ class DatabaseService { } try { - // 获取本地未同步的主机 - const localHosts = this.runQuery('SELECT * FROM hosts WHERE is_synced = 0'); + // 获取本地所有主机 + const localHosts = this.runQuery('SELECT * FROM hosts'); + let synced = 0; for (const host of localHosts) { - await this.mysqlConnection.execute(` - INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - name = VALUES(name), - port = VALUES(port), - username = VALUES(username), - password = VALUES(password), - private_key = VALUES(private_key), - group_name = VALUES(group_name), - color = VALUES(color), - description = VALUES(description) - `, [host.name, host.host, host.port, host.username, host.password, host.private_key, host.group_name, host.color, host.description]); + // 检查远程是否存在 + const [existing] = await this.mysqlConnection.execute( + 'SELECT host, updated_at FROM hosts WHERE host = ?', [host.host] + ); + + if (existing.length === 0) { + // 远程不存在,插入 + await this.mysqlConnection.execute(` + INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [host.name, host.host, host.port, host.username, host.password, + host.private_key, host.group_name, host.color, host.description, + host.updated_at || new Date().toISOString()]); + synced++; + } else { + // 远程存在,比较时间 + const localTime = new Date(host.updated_at || 0).getTime(); + const remoteTime = new Date(existing[0].updated_at || 0).getTime(); + + if (localTime > remoteTime) { + // 本地更新,才覆盖远程 + await this.mysqlConnection.execute(` + UPDATE hosts SET name=?, port=?, username=?, password=?, private_key=?, + group_name=?, color=?, description=?, updated_at=? + WHERE host=? + `, [host.name, host.port, host.username, host.password, host.private_key, + host.group_name, host.color, host.description, host.updated_at, host.host]); + synced++; + } + } // 标记为已同步 this.sqliteDb.run('UPDATE hosts SET is_synced = 1 WHERE id = ?', [host.id]); } this.saveDatabase(); - - // 记录同步日志 - this.sqliteDb.run( - `INSERT INTO sync_log (sync_type, status, details) VALUES ('upload', 'success', ?)`, - [JSON.stringify({ synced_hosts: localHosts.length })] - ); - this.saveDatabase(); - - return { success: true, synced: localHosts.length }; + return { success: true, synced }; } catch (error) { console.error('❌ 同步到远程失败:', error); return { success: false, error: error.message }; @@ -321,7 +427,7 @@ class DatabaseService { } /** - * 从远程同步数据 + * 从远程同步数据 - 以 host 为唯一标识,取最新记录 */ async syncFromRemote() { if (!this.isRemoteConnected) { @@ -331,34 +437,42 @@ class DatabaseService { try { // 获取远程主机 const [remoteHosts] = await this.mysqlConnection.execute('SELECT * FROM hosts'); + let synced = 0; - for (const host of remoteHosts) { - this.sqliteDb.run(` - INSERT OR REPLACE INTO hosts (id, name, host, port, username, password, private_key, group_name, color, description, is_synced) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) - `, [host.id, host.name, host.host, host.port, host.username, host.password, host.private_key, host.group_name, host.color, host.description]); - } - - // 同步命令 - const [remoteCommands] = await this.mysqlConnection.execute('SELECT * FROM commands'); - - for (const cmd of remoteCommands) { - this.sqliteDb.run(` - INSERT OR REPLACE INTO commands (id, command, description, category, usage_count) - VALUES (?, ?, ?, ?, ?) - `, [cmd.id, cmd.command, cmd.description, cmd.category, cmd.usage_count]); + for (const remote of remoteHosts) { + // 检查本地是否存在 + const local = this.runQuerySingle('SELECT * FROM hosts WHERE host = ?', [remote.host]); + + if (!local) { + // 本地不存在,插入 + this.sqliteDb.run(` + INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description, updated_at, is_synced) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) + `, [remote.name, remote.host, remote.port, remote.username, remote.password, + remote.private_key, remote.group_name, remote.color, remote.description, + remote.updated_at?.toISOString() || new Date().toISOString()]); + synced++; + } else { + // 本地存在,比较时间 + const localTime = new Date(local.updated_at || 0).getTime(); + const remoteTime = new Date(remote.updated_at || 0).getTime(); + + if (remoteTime >= localTime) { + // 远程更新或相同,覆盖本地 + this.sqliteDb.run(` + UPDATE hosts SET name=?, port=?, username=?, password=?, private_key=?, + group_name=?, color=?, description=?, updated_at=?, is_synced=1 + WHERE host=? + `, [remote.name, remote.port, remote.username, remote.password, remote.private_key, + remote.group_name, remote.color, remote.description, + remote.updated_at?.toISOString() || new Date().toISOString(), remote.host]); + synced++; + } + } } this.saveDatabase(); - - // 记录同步日志 - this.sqliteDb.run( - `INSERT INTO sync_log (sync_type, status, details) VALUES ('download', 'success', ?)`, - [JSON.stringify({ hosts: remoteHosts.length, commands: remoteCommands.length })] - ); - this.saveDatabase(); - - return { success: true, hosts: remoteHosts.length, commands: remoteCommands.length }; + return { success: true, hosts: synced }; } catch (error) { console.error('❌ 从远程同步失败:', error); return { success: false, error: error.message };