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 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();

View File

@ -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'),
},
// 主机管理

View File

@ -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}
/>
)}
</AnimatePresence>

View File

@ -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}` });
}

View File

@ -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) {
// 检查远程是否存在
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)
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]);
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) {
for (const remote of remoteHosts) {
// 检查本地是否存在
const local = this.runQuerySingle('SELECT * FROM hosts WHERE host = ?', [remote.host]);
if (!local) {
// 本地不存在,插入
this.sqliteDb.run(`
INSERT OR REPLACE INTO hosts (id, name, host, port, username, password, private_key, group_name, color, description, is_synced)
INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description, updated_at, 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]);
}
`, [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();
// 同步命令
const [remoteCommands] = await this.mysqlConnection.execute('SELECT * FROM commands');
for (const cmd of remoteCommands) {
if (remoteTime >= localTime) {
// 远程更新或相同,覆盖本地
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]);
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 };