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.
This commit is contained in:
Ethanfly 2025-12-26 20:07:30 +08:00
parent 95f842f6cb
commit b7f6e9fcf6
5 changed files with 253 additions and 57 deletions

52
main.js
View File

@ -3,12 +3,21 @@
*/ */
const { app, BrowserWindow, ipcMain, Menu } = require('electron'); const { app, BrowserWindow, ipcMain, Menu } = require('electron');
const path = require('path'); const path = require('path');
const Store = require('electron-store');
const databaseService = require('./src/services/database'); const databaseService = require('./src/services/database');
const sshService = require('./src/services/ssh'); const sshService = require('./src/services/ssh');
let mainWindow; let mainWindow;
const isDev = process.env.NODE_ENV !== 'production' || !app.isPackaged; const isDev = process.env.NODE_ENV !== 'production' || !app.isPackaged;
// 配置存储
const configStore = new Store({
name: 'easyshell-config',
defaults: {
mysqlConfig: null,
},
});
// 活动的SSH连接 // 活动的SSH连接
const activeConnections = new Map(); const activeConnections = new Map();
@ -49,6 +58,21 @@ app.whenReady().then(async () => {
// 初始化本地数据库 (异步) // 初始化本地数据库 (异步)
await databaseService.initLocalDatabase(); 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(); createWindow();
app.on('activate', () => { app.on('activate', () => {
@ -58,9 +82,21 @@ app.whenReady().then(async () => {
}); });
}); });
app.on('window-all-closed', () => { app.on('window-all-closed', async () => {
// 关闭所有SSH连接 // 关闭所有SSH连接
sshService.disconnectAll(); sshService.disconnectAll();
// 关闭前自动同步到远程
if (databaseService.isRemoteConnected) {
try {
console.log('📤 正在同步数据到远程...');
await databaseService.syncToRemote();
console.log('✅ 数据同步完成');
} catch (err) {
console.error('❌ 关闭前同步失败:', err.message);
}
}
// 关闭数据库 // 关闭数据库
databaseService.close(); databaseService.close();
@ -93,6 +129,16 @@ ipcMain.handle('window:isMaximized', () => {
// ========== 数据库 IPC ========== // ========== 数据库 IPC ==========
// 配置管理
ipcMain.handle('db:saveConfig', (event, config) => {
configStore.set('mysqlConfig', config);
return { success: true };
});
ipcMain.handle('db:getConfig', () => {
return configStore.get('mysqlConfig');
});
// MySQL连接 // MySQL连接
ipcMain.handle('db:connectMySQL', async (event, config) => { ipcMain.handle('db:connectMySQL', async (event, config) => {
return await databaseService.connectMySQL(config); return await databaseService.connectMySQL(config);
@ -115,6 +161,10 @@ ipcMain.handle('db:syncFromRemote', async () => {
return await databaseService.syncFromRemote(); return await databaseService.syncFromRemote();
}); });
ipcMain.handle('db:smartSync', async () => {
return await databaseService.smartSync();
});
// 主机管理 // 主机管理
ipcMain.handle('hosts:getAll', () => { ipcMain.handle('hosts:getAll', () => {
return databaseService.getAllHosts(); return databaseService.getAllHosts();

View File

@ -15,11 +15,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
// 数据库操作 // 数据库操作
db: { db: {
saveConfig: (config) => ipcRenderer.invoke('db:saveConfig', config),
getConfig: () => ipcRenderer.invoke('db:getConfig'),
connectMySQL: (config) => ipcRenderer.invoke('db:connectMySQL', config), connectMySQL: (config) => ipcRenderer.invoke('db:connectMySQL', config),
disconnectMySQL: () => ipcRenderer.invoke('db:disconnectMySQL'), disconnectMySQL: () => ipcRenderer.invoke('db:disconnectMySQL'),
isRemoteConnected: () => ipcRenderer.invoke('db:isRemoteConnected'), isRemoteConnected: () => ipcRenderer.invoke('db:isRemoteConnected'),
syncToRemote: () => ipcRenderer.invoke('db:syncToRemote'), syncToRemote: () => ipcRenderer.invoke('db:syncToRemote'),
syncFromRemote: () => ipcRenderer.invoke('db:syncFromRemote'), syncFromRemote: () => ipcRenderer.invoke('db:syncFromRemote'),
smartSync: () => ipcRenderer.invoke('db:smartSync'),
}, },
// 主机管理 // 主机管理

View File

@ -31,8 +31,12 @@ function App() {
if (window.electronAPI) { if (window.electronAPI) {
const connected = await window.electronAPI.db.isRemoteConnected(); const connected = await window.electronAPI.db.isRemoteConnected();
setIsRemoteConnected(connected); setIsRemoteConnected(connected);
// 如果已连接,刷新主机列表(因为启动时可能已自动同步)
if (connected) {
loadHosts();
}
} }
}, []); }, [loadHosts]);
useEffect(() => { useEffect(() => {
loadHosts(); loadHosts();
@ -254,6 +258,7 @@ function App() {
setIsRemoteConnected(connected); setIsRemoteConnected(connected);
if (connected) loadHosts(); if (connected) loadHosts();
}} }}
onHostsUpdate={loadHosts}
/> />
)} )}
</AnimatePresence> </AnimatePresence>

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { import {
FiX, FiX,
@ -13,7 +13,7 @@ import {
FiAlertCircle, FiAlertCircle,
} from 'react-icons/fi'; } from 'react-icons/fi';
function Settings({ onClose, isRemoteConnected, onConnectionChange }) { function Settings({ onClose, isRemoteConnected, onConnectionChange, onHostsUpdate }) {
const [activeTab, setActiveTab] = useState('database'); const [activeTab, setActiveTab] = useState('database');
const [connecting, setConnecting] = useState(false); const [connecting, setConnecting] = useState(false);
const [syncing, setSyncing] = useState(false); const [syncing, setSyncing] = useState(false);
@ -26,6 +26,19 @@ function Settings({ onClose, isRemoteConnected, onConnectionChange }) {
database: 'easyshell', database: 'easyshell',
}); });
// 加载保存的配置
useEffect(() => {
const loadConfig = async () => {
if (window.electronAPI) {
const savedConfig = await window.electronAPI.db.getConfig();
if (savedConfig) {
setMysqlConfig(savedConfig);
}
}
};
loadConfig();
}, []);
const handleConnect = async () => { const handleConnect = async () => {
if (!window.electronAPI) return; if (!window.electronAPI) return;
@ -33,10 +46,20 @@ function Settings({ onClose, isRemoteConnected, onConnectionChange }) {
setMessage(null); setMessage(null);
try { try {
// 保存配置
await window.electronAPI.db.saveConfig(mysqlConfig);
const result = await window.electronAPI.db.connectMySQL(mysqlConfig); const result = await window.electronAPI.db.connectMySQL(mysqlConfig);
if (result.success) { if (result.success) {
setMessage({ type: 'success', text: '数据库连接成功!已自动创建数据库和表结构' }); setMessage({ type: 'success', text: '数据库连接成功!正在同步数据...' });
onConnectionChange(true); onConnectionChange(true);
// 连接成功后自动同步
const syncResult = await window.electronAPI.db.syncFromRemote();
if (syncResult.success) {
setMessage({ type: 'success', text: `连接成功!已同步 ${syncResult.hosts} 条主机信息` });
onHostsUpdate?.(); // 刷新主机列表
}
} else { } else {
setMessage({ type: 'error', text: `连接失败: ${result.error}` }); setMessage({ type: 'error', text: `连接失败: ${result.error}` });
onConnectionChange(false); onConnectionChange(false);
@ -87,8 +110,9 @@ function Settings({ onClose, isRemoteConnected, onConnectionChange }) {
if (result.success) { if (result.success) {
setMessage({ setMessage({
type: 'success', type: 'success',
text: `同步成功!已下载 ${result.hosts} 条主机信息${result.commands} 条命令` text: `同步成功!已同步 ${result.hosts} 条主机信息`
}); });
onHostsUpdate?.(); // 刷新主机列表
} else { } else {
setMessage({ type: 'error', text: `同步失败: ${result.error}` }); setMessage({ type: 'error', text: `同步失败: ${result.error}` });
} }

View File

@ -223,7 +223,8 @@ class DatabaseService {
description TEXT, description TEXT,
last_connected_at DATETIME, last_connected_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 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 ) 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() { async syncToRemote() {
if (!this.isRemoteConnected) { if (!this.isRemoteConnected) {
@ -282,38 +378,48 @@ class DatabaseService {
} }
try { 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) { for (const host of localHosts) {
await this.mysqlConnection.execute(` // 检查远程是否存在
INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description) const [existing] = await this.mysqlConnection.execute(
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) 'SELECT host, updated_at FROM hosts WHERE host = ?', [host.host]
ON DUPLICATE KEY UPDATE );
name = VALUES(name),
port = VALUES(port), if (existing.length === 0) {
username = VALUES(username), // 远程不存在,插入
password = VALUES(password), await this.mysqlConnection.execute(`
private_key = VALUES(private_key), INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description, updated_at)
group_name = VALUES(group_name), VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
color = VALUES(color), `, [host.name, host.host, host.port, host.username, host.password,
description = VALUES(description) host.private_key, host.group_name, host.color, host.description,
`, [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.sqliteDb.run('UPDATE hosts SET is_synced = 1 WHERE id = ?', [host.id]);
} }
this.saveDatabase(); this.saveDatabase();
return { success: true, synced };
// 记录同步日志
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 };
} catch (error) { } catch (error) {
console.error('❌ 同步到远程失败:', error); console.error('❌ 同步到远程失败:', error);
return { success: false, error: error.message }; return { success: false, error: error.message };
@ -321,7 +427,7 @@ class DatabaseService {
} }
/** /**
* 从远程同步数据 * 从远程同步数据 - host 为唯一标识取最新记录
*/ */
async syncFromRemote() { async syncFromRemote() {
if (!this.isRemoteConnected) { if (!this.isRemoteConnected) {
@ -331,34 +437,42 @@ class DatabaseService {
try { try {
// 获取远程主机 // 获取远程主机
const [remoteHosts] = await this.mysqlConnection.execute('SELECT * FROM hosts'); const [remoteHosts] = await this.mysqlConnection.execute('SELECT * FROM hosts');
let synced = 0;
for (const host of remoteHosts) { for (const remote of remoteHosts) {
this.sqliteDb.run(` // 检查本地是否存在
INSERT OR REPLACE INTO hosts (id, name, host, port, username, password, private_key, group_name, color, description, is_synced) const local = this.runQuerySingle('SELECT * FROM hosts WHERE host = ?', [remote.host]);
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
`, [host.id, host.name, host.host, host.port, host.username, host.password, host.private_key, host.group_name, host.color, host.description]); if (!local) {
} // 本地不存在,插入
this.sqliteDb.run(`
// 同步命令 INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description, updated_at, is_synced)
const [remoteCommands] = await this.mysqlConnection.execute('SELECT * FROM commands'); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
`, [remote.name, remote.host, remote.port, remote.username, remote.password,
for (const cmd of remoteCommands) { remote.private_key, remote.group_name, remote.color, remote.description,
this.sqliteDb.run(` remote.updated_at?.toISOString() || new Date().toISOString()]);
INSERT OR REPLACE INTO commands (id, command, description, category, usage_count) synced++;
VALUES (?, ?, ?, ?, ?) } else {
`, [cmd.id, cmd.command, cmd.description, cmd.category, cmd.usage_count]); // 本地存在,比较时间
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.saveDatabase();
return { success: true, hosts: synced };
// 记录同步日志
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 };
} catch (error) { } catch (error) {
console.error('❌ 从远程同步失败:', error); console.error('❌ 从远程同步失败:', error);
return { success: false, error: error.message }; return { success: false, error: error.message };