using System; using System.Data; using System.IO; using System.Linq; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Data.Sqlite; using Npgsql; using Dapper; namespace DatabaseCleanupTool { // 配置类 class AppConfig { [JsonPropertyName("SourceDatabase")] public string SourceDatabase { get; set; } = @"D:\NewProductionLineMonitorDB.db"; [JsonPropertyName("TargetDatabase")] public string TargetDatabase { get; set; } = @"D:\NewProductionLineMonitorDB_Dev.db"; [JsonPropertyName("KeepDays")] public int KeepDays { get; set; } = 14; [JsonPropertyName("SyncIntervalHours")] public int SyncIntervalHours { get; set; } = 24; [JsonPropertyName("IncrementalMode")] public bool IncrementalMode { get; set; } = true; [JsonPropertyName("StructureTables")] public List StructureTables { get; set; } = new(); [JsonPropertyName("DataTables")] public Dictionary DataTables { get; set; } = new(); [JsonPropertyName("TableStructureChange")] public TableStructureChangeConfig TableStructureChange { get; set; } = new(); [JsonPropertyName("PostgreSQLConnectionString")] public string? PostgreSQLConnectionString { get; set; } [JsonPropertyName("SourceType")] public string SourceType { get; set; } = "SQLite"; // SQLite 或 PostgreSQL } class TableStructureChangeConfig { [JsonPropertyName("AutoFullSync")] public bool AutoFullSync { get; set; } = false; [JsonPropertyName("SyncNewTablesOnly")] public bool SyncNewTablesOnly { get; set; } = true; [JsonPropertyName("CheckIntervalHours")] public int CheckIntervalHours { get; set; } = 24; } class Program { // 配置对象 static AppConfig _config = new AppConfig(); // 运行时配置(可通过交互菜单修改) static string SOURCE_DB => _config.SourceDatabase; static string TARGET_DB => _config.TargetDatabase; static int KEEP_DAYS => _config.KeepDays; // 定期同步配置 static bool _watchMode = false; static int _syncIntervalHours { get => _config.SyncIntervalHours; set => _config.SyncIntervalHours = value; } static string _logFile = "DatabaseCleanupTool.log"; static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); static bool _interactiveMode = true; // 是否显示交互菜单 static bool _incrementalMode { get => _config.IncrementalMode; set => _config.IncrementalMode = value; } static bool _fullSyncMode = false; // 完整同步模式(删除重建) // 表配置(从配置文件读取) static string[] STRUCTURE_TABLES => _config.StructureTables.ToArray(); static Dictionary DATA_TABLES => _config.DataTables; // 表结构变化配置 static bool _autoFullSync => _config.TableStructureChange.AutoFullSync; static bool _syncNewTablesOnly => _config.TableStructureChange.SyncNewTablesOnly; static DateTime _lastStructureCheck = DateTime.MinValue; static int _structureCheckIntervalHours => _config.TableStructureChange.CheckIntervalHours; // 数据源类型 static bool _isPostgreSQLSource => _config.SourceType?.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase) == true; static string? _pgConnectionString => _config.PostgreSQLConnectionString; static void Main(string[] args) { // 加载配置文件 LoadConfiguration(); // 解析命令行参数 ParseArguments(args); // 显示标题 Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("╔═══════════════════════════════════════════════════════════════╗"); Console.WriteLine("║ 生产线监控数据库清理工具 v2.0 ║"); Console.WriteLine("║ Database Cleanup Tool for Production Line Monitor ║"); Console.WriteLine("╚═══════════════════════════════════════════════════════════════╝"); Console.ResetColor(); // 如果指定了命令行参数,跳过交互菜单(除非明确指定--interactive) if (args.Length > 0) { var hasInteractive = args.Any(a => a.ToLower() == "--interactive" || a.ToLower() == "--menu"); if (!hasInteractive) { _interactiveMode = false; } } // 交互式配置菜单 if (_interactiveMode) { ShowInteractiveMenu(); } else { // 非交互模式,显示配置但不询问保存 ShowConfiguration(askSave: false); } // 检查源数据库 if (!_isPostgreSQLSource) { if (!File.Exists(SOURCE_DB)) { Error($"错误: 源数据库不存在: {SOURCE_DB}"); Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); return; } } else { if (string.IsNullOrEmpty(_pgConnectionString)) { Error("错误: PostgreSQL 连接字符串未配置"); Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); return; } // 测试 PostgreSQL 连接 try { using var testConn = new NpgsqlConnection(_pgConnectionString); testConn.Open(); Info("✅ PostgreSQL 连接测试成功"); } catch (Exception ex) { Error($"错误: PostgreSQL 连接失败: {ex.Message}"); Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); return; } } // 设置Ctrl+C处理 Console.CancelKeyPress += (sender, e) => { e.Cancel = true; _cancellationTokenSource.Cancel(); WriteLog("收到退出信号,正在停止..."); Console.WriteLine("\n\n正在停止,请稍候..."); }; if (_watchMode) { // 定期同步模式 RunWatchMode().Wait(); } else { // 单次执行模式 RunOnce(); } } static void ShowInteractiveMenu() { Console.WriteLine("\n╔═══════════════════════════════════════════════════════════════╗"); Console.WriteLine("║ 配置菜单 ║"); Console.WriteLine("╚═══════════════════════════════════════════════════════════════╝\n"); // 1. 运行模式 Console.WriteLine("【1】运行模式"); Console.WriteLine(" 1. 单次执行(执行一次后退出)"); Console.WriteLine(" 2. 定期同步(持续运行,定期同步)"); Console.Write($" 当前选择: {(_watchMode ? "2. 定期同步" : "1. 单次执行")} → 请选择 (1/2,直接回车保持当前): "); var modeChoice = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(modeChoice)) { _watchMode = (modeChoice == "2"); } // 2. 如果选择定期同步,设置同步间隔和模式 if (_watchMode) { Console.WriteLine($"\n【2】同步间隔(当前: 每 {_syncIntervalHours} 小时)"); Console.WriteLine(" 1. 每 6 小时"); Console.WriteLine(" 2. 每 12 小时"); Console.WriteLine(" 3. 每 24 小时(默认)"); Console.WriteLine(" 4. 每 48 小时"); Console.WriteLine(" 5. 自定义"); Console.Write(" 请选择 (1-5,直接回车保持当前): "); var intervalChoice = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(intervalChoice)) { switch (intervalChoice) { case "1": _syncIntervalHours = 6; break; case "2": _syncIntervalHours = 12; break; case "3": _syncIntervalHours = 24; break; case "4": _syncIntervalHours = 48; break; case "5": Console.Write(" 请输入小时数: "); if (int.TryParse(Console.ReadLine(), out int customHours) && customHours > 0) { _syncIntervalHours = customHours; } break; } } // 2.1 同步模式(仅定期同步时) Console.WriteLine($"\n【2.1】同步模式(当前: {(_incrementalMode ? "增量同步" : "完整同步")})"); Console.WriteLine(" 1. 增量同步(推荐,只同步新增数据,速度快)"); Console.WriteLine(" 2. 完整同步(删除重建,确保数据一致性)"); Console.Write(" 请选择 (1/2,直接回车保持当前): "); var syncModeChoice = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(syncModeChoice)) { _config.IncrementalMode = (syncModeChoice == "1"); _fullSyncMode = (syncModeChoice == "2"); } } // 3. 保留天数 Console.WriteLine($"\n【3】保留天数(当前: {KEEP_DAYS} 天)"); Console.WriteLine(" 1. 7 天(1周)"); Console.WriteLine(" 2. 14 天(2周,默认)"); Console.WriteLine(" 3. 30 天(1个月)"); Console.WriteLine(" 4. 自定义"); Console.Write(" 请选择 (1-4,直接回车保持当前): "); var daysChoice = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(daysChoice)) { switch (daysChoice) { case "1": _config.KeepDays = 7; break; case "2": _config.KeepDays = 14; break; case "3": _config.KeepDays = 30; break; case "4": Console.Write(" 请输入天数: "); if (int.TryParse(Console.ReadLine(), out int customDays) && customDays > 0) { _config.KeepDays = customDays; } break; } } // 4. 源数据库路径 Console.WriteLine($"\n【4】源数据库路径"); Console.WriteLine($" 当前: {SOURCE_DB}"); Console.Write(" 是否修改? (y/n,直接回车保持当前): "); var changeSource = Console.ReadLine()?.ToLower(); if (changeSource == "y") { Console.Write(" 请输入源数据库完整路径: "); var newSource = Console.ReadLine()?.Trim(); if (!string.IsNullOrWhiteSpace(newSource)) { _config.SourceDatabase = newSource; } } // 5. 目标数据库路径 Console.WriteLine($"\n【5】目标数据库路径"); Console.WriteLine($" 当前: {TARGET_DB}"); Console.Write(" 是否修改? (y/n,直接回车保持当前): "); var changeTarget = Console.ReadLine()?.ToLower(); if (changeTarget == "y") { Console.Write(" 请输入目标数据库完整路径: "); var newTarget = Console.ReadLine()?.Trim(); if (!string.IsNullOrWhiteSpace(newTarget)) { _config.TargetDatabase = newTarget; } } // 6. 数据源类型 Console.WriteLine($"\n【6】数据源类型(当前: {_config.SourceType})"); Console.WriteLine(" 1. SQLite(从SQLite数据库复制)"); Console.WriteLine(" 2. PostgreSQL(从PostgreSQL数据库复制)"); Console.Write(" 请选择 (1/2,直接回车保持当前): "); var sourceTypeChoice = Console.ReadLine(); if (!string.IsNullOrWhiteSpace(sourceTypeChoice)) { _config.SourceType = sourceTypeChoice == "2" ? "PostgreSQL" : "SQLite"; if (_config.SourceType == "PostgreSQL") { Console.Write(" 请输入PostgreSQL连接字符串: "); var pgConnStr = Console.ReadLine()?.Trim(); if (!string.IsNullOrWhiteSpace(pgConnStr)) { _config.PostgreSQLConnectionString = pgConnStr; } } } Console.WriteLine("\n" + new string('═', 70)); // 显示配置并询问是否保存 ShowConfiguration(askSave: true); } static void SaveConfiguration() { try { var options = new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; var json = JsonSerializer.Serialize(_config, options); File.WriteAllText("appsettings.json", json); Success("✅ 配置已保存到 appsettings.json"); WriteLog("配置已保存到 appsettings.json"); } catch (Exception ex) { Error($"⚠️ 保存配置失败: {ex.Message}"); WriteLog($"保存配置失败: {ex.Message}"); } } static void ShowConfiguration(bool askSave = false) { Console.WriteLine($"\n【最终配置】"); Console.WriteLine($" 数据源类型: {_config.SourceType}"); if (_isPostgreSQLSource) { var connStr = _pgConnectionString ?? ""; var displayConnStr = connStr.Length > 60 ? connStr.Substring(0, 60) + "..." : connStr; Console.WriteLine($" PostgreSQL连接: {displayConnStr}"); } else { Console.WriteLine($" 源数据库: {SOURCE_DB}"); } Console.WriteLine($" 目标数据库: {TARGET_DB}"); Console.WriteLine($" 保留天数: 最近 {KEEP_DAYS} 天"); Console.WriteLine($" 截止日期: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); Console.WriteLine($" 起始日期: {DateTime.Now.AddDays(-KEEP_DAYS):yyyy-MM-dd HH:mm:ss}"); if (_watchMode) { Console.WriteLine($" 运行模式: 定期同步模式"); Console.WriteLine($" 同步模式: {(_incrementalMode ? "增量同步(只同步新增数据)" : "完整同步(删除重建)")}"); Console.WriteLine($" 同步间隔: 每 {_syncIntervalHours} 小时"); Console.WriteLine($" 日志文件: {_logFile}"); Console.WriteLine($" 提示: 在定期同步模式下,按 'S' 键可立即同步"); } else { Console.WriteLine($" 运行模式: 单次执行模式(完整同步)"); } // 如果是在交互菜单中,询问是否保存 if (askSave) { Console.Write("\n是否保存当前配置到 appsettings.json? (y/n,直接回车不保存): "); var saveChoice = Console.ReadLine()?.ToLower(); if (saveChoice == "y") { SaveConfiguration(); } } } static void ParseArguments(string[] args) { for (int i = 0; i < args.Length; i++) { var arg = args[i].ToLower(); if (arg == "--watch" || arg == "-w" || arg == "--continuous" || arg == "-c") { _watchMode = true; _interactiveMode = false; // 命令行参数模式,跳过交互菜单 } else if (arg == "--interval" || arg == "-i") { if (i + 1 < args.Length && int.TryParse(args[i + 1], out int hours)) { _syncIntervalHours = hours; i++; // 跳过下一个参数 } else { Console.WriteLine("警告: --interval 参数需要指定小时数,使用默认值24小时"); } } else if (arg == "--source" || arg == "-s") { if (i + 1 < args.Length) { _config.SourceDatabase = args[i + 1]; i++; } } else if (arg == "--target" || arg == "-t") { if (i + 1 < args.Length) { _config.TargetDatabase = args[i + 1]; i++; } } else if (arg == "--days" || arg == "-d") { if (i + 1 < args.Length && int.TryParse(args[i + 1], out int days)) { _config.KeepDays = days; i++; } } else if (arg == "--interactive" || arg == "--menu") { _interactiveMode = true; // 强制显示交互菜单 } else if (arg == "--help" || arg == "-h") { ShowHelp(); Environment.Exit(0); } } } static void ShowHelp() { Console.WriteLine("\n使用方法:"); Console.WriteLine(" DatabaseCleanupTool.exe [选项]"); Console.WriteLine("\n选项:"); Console.WriteLine(" --watch, -w, --continuous, -c 启用定期同步模式"); Console.WriteLine(" --interval, -i <小时数> 设置同步间隔(默认24小时)"); Console.WriteLine(" --source, -s <路径> 源数据库路径"); Console.WriteLine(" --target, -t <路径> 目标数据库路径"); Console.WriteLine(" --days, -d <天数> 保留天数(默认14天)"); Console.WriteLine(" --interactive, --menu 显示交互式配置菜单"); Console.WriteLine(" --help, -h 显示帮助信息"); Console.WriteLine("\n示例:"); Console.WriteLine(" DatabaseCleanupTool.exe # 显示交互菜单"); Console.WriteLine(" DatabaseCleanupTool.exe --watch # 定期同步,每24小时"); Console.WriteLine(" DatabaseCleanupTool.exe --watch --interval 12 # 定期同步,每12小时"); Console.WriteLine(" DatabaseCleanupTool.exe --source D:\\DB.db --target D:\\Dev.db # 指定路径"); Console.WriteLine("\n交互式菜单:"); Console.WriteLine(" - 不带任何参数运行程序,会显示交互式配置菜单"); Console.WriteLine(" - 可以方便地选择运行模式、同步间隔、保留天数等"); Console.WriteLine("\n定期同步模式:"); Console.WriteLine(" - 程序将持续运行,定期同步数据库"); Console.WriteLine(" - 按 Ctrl+C 可以优雅退出"); Console.WriteLine(" - 同步日志会保存到 DatabaseCleanupTool.log 文件"); } static async Task RunWatchMode() { WriteLog("启动定期同步模式"); WriteLog($"同步间隔: 每 {_syncIntervalHours} 小时"); WriteLog($"同步模式: {(_incrementalMode ? "增量同步" : "完整同步")}"); Console.WriteLine("\n✅ 定期同步模式已启动"); Console.WriteLine($" 同步模式: {(_incrementalMode ? "增量同步(只同步新增数据)" : "完整同步(删除重建)")}"); Console.WriteLine($" 同步间隔: 每 {_syncIntervalHours} 小时"); Console.WriteLine($" 日志文件: {_logFile}"); Console.WriteLine($" 按 'S' 键可立即同步"); Console.WriteLine($" 按 Ctrl+C 退出\n"); var nextSyncTime = DateTime.Now; // 立即执行第一次同步 var syncCount = 0; var manualSyncRequested = false; // 启动键盘监听任务(用于立即同步) var keyboardTask = Task.Run(async () => { while (!_cancellationTokenSource.Token.IsCancellationRequested) { if (Console.KeyAvailable) { var key = Console.ReadKey(true); if (key.Key == ConsoleKey.S && !char.IsControl(key.KeyChar)) { manualSyncRequested = true; Console.WriteLine($"\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 收到立即同步请求..."); } } await Task.Delay(100, _cancellationTokenSource.Token); } }); while (!_cancellationTokenSource.Token.IsCancellationRequested) { try { // 检查是否到了同步时间或收到立即同步请求 if (DateTime.Now >= nextSyncTime || manualSyncRequested) { syncCount++; var isManual = manualSyncRequested; manualSyncRequested = false; WriteLog($"========== 开始第 {syncCount} 次同步 {(isManual ? "(手动触发)" : "")} =========="); Console.WriteLine($"\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 开始第 {syncCount} 次同步{(isManual ? " (手动触发)" : "")}..."); var startTime = DateTime.Now; // 根据模式选择同步方法 if (_incrementalMode && File.Exists(TARGET_DB)) { RunIncrementalSync(); } else { RunSync(); // 完整同步 } var endTime = DateTime.Now; var duration = (endTime - startTime).TotalSeconds; WriteLog($"第 {syncCount} 次同步完成,耗时: {duration:F2} 秒"); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 同步完成,耗时: {duration:F2} 秒"); // 计算下次同步时间(如果是手动触发,不重置定时器) if (!isManual) { nextSyncTime = DateTime.Now.AddHours(_syncIntervalHours); WriteLog($"下次同步时间: {nextSyncTime:yyyy-MM-dd HH:mm:ss}"); Console.WriteLine($"下次同步时间: {nextSyncTime:yyyy-MM-dd HH:mm:ss}\n"); } else { Console.WriteLine($"下次自动同步时间: {nextSyncTime:yyyy-MM-dd HH:mm:ss}\n"); } } else { // 显示倒计时(每分钟更新一次) var remaining = nextSyncTime - DateTime.Now; if (remaining.TotalSeconds > 0 && DateTime.Now.Second < 5) // 每分钟的前5秒显示 { var hours = (int)remaining.TotalHours; var minutes = remaining.Minutes; Console.Write($"\r等待下次同步,剩余时间: {hours}小时 {minutes}分钟... (按 'S' 立即同步)"); } } // 等待10秒再检查(平衡响应速度和CPU占用) await Task.Delay(TimeSpan.FromSeconds(10), _cancellationTokenSource.Token); } catch (TaskCanceledException) { // 正常退出 break; } catch (Exception ex) { WriteLog($"同步过程中发生错误: {ex.Message}"); Error($"同步错误: {ex.Message}"); // 发生错误后,等待1小时再重试 await Task.Delay(TimeSpan.FromHours(1), _cancellationTokenSource.Token); } } WriteLog("定期同步模式已停止"); Console.WriteLine("\n定期同步模式已停止"); } static void RunOnce() { try { // 确认继续 Console.Write("\n是否继续? (y/n): "); var confirm = Console.ReadLine()?.ToLower(); if (confirm != "y") { Console.WriteLine("操作已取消"); return; } RunSync(); } catch (Exception ex) { Error($"\n❌ 错误: {ex.Message}"); Error($"详细信息: {ex.StackTrace}"); } Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); } static void RunSync() { var startTime = DateTime.Now; WriteLog($"开始完整同步,时间: {startTime:yyyy-MM-dd HH:mm:ss}"); // 步骤1: 删除旧的目标数据库 if (File.Exists(TARGET_DB)) { Info("删除旧的目标数据库..."); File.Delete(TARGET_DB); } // 步骤2: 复制数据库结构 Info("复制数据库结构..."); if (_isPostgreSQLSource) { CopyDatabaseStructureFromPostgreSQL(); } else { CopyDatabaseStructure(); } // 步骤3: 复制结构表数据 Info("复制结构表数据..."); foreach (var table in STRUCTURE_TABLES) { if (_isPostgreSQLSource) { CopyStructureTableFromPostgreSQL(table); } else { CopyStructureTable(table); } } // 步骤4: 复制数据表(按时间过滤) Info($"复制数据表(保留最近 {KEEP_DAYS} 天)..."); var cutoffDate = DateTime.Now.AddDays(-KEEP_DAYS); foreach (var (table, dateColumn) in DATA_TABLES) { if (_isPostgreSQLSource) { CopyDataTableFromPostgreSQL(table, dateColumn, cutoffDate); } else { CopyDataTable(table, dateColumn, cutoffDate); } } // 步骤5: 优化数据库 Info("优化目标数据库..."); OptimizeDatabase(); // 步骤6: 生成报告 var endTime = DateTime.Now; GenerateReport(startTime, endTime); Success($"\n✅ 完成! 耗时: {(endTime - startTime).TotalSeconds:F2} 秒"); Success($"新数据库位置: {TARGET_DB}"); Success($"新数据库大小: {GetFileSize(TARGET_DB)}"); WriteLog($"完整同步完成,耗时: {(endTime - startTime).TotalSeconds:F2} 秒"); } static void RunIncrementalSync() { var startTime = DateTime.Now; WriteLog($"开始增量同步,时间: {startTime:yyyy-MM-dd HH:mm:ss}"); // 检查目标数据库是否存在 if (!File.Exists(TARGET_DB)) { Info("目标数据库不存在,执行完整同步..."); RunSync(); return; } // 检查表结构是否一致(根据配置决定是否检查) var structureChanged = CheckTableStructure(out var newTables, out var missingTables); if (structureChanged) { if (_autoFullSync) { // 配置为自动全量同步 Warn("检测到表结构变化,执行完整同步..."); WriteLog("表结构不一致,切换到完整同步模式"); RunSync(); return; } else if (_syncNewTablesOnly && (newTables.Any() || missingTables.Any())) { // 配置为只同步新表 Warn($"检测到表结构变化:新增表 {string.Join(", ", newTables)},缺失表 {string.Join(", ", missingTables)}"); WriteLog($"表结构变化:新增 {newTables.Count} 个表,缺失 {missingTables.Count} 个表"); // 增量处理新表 SyncNewTablesOnly(newTables, missingTables); // 继续增量同步 } else { // 其他情况,继续增量同步(可能只是列变化,不影响数据同步) Warn("检测到表结构变化,但配置为继续增量同步..."); } } Info("增量同步模式:只同步新增数据..."); using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly"); using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}"); sourceConn.Open(); targetConn.Open(); var totalNewRows = 0; var cutoffDate = DateTime.Now.AddDays(-KEEP_DAYS); // 1. 同步结构表(全量更新,因为配置可能变化) Info("更新结构表数据..."); foreach (var table in STRUCTURE_TABLES) { var updated = SyncStructureTableIncremental(sourceConn, targetConn, table); if (updated > 0) { totalNewRows += updated; } } // 2. 增量同步数据表(只同步新增数据) Info($"增量同步数据表(保留最近 {KEEP_DAYS} 天)..."); foreach (var (table, dateColumn) in DATA_TABLES) { var newRows = SyncDataTableIncremental(sourceConn, targetConn, table, dateColumn, cutoffDate); totalNewRows += newRows; } // 3. 清理过期数据(删除超过保留天数的数据) Info("清理过期数据..."); var deletedRows = CleanupExpiredData(targetConn, cutoffDate); // 4. 优化数据库 Info("优化目标数据库..."); OptimizeDatabase(); var endTime = DateTime.Now; var duration = (endTime - startTime).TotalSeconds; Success($"\n✅ 增量同步完成! 耗时: {duration:F2} 秒"); Success($"新增数据: {totalNewRows:N0} 行"); Success($"删除过期数据: {deletedRows:N0} 行"); Success($"数据库大小: {GetFileSize(TARGET_DB)}"); WriteLog($"增量同步完成,耗时: {duration:F2} 秒,新增: {totalNewRows} 行,删除: {deletedRows} 行"); } static void LoadConfiguration() { var configFile = "appsettings.json"; if (File.Exists(configFile)) { try { var json = File.ReadAllText(configFile); _config = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip }) ?? new AppConfig(); Info($"✅ 已加载配置文件: {configFile}"); } catch (Exception ex) { Warn($"⚠️ 加载配置文件失败: {ex.Message},使用默认配置"); _config = new AppConfig(); } } else { Warn($"⚠️ 配置文件不存在: {configFile},使用默认配置"); // 创建默认配置文件 SaveDefaultConfiguration(); } } static void SaveDefaultConfiguration() { try { // 初始化默认配置 var defaultConfig = new AppConfig { SourceDatabase = @"D:\NewProductionLineMonitorDB.db", TargetDatabase = @"D:\NewProductionLineMonitorDB_Dev.db", KeepDays = 14, SyncIntervalHours = 24, IncrementalMode = true, StructureTables = new List { "__EFMigrationsHistory", "ProductionLines", "Machines", "Roles", "Users", "Menus", "RoleMenus", "Recipes", "Cims", "ElectricEnergyMeters" }, DataTables = new Dictionary { { "MachineOutPutPerHours", "DataTime" }, { "MachineFaultRecords", "StartTime" }, { "MachineStatistics", "Date" }, { "ElectricEnergyMeterLogs", "CreateTime" }, { "MachinePowerConsumptions", "CreateTime" }, { "MachineProcessBTs", "CreateTime" }, { "MachineProcessFOGs", "CreateTime" }, { "MachineProcessPSs", "CreateTime" }, { "MachineProcessPSEPDs", "CreateTime" }, { "MachineFaultComparisons", "CreateTime" } }, TableStructureChange = new TableStructureChangeConfig { AutoFullSync = false, SyncNewTablesOnly = true, CheckIntervalHours = 24 } }; var options = new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; var json = JsonSerializer.Serialize(defaultConfig, options); File.WriteAllText("appsettings.json", json); _config = defaultConfig; Info("✅ 已创建默认配置文件: appsettings.json"); } catch (Exception ex) { Warn($"⚠️ 创建默认配置文件失败: {ex.Message}"); } } static bool CheckTableStructure(out List newTables, out List missingTables) { newTables = new List(); missingTables = new List(); // 定期检查表结构(根据配置的检查间隔) var now = DateTime.Now; if ((now - _lastStructureCheck).TotalHours < _structureCheckIntervalHours) { // 还没到检查时间,返回false(假设没有变化) return false; } _lastStructureCheck = now; try { using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly"); using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}"); sourceConn.Open(); targetConn.Open(); // 获取源数据库的所有表 var sourceTables = sourceConn.Query( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name") .ToHashSet(); // 获取目标数据库的所有表 var targetTables = targetConn.Query( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name") .ToHashSet(); // 检查表是否一致 if (!sourceTables.SetEquals(targetTables)) { newTables = sourceTables.Except(targetTables).ToList(); missingTables = targetTables.Except(sourceTables).ToList(); if (newTables.Any()) { Warn($"源数据库新增表: {string.Join(", ", newTables)}"); } if (missingTables.Any()) { Warn($"目标数据库缺失表: {string.Join(", ", missingTables)}"); } return true; } // 检查每个表的列结构 foreach (var table in sourceTables) { var sourceColumns = sourceConn.Query( $"PRAGMA table_info([{table}])").Select(r => (string)r.name).OrderBy(n => n).ToList(); var targetColumns = targetConn.Query( $"PRAGMA table_info([{table}])").Select(r => (string)r.name).OrderBy(n => n).ToList(); if (sourceColumns.Count != targetColumns.Count) { Warn($"表 {table} 的列数不一致(源: {sourceColumns.Count}, 目标: {targetColumns.Count})"); return true; // 列结构变化,返回true } // 检查列名是否一致 if (!sourceColumns.SequenceEqual(targetColumns)) { var missing = sourceColumns.Except(targetColumns).ToList(); var extra = targetColumns.Except(sourceColumns).ToList(); if (missing.Any()) Warn($"表 {table} 缺少列: {string.Join(", ", missing)}"); if (extra.Any()) Warn($"表 {table} 多余列: {string.Join(", ", extra)}"); return true; // 列结构变化,返回true } } return false; // 表结构一致 } catch (Exception ex) { Warn($"检查表结构时出错: {ex.Message}"); return false; // 出错时返回false,不中断同步 } } static int SyncStructureTableIncremental(SqliteConnection sourceConn, SqliteConnection targetConn, string tableName) { try { // 检查表是否存在 var tableExists = sourceConn.ExecuteScalar( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name", new { name = tableName }); if (tableExists == 0) { return 0; } // 获取源表的所有数据 var sourceData = sourceConn.Query($"SELECT * FROM [{tableName}]").ToList(); // 清空目标表 targetConn.Execute($"DELETE FROM [{tableName}]"); // 重新插入所有数据 if (sourceData.Count > 0) { using var transaction = targetConn.BeginTransaction(); foreach (var row in sourceData) { var dict = (IDictionary)row; var columns = string.Join(", ", dict.Keys.Select(k => $"[{k}]")); var parameters = string.Join(", ", dict.Keys.Select(k => $"@{k}")); var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})"; targetConn.Execute(sql, dict, transaction); } transaction.Commit(); } Info($" ✅ {tableName,-30} (更新 {sourceData.Count:N0} 行)"); return sourceData.Count; } catch (Exception ex) { Warn($" ⚠️ 同步结构表 {tableName} 时出错: {ex.Message}"); return 0; } } static int SyncDataTableIncremental(SqliteConnection sourceConn, SqliteConnection targetConn, string tableName, string dateColumn, DateTime cutoffDate) { try { // 检查表是否存在 var tableExists = sourceConn.ExecuteScalar( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name", new { name = tableName }); if (tableExists == 0) { return 0; } // 获取目标表中最大的时间戳(用于增量同步) DateTime? maxDate = null; try { var maxDateStr = targetConn.ExecuteScalar( $"SELECT MAX([{dateColumn}]) FROM [{tableName}]"); if (!string.IsNullOrEmpty(maxDateStr) && DateTime.TryParse(maxDateStr, out var parsedDate)) { maxDate = parsedDate; } } catch { // 如果表为空或不存在,maxDate 保持为 null } // 查询需要同步的新数据(大于最大时间戳,且在保留天数内) object parameters; string query; if (maxDate.HasValue) { query = $@"SELECT * FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate AND [{dateColumn}] > @maxDate"; parameters = new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss"), maxDate = maxDate.Value.ToString("yyyy-MM-dd HH:mm:ss") }; } else { query = $@"SELECT * FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate"; parameters = new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss") }; } var newRows = sourceConn.Query(query, parameters).ToList(); if (newRows.Count == 0) { Info($" 📋 {tableName,-30} (无新数据)"); return 0; } // 批量插入新数据 using var transaction = targetConn.BeginTransaction(); var inserted = 0; foreach (var row in newRows) { var dict = (IDictionary)row; var columns = string.Join(", ", dict.Keys.Select(k => $"[{k}]")); var paramsList = string.Join(", ", dict.Keys.Select(k => $"@{k}")); var sql = $"INSERT OR IGNORE INTO [{tableName}] ({columns}) VALUES ({paramsList})"; targetConn.Execute(sql, dict, transaction); inserted++; } transaction.Commit(); Info($" ✅ {tableName,-30} (新增 {inserted:N0} 行)"); return inserted; } catch (Exception ex) { Warn($" ⚠️ 增量同步表 {tableName} 时出错: {ex.Message}"); return 0; } } static void SyncNewTablesOnly(List newTables, List missingTables) { using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly"); using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}"); sourceConn.Open(); targetConn.Open(); Info("增量处理新表..."); // 1. 处理源数据库新增的表 foreach (var tableName in newTables) { try { // 复制表结构 var createSql = sourceConn.Query( "SELECT sql FROM sqlite_master WHERE type='table' AND name=@name", new { name = tableName }).FirstOrDefault(); if (!string.IsNullOrEmpty(createSql)) { targetConn.Execute(createSql); Info($" ✅ 已创建新表结构: {tableName}"); } // 判断是结构表还是数据表 bool isStructureTable = STRUCTURE_TABLES.Contains(tableName); bool isDataTable = DATA_TABLES.ContainsKey(tableName); if (isStructureTable) { // 结构表:全量复制 var count = SyncStructureTableIncremental(sourceConn, targetConn, tableName); Info($" ✅ 已同步结构表数据: {tableName} ({count:N0} 行)"); } else if (isDataTable) { // 数据表:按时间过滤复制 var dateColumn = DATA_TABLES[tableName]; var cutoffDate = DateTime.Now.AddDays(-KEEP_DAYS); var count = CopyDataTableForNewTable(sourceConn, targetConn, tableName, dateColumn, cutoffDate); Info($" ✅ 已同步数据表数据: {tableName} ({count:N0} 行,保留最近 {KEEP_DAYS} 天)"); } else { // 未配置的表:只复制结构,不复制数据 Warn($" ⚠️ 表 {tableName} 未在配置中,只复制结构,不复制数据"); Info($" 💡 提示: 如需复制数据,请在 appsettings.json 中添加配置"); } } catch (Exception ex) { Warn($" ⚠️ 处理新表 {tableName} 时出错: {ex.Message}"); } } // 2. 处理目标数据库多余的表(源数据库已删除) foreach (var tableName in missingTables) { try { // 可以选择删除或保留 // 这里选择保留,因为可能是历史数据 Warn($" ⚠️ 目标数据库存在源数据库已删除的表: {tableName}(保留)"); } catch (Exception ex) { Warn($" ⚠️ 处理缺失表 {tableName} 时出错: {ex.Message}"); } } } static int CopyDataTableForNewTable(SqliteConnection sourceConn, SqliteConnection targetConn, string tableName, string dateColumn, DateTime cutoffDate) { try { // 获取要复制的行数 var keepCount = sourceConn.ExecuteScalar( $"SELECT COUNT(*) FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate", new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss") }); if (keepCount == 0) { return 0; } // 流式处理:分批查询和插入 using var transaction = targetConn.BeginTransaction(); var batchSize = 1000; var insertedCount = 0; var offset = 0; while (insertedCount < keepCount) { var currentBatch = sourceConn.Query( $"SELECT * FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate LIMIT @limit OFFSET @offset", new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss"), limit = batchSize, offset = offset }).ToList(); if (currentBatch.Count == 0) break; // 批量插入 InsertBatch(targetConn, tableName, currentBatch.Cast().ToList(), transaction); insertedCount += currentBatch.Count; offset += batchSize; } transaction.Commit(); return insertedCount; } catch (Exception ex) { Warn($" ⚠️ 复制新数据表 {tableName} 时出错: {ex.Message}"); return 0; } } static int CleanupExpiredData(SqliteConnection targetConn, DateTime cutoffDate) { var totalDeleted = 0; foreach (var (table, dateColumn) in DATA_TABLES) { try { var deleted = targetConn.Execute( $"DELETE FROM [{table}] WHERE [{dateColumn}] < @cutoffDate", new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss") }); totalDeleted += deleted; } catch (Exception ex) { Warn($" ⚠️ 清理表 {table} 过期数据时出错: {ex.Message}"); } } return totalDeleted; } static void WriteLog(string message) { try { var logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}"; File.AppendAllText(_logFile, logMessage + Environment.NewLine); } catch { // 忽略日志写入错误 } } static void CopyDatabaseStructure() { // 使用只读模式打开源数据库,避免创建WAL文件 using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly"); sourceConn.Open(); using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}"); targetConn.Open(); // 获取所有表的创建语句 var tables = sourceConn.Query( "SELECT sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"); foreach (var createSql in tables) { if (!string.IsNullOrEmpty(createSql)) { targetConn.Execute(createSql); } } // 复制索引 var indexes = sourceConn.Query( "SELECT sql FROM sqlite_master WHERE type='index' AND sql IS NOT NULL"); foreach (var indexSql in indexes) { try { targetConn.Execute(indexSql); } catch { // 忽略已存在的索引 } } } static void CopyDatabaseStructureFromPostgreSQL() { if (string.IsNullOrEmpty(_pgConnectionString)) { throw new Exception("PostgreSQL 连接字符串未配置"); } using var pgConn = new NpgsqlConnection(_pgConnectionString); pgConn.Open(); using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}"); targetConn.Open(); // 获取所有表名 var tables = pgConn.Query( @"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name"); foreach (var tableName in tables) { try { // 获取表结构 var columns = pgConn.Query( @"SELECT column_name, data_type, character_maximum_length, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = @tableName ORDER BY ordinal_position", new { tableName }); if (!columns.Any()) continue; // 构建 CREATE TABLE 语句 var columnDefs = new List(); foreach (var col in columns) { var colName = (string)col.column_name; var dataType = MapPostgreSQLTypeToSQLite((string)col.data_type, col.character_maximum_length); var nullable = (string)col.is_nullable == "YES" ? "" : " NOT NULL"; var defaultValue = col.column_default != null ? $" DEFAULT {col.column_default}" : ""; columnDefs.Add($"[{colName}] {dataType}{nullable}{defaultValue}"); } var createTableSql = $"CREATE TABLE IF NOT EXISTS [{tableName}] ({string.Join(", ", columnDefs)})"; targetConn.Execute(createTableSql); Info($" ✅ 已创建表: {tableName}"); } catch (Exception ex) { Warn($" ⚠️ 创建表 {tableName} 失败: {ex.Message}"); } } } static string MapPostgreSQLTypeToSQLite(string pgType, object? maxLength) { // PostgreSQL 类型到 SQLite 类型映射 return pgType.ToLower() switch { "integer" or "int" or "int4" => "INTEGER", "bigint" or "int8" => "INTEGER", "smallint" or "int2" => "INTEGER", "real" or "float4" => "REAL", "double precision" or "float8" => "REAL", "numeric" or "decimal" => "REAL", "boolean" or "bool" => "INTEGER", // SQLite 用 0/1 表示布尔值 "text" => "TEXT", "character varying" or "varchar" => maxLength != null ? $"TEXT" : "TEXT", "character" or "char" => maxLength != null ? $"TEXT" : "TEXT", "timestamp" or "timestamp without time zone" => "TEXT", "timestamp with time zone" => "TEXT", "date" => "TEXT", "time" => "TEXT", "uuid" => "TEXT", "json" or "jsonb" => "TEXT", "bytea" => "BLOB", _ => "TEXT" // 默认映射为 TEXT }; } static void CopyStructureTable(string tableName) { using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly"); using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}"); sourceConn.Open(); targetConn.Open(); // 检查表是否存在 var tableExists = sourceConn.ExecuteScalar( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name", new { name = tableName }); if (tableExists == 0) { Warn($" ⚠️ 表不存在: {tableName},跳过"); return; } // 获取行数 var count = sourceConn.ExecuteScalar($"SELECT COUNT(*) FROM [{tableName}]"); if (count == 0) { Info($" 📋 {tableName,-30} (0 行)"); return; } // 复制数据 var data = sourceConn.Query($"SELECT * FROM [{tableName}]"); using var transaction = targetConn.BeginTransaction(); foreach (var row in data) { var dict = (IDictionary)row; var columns = string.Join(", ", dict.Keys.Select(k => $"[{k}]")); var parameters = string.Join(", ", dict.Keys.Select(k => $"@{k}")); var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})"; targetConn.Execute(sql, dict, transaction); } transaction.Commit(); Success($" ✅ {tableName,-30} ({count:N0} 行)"); } static void CopyStructureTableFromPostgreSQL(string tableName) { if (string.IsNullOrEmpty(_pgConnectionString)) { Warn($" ⚠️ PostgreSQL 连接字符串未配置,跳过表: {tableName}"); return; } using var pgConn = new NpgsqlConnection(_pgConnectionString); using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}"); pgConn.Open(); targetConn.Open(); // 检查表是否存在 var tableExists = pgConn.ExecuteScalar( @"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = @tableName", new { tableName }); if (tableExists == 0) { Warn($" ⚠️ 表不存在: {tableName},跳过"); return; } // 获取行数 var count = pgConn.ExecuteScalar($"SELECT COUNT(*) FROM \"{tableName}\""); if (count == 0) { Info($" 📋 {tableName,-30} (0 行)"); return; } // 分批复制数据 using var transaction = targetConn.BeginTransaction(); var batchSize = 1000; var offset = 0; var totalInserted = 0; while (true) { var data = pgConn.Query($"SELECT * FROM \"{tableName}\" LIMIT {batchSize} OFFSET {offset}").ToList(); if (data.Count == 0) break; foreach (var row in data) { var dict = (IDictionary)row; // 处理 PostgreSQL 特殊类型 var processedDict = ProcessPostgreSQLData(dict); var columns = string.Join(", ", processedDict.Keys.Select(k => $"[{k}]")); var parameters = string.Join(", ", processedDict.Keys.Select(k => $"@{k}")); var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})"; targetConn.Execute(sql, processedDict, transaction); totalInserted++; } offset += batchSize; if (totalInserted % 1000 == 0) { Console.Write($"\r ⏳ {tableName,-30} ({totalInserted:N0}/{count:N0})"); } } transaction.Commit(); Console.Write("\r"); Success($" ✅ {tableName,-30} ({totalInserted:N0} 行)"); } static Dictionary ProcessPostgreSQLData(IDictionary dict) { var result = new Dictionary(); foreach (var kvp in dict) { var value = kvp.Value; // 处理 PostgreSQL 特殊类型 if (value == null || value == DBNull.Value) { result[kvp.Key] = null; } else if (value is bool boolValue) { // SQLite 用 0/1 表示布尔值 result[kvp.Key] = boolValue ? 1 : 0; } else if (value is DateTime dateTimeValue) { // 转换为字符串 result[kvp.Key] = dateTimeValue.ToString("yyyy-MM-dd HH:mm:ss"); } else if (value is DateTimeOffset dateTimeOffsetValue) { result[kvp.Key] = dateTimeOffsetValue.ToString("yyyy-MM-dd HH:mm:ss"); } else if (value is Guid guidValue) { result[kvp.Key] = guidValue.ToString(); } else { result[kvp.Key] = value; } } return result; } static void CopyDataTable(string tableName, string dateColumn, DateTime cutoffDate) { using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly"); using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}"); sourceConn.Open(); targetConn.Open(); // 检查表是否存在 var tableExists = sourceConn.ExecuteScalar( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name", new { name = tableName }); if (tableExists == 0) { Warn($" ⚠️ 表不存在: {tableName},跳过"); return; } // 获取总行数和要复制的行数 var totalCount = sourceConn.ExecuteScalar($"SELECT COUNT(*) FROM [{tableName}]"); var keepCount = sourceConn.ExecuteScalar( $"SELECT COUNT(*) FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate", new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss") }); if (keepCount == 0) { Info($" 📋 {tableName,-30} (0 行,共 {totalCount:N0} 行)"); return; } // 流式处理:分批查询和插入,避免一次性加载所有数据到内存 using var transaction = targetConn.BeginTransaction(); var batchSize = 1000; var insertedCount = 0; var offset = 0; var lastUpdateTime = DateTime.Now; // 使用LIMIT和OFFSET分批查询 while (insertedCount < keepCount) { var currentBatch = sourceConn.Query( $"SELECT * FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate LIMIT @limit OFFSET @offset", new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss"), limit = batchSize, offset = offset }).ToList(); if (currentBatch.Count == 0) break; // 批量插入 InsertBatch(targetConn, tableName, currentBatch.Cast().ToList(), transaction); insertedCount += currentBatch.Count; offset += batchSize; // 每1000行或每2秒更新一次进度显示 var now = DateTime.Now; if (insertedCount % 1000 == 0 || (now - lastUpdateTime).TotalSeconds >= 2) { var percent = keepCount > 0 ? (insertedCount * 100.0 / keepCount) : 0; Console.Write($"\r ⏳ {tableName,-30} ({insertedCount:N0}/{keepCount:N0} - {percent:F1}%)"); lastUpdateTime = now; } } transaction.Commit(); var deletedCount = totalCount - keepCount; var deletedPercent = totalCount > 0 ? (deletedCount * 100.0 / totalCount) : 0; Console.Write("\r"); Success($" ✅ {tableName,-30} (保留 {keepCount:N0} 行,删除 {deletedCount:N0} 行 [{deletedPercent:F1}%])"); } static void CopyDataTableFromPostgreSQL(string tableName, string dateColumn, DateTime cutoffDate) { if (string.IsNullOrEmpty(_pgConnectionString)) { Warn($" ⚠️ PostgreSQL 连接字符串未配置,跳过表: {tableName}"); return; } using var pgConn = new NpgsqlConnection(_pgConnectionString); using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}"); pgConn.Open(); targetConn.Open(); // 检查表是否存在 var tableExists = pgConn.ExecuteScalar( @"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = @tableName", new { tableName }); if (tableExists == 0) { Warn($" ⚠️ 表不存在: {tableName},跳过"); return; } // 获取总行数和要复制的行数 var totalCount = pgConn.ExecuteScalar($"SELECT COUNT(*) FROM \"{tableName}\""); var keepCount = pgConn.ExecuteScalar( $"SELECT COUNT(*) FROM \"{tableName}\" WHERE \"{dateColumn}\" >= @cutoffDate", new { cutoffDate = cutoffDate }); if (keepCount == 0) { Info($" 📋 {tableName,-30} (0 行,共 {totalCount:N0} 行)"); return; } // 流式处理:分批查询和插入 using var transaction = targetConn.BeginTransaction(); var batchSize = 1000; var insertedCount = 0L; var offset = 0; var lastUpdateTime = DateTime.Now; // 使用LIMIT和OFFSET分批查询 while (insertedCount < keepCount) { var currentBatch = pgConn.Query( $"SELECT * FROM \"{tableName}\" WHERE \"{dateColumn}\" >= @cutoffDate ORDER BY \"{dateColumn}\" LIMIT {batchSize} OFFSET {offset}", new { cutoffDate = cutoffDate }).ToList(); if (currentBatch.Count == 0) break; // 批量插入 foreach (var row in currentBatch) { var dict = (IDictionary)row; var processedDict = ProcessPostgreSQLData(dict); var columns = string.Join(", ", processedDict.Keys.Select(k => $"[{k}]")); var parameters = string.Join(", ", processedDict.Keys.Select(k => $"@{k}")); var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})"; targetConn.Execute(sql, processedDict, transaction); insertedCount++; } offset += batchSize; // 每1000行或每2秒更新一次进度显示 var now = DateTime.Now; if (insertedCount % 1000 == 0 || (now - lastUpdateTime).TotalSeconds >= 2) { var percent = keepCount > 0 ? (insertedCount * 100.0 / keepCount) : 0; Console.Write($"\r ⏳ {tableName,-30} ({insertedCount:N0}/{keepCount:N0} - {percent:F1}%)"); lastUpdateTime = now; } } transaction.Commit(); var deletedCount = totalCount - keepCount; var deletedPercent = totalCount > 0 ? (deletedCount * 100.0 / totalCount) : 0; Console.Write("\r"); Success($" ✅ {tableName,-30} (保留 {keepCount:N0} 行,删除 {deletedCount:N0} 行 [{deletedPercent:F1}%])"); } static void InsertBatch(SqliteConnection conn, string tableName, List batch, SqliteTransaction transaction) { if (batch.Count == 0) return; var firstRow = (IDictionary)batch[0]; var columns = string.Join(", ", firstRow.Keys.Select(k => $"[{k}]")); var parameters = string.Join(", ", firstRow.Keys.Select(k => $"@{k}")); var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})"; // 使用批量插入优化性能 foreach (var row in batch) { conn.Execute(sql, row, transaction); } } static void OptimizeDatabase() { using var conn = new SqliteConnection($"Data Source={TARGET_DB}"); conn.Open(); // SQLite不支持存储过程,使用CommandType.Text conn.Execute("VACUUM", commandType: CommandType.Text); conn.Execute("ANALYZE", commandType: CommandType.Text); } static void GenerateReport(DateTime startTime, DateTime endTime) { using var conn = new SqliteConnection($"Data Source={TARGET_DB}"); conn.Open(); Console.WriteLine("\n" + new string('=', 70)); Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("【数据库统计报告】"); Console.ResetColor(); Console.WriteLine(new string('=', 70)); // 获取所有表的行数 var tables = conn.Query( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"); Console.WriteLine($"\n{"表名",-35} {"行数",15}"); Console.WriteLine(new string('-', 50)); var totalRows = 0; foreach (var table in tables) { var count = conn.ExecuteScalar($"SELECT COUNT(*) FROM [{table}]"); totalRows += count; Console.WriteLine($"{table,-35} {count,15:N0}"); } Console.WriteLine(new string('-', 50)); Console.WriteLine($"{"总计",-35} {totalRows,15:N0}"); Console.WriteLine($"\n耗时: {(endTime - startTime).TotalSeconds:F2} 秒"); Console.WriteLine($"源数据库大小: {GetFileSize(SOURCE_DB)}"); Console.WriteLine($"新数据库大小: {GetFileSize(TARGET_DB)}"); var sourceSize = new FileInfo(SOURCE_DB).Length; var targetSize = new FileInfo(TARGET_DB).Length; var reduction = sourceSize - targetSize; var reductionPercent = (reduction * 100.0 / sourceSize); Console.WriteLine($"减少大小: {FormatSize(reduction)} ({reductionPercent:F1}%)"); Console.WriteLine(new string('=', 70)); } static string GetFileSize(string filePath) { var fileInfo = new FileInfo(filePath); return FormatSize(fileInfo.Length); } static string FormatSize(long bytes) { string[] sizes = { "B", "KB", "MB", "GB" }; double len = bytes; int order = 0; while (len >= 1024 && order < sizes.Length - 1) { order++; len = len / 1024; } return $"{len:0.##} {sizes[order]}"; } static void Info(string message) { Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}"); Console.ResetColor(); } static void Success(string message) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(message); Console.ResetColor(); } static void Warn(string message) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(message); Console.ResetColor(); } static void Error(string message) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(message); Console.ResetColor(); } } }