Program.cs 74 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818
  1. using System;
  2. using System.Data;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Collections.Generic;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using System.Text.Json;
  9. using System.Text.Json.Serialization;
  10. using Microsoft.Data.Sqlite;
  11. using Npgsql;
  12. using Dapper;
  13. namespace DatabaseCleanupTool
  14. {
  15. // 配置类
  16. class AppConfig
  17. {
  18. [JsonPropertyName("SourceDatabase")]
  19. public string SourceDatabase { get; set; } = @"D:\NewProductionLineMonitorDB.db";
  20. [JsonPropertyName("TargetDatabase")]
  21. public string TargetDatabase { get; set; } = @"D:\NewProductionLineMonitorDB_Dev.db";
  22. [JsonPropertyName("KeepDays")]
  23. public int KeepDays { get; set; } = 14;
  24. [JsonPropertyName("SyncIntervalHours")]
  25. public int SyncIntervalHours { get; set; } = 24;
  26. [JsonPropertyName("IncrementalMode")]
  27. public bool IncrementalMode { get; set; } = true;
  28. [JsonPropertyName("StructureTables")]
  29. public List<string> StructureTables { get; set; } = new();
  30. [JsonPropertyName("DataTables")]
  31. public Dictionary<string, string> DataTables { get; set; } = new();
  32. [JsonPropertyName("TableStructureChange")]
  33. public TableStructureChangeConfig TableStructureChange { get; set; } = new();
  34. [JsonPropertyName("PostgreSQLConnectionString")]
  35. public string? PostgreSQLConnectionString { get; set; }
  36. [JsonPropertyName("SourceType")]
  37. public string SourceType { get; set; } = "SQLite"; // SQLite 或 PostgreSQL
  38. }
  39. class TableStructureChangeConfig
  40. {
  41. [JsonPropertyName("AutoFullSync")]
  42. public bool AutoFullSync { get; set; } = false;
  43. [JsonPropertyName("SyncNewTablesOnly")]
  44. public bool SyncNewTablesOnly { get; set; } = true;
  45. [JsonPropertyName("CheckIntervalHours")]
  46. public int CheckIntervalHours { get; set; } = 24;
  47. }
  48. class Program
  49. {
  50. // 配置对象
  51. static AppConfig _config = new AppConfig();
  52. // 运行时配置(可通过交互菜单修改)
  53. static string SOURCE_DB => _config.SourceDatabase;
  54. static string TARGET_DB => _config.TargetDatabase;
  55. static int KEEP_DAYS => _config.KeepDays;
  56. // 定期同步配置
  57. static bool _watchMode = false;
  58. static int _syncIntervalHours
  59. {
  60. get => _config.SyncIntervalHours;
  61. set => _config.SyncIntervalHours = value;
  62. }
  63. static string _logFile = "DatabaseCleanupTool.log";
  64. static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
  65. static bool _interactiveMode = true; // 是否显示交互菜单
  66. static bool _incrementalMode
  67. {
  68. get => _config.IncrementalMode;
  69. set => _config.IncrementalMode = value;
  70. }
  71. static bool _fullSyncMode = false; // 完整同步模式(删除重建)
  72. // 表配置(从配置文件读取)
  73. static string[] STRUCTURE_TABLES => _config.StructureTables.ToArray();
  74. static Dictionary<string, string> DATA_TABLES => _config.DataTables;
  75. // 表结构变化配置
  76. static bool _autoFullSync => _config.TableStructureChange.AutoFullSync;
  77. static bool _syncNewTablesOnly => _config.TableStructureChange.SyncNewTablesOnly;
  78. static DateTime _lastStructureCheck = DateTime.MinValue;
  79. static int _structureCheckIntervalHours => _config.TableStructureChange.CheckIntervalHours;
  80. // 数据源类型
  81. static bool _isPostgreSQLSource => _config.SourceType?.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase) == true;
  82. static string? _pgConnectionString => _config.PostgreSQLConnectionString;
  83. static void Main(string[] args)
  84. {
  85. // 加载配置文件
  86. LoadConfiguration();
  87. // 解析命令行参数
  88. ParseArguments(args);
  89. // 显示标题
  90. Console.ForegroundColor = ConsoleColor.Cyan;
  91. Console.WriteLine("╔═══════════════════════════════════════════════════════════════╗");
  92. Console.WriteLine("║ 生产线监控数据库清理工具 v2.0 ║");
  93. Console.WriteLine("║ Database Cleanup Tool for Production Line Monitor ║");
  94. Console.WriteLine("╚═══════════════════════════════════════════════════════════════╝");
  95. Console.ResetColor();
  96. // 如果指定了命令行参数,跳过交互菜单(除非明确指定--interactive)
  97. if (args.Length > 0)
  98. {
  99. var hasInteractive = args.Any(a => a.ToLower() == "--interactive" || a.ToLower() == "--menu");
  100. if (!hasInteractive)
  101. {
  102. _interactiveMode = false;
  103. }
  104. }
  105. // 交互式配置菜单
  106. if (_interactiveMode)
  107. {
  108. ShowInteractiveMenu();
  109. }
  110. else
  111. {
  112. // 非交互模式,显示配置但不询问保存
  113. ShowConfiguration(askSave: false);
  114. }
  115. // 检查源数据库
  116. if (!_isPostgreSQLSource)
  117. {
  118. if (!File.Exists(SOURCE_DB))
  119. {
  120. Error($"错误: 源数据库不存在: {SOURCE_DB}");
  121. Console.WriteLine("\n按任意键退出...");
  122. Console.ReadKey();
  123. return;
  124. }
  125. }
  126. else
  127. {
  128. if (string.IsNullOrEmpty(_pgConnectionString))
  129. {
  130. Error("错误: PostgreSQL 连接字符串未配置");
  131. Console.WriteLine("\n按任意键退出...");
  132. Console.ReadKey();
  133. return;
  134. }
  135. // 测试 PostgreSQL 连接
  136. try
  137. {
  138. using var testConn = new NpgsqlConnection(_pgConnectionString);
  139. testConn.Open();
  140. Info("✅ PostgreSQL 连接测试成功");
  141. }
  142. catch (Exception ex)
  143. {
  144. Error($"错误: PostgreSQL 连接失败: {ex.Message}");
  145. Console.WriteLine("\n按任意键退出...");
  146. Console.ReadKey();
  147. return;
  148. }
  149. }
  150. // 设置Ctrl+C处理
  151. Console.CancelKeyPress += (sender, e) =>
  152. {
  153. e.Cancel = true;
  154. _cancellationTokenSource.Cancel();
  155. WriteLog("收到退出信号,正在停止...");
  156. Console.WriteLine("\n\n正在停止,请稍候...");
  157. };
  158. if (_watchMode)
  159. {
  160. // 定期同步模式
  161. RunWatchMode().Wait();
  162. }
  163. else
  164. {
  165. // 单次执行模式
  166. RunOnce();
  167. }
  168. }
  169. static void ShowInteractiveMenu()
  170. {
  171. Console.WriteLine("\n╔═══════════════════════════════════════════════════════════════╗");
  172. Console.WriteLine("║ 配置菜单 ║");
  173. Console.WriteLine("╚═══════════════════════════════════════════════════════════════╝\n");
  174. // 1. 运行模式
  175. Console.WriteLine("【1】运行模式");
  176. Console.WriteLine(" 1. 单次执行(执行一次后退出)");
  177. Console.WriteLine(" 2. 定期同步(持续运行,定期同步)");
  178. Console.Write($" 当前选择: {(_watchMode ? "2. 定期同步" : "1. 单次执行")} → 请选择 (1/2,直接回车保持当前): ");
  179. var modeChoice = Console.ReadLine();
  180. if (!string.IsNullOrWhiteSpace(modeChoice))
  181. {
  182. _watchMode = (modeChoice == "2");
  183. }
  184. // 2. 如果选择定期同步,设置同步间隔和模式
  185. if (_watchMode)
  186. {
  187. Console.WriteLine($"\n【2】同步间隔(当前: 每 {_syncIntervalHours} 小时)");
  188. Console.WriteLine(" 1. 每 6 小时");
  189. Console.WriteLine(" 2. 每 12 小时");
  190. Console.WriteLine(" 3. 每 24 小时(默认)");
  191. Console.WriteLine(" 4. 每 48 小时");
  192. Console.WriteLine(" 5. 自定义");
  193. Console.Write(" 请选择 (1-5,直接回车保持当前): ");
  194. var intervalChoice = Console.ReadLine();
  195. if (!string.IsNullOrWhiteSpace(intervalChoice))
  196. {
  197. switch (intervalChoice)
  198. {
  199. case "1": _syncIntervalHours = 6; break;
  200. case "2": _syncIntervalHours = 12; break;
  201. case "3": _syncIntervalHours = 24; break;
  202. case "4": _syncIntervalHours = 48; break;
  203. case "5":
  204. Console.Write(" 请输入小时数: ");
  205. if (int.TryParse(Console.ReadLine(), out int customHours) && customHours > 0)
  206. {
  207. _syncIntervalHours = customHours;
  208. }
  209. break;
  210. }
  211. }
  212. // 2.1 同步模式(仅定期同步时)
  213. Console.WriteLine($"\n【2.1】同步模式(当前: {(_incrementalMode ? "增量同步" : "完整同步")})");
  214. Console.WriteLine(" 1. 增量同步(推荐,只同步新增数据,速度快)");
  215. Console.WriteLine(" 2. 完整同步(删除重建,确保数据一致性)");
  216. Console.Write(" 请选择 (1/2,直接回车保持当前): ");
  217. var syncModeChoice = Console.ReadLine();
  218. if (!string.IsNullOrWhiteSpace(syncModeChoice))
  219. {
  220. _config.IncrementalMode = (syncModeChoice == "1");
  221. _fullSyncMode = (syncModeChoice == "2");
  222. }
  223. }
  224. // 3. 保留天数
  225. Console.WriteLine($"\n【3】保留天数(当前: {KEEP_DAYS} 天)");
  226. Console.WriteLine(" 1. 7 天(1周)");
  227. Console.WriteLine(" 2. 14 天(2周,默认)");
  228. Console.WriteLine(" 3. 30 天(1个月)");
  229. Console.WriteLine(" 4. 自定义");
  230. Console.Write(" 请选择 (1-4,直接回车保持当前): ");
  231. var daysChoice = Console.ReadLine();
  232. if (!string.IsNullOrWhiteSpace(daysChoice))
  233. {
  234. switch (daysChoice)
  235. {
  236. case "1": _config.KeepDays = 7; break;
  237. case "2": _config.KeepDays = 14; break;
  238. case "3": _config.KeepDays = 30; break;
  239. case "4":
  240. Console.Write(" 请输入天数: ");
  241. if (int.TryParse(Console.ReadLine(), out int customDays) && customDays > 0)
  242. {
  243. _config.KeepDays = customDays;
  244. }
  245. break;
  246. }
  247. }
  248. // 4. 源数据库路径
  249. Console.WriteLine($"\n【4】源数据库路径");
  250. Console.WriteLine($" 当前: {SOURCE_DB}");
  251. Console.Write(" 是否修改? (y/n,直接回车保持当前): ");
  252. var changeSource = Console.ReadLine()?.ToLower();
  253. if (changeSource == "y")
  254. {
  255. Console.Write(" 请输入源数据库完整路径: ");
  256. var newSource = Console.ReadLine()?.Trim();
  257. if (!string.IsNullOrWhiteSpace(newSource))
  258. {
  259. _config.SourceDatabase = newSource;
  260. }
  261. }
  262. // 5. 目标数据库路径
  263. Console.WriteLine($"\n【5】目标数据库路径");
  264. Console.WriteLine($" 当前: {TARGET_DB}");
  265. Console.Write(" 是否修改? (y/n,直接回车保持当前): ");
  266. var changeTarget = Console.ReadLine()?.ToLower();
  267. if (changeTarget == "y")
  268. {
  269. Console.Write(" 请输入目标数据库完整路径: ");
  270. var newTarget = Console.ReadLine()?.Trim();
  271. if (!string.IsNullOrWhiteSpace(newTarget))
  272. {
  273. _config.TargetDatabase = newTarget;
  274. }
  275. }
  276. // 6. 数据源类型
  277. Console.WriteLine($"\n【6】数据源类型(当前: {_config.SourceType})");
  278. Console.WriteLine(" 1. SQLite(从SQLite数据库复制)");
  279. Console.WriteLine(" 2. PostgreSQL(从PostgreSQL数据库复制)");
  280. Console.Write(" 请选择 (1/2,直接回车保持当前): ");
  281. var sourceTypeChoice = Console.ReadLine();
  282. if (!string.IsNullOrWhiteSpace(sourceTypeChoice))
  283. {
  284. _config.SourceType = sourceTypeChoice == "2" ? "PostgreSQL" : "SQLite";
  285. if (_config.SourceType == "PostgreSQL")
  286. {
  287. Console.Write(" 请输入PostgreSQL连接字符串: ");
  288. var pgConnStr = Console.ReadLine()?.Trim();
  289. if (!string.IsNullOrWhiteSpace(pgConnStr))
  290. {
  291. _config.PostgreSQLConnectionString = pgConnStr;
  292. }
  293. }
  294. }
  295. Console.WriteLine("\n" + new string('═', 70));
  296. // 显示配置并询问是否保存
  297. ShowConfiguration(askSave: true);
  298. }
  299. static void SaveConfiguration()
  300. {
  301. try
  302. {
  303. var options = new JsonSerializerOptions
  304. {
  305. WriteIndented = true,
  306. Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
  307. };
  308. var json = JsonSerializer.Serialize(_config, options);
  309. File.WriteAllText("appsettings.json", json);
  310. Success("✅ 配置已保存到 appsettings.json");
  311. WriteLog("配置已保存到 appsettings.json");
  312. }
  313. catch (Exception ex)
  314. {
  315. Error($"⚠️ 保存配置失败: {ex.Message}");
  316. WriteLog($"保存配置失败: {ex.Message}");
  317. }
  318. }
  319. static void ShowConfiguration(bool askSave = false)
  320. {
  321. Console.WriteLine($"\n【最终配置】");
  322. Console.WriteLine($" 数据源类型: {_config.SourceType}");
  323. if (_isPostgreSQLSource)
  324. {
  325. var connStr = _pgConnectionString ?? "";
  326. var displayConnStr = connStr.Length > 60 ? connStr.Substring(0, 60) + "..." : connStr;
  327. Console.WriteLine($" PostgreSQL连接: {displayConnStr}");
  328. }
  329. else
  330. {
  331. Console.WriteLine($" 源数据库: {SOURCE_DB}");
  332. }
  333. Console.WriteLine($" 目标数据库: {TARGET_DB}");
  334. Console.WriteLine($" 保留天数: 最近 {KEEP_DAYS} 天");
  335. Console.WriteLine($" 截止日期: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
  336. Console.WriteLine($" 起始日期: {DateTime.Now.AddDays(-KEEP_DAYS):yyyy-MM-dd HH:mm:ss}");
  337. if (_watchMode)
  338. {
  339. Console.WriteLine($" 运行模式: 定期同步模式");
  340. Console.WriteLine($" 同步模式: {(_incrementalMode ? "增量同步(只同步新增数据)" : "完整同步(删除重建)")}");
  341. Console.WriteLine($" 同步间隔: 每 {_syncIntervalHours} 小时");
  342. Console.WriteLine($" 日志文件: {_logFile}");
  343. Console.WriteLine($" 提示: 在定期同步模式下,按 'S' 键可立即同步");
  344. }
  345. else
  346. {
  347. Console.WriteLine($" 运行模式: 单次执行模式(完整同步)");
  348. }
  349. // 如果是在交互菜单中,询问是否保存
  350. if (askSave)
  351. {
  352. Console.Write("\n是否保存当前配置到 appsettings.json? (y/n,直接回车不保存): ");
  353. var saveChoice = Console.ReadLine()?.ToLower();
  354. if (saveChoice == "y")
  355. {
  356. SaveConfiguration();
  357. }
  358. }
  359. }
  360. static void ParseArguments(string[] args)
  361. {
  362. for (int i = 0; i < args.Length; i++)
  363. {
  364. var arg = args[i].ToLower();
  365. if (arg == "--watch" || arg == "-w" || arg == "--continuous" || arg == "-c")
  366. {
  367. _watchMode = true;
  368. _interactiveMode = false; // 命令行参数模式,跳过交互菜单
  369. }
  370. else if (arg == "--interval" || arg == "-i")
  371. {
  372. if (i + 1 < args.Length && int.TryParse(args[i + 1], out int hours))
  373. {
  374. _syncIntervalHours = hours;
  375. i++; // 跳过下一个参数
  376. }
  377. else
  378. {
  379. Console.WriteLine("警告: --interval 参数需要指定小时数,使用默认值24小时");
  380. }
  381. }
  382. else if (arg == "--source" || arg == "-s")
  383. {
  384. if (i + 1 < args.Length)
  385. {
  386. _config.SourceDatabase = args[i + 1];
  387. i++;
  388. }
  389. }
  390. else if (arg == "--target" || arg == "-t")
  391. {
  392. if (i + 1 < args.Length)
  393. {
  394. _config.TargetDatabase = args[i + 1];
  395. i++;
  396. }
  397. }
  398. else if (arg == "--days" || arg == "-d")
  399. {
  400. if (i + 1 < args.Length && int.TryParse(args[i + 1], out int days))
  401. {
  402. _config.KeepDays = days;
  403. i++;
  404. }
  405. }
  406. else if (arg == "--interactive" || arg == "--menu")
  407. {
  408. _interactiveMode = true; // 强制显示交互菜单
  409. }
  410. else if (arg == "--help" || arg == "-h")
  411. {
  412. ShowHelp();
  413. Environment.Exit(0);
  414. }
  415. }
  416. }
  417. static void ShowHelp()
  418. {
  419. Console.WriteLine("\n使用方法:");
  420. Console.WriteLine(" DatabaseCleanupTool.exe [选项]");
  421. Console.WriteLine("\n选项:");
  422. Console.WriteLine(" --watch, -w, --continuous, -c 启用定期同步模式");
  423. Console.WriteLine(" --interval, -i <小时数> 设置同步间隔(默认24小时)");
  424. Console.WriteLine(" --source, -s <路径> 源数据库路径");
  425. Console.WriteLine(" --target, -t <路径> 目标数据库路径");
  426. Console.WriteLine(" --days, -d <天数> 保留天数(默认14天)");
  427. Console.WriteLine(" --interactive, --menu 显示交互式配置菜单");
  428. Console.WriteLine(" --help, -h 显示帮助信息");
  429. Console.WriteLine("\n示例:");
  430. Console.WriteLine(" DatabaseCleanupTool.exe # 显示交互菜单");
  431. Console.WriteLine(" DatabaseCleanupTool.exe --watch # 定期同步,每24小时");
  432. Console.WriteLine(" DatabaseCleanupTool.exe --watch --interval 12 # 定期同步,每12小时");
  433. Console.WriteLine(" DatabaseCleanupTool.exe --source D:\\DB.db --target D:\\Dev.db # 指定路径");
  434. Console.WriteLine("\n交互式菜单:");
  435. Console.WriteLine(" - 不带任何参数运行程序,会显示交互式配置菜单");
  436. Console.WriteLine(" - 可以方便地选择运行模式、同步间隔、保留天数等");
  437. Console.WriteLine("\n定期同步模式:");
  438. Console.WriteLine(" - 程序将持续运行,定期同步数据库");
  439. Console.WriteLine(" - 按 Ctrl+C 可以优雅退出");
  440. Console.WriteLine(" - 同步日志会保存到 DatabaseCleanupTool.log 文件");
  441. }
  442. static async Task RunWatchMode()
  443. {
  444. WriteLog("启动定期同步模式");
  445. WriteLog($"同步间隔: 每 {_syncIntervalHours} 小时");
  446. WriteLog($"同步模式: {(_incrementalMode ? "增量同步" : "完整同步")}");
  447. Console.WriteLine("\n✅ 定期同步模式已启动");
  448. Console.WriteLine($" 同步模式: {(_incrementalMode ? "增量同步(只同步新增数据)" : "完整同步(删除重建)")}");
  449. Console.WriteLine($" 同步间隔: 每 {_syncIntervalHours} 小时");
  450. Console.WriteLine($" 日志文件: {_logFile}");
  451. Console.WriteLine($" 按 'S' 键可立即同步");
  452. Console.WriteLine($" 按 Ctrl+C 退出\n");
  453. var nextSyncTime = DateTime.Now; // 立即执行第一次同步
  454. var syncCount = 0;
  455. var manualSyncRequested = false;
  456. // 启动键盘监听任务(用于立即同步)
  457. var keyboardTask = Task.Run(async () =>
  458. {
  459. while (!_cancellationTokenSource.Token.IsCancellationRequested)
  460. {
  461. if (Console.KeyAvailable)
  462. {
  463. var key = Console.ReadKey(true);
  464. if (key.Key == ConsoleKey.S && !char.IsControl(key.KeyChar))
  465. {
  466. manualSyncRequested = true;
  467. Console.WriteLine($"\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 收到立即同步请求...");
  468. }
  469. }
  470. await Task.Delay(100, _cancellationTokenSource.Token);
  471. }
  472. });
  473. while (!_cancellationTokenSource.Token.IsCancellationRequested)
  474. {
  475. try
  476. {
  477. // 检查是否到了同步时间或收到立即同步请求
  478. if (DateTime.Now >= nextSyncTime || manualSyncRequested)
  479. {
  480. syncCount++;
  481. var isManual = manualSyncRequested;
  482. manualSyncRequested = false;
  483. WriteLog($"========== 开始第 {syncCount} 次同步 {(isManual ? "(手动触发)" : "")} ==========");
  484. Console.WriteLine($"\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 开始第 {syncCount} 次同步{(isManual ? " (手动触发)" : "")}...");
  485. var startTime = DateTime.Now;
  486. // 根据模式选择同步方法
  487. if (_incrementalMode && File.Exists(TARGET_DB))
  488. {
  489. RunIncrementalSync();
  490. }
  491. else
  492. {
  493. RunSync(); // 完整同步
  494. }
  495. var endTime = DateTime.Now;
  496. var duration = (endTime - startTime).TotalSeconds;
  497. WriteLog($"第 {syncCount} 次同步完成,耗时: {duration:F2} 秒");
  498. Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 同步完成,耗时: {duration:F2} 秒");
  499. // 计算下次同步时间(如果是手动触发,不重置定时器)
  500. if (!isManual)
  501. {
  502. nextSyncTime = DateTime.Now.AddHours(_syncIntervalHours);
  503. WriteLog($"下次同步时间: {nextSyncTime:yyyy-MM-dd HH:mm:ss}");
  504. Console.WriteLine($"下次同步时间: {nextSyncTime:yyyy-MM-dd HH:mm:ss}\n");
  505. }
  506. else
  507. {
  508. Console.WriteLine($"下次自动同步时间: {nextSyncTime:yyyy-MM-dd HH:mm:ss}\n");
  509. }
  510. }
  511. else
  512. {
  513. // 显示倒计时(每分钟更新一次)
  514. var remaining = nextSyncTime - DateTime.Now;
  515. if (remaining.TotalSeconds > 0 && DateTime.Now.Second < 5) // 每分钟的前5秒显示
  516. {
  517. var hours = (int)remaining.TotalHours;
  518. var minutes = remaining.Minutes;
  519. Console.Write($"\r等待下次同步,剩余时间: {hours}小时 {minutes}分钟... (按 'S' 立即同步)");
  520. }
  521. }
  522. // 等待10秒再检查(平衡响应速度和CPU占用)
  523. await Task.Delay(TimeSpan.FromSeconds(10), _cancellationTokenSource.Token);
  524. }
  525. catch (TaskCanceledException)
  526. {
  527. // 正常退出
  528. break;
  529. }
  530. catch (Exception ex)
  531. {
  532. WriteLog($"同步过程中发生错误: {ex.Message}");
  533. Error($"同步错误: {ex.Message}");
  534. // 发生错误后,等待1小时再重试
  535. await Task.Delay(TimeSpan.FromHours(1), _cancellationTokenSource.Token);
  536. }
  537. }
  538. WriteLog("定期同步模式已停止");
  539. Console.WriteLine("\n定期同步模式已停止");
  540. }
  541. static void RunOnce()
  542. {
  543. try
  544. {
  545. // 确认继续
  546. Console.Write("\n是否继续? (y/n): ");
  547. var confirm = Console.ReadLine()?.ToLower();
  548. if (confirm != "y")
  549. {
  550. Console.WriteLine("操作已取消");
  551. return;
  552. }
  553. RunSync();
  554. }
  555. catch (Exception ex)
  556. {
  557. Error($"\n❌ 错误: {ex.Message}");
  558. Error($"详细信息: {ex.StackTrace}");
  559. }
  560. Console.WriteLine("\n按任意键退出...");
  561. Console.ReadKey();
  562. }
  563. static void RunSync()
  564. {
  565. var startTime = DateTime.Now;
  566. WriteLog($"开始完整同步,时间: {startTime:yyyy-MM-dd HH:mm:ss}");
  567. // 步骤1: 删除旧的目标数据库
  568. if (File.Exists(TARGET_DB))
  569. {
  570. Info("删除旧的目标数据库...");
  571. File.Delete(TARGET_DB);
  572. }
  573. // 步骤2: 复制数据库结构
  574. Info("复制数据库结构...");
  575. if (_isPostgreSQLSource)
  576. {
  577. CopyDatabaseStructureFromPostgreSQL();
  578. }
  579. else
  580. {
  581. CopyDatabaseStructure();
  582. }
  583. // 步骤3: 复制结构表数据
  584. Info("复制结构表数据...");
  585. foreach (var table in STRUCTURE_TABLES)
  586. {
  587. if (_isPostgreSQLSource)
  588. {
  589. CopyStructureTableFromPostgreSQL(table);
  590. }
  591. else
  592. {
  593. CopyStructureTable(table);
  594. }
  595. }
  596. // 步骤4: 复制数据表(按时间过滤)
  597. Info($"复制数据表(保留最近 {KEEP_DAYS} 天)...");
  598. var cutoffDate = DateTime.Now.AddDays(-KEEP_DAYS);
  599. foreach (var (table, dateColumn) in DATA_TABLES)
  600. {
  601. if (_isPostgreSQLSource)
  602. {
  603. CopyDataTableFromPostgreSQL(table, dateColumn, cutoffDate);
  604. }
  605. else
  606. {
  607. CopyDataTable(table, dateColumn, cutoffDate);
  608. }
  609. }
  610. // 步骤5: 优化数据库
  611. Info("优化目标数据库...");
  612. OptimizeDatabase();
  613. // 步骤6: 生成报告
  614. var endTime = DateTime.Now;
  615. GenerateReport(startTime, endTime);
  616. Success($"\n✅ 完成! 耗时: {(endTime - startTime).TotalSeconds:F2} 秒");
  617. Success($"新数据库位置: {TARGET_DB}");
  618. Success($"新数据库大小: {GetFileSize(TARGET_DB)}");
  619. WriteLog($"完整同步完成,耗时: {(endTime - startTime).TotalSeconds:F2} 秒");
  620. }
  621. static void RunIncrementalSync()
  622. {
  623. var startTime = DateTime.Now;
  624. WriteLog($"开始增量同步,时间: {startTime:yyyy-MM-dd HH:mm:ss}");
  625. // 检查目标数据库是否存在
  626. if (!File.Exists(TARGET_DB))
  627. {
  628. Info("目标数据库不存在,执行完整同步...");
  629. RunSync();
  630. return;
  631. }
  632. // 检查表结构是否一致(根据配置决定是否检查)
  633. var structureChanged = CheckTableStructure(out var newTables, out var missingTables);
  634. if (structureChanged)
  635. {
  636. if (_autoFullSync)
  637. {
  638. // 配置为自动全量同步
  639. Warn("检测到表结构变化,执行完整同步...");
  640. WriteLog("表结构不一致,切换到完整同步模式");
  641. RunSync();
  642. return;
  643. }
  644. else if (_syncNewTablesOnly && (newTables.Any() || missingTables.Any()))
  645. {
  646. // 配置为只同步新表
  647. Warn($"检测到表结构变化:新增表 {string.Join(", ", newTables)},缺失表 {string.Join(", ", missingTables)}");
  648. WriteLog($"表结构变化:新增 {newTables.Count} 个表,缺失 {missingTables.Count} 个表");
  649. // 增量处理新表
  650. SyncNewTablesOnly(newTables, missingTables);
  651. // 继续增量同步
  652. }
  653. else
  654. {
  655. // 其他情况,继续增量同步(可能只是列变化,不影响数据同步)
  656. Warn("检测到表结构变化,但配置为继续增量同步...");
  657. }
  658. }
  659. Info("增量同步模式:只同步新增数据...");
  660. using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly");
  661. using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}");
  662. sourceConn.Open();
  663. targetConn.Open();
  664. var totalNewRows = 0;
  665. var cutoffDate = DateTime.Now.AddDays(-KEEP_DAYS);
  666. // 1. 同步结构表(全量更新,因为配置可能变化)
  667. Info("更新结构表数据...");
  668. foreach (var table in STRUCTURE_TABLES)
  669. {
  670. var updated = SyncStructureTableIncremental(sourceConn, targetConn, table);
  671. if (updated > 0)
  672. {
  673. totalNewRows += updated;
  674. }
  675. }
  676. // 2. 增量同步数据表(只同步新增数据)
  677. Info($"增量同步数据表(保留最近 {KEEP_DAYS} 天)...");
  678. foreach (var (table, dateColumn) in DATA_TABLES)
  679. {
  680. var newRows = SyncDataTableIncremental(sourceConn, targetConn, table, dateColumn, cutoffDate);
  681. totalNewRows += newRows;
  682. }
  683. // 3. 清理过期数据(删除超过保留天数的数据)
  684. Info("清理过期数据...");
  685. var deletedRows = CleanupExpiredData(targetConn, cutoffDate);
  686. // 4. 优化数据库
  687. Info("优化目标数据库...");
  688. OptimizeDatabase();
  689. var endTime = DateTime.Now;
  690. var duration = (endTime - startTime).TotalSeconds;
  691. Success($"\n✅ 增量同步完成! 耗时: {duration:F2} 秒");
  692. Success($"新增数据: {totalNewRows:N0} 行");
  693. Success($"删除过期数据: {deletedRows:N0} 行");
  694. Success($"数据库大小: {GetFileSize(TARGET_DB)}");
  695. WriteLog($"增量同步完成,耗时: {duration:F2} 秒,新增: {totalNewRows} 行,删除: {deletedRows} 行");
  696. }
  697. static void LoadConfiguration()
  698. {
  699. var configFile = "appsettings.json";
  700. if (File.Exists(configFile))
  701. {
  702. try
  703. {
  704. var json = File.ReadAllText(configFile);
  705. _config = JsonSerializer.Deserialize<AppConfig>(json, new JsonSerializerOptions
  706. {
  707. PropertyNameCaseInsensitive = true,
  708. ReadCommentHandling = JsonCommentHandling.Skip
  709. }) ?? new AppConfig();
  710. Info($"✅ 已加载配置文件: {configFile}");
  711. }
  712. catch (Exception ex)
  713. {
  714. Warn($"⚠️ 加载配置文件失败: {ex.Message},使用默认配置");
  715. _config = new AppConfig();
  716. }
  717. }
  718. else
  719. {
  720. Warn($"⚠️ 配置文件不存在: {configFile},使用默认配置");
  721. // 创建默认配置文件
  722. SaveDefaultConfiguration();
  723. }
  724. }
  725. static void SaveDefaultConfiguration()
  726. {
  727. try
  728. {
  729. // 初始化默认配置
  730. var defaultConfig = new AppConfig
  731. {
  732. SourceDatabase = @"D:\NewProductionLineMonitorDB.db",
  733. TargetDatabase = @"D:\NewProductionLineMonitorDB_Dev.db",
  734. KeepDays = 14,
  735. SyncIntervalHours = 24,
  736. IncrementalMode = true,
  737. StructureTables = new List<string>
  738. {
  739. "__EFMigrationsHistory",
  740. "ProductionLines",
  741. "Machines",
  742. "Roles",
  743. "Users",
  744. "Menus",
  745. "RoleMenus",
  746. "Recipes",
  747. "Cims",
  748. "ElectricEnergyMeters"
  749. },
  750. DataTables = new Dictionary<string, string>
  751. {
  752. { "MachineOutPutPerHours", "DataTime" },
  753. { "MachineFaultRecords", "StartTime" },
  754. { "MachineStatistics", "Date" },
  755. { "ElectricEnergyMeterLogs", "CreateTime" },
  756. { "MachinePowerConsumptions", "CreateTime" },
  757. { "MachineProcessBTs", "CreateTime" },
  758. { "MachineProcessFOGs", "CreateTime" },
  759. { "MachineProcessPSs", "CreateTime" },
  760. { "MachineProcessPSEPDs", "CreateTime" },
  761. { "MachineFaultComparisons", "CreateTime" }
  762. },
  763. TableStructureChange = new TableStructureChangeConfig
  764. {
  765. AutoFullSync = false,
  766. SyncNewTablesOnly = true,
  767. CheckIntervalHours = 24
  768. }
  769. };
  770. var options = new JsonSerializerOptions
  771. {
  772. WriteIndented = true,
  773. Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
  774. };
  775. var json = JsonSerializer.Serialize(defaultConfig, options);
  776. File.WriteAllText("appsettings.json", json);
  777. _config = defaultConfig;
  778. Info("✅ 已创建默认配置文件: appsettings.json");
  779. }
  780. catch (Exception ex)
  781. {
  782. Warn($"⚠️ 创建默认配置文件失败: {ex.Message}");
  783. }
  784. }
  785. static bool CheckTableStructure(out List<string> newTables, out List<string> missingTables)
  786. {
  787. newTables = new List<string>();
  788. missingTables = new List<string>();
  789. // 定期检查表结构(根据配置的检查间隔)
  790. var now = DateTime.Now;
  791. if ((now - _lastStructureCheck).TotalHours < _structureCheckIntervalHours)
  792. {
  793. // 还没到检查时间,返回false(假设没有变化)
  794. return false;
  795. }
  796. _lastStructureCheck = now;
  797. try
  798. {
  799. using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly");
  800. using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}");
  801. sourceConn.Open();
  802. targetConn.Open();
  803. // 获取源数据库的所有表
  804. var sourceTables = sourceConn.Query<string>(
  805. "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
  806. .ToHashSet();
  807. // 获取目标数据库的所有表
  808. var targetTables = targetConn.Query<string>(
  809. "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
  810. .ToHashSet();
  811. // 检查表是否一致
  812. if (!sourceTables.SetEquals(targetTables))
  813. {
  814. newTables = sourceTables.Except(targetTables).ToList();
  815. missingTables = targetTables.Except(sourceTables).ToList();
  816. if (newTables.Any())
  817. {
  818. Warn($"源数据库新增表: {string.Join(", ", newTables)}");
  819. }
  820. if (missingTables.Any())
  821. {
  822. Warn($"目标数据库缺失表: {string.Join(", ", missingTables)}");
  823. }
  824. return true;
  825. }
  826. // 检查每个表的列结构
  827. foreach (var table in sourceTables)
  828. {
  829. var sourceColumns = sourceConn.Query<dynamic>(
  830. $"PRAGMA table_info([{table}])").Select(r => (string)r.name).OrderBy(n => n).ToList();
  831. var targetColumns = targetConn.Query<dynamic>(
  832. $"PRAGMA table_info([{table}])").Select(r => (string)r.name).OrderBy(n => n).ToList();
  833. if (sourceColumns.Count != targetColumns.Count)
  834. {
  835. Warn($"表 {table} 的列数不一致(源: {sourceColumns.Count}, 目标: {targetColumns.Count})");
  836. return true; // 列结构变化,返回true
  837. }
  838. // 检查列名是否一致
  839. if (!sourceColumns.SequenceEqual(targetColumns))
  840. {
  841. var missing = sourceColumns.Except(targetColumns).ToList();
  842. var extra = targetColumns.Except(sourceColumns).ToList();
  843. if (missing.Any())
  844. Warn($"表 {table} 缺少列: {string.Join(", ", missing)}");
  845. if (extra.Any())
  846. Warn($"表 {table} 多余列: {string.Join(", ", extra)}");
  847. return true; // 列结构变化,返回true
  848. }
  849. }
  850. return false; // 表结构一致
  851. }
  852. catch (Exception ex)
  853. {
  854. Warn($"检查表结构时出错: {ex.Message}");
  855. return false; // 出错时返回false,不中断同步
  856. }
  857. }
  858. static int SyncStructureTableIncremental(SqliteConnection sourceConn, SqliteConnection targetConn, string tableName)
  859. {
  860. try
  861. {
  862. // 检查表是否存在
  863. var tableExists = sourceConn.ExecuteScalar<int>(
  864. "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name",
  865. new { name = tableName });
  866. if (tableExists == 0)
  867. {
  868. return 0;
  869. }
  870. // 获取源表的所有数据
  871. var sourceData = sourceConn.Query($"SELECT * FROM [{tableName}]").ToList();
  872. // 清空目标表
  873. targetConn.Execute($"DELETE FROM [{tableName}]");
  874. // 重新插入所有数据
  875. if (sourceData.Count > 0)
  876. {
  877. using var transaction = targetConn.BeginTransaction();
  878. foreach (var row in sourceData)
  879. {
  880. var dict = (IDictionary<string, object>)row;
  881. var columns = string.Join(", ", dict.Keys.Select(k => $"[{k}]"));
  882. var parameters = string.Join(", ", dict.Keys.Select(k => $"@{k}"));
  883. var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})";
  884. targetConn.Execute(sql, dict, transaction);
  885. }
  886. transaction.Commit();
  887. }
  888. Info($" ✅ {tableName,-30} (更新 {sourceData.Count:N0} 行)");
  889. return sourceData.Count;
  890. }
  891. catch (Exception ex)
  892. {
  893. Warn($" ⚠️ 同步结构表 {tableName} 时出错: {ex.Message}");
  894. return 0;
  895. }
  896. }
  897. static int SyncDataTableIncremental(SqliteConnection sourceConn, SqliteConnection targetConn,
  898. string tableName, string dateColumn, DateTime cutoffDate)
  899. {
  900. try
  901. {
  902. // 检查表是否存在
  903. var tableExists = sourceConn.ExecuteScalar<int>(
  904. "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name",
  905. new { name = tableName });
  906. if (tableExists == 0)
  907. {
  908. return 0;
  909. }
  910. // 获取目标表中最大的时间戳(用于增量同步)
  911. DateTime? maxDate = null;
  912. try
  913. {
  914. var maxDateStr = targetConn.ExecuteScalar<string>(
  915. $"SELECT MAX([{dateColumn}]) FROM [{tableName}]");
  916. if (!string.IsNullOrEmpty(maxDateStr) && DateTime.TryParse(maxDateStr, out var parsedDate))
  917. {
  918. maxDate = parsedDate;
  919. }
  920. }
  921. catch
  922. {
  923. // 如果表为空或不存在,maxDate 保持为 null
  924. }
  925. // 查询需要同步的新数据(大于最大时间戳,且在保留天数内)
  926. object parameters;
  927. string query;
  928. if (maxDate.HasValue)
  929. {
  930. query = $@"SELECT * FROM [{tableName}]
  931. WHERE [{dateColumn}] >= @cutoffDate AND [{dateColumn}] > @maxDate";
  932. parameters = new
  933. {
  934. cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss"),
  935. maxDate = maxDate.Value.ToString("yyyy-MM-dd HH:mm:ss")
  936. };
  937. }
  938. else
  939. {
  940. query = $@"SELECT * FROM [{tableName}]
  941. WHERE [{dateColumn}] >= @cutoffDate";
  942. parameters = new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss") };
  943. }
  944. var newRows = sourceConn.Query(query, parameters).ToList();
  945. if (newRows.Count == 0)
  946. {
  947. Info($" 📋 {tableName,-30} (无新数据)");
  948. return 0;
  949. }
  950. // 批量插入新数据
  951. using var transaction = targetConn.BeginTransaction();
  952. var inserted = 0;
  953. foreach (var row in newRows)
  954. {
  955. var dict = (IDictionary<string, object>)row;
  956. var columns = string.Join(", ", dict.Keys.Select(k => $"[{k}]"));
  957. var paramsList = string.Join(", ", dict.Keys.Select(k => $"@{k}"));
  958. var sql = $"INSERT OR IGNORE INTO [{tableName}] ({columns}) VALUES ({paramsList})";
  959. targetConn.Execute(sql, dict, transaction);
  960. inserted++;
  961. }
  962. transaction.Commit();
  963. Info($" ✅ {tableName,-30} (新增 {inserted:N0} 行)");
  964. return inserted;
  965. }
  966. catch (Exception ex)
  967. {
  968. Warn($" ⚠️ 增量同步表 {tableName} 时出错: {ex.Message}");
  969. return 0;
  970. }
  971. }
  972. static void SyncNewTablesOnly(List<string> newTables, List<string> missingTables)
  973. {
  974. using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly");
  975. using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}");
  976. sourceConn.Open();
  977. targetConn.Open();
  978. Info("增量处理新表...");
  979. // 1. 处理源数据库新增的表
  980. foreach (var tableName in newTables)
  981. {
  982. try
  983. {
  984. // 复制表结构
  985. var createSql = sourceConn.Query<string>(
  986. "SELECT sql FROM sqlite_master WHERE type='table' AND name=@name",
  987. new { name = tableName }).FirstOrDefault();
  988. if (!string.IsNullOrEmpty(createSql))
  989. {
  990. targetConn.Execute(createSql);
  991. Info($" ✅ 已创建新表结构: {tableName}");
  992. }
  993. // 判断是结构表还是数据表
  994. bool isStructureTable = STRUCTURE_TABLES.Contains(tableName);
  995. bool isDataTable = DATA_TABLES.ContainsKey(tableName);
  996. if (isStructureTable)
  997. {
  998. // 结构表:全量复制
  999. var count = SyncStructureTableIncremental(sourceConn, targetConn, tableName);
  1000. Info($" ✅ 已同步结构表数据: {tableName} ({count:N0} 行)");
  1001. }
  1002. else if (isDataTable)
  1003. {
  1004. // 数据表:按时间过滤复制
  1005. var dateColumn = DATA_TABLES[tableName];
  1006. var cutoffDate = DateTime.Now.AddDays(-KEEP_DAYS);
  1007. var count = CopyDataTableForNewTable(sourceConn, targetConn, tableName, dateColumn, cutoffDate);
  1008. Info($" ✅ 已同步数据表数据: {tableName} ({count:N0} 行,保留最近 {KEEP_DAYS} 天)");
  1009. }
  1010. else
  1011. {
  1012. // 未配置的表:只复制结构,不复制数据
  1013. Warn($" ⚠️ 表 {tableName} 未在配置中,只复制结构,不复制数据");
  1014. Info($" 💡 提示: 如需复制数据,请在 appsettings.json 中添加配置");
  1015. }
  1016. }
  1017. catch (Exception ex)
  1018. {
  1019. Warn($" ⚠️ 处理新表 {tableName} 时出错: {ex.Message}");
  1020. }
  1021. }
  1022. // 2. 处理目标数据库多余的表(源数据库已删除)
  1023. foreach (var tableName in missingTables)
  1024. {
  1025. try
  1026. {
  1027. // 可以选择删除或保留
  1028. // 这里选择保留,因为可能是历史数据
  1029. Warn($" ⚠️ 目标数据库存在源数据库已删除的表: {tableName}(保留)");
  1030. }
  1031. catch (Exception ex)
  1032. {
  1033. Warn($" ⚠️ 处理缺失表 {tableName} 时出错: {ex.Message}");
  1034. }
  1035. }
  1036. }
  1037. static int CopyDataTableForNewTable(SqliteConnection sourceConn, SqliteConnection targetConn,
  1038. string tableName, string dateColumn, DateTime cutoffDate)
  1039. {
  1040. try
  1041. {
  1042. // 获取要复制的行数
  1043. var keepCount = sourceConn.ExecuteScalar<int>(
  1044. $"SELECT COUNT(*) FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate",
  1045. new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss") });
  1046. if (keepCount == 0)
  1047. {
  1048. return 0;
  1049. }
  1050. // 流式处理:分批查询和插入
  1051. using var transaction = targetConn.BeginTransaction();
  1052. var batchSize = 1000;
  1053. var insertedCount = 0;
  1054. var offset = 0;
  1055. while (insertedCount < keepCount)
  1056. {
  1057. var currentBatch = sourceConn.Query(
  1058. $"SELECT * FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate LIMIT @limit OFFSET @offset",
  1059. new
  1060. {
  1061. cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss"),
  1062. limit = batchSize,
  1063. offset = offset
  1064. }).ToList();
  1065. if (currentBatch.Count == 0)
  1066. break;
  1067. // 批量插入
  1068. InsertBatch(targetConn, tableName, currentBatch.Cast<object>().ToList(), transaction);
  1069. insertedCount += currentBatch.Count;
  1070. offset += batchSize;
  1071. }
  1072. transaction.Commit();
  1073. return insertedCount;
  1074. }
  1075. catch (Exception ex)
  1076. {
  1077. Warn($" ⚠️ 复制新数据表 {tableName} 时出错: {ex.Message}");
  1078. return 0;
  1079. }
  1080. }
  1081. static int CleanupExpiredData(SqliteConnection targetConn, DateTime cutoffDate)
  1082. {
  1083. var totalDeleted = 0;
  1084. foreach (var (table, dateColumn) in DATA_TABLES)
  1085. {
  1086. try
  1087. {
  1088. var deleted = targetConn.Execute(
  1089. $"DELETE FROM [{table}] WHERE [{dateColumn}] < @cutoffDate",
  1090. new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss") });
  1091. totalDeleted += deleted;
  1092. }
  1093. catch (Exception ex)
  1094. {
  1095. Warn($" ⚠️ 清理表 {table} 过期数据时出错: {ex.Message}");
  1096. }
  1097. }
  1098. return totalDeleted;
  1099. }
  1100. static void WriteLog(string message)
  1101. {
  1102. try
  1103. {
  1104. var logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}";
  1105. File.AppendAllText(_logFile, logMessage + Environment.NewLine);
  1106. }
  1107. catch
  1108. {
  1109. // 忽略日志写入错误
  1110. }
  1111. }
  1112. static void CopyDatabaseStructure()
  1113. {
  1114. // 使用只读模式打开源数据库,避免创建WAL文件
  1115. using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly");
  1116. sourceConn.Open();
  1117. using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}");
  1118. targetConn.Open();
  1119. // 获取所有表的创建语句
  1120. var tables = sourceConn.Query<string>(
  1121. "SELECT sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
  1122. foreach (var createSql in tables)
  1123. {
  1124. if (!string.IsNullOrEmpty(createSql))
  1125. {
  1126. targetConn.Execute(createSql);
  1127. }
  1128. }
  1129. // 复制索引
  1130. var indexes = sourceConn.Query<string>(
  1131. "SELECT sql FROM sqlite_master WHERE type='index' AND sql IS NOT NULL");
  1132. foreach (var indexSql in indexes)
  1133. {
  1134. try
  1135. {
  1136. targetConn.Execute(indexSql);
  1137. }
  1138. catch
  1139. {
  1140. // 忽略已存在的索引
  1141. }
  1142. }
  1143. }
  1144. static void CopyDatabaseStructureFromPostgreSQL()
  1145. {
  1146. if (string.IsNullOrEmpty(_pgConnectionString))
  1147. {
  1148. throw new Exception("PostgreSQL 连接字符串未配置");
  1149. }
  1150. using var pgConn = new NpgsqlConnection(_pgConnectionString);
  1151. pgConn.Open();
  1152. using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}");
  1153. targetConn.Open();
  1154. // 获取所有表名
  1155. var tables = pgConn.Query<string>(
  1156. @"SELECT table_name
  1157. FROM information_schema.tables
  1158. WHERE table_schema = 'public'
  1159. AND table_type = 'BASE TABLE'
  1160. ORDER BY table_name");
  1161. foreach (var tableName in tables)
  1162. {
  1163. try
  1164. {
  1165. // 获取表结构
  1166. var columns = pgConn.Query<dynamic>(
  1167. @"SELECT
  1168. column_name,
  1169. data_type,
  1170. character_maximum_length,
  1171. is_nullable,
  1172. column_default
  1173. FROM information_schema.columns
  1174. WHERE table_schema = 'public'
  1175. AND table_name = @tableName
  1176. ORDER BY ordinal_position",
  1177. new { tableName });
  1178. if (!columns.Any())
  1179. continue;
  1180. // 构建 CREATE TABLE 语句
  1181. var columnDefs = new List<string>();
  1182. foreach (var col in columns)
  1183. {
  1184. var colName = (string)col.column_name;
  1185. var dataType = MapPostgreSQLTypeToSQLite((string)col.data_type, col.character_maximum_length);
  1186. var nullable = (string)col.is_nullable == "YES" ? "" : " NOT NULL";
  1187. var defaultValue = col.column_default != null ? $" DEFAULT {col.column_default}" : "";
  1188. columnDefs.Add($"[{colName}] {dataType}{nullable}{defaultValue}");
  1189. }
  1190. var createTableSql = $"CREATE TABLE IF NOT EXISTS [{tableName}] ({string.Join(", ", columnDefs)})";
  1191. targetConn.Execute(createTableSql);
  1192. Info($" ✅ 已创建表: {tableName}");
  1193. }
  1194. catch (Exception ex)
  1195. {
  1196. Warn($" ⚠️ 创建表 {tableName} 失败: {ex.Message}");
  1197. }
  1198. }
  1199. }
  1200. static string MapPostgreSQLTypeToSQLite(string pgType, object? maxLength)
  1201. {
  1202. // PostgreSQL 类型到 SQLite 类型映射
  1203. return pgType.ToLower() switch
  1204. {
  1205. "integer" or "int" or "int4" => "INTEGER",
  1206. "bigint" or "int8" => "INTEGER",
  1207. "smallint" or "int2" => "INTEGER",
  1208. "real" or "float4" => "REAL",
  1209. "double precision" or "float8" => "REAL",
  1210. "numeric" or "decimal" => "REAL",
  1211. "boolean" or "bool" => "INTEGER", // SQLite 用 0/1 表示布尔值
  1212. "text" => "TEXT",
  1213. "character varying" or "varchar" => maxLength != null ? $"TEXT" : "TEXT",
  1214. "character" or "char" => maxLength != null ? $"TEXT" : "TEXT",
  1215. "timestamp" or "timestamp without time zone" => "TEXT",
  1216. "timestamp with time zone" => "TEXT",
  1217. "date" => "TEXT",
  1218. "time" => "TEXT",
  1219. "uuid" => "TEXT",
  1220. "json" or "jsonb" => "TEXT",
  1221. "bytea" => "BLOB",
  1222. _ => "TEXT" // 默认映射为 TEXT
  1223. };
  1224. }
  1225. static void CopyStructureTable(string tableName)
  1226. {
  1227. using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly");
  1228. using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}");
  1229. sourceConn.Open();
  1230. targetConn.Open();
  1231. // 检查表是否存在
  1232. var tableExists = sourceConn.ExecuteScalar<int>(
  1233. "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name",
  1234. new { name = tableName });
  1235. if (tableExists == 0)
  1236. {
  1237. Warn($" ⚠️ 表不存在: {tableName},跳过");
  1238. return;
  1239. }
  1240. // 获取行数
  1241. var count = sourceConn.ExecuteScalar<int>($"SELECT COUNT(*) FROM [{tableName}]");
  1242. if (count == 0)
  1243. {
  1244. Info($" 📋 {tableName,-30} (0 行)");
  1245. return;
  1246. }
  1247. // 复制数据
  1248. var data = sourceConn.Query($"SELECT * FROM [{tableName}]");
  1249. using var transaction = targetConn.BeginTransaction();
  1250. foreach (var row in data)
  1251. {
  1252. var dict = (IDictionary<string, object>)row;
  1253. var columns = string.Join(", ", dict.Keys.Select(k => $"[{k}]"));
  1254. var parameters = string.Join(", ", dict.Keys.Select(k => $"@{k}"));
  1255. var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})";
  1256. targetConn.Execute(sql, dict, transaction);
  1257. }
  1258. transaction.Commit();
  1259. Success($" ✅ {tableName,-30} ({count:N0} 行)");
  1260. }
  1261. static void CopyStructureTableFromPostgreSQL(string tableName)
  1262. {
  1263. if (string.IsNullOrEmpty(_pgConnectionString))
  1264. {
  1265. Warn($" ⚠️ PostgreSQL 连接字符串未配置,跳过表: {tableName}");
  1266. return;
  1267. }
  1268. using var pgConn = new NpgsqlConnection(_pgConnectionString);
  1269. using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}");
  1270. pgConn.Open();
  1271. targetConn.Open();
  1272. // 检查表是否存在
  1273. var tableExists = pgConn.ExecuteScalar<int>(
  1274. @"SELECT COUNT(*)
  1275. FROM information_schema.tables
  1276. WHERE table_schema = 'public'
  1277. AND table_name = @tableName",
  1278. new { tableName });
  1279. if (tableExists == 0)
  1280. {
  1281. Warn($" ⚠️ 表不存在: {tableName},跳过");
  1282. return;
  1283. }
  1284. // 获取行数
  1285. var count = pgConn.ExecuteScalar<long>($"SELECT COUNT(*) FROM \"{tableName}\"");
  1286. if (count == 0)
  1287. {
  1288. Info($" 📋 {tableName,-30} (0 行)");
  1289. return;
  1290. }
  1291. // 分批复制数据
  1292. using var transaction = targetConn.BeginTransaction();
  1293. var batchSize = 1000;
  1294. var offset = 0;
  1295. var totalInserted = 0;
  1296. while (true)
  1297. {
  1298. var data = pgConn.Query($"SELECT * FROM \"{tableName}\" LIMIT {batchSize} OFFSET {offset}").ToList();
  1299. if (data.Count == 0)
  1300. break;
  1301. foreach (var row in data)
  1302. {
  1303. var dict = (IDictionary<string, object>)row;
  1304. // 处理 PostgreSQL 特殊类型
  1305. var processedDict = ProcessPostgreSQLData(dict);
  1306. var columns = string.Join(", ", processedDict.Keys.Select(k => $"[{k}]"));
  1307. var parameters = string.Join(", ", processedDict.Keys.Select(k => $"@{k}"));
  1308. var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})";
  1309. targetConn.Execute(sql, processedDict, transaction);
  1310. totalInserted++;
  1311. }
  1312. offset += batchSize;
  1313. if (totalInserted % 1000 == 0)
  1314. {
  1315. Console.Write($"\r ⏳ {tableName,-30} ({totalInserted:N0}/{count:N0})");
  1316. }
  1317. }
  1318. transaction.Commit();
  1319. Console.Write("\r");
  1320. Success($" ✅ {tableName,-30} ({totalInserted:N0} 行)");
  1321. }
  1322. static Dictionary<string, object> ProcessPostgreSQLData(IDictionary<string, object> dict)
  1323. {
  1324. var result = new Dictionary<string, object>();
  1325. foreach (var kvp in dict)
  1326. {
  1327. var value = kvp.Value;
  1328. // 处理 PostgreSQL 特殊类型
  1329. if (value == null || value == DBNull.Value)
  1330. {
  1331. result[kvp.Key] = null;
  1332. }
  1333. else if (value is bool boolValue)
  1334. {
  1335. // SQLite 用 0/1 表示布尔值
  1336. result[kvp.Key] = boolValue ? 1 : 0;
  1337. }
  1338. else if (value is DateTime dateTimeValue)
  1339. {
  1340. // 转换为字符串
  1341. result[kvp.Key] = dateTimeValue.ToString("yyyy-MM-dd HH:mm:ss");
  1342. }
  1343. else if (value is DateTimeOffset dateTimeOffsetValue)
  1344. {
  1345. result[kvp.Key] = dateTimeOffsetValue.ToString("yyyy-MM-dd HH:mm:ss");
  1346. }
  1347. else if (value is Guid guidValue)
  1348. {
  1349. result[kvp.Key] = guidValue.ToString();
  1350. }
  1351. else
  1352. {
  1353. result[kvp.Key] = value;
  1354. }
  1355. }
  1356. return result;
  1357. }
  1358. static void CopyDataTable(string tableName, string dateColumn, DateTime cutoffDate)
  1359. {
  1360. using var sourceConn = new SqliteConnection($"Data Source={SOURCE_DB};Mode=ReadOnly");
  1361. using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}");
  1362. sourceConn.Open();
  1363. targetConn.Open();
  1364. // 检查表是否存在
  1365. var tableExists = sourceConn.ExecuteScalar<int>(
  1366. "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=@name",
  1367. new { name = tableName });
  1368. if (tableExists == 0)
  1369. {
  1370. Warn($" ⚠️ 表不存在: {tableName},跳过");
  1371. return;
  1372. }
  1373. // 获取总行数和要复制的行数
  1374. var totalCount = sourceConn.ExecuteScalar<int>($"SELECT COUNT(*) FROM [{tableName}]");
  1375. var keepCount = sourceConn.ExecuteScalar<int>(
  1376. $"SELECT COUNT(*) FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate",
  1377. new { cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss") });
  1378. if (keepCount == 0)
  1379. {
  1380. Info($" 📋 {tableName,-30} (0 行,共 {totalCount:N0} 行)");
  1381. return;
  1382. }
  1383. // 流式处理:分批查询和插入,避免一次性加载所有数据到内存
  1384. using var transaction = targetConn.BeginTransaction();
  1385. var batchSize = 1000;
  1386. var insertedCount = 0;
  1387. var offset = 0;
  1388. var lastUpdateTime = DateTime.Now;
  1389. // 使用LIMIT和OFFSET分批查询
  1390. while (insertedCount < keepCount)
  1391. {
  1392. var currentBatch = sourceConn.Query(
  1393. $"SELECT * FROM [{tableName}] WHERE [{dateColumn}] >= @cutoffDate LIMIT @limit OFFSET @offset",
  1394. new
  1395. {
  1396. cutoffDate = cutoffDate.ToString("yyyy-MM-dd HH:mm:ss"),
  1397. limit = batchSize,
  1398. offset = offset
  1399. }).ToList();
  1400. if (currentBatch.Count == 0)
  1401. break;
  1402. // 批量插入
  1403. InsertBatch(targetConn, tableName, currentBatch.Cast<object>().ToList(), transaction);
  1404. insertedCount += currentBatch.Count;
  1405. offset += batchSize;
  1406. // 每1000行或每2秒更新一次进度显示
  1407. var now = DateTime.Now;
  1408. if (insertedCount % 1000 == 0 || (now - lastUpdateTime).TotalSeconds >= 2)
  1409. {
  1410. var percent = keepCount > 0 ? (insertedCount * 100.0 / keepCount) : 0;
  1411. Console.Write($"\r ⏳ {tableName,-30} ({insertedCount:N0}/{keepCount:N0} - {percent:F1}%)");
  1412. lastUpdateTime = now;
  1413. }
  1414. }
  1415. transaction.Commit();
  1416. var deletedCount = totalCount - keepCount;
  1417. var deletedPercent = totalCount > 0 ? (deletedCount * 100.0 / totalCount) : 0;
  1418. Console.Write("\r");
  1419. Success($" ✅ {tableName,-30} (保留 {keepCount:N0} 行,删除 {deletedCount:N0} 行 [{deletedPercent:F1}%])");
  1420. }
  1421. static void CopyDataTableFromPostgreSQL(string tableName, string dateColumn, DateTime cutoffDate)
  1422. {
  1423. if (string.IsNullOrEmpty(_pgConnectionString))
  1424. {
  1425. Warn($" ⚠️ PostgreSQL 连接字符串未配置,跳过表: {tableName}");
  1426. return;
  1427. }
  1428. using var pgConn = new NpgsqlConnection(_pgConnectionString);
  1429. using var targetConn = new SqliteConnection($"Data Source={TARGET_DB}");
  1430. pgConn.Open();
  1431. targetConn.Open();
  1432. // 检查表是否存在
  1433. var tableExists = pgConn.ExecuteScalar<int>(
  1434. @"SELECT COUNT(*)
  1435. FROM information_schema.tables
  1436. WHERE table_schema = 'public'
  1437. AND table_name = @tableName",
  1438. new { tableName });
  1439. if (tableExists == 0)
  1440. {
  1441. Warn($" ⚠️ 表不存在: {tableName},跳过");
  1442. return;
  1443. }
  1444. // 获取总行数和要复制的行数
  1445. var totalCount = pgConn.ExecuteScalar<long>($"SELECT COUNT(*) FROM \"{tableName}\"");
  1446. var keepCount = pgConn.ExecuteScalar<long>(
  1447. $"SELECT COUNT(*) FROM \"{tableName}\" WHERE \"{dateColumn}\" >= @cutoffDate",
  1448. new { cutoffDate = cutoffDate });
  1449. if (keepCount == 0)
  1450. {
  1451. Info($" 📋 {tableName,-30} (0 行,共 {totalCount:N0} 行)");
  1452. return;
  1453. }
  1454. // 流式处理:分批查询和插入
  1455. using var transaction = targetConn.BeginTransaction();
  1456. var batchSize = 1000;
  1457. var insertedCount = 0L;
  1458. var offset = 0;
  1459. var lastUpdateTime = DateTime.Now;
  1460. // 使用LIMIT和OFFSET分批查询
  1461. while (insertedCount < keepCount)
  1462. {
  1463. var currentBatch = pgConn.Query(
  1464. $"SELECT * FROM \"{tableName}\" WHERE \"{dateColumn}\" >= @cutoffDate ORDER BY \"{dateColumn}\" LIMIT {batchSize} OFFSET {offset}",
  1465. new { cutoffDate = cutoffDate }).ToList();
  1466. if (currentBatch.Count == 0)
  1467. break;
  1468. // 批量插入
  1469. foreach (var row in currentBatch)
  1470. {
  1471. var dict = (IDictionary<string, object>)row;
  1472. var processedDict = ProcessPostgreSQLData(dict);
  1473. var columns = string.Join(", ", processedDict.Keys.Select(k => $"[{k}]"));
  1474. var parameters = string.Join(", ", processedDict.Keys.Select(k => $"@{k}"));
  1475. var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})";
  1476. targetConn.Execute(sql, processedDict, transaction);
  1477. insertedCount++;
  1478. }
  1479. offset += batchSize;
  1480. // 每1000行或每2秒更新一次进度显示
  1481. var now = DateTime.Now;
  1482. if (insertedCount % 1000 == 0 || (now - lastUpdateTime).TotalSeconds >= 2)
  1483. {
  1484. var percent = keepCount > 0 ? (insertedCount * 100.0 / keepCount) : 0;
  1485. Console.Write($"\r ⏳ {tableName,-30} ({insertedCount:N0}/{keepCount:N0} - {percent:F1}%)");
  1486. lastUpdateTime = now;
  1487. }
  1488. }
  1489. transaction.Commit();
  1490. var deletedCount = totalCount - keepCount;
  1491. var deletedPercent = totalCount > 0 ? (deletedCount * 100.0 / totalCount) : 0;
  1492. Console.Write("\r");
  1493. Success($" ✅ {tableName,-30} (保留 {keepCount:N0} 行,删除 {deletedCount:N0} 行 [{deletedPercent:F1}%])");
  1494. }
  1495. static void InsertBatch(SqliteConnection conn, string tableName, List<object> batch, SqliteTransaction transaction)
  1496. {
  1497. if (batch.Count == 0) return;
  1498. var firstRow = (IDictionary<string, object>)batch[0];
  1499. var columns = string.Join(", ", firstRow.Keys.Select(k => $"[{k}]"));
  1500. var parameters = string.Join(", ", firstRow.Keys.Select(k => $"@{k}"));
  1501. var sql = $"INSERT INTO [{tableName}] ({columns}) VALUES ({parameters})";
  1502. // 使用批量插入优化性能
  1503. foreach (var row in batch)
  1504. {
  1505. conn.Execute(sql, row, transaction);
  1506. }
  1507. }
  1508. static void OptimizeDatabase()
  1509. {
  1510. using var conn = new SqliteConnection($"Data Source={TARGET_DB}");
  1511. conn.Open();
  1512. // SQLite不支持存储过程,使用CommandType.Text
  1513. conn.Execute("VACUUM", commandType: CommandType.Text);
  1514. conn.Execute("ANALYZE", commandType: CommandType.Text);
  1515. }
  1516. static void GenerateReport(DateTime startTime, DateTime endTime)
  1517. {
  1518. using var conn = new SqliteConnection($"Data Source={TARGET_DB}");
  1519. conn.Open();
  1520. Console.WriteLine("\n" + new string('=', 70));
  1521. Console.ForegroundColor = ConsoleColor.Yellow;
  1522. Console.WriteLine("【数据库统计报告】");
  1523. Console.ResetColor();
  1524. Console.WriteLine(new string('=', 70));
  1525. // 获取所有表的行数
  1526. var tables = conn.Query<string>(
  1527. "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
  1528. Console.WriteLine($"\n{"表名",-35} {"行数",15}");
  1529. Console.WriteLine(new string('-', 50));
  1530. var totalRows = 0;
  1531. foreach (var table in tables)
  1532. {
  1533. var count = conn.ExecuteScalar<int>($"SELECT COUNT(*) FROM [{table}]");
  1534. totalRows += count;
  1535. Console.WriteLine($"{table,-35} {count,15:N0}");
  1536. }
  1537. Console.WriteLine(new string('-', 50));
  1538. Console.WriteLine($"{"总计",-35} {totalRows,15:N0}");
  1539. Console.WriteLine($"\n耗时: {(endTime - startTime).TotalSeconds:F2} 秒");
  1540. Console.WriteLine($"源数据库大小: {GetFileSize(SOURCE_DB)}");
  1541. Console.WriteLine($"新数据库大小: {GetFileSize(TARGET_DB)}");
  1542. var sourceSize = new FileInfo(SOURCE_DB).Length;
  1543. var targetSize = new FileInfo(TARGET_DB).Length;
  1544. var reduction = sourceSize - targetSize;
  1545. var reductionPercent = (reduction * 100.0 / sourceSize);
  1546. Console.WriteLine($"减少大小: {FormatSize(reduction)} ({reductionPercent:F1}%)");
  1547. Console.WriteLine(new string('=', 70));
  1548. }
  1549. static string GetFileSize(string filePath)
  1550. {
  1551. var fileInfo = new FileInfo(filePath);
  1552. return FormatSize(fileInfo.Length);
  1553. }
  1554. static string FormatSize(long bytes)
  1555. {
  1556. string[] sizes = { "B", "KB", "MB", "GB" };
  1557. double len = bytes;
  1558. int order = 0;
  1559. while (len >= 1024 && order < sizes.Length - 1)
  1560. {
  1561. order++;
  1562. len = len / 1024;
  1563. }
  1564. return $"{len:0.##} {sizes[order]}";
  1565. }
  1566. static void Info(string message)
  1567. {
  1568. Console.ForegroundColor = ConsoleColor.Cyan;
  1569. Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
  1570. Console.ResetColor();
  1571. }
  1572. static void Success(string message)
  1573. {
  1574. Console.ForegroundColor = ConsoleColor.Green;
  1575. Console.WriteLine(message);
  1576. Console.ResetColor();
  1577. }
  1578. static void Warn(string message)
  1579. {
  1580. Console.ForegroundColor = ConsoleColor.Yellow;
  1581. Console.WriteLine(message);
  1582. Console.ResetColor();
  1583. }
  1584. static void Error(string message)
  1585. {
  1586. Console.ForegroundColor = ConsoleColor.Red;
  1587. Console.WriteLine(message);
  1588. Console.ResetColor();
  1589. }
  1590. }
  1591. }