using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.IO; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Windows.Forms; namespace MesDbBackup { /// /// MES 数据库备份桌面客户端:窗口内完成连接、选目录和备份,不打开浏览器。 /// 实际导出仍调用 backup-db.ps1,不改原备份逻辑。 /// internal static class Program { internal const string MutexName = "Global\\MES_Cloud_DB_Backup_UI"; internal const string MarkerName = ".packed-by-mes-backup"; [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); bool createdNew; using (var mutex = new Mutex(true, MutexName, out createdNew)) { if (!createdNew) { MessageBox.Show( "备份客户端已经在运行,请查看任务栏。", "MES 数据库备份", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } Application.Run(new MainForm()); } } } /// /// 深空 AI 主题:暗底玻璃卡片 + 青绿/紫罗兰高光。 /// internal static class Theme { public static readonly Color Bg = Color.FromArgb(7, 8, 13); public static readonly Color Header = Color.FromArgb(9, 12, 20); public static readonly Color HeaderText = Color.FromArgb(238, 244, 255); public static readonly Color HeaderMuted = Color.FromArgb(139, 149, 171); public static readonly Color Cyan = Color.FromArgb(94, 234, 212); public static readonly Color CyanDeep = Color.FromArgb(34, 211, 238); public static readonly Color Violet = Color.FromArgb(129, 140, 248); public static readonly Color Ink = Color.FromArgb(6, 16, 24); public static readonly Color Gold = Color.FromArgb(94, 234, 212); public static readonly Color Card = Color.FromArgb(18, 22, 32); public static readonly Color Line = Color.FromArgb(48, 80, 92, 108); public static readonly Color GhostBorder = Color.FromArgb(70, 255, 255, 255); public static readonly Color Text = Color.FromArgb(238, 244, 255); public static readonly Color Muted = Color.FromArgb(139, 149, 171); public static readonly Color Accent = Color.FromArgb(34, 211, 238); public static readonly Color AccentHover = Color.FromArgb(94, 234, 212); public static readonly Color Ok = Color.FromArgb(52, 211, 153); public static readonly Color Warn = Color.FromArgb(251, 191, 36); public static readonly Color Bad = Color.FromArgb(251, 113, 133); public static readonly Color Input = Color.FromArgb(8, 10, 16); public static readonly Color InputMuted = Color.FromArgb(12, 16, 24); public static readonly Color Go = Color.FromArgb(34, 211, 238); public static readonly Color LogBg = Color.FromArgb(5, 7, 12); public static readonly Color LogText = Color.FromArgb(159, 232, 220); public static readonly Color Shadow = Color.FromArgb(90, 0, 0, 0); public static readonly Color Mesh = Color.FromArgb(18, 148, 163, 184); public static readonly Font Ui = new Font("Microsoft YaHei UI", 10F); public static readonly Font Title = new Font("Microsoft YaHei UI", 16F, FontStyle.Bold); public static readonly Font Caption = new Font("Microsoft YaHei UI", 8.5F); public static readonly Font Section = new Font("Microsoft YaHei UI", 11F, FontStyle.Bold); public static readonly Font Mono = new Font("Consolas", 9F); public static readonly Font Mark = new Font("Segoe UI", 16F, FontStyle.Bold); public static GraphicsPath RoundRect(Rectangle bounds, int radius) { var path = new GraphicsPath(); int d = Math.Max(2, radius * 2); path.AddArc(bounds.X, bounds.Y, d, d, 180, 90); path.AddArc(bounds.Right - d, bounds.Y, d, d, 270, 90); path.AddArc(bounds.Right - d, bounds.Bottom - d, d, d, 0, 90); path.AddArc(bounds.X, bounds.Bottom - d, d, d, 90, 90); path.CloseFigure(); return path; } } /// /// 暗色玻璃卡片,顶部一道青绿高光。 /// internal sealed class CardPanel : Panel { public CardPanel() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true); BackColor = Color.Transparent; Padding = new Padding(8); Margin = new Padding(0, 0, 0, 12); } protected override void OnPaint(PaintEventArgs e) { e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; var rect = new Rectangle(8, 4, Math.Max(8, Width - 18), Math.Max(8, Height - 12)); using (GraphicsPath shadow = Theme.RoundRect(new Rectangle(rect.X + 3, rect.Y + 6, rect.Width, rect.Height), 16)) using (var brush = new SolidBrush(Theme.Shadow)) { e.Graphics.FillPath(brush, shadow); } using (GraphicsPath path = Theme.RoundRect(rect, 16)) using (var fill = new SolidBrush(Theme.Card)) using (var pen = new Pen(Color.FromArgb(55, 94, 234, 212))) { e.Graphics.FillPath(fill, path); e.Graphics.DrawPath(pen, path); } using (var glow = new Pen(Color.FromArgb(140, 94, 234, 212), 1F)) { e.Graphics.DrawLine(glow, rect.X + 18, rect.Y + 1, rect.Right - 18, rect.Y + 1); } } } /// /// 青绿渐变进度条,替代系统默认绿条。 /// internal sealed class AiProgressBar : Control { int _value; int _phase; ProgressBarStyle _style = ProgressBarStyle.Continuous; readonly System.Windows.Forms.Timer _tick; public int Minimum; public int Maximum = 100; public AiProgressBar() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true); Height = 8; _tick = new System.Windows.Forms.Timer(); _tick.Interval = 28; _tick.Tick += delegate { _phase = (_phase + 5) % 400; Invalidate(); }; } public int Value { get { return _value; } set { _value = Math.Max(Minimum, Math.Min(Maximum, value)); Invalidate(); } } public ProgressBarStyle Style { get { return _style; } set { _style = value; if (_style == ProgressBarStyle.Marquee) { _tick.Start(); } else { _tick.Stop(); } Invalidate(); } } protected override void OnPaint(PaintEventArgs e) { if (Width < 4 || Height < 4) { return; } e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; var bg = new Rectangle(0, 0, Math.Max(1, Width - 1), Math.Max(1, Height - 1)); using (GraphicsPath path = Theme.RoundRect(bg, Math.Max(2, Height / 2))) using (var fill = new SolidBrush(Color.FromArgb(255, 12, 16, 24))) { e.Graphics.FillPath(fill, path); } if (_style == ProgressBarStyle.Marquee) { int w = Math.Max(48, Width / 3); int x = (_phase * (Width + w) / 400) - w; using (var lg = new LinearGradientBrush(new Rectangle(x, 0, w, Height), Theme.CyanDeep, Theme.Violet, 0f)) { e.Graphics.FillRectangle(lg, new Rectangle(x, 1, w, Math.Max(1, Height - 2))); } } else if (_value > 0 && Maximum > 0) { int w = Math.Max(4, (int)((Width - 2) * (_value / (double)Maximum))); using (var lg = new LinearGradientBrush(new Rectangle(0, 0, w, Height), Theme.CyanDeep, Theme.Violet, 0f)) using (GraphicsPath path = Theme.RoundRect(new Rectangle(0, 0, w, Math.Max(1, Height - 1)), Math.Max(2, Height / 2))) { e.Graphics.FillPath(lg, path); } } } protected override void Dispose(bool disposing) { if (disposing) { _tick.Dispose(); } base.Dispose(disposing); } } internal sealed class DbConfig { public string Engine = "mysql"; public string Host = "127.0.0.1"; public int Port = 3306; public string Database = "mes_cloud"; public string User = "mes_cloud"; public string Password = "s2ENj7KaEySTC7dW"; } internal static class DbConfigStore { public static string ConfigPath(string workDir) { return Path.Combine(workDir, "db-config.json"); } public static DbConfig Load(string workDir) { string path = ConfigPath(workDir); if (!File.Exists(path)) { return new DbConfig(); } try { string raw = File.ReadAllText(path, Encoding.UTF8); var lines = new List(); foreach (string line in raw.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)) { if (!line.TrimStart().StartsWith("#")) { lines.Add(line); } } string json = string.Join("\n", lines.ToArray()).Trim(); int start = json.IndexOf('{'); int end = json.LastIndexOf('}'); if (start >= 0 && end > start) { json = json.Substring(start, end - start + 1); } var cfg = new DbConfig(); cfg.Host = ReadString(json, "host", cfg.Host); cfg.Database = ReadString(json, "database", cfg.Database); cfg.User = ReadString(json, "user", cfg.User); cfg.Password = ReadString(json, "password", cfg.Password); int port = ReadInt(json, "port", cfg.Port); if (port < 1 || port > 65535) { throw new Exception("端口必须在 1-65535"); } cfg.Port = port; cfg.Host = NormalizeHost(cfg.Host); if (string.IsNullOrWhiteSpace(cfg.User)) { throw new Exception("请填写账号"); } return cfg; } catch { return new DbConfig(); } } public static void Save(string workDir, DbConfig cfg) { cfg.Host = NormalizeHost(cfg.Host); if (string.IsNullOrWhiteSpace(cfg.User)) { throw new Exception("请填写账号"); } if (cfg.Port < 1 || cfg.Port > 65535) { throw new Exception("端口必须在 1-65535"); } string json = "{\r\n" + " \"engine\": \"mysql\",\r\n" + " \"host\": \"" + Escape(cfg.Host) + "\",\r\n" + " \"port\": " + cfg.Port + ",\r\n" + " \"database\": \"" + Escape(cfg.Database) + "\",\r\n" + " \"user\": \"" + Escape(cfg.User) + "\",\r\n" + " \"password\": \"" + Escape(cfg.Password) + "\"\r\n" + "}"; File.WriteAllText(ConfigPath(workDir), json, new UTF8Encoding(false)); } static string NormalizeHost(string host) { if (string.IsNullOrWhiteSpace(host)) { return "127.0.0.1"; } host = host.Trim(); Match jdbc = Regex.Match(host, @"jdbc:mysql://([^:/]+)"); if (jdbc.Success) { host = jdbc.Groups[1].Value; } if (host.StartsWith("mysql://", StringComparison.OrdinalIgnoreCase)) { host = host.Substring(8); } if (Regex.IsMatch(host, @"\s|/")) { throw new Exception("IP/主机名不合法"); } return host; } static string ReadString(string json, string key, string fallback) { Match m = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\""); if (!m.Success) { return fallback; } return Unescape(m.Groups[1].Value); } static int ReadInt(string json, string key, int fallback) { Match m = Regex.Match(json, "\"" + key + "\"\\s*:\\s*(-?\\d+)"); int n; if (m.Success && int.TryParse(m.Groups[1].Value, out n)) { return n; } return fallback; } static string Escape(string s) { if (s == null) { return ""; } return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r").Replace("\n", "\\n"); } static string Unescape(string s) { var sb = new StringBuilder(); for (int i = 0; i < s.Length; i++) { if (s[i] == '\\' && i + 1 < s.Length) { char n = s[++i]; if (n == '"') { sb.Append('"'); } else if (n == '\\') { sb.Append('\\'); } else if (n == 'n') { sb.Append('\n'); } else if (n == 'r') { sb.Append('\r'); } else if (n == 't') { sb.Append('\t'); } else { sb.Append(n); } } else { sb.Append(s[i]); } } return sb.ToString(); } } internal static class MysqlUtil { static readonly Dictionary Cache = new Dictionary(StringComparer.OrdinalIgnoreCase); public static string FindTool(string name) { if (Cache.ContainsKey(name)) { return Cache[name]; } string exeName = name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? name : name + ".exe"; string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? ""; foreach (string dir in pathEnv.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) { try { string p = Path.Combine(dir.Trim().Trim('"'), exeName); if (File.Exists(p)) { Cache[name] = p; return p; } } catch { } } string[] roots = new[] { @"C:\Program Files\MySQL", @"C:\Program Files (x86)\MySQL", @"C:\mysql", @"D:\MySQL", @"E:\MySQL", @"C:\Program Files\MariaDB" }; foreach (string root in roots) { if (!Directory.Exists(root)) { continue; } try { foreach (string file in Directory.GetFiles(root, exeName, SearchOption.AllDirectories)) { string bin = Path.GetFileName(Path.GetDirectoryName(file)); if (string.Equals(bin, "bin", StringComparison.OrdinalIgnoreCase)) { Cache[name] = file; return file; } } } catch { } } return null; } public static void WriteCnf(DbConfig cfg, string cnfPath) { string pwd = cfg.Password ?? ""; pwd = pwd.Replace("\\", "\\\\").Replace("\"", "\\\""); string text = "[client]\r\n" + "host=" + cfg.Host + "\r\n" + "port=" + cfg.Port + "\r\n" + "user=" + cfg.User + "\r\n" + "password=\"" + pwd + "\"\r\n" + "default-character-set=utf8mb4\r\n"; File.WriteAllText(cnfPath, text, Encoding.ASCII); } public static string TestConnection(DbConfig cfg) { string mysql = FindTool("mysql"); if (mysql == null) { throw new Exception("找不到 mysql.exe,请安装 MySQL 客户端"); } string cnf = Path.Combine(Path.GetTempPath(), "mes_ui_test_" + Process.GetCurrentProcess().Id + ".cnf"); WriteCnf(cfg, cnf); try { string output; int code = Run(mysql, new[] { "--defaults-extra-file=" + cnf, "--connect-timeout=5", "-N", "-s", "-e", "SELECT 1" }, 15000, out output); if (code != 0) { throw new Exception("连接失败: " + (output ?? "").Trim()); } } finally { TryDelete(cnf); } return "MySQL 连接成功: " + cfg.Host + ":" + cfg.Port + " / " + cfg.Database; } public static long GetDbSizeBytes(DbConfig cfg) { try { string mysql = FindTool("mysql"); if (mysql == null) { return 0; } string cnf = Path.Combine(Path.GetTempPath(), "mes_ui_size_" + Process.GetCurrentProcess().Id + ".cnf"); WriteCnf(cfg, cnf); try { string db = (cfg.Database ?? "").Replace("'", "''"); string sql = "SELECT IFNULL(SUM(data_length+index_length),0) FROM information_schema.tables WHERE table_schema='" + db + "'"; string output; int code = Run(mysql, new[] { "--defaults-extra-file=" + cnf, "--connect-timeout=3", "-N", "-s", "-e", sql }, 8000, out output); if (code != 0) { return 0; } long n; if (long.TryParse((output ?? "").Trim(), out n)) { return n; } } finally { TryDelete(cnf); } } catch { } return 0; } static int Run(string file, string[] args, int timeoutMs, out string output) { var psi = new ProcessStartInfo(); psi.FileName = file; psi.Arguments = QuoteArgs(args); psi.UseShellExecute = false; psi.CreateNoWindow = true; psi.RedirectStandardOutput = true; psi.RedirectStandardError = true; psi.StandardOutputEncoding = Encoding.UTF8; psi.StandardErrorEncoding = Encoding.UTF8; using (var proc = new Process()) { proc.StartInfo = psi; var sb = new StringBuilder(); proc.OutputDataReceived += delegate(object s, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { sb.AppendLine(e.Data); } }; proc.ErrorDataReceived += delegate(object s, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { sb.AppendLine(e.Data); } }; proc.Start(); proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); if (!proc.WaitForExit(timeoutMs)) { try { proc.Kill(); } catch { } throw new Exception("等待超时"); } proc.WaitForExit(); output = sb.ToString(); return proc.ExitCode; } } static string QuoteArgs(string[] args) { var sb = new StringBuilder(); for (int i = 0; i < args.Length; i++) { if (i > 0) { sb.Append(' '); } string a = args[i] ?? ""; sb.Append('"').Append(a.Replace("\"", "\\\"")).Append('"'); } return sb.ToString(); } static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } internal sealed class MainForm : Form { static readonly string[] PackedFiles = new string[] { "backup-ui-server.ps1", "backup-db.ps1", "db-config.ps1", "backup-ui.html", "restore-db.ps1", "install-backup-task.bat", "使用说明.txt" }; readonly string _workDir; readonly TextBox _host; readonly TextBox _port; readonly TextBox _database; readonly TextBox _user; readonly TextBox _password; readonly Label _cfgStatus; readonly Label _picked; readonly Label _spaceHint; readonly Label _progHint; readonly Label _runStatus; readonly CheckBox _keepOld; readonly AiProgressBar _bar; readonly TextBox _log; readonly Button _btnTest; readonly Button _btnSave; readonly Button _btnBrowse; readonly Button _btnBackup; readonly System.Windows.Forms.Timer _poll; string _selectedPath = ""; Process _backupProc; string _outFile = ""; DateTime _startedAt; long _dbBytes; bool _running; public MainForm() { string exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (string.IsNullOrEmpty(exeDir)) { exeDir = AppDomain.CurrentDomain.BaseDirectory; } _workDir = PrepareWorkDir(exeDir); Text = "MES 智能备份控制台"; Font = Theme.Ui; BackColor = Theme.Bg; ForeColor = Theme.Text; StartPosition = FormStartPosition.CenterScreen; MinimumSize = new Size(1020, 680); Size = new Size(1140, 740); DoubleBuffered = true; var header = new Panel { Dock = DockStyle.Top, Height = 96, BackColor = Theme.Header }; header.Paint += delegate(object s, PaintEventArgs e) { e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; using (var g = new LinearGradientBrush(header.ClientRectangle, Color.FromArgb(10, 14, 24), Color.FromArgb(18, 12, 36), LinearGradientMode.Horizontal)) { e.Graphics.FillRectangle(g, header.ClientRectangle); } using (var glow = new Pen(Color.FromArgb(180, 94, 234, 212), 2F)) { e.Graphics.DrawLine(glow, 0, 0, header.Width, 0); } using (var line = new Pen(Color.FromArgb(60, 129, 140, 248))) { e.Graphics.DrawLine(line, 0, header.Height - 1, header.Width, header.Height - 1); } var markRect = new Rectangle(26, 24, 48, 48); using (GraphicsPath path = Theme.RoundRect(markRect, 14)) using (var g = new LinearGradientBrush(markRect, Theme.Cyan, Theme.Violet, 145f)) { e.Graphics.FillPath(g, path); } TextRenderer.DrawText(e.Graphics, "M", Theme.Mark, markRect, Theme.Ink, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); TextRenderer.DrawText(e.Graphics, "NEURAL ARCHIVE", Theme.Caption, new Point(86, 16), Theme.Cyan); TextRenderer.DrawText(e.Graphics, "MES 智能备份控制台", Theme.Title, new Point(84, 34), Theme.HeaderText); TextRenderer.DrawText(e.Graphics, "独立工具 · 仅 MySQL · 不接入生产系统", Theme.Caption, new Point(86, 66), Theme.HeaderMuted); string[] chips = new[] { "AIR-GAPPED", "MYSQL", "LOCAL ONLY" }; int x = header.Width - 22; for (int i = 0; i < chips.Length; i++) { Size sz = TextRenderer.MeasureText(chips[i], Theme.Caption); int w = sz.Width + 16; x -= w + 8; var chipRect = new Rectangle(x, 36, w, 24); using (GraphicsPath path = Theme.RoundRect(chipRect, 12)) using (var fill = new SolidBrush(Color.FromArgb(40, 255, 255, 255))) using (var pen = new Pen(i == 2 ? Color.FromArgb(120, 94, 234, 212) : Color.FromArgb(50, 255, 255, 255))) { e.Graphics.FillPath(fill, path); e.Graphics.DrawPath(pen, path); } TextRenderer.DrawText(e.Graphics, chips[i], Theme.Caption, chipRect, i == 2 ? Theme.Cyan : Theme.Muted, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); } }; var grid = new TableLayoutPanel(); grid.Dock = DockStyle.Fill; grid.ColumnCount = 2; grid.RowCount = 1; grid.BackColor = Theme.Bg; grid.Padding = new Padding(18, 12, 14, 12); grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 56F)); grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 44F)); grid.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); grid.Paint += PaintMesh; _host = MakeInput(); _port = MakeInput(); _database = MakeInput(); _user = MakeInput(); _password = MakeInput(); _password.UseSystemPasswordChar = true; var engine = MakeInput(); engine.Text = "MySQL"; engine.ReadOnly = true; engine.BackColor = Theme.InputMuted; engine.ForeColor = Theme.Muted; engine.TabStop = false; var connCard = MakeCard(); var connBody = MakeBody(); connBody.RowCount = 4; connBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); connBody.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); connBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); connBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 24)); var connHead = MakeHead("01", "连接矩阵", "先测试,再保存", null); var form = new TableLayoutPanel(); form.Dock = DockStyle.Fill; form.ColumnCount = 2; form.RowCount = 3; form.BackColor = Theme.Card; form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F)); for (int i = 0; i < 3; i++) { form.RowStyles.Add(new RowStyle(SizeType.Percent, 33.3F)); } form.Controls.Add(MakeField("类型", engine), 0, 0); form.Controls.Add(MakeField("端口", _port), 1, 0); form.Controls.Add(MakeField("IP / 主机", _host), 0, 1); form.Controls.Add(MakeField("库名", _database), 1, 1); form.Controls.Add(MakeField("账号", _user), 0, 2); form.Controls.Add(MakeField("密码", _password), 1, 2); var connActions = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Card }; _btnTest = MakeButton("测试连接", false); _btnTest.Location = new Point(4, 4); _btnSave = MakeGoldButton("保存配置"); _btnSave.Location = new Point(122, 4); _btnTest.Click += OnTest; _btnSave.Click += OnSave; connActions.Controls.Add(_btnTest); connActions.Controls.Add(_btnSave); _cfgStatus = new Label { Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, ForeColor = Theme.Muted, BackColor = Theme.Card, Padding = new Padding(6, 0, 0, 0), Text = "填写 IP、账号、密码后点「测试连接」" }; connBody.Controls.Add(connHead, 0, 0); connBody.Controls.Add(form, 0, 1); connBody.Controls.Add(connActions, 0, 2); connBody.Controls.Add(_cfgStatus, 0, 3); connCard.Controls.Add(connBody); var bakCard = MakeCard(); var bakBody = MakeBody(); bakBody.RowCount = 7; bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); bakBody.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 22)); bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 28)); bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 12)); bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 22)); bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 48)); _btnBrowse = MakeButton("浏览文件夹", false); _btnBrowse.Width = 128; _btnBrowse.Click += OnBrowse; var bakHead = MakeHead("02", "归档目标", null, _btnBrowse); _picked = new Label { Dock = DockStyle.Fill, BackColor = Theme.Input, ForeColor = Theme.Muted, Text = " 尚未选择保存位置", TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(12, 0, 12, 0) }; _picked.Paint += delegate(object s, PaintEventArgs e) { using (var p = new Pen(Color.FromArgb(90, 94, 234, 212)) { DashStyle = DashStyle.Dash }) { e.Graphics.DrawRectangle(p, 1, 1, _picked.Width - 3, _picked.Height - 3); } }; _spaceHint = new Label { Dock = DockStyle.Fill, ForeColor = Theme.Muted, BackColor = Theme.Card, TextAlign = ContentAlignment.MiddleLeft, Text = "插入 U 盘或移动硬盘后选择目录,无需手输路径。" }; _keepOld = new CheckBox { Text = "同时删除该目录中超过 7 天的旧备份(U 盘建议不勾选)", ForeColor = Theme.Muted, Dock = DockStyle.Fill, BackColor = Theme.Card, FlatStyle = FlatStyle.Flat, TextAlign = ContentAlignment.MiddleLeft }; _bar = new AiProgressBar { Dock = DockStyle.Fill, Minimum = 0, Maximum = 100 }; _progHint = new Label { Dock = DockStyle.Fill, ForeColor = Theme.Muted, BackColor = Theme.Card, TextAlign = ContentAlignment.MiddleLeft, Text = "" }; var bakGo = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Card }; _btnBackup = MakeButton("开始备份", true); _btnBackup.Width = 148; _btnBackup.Height = 36; _btnBackup.Font = new Font("Microsoft YaHei UI", 11F, FontStyle.Bold); _btnBackup.Location = new Point(4, 4); _btnBackup.Click += OnBackup; _runStatus = new Label { AutoSize = false, Location = new Point(164, 10), Size = new Size(360, 24), Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, ForeColor = Theme.Muted, BackColor = Theme.Card, Text = "" }; bakGo.Controls.Add(_btnBackup); bakGo.Controls.Add(_runStatus); bakBody.Controls.Add(bakHead, 0, 0); bakBody.Controls.Add(_picked, 0, 1); bakBody.Controls.Add(_spaceHint, 0, 2); bakBody.Controls.Add(_keepOld, 0, 3); bakBody.Controls.Add(_bar, 0, 4); bakBody.Controls.Add(_progHint, 0, 5); bakBody.Controls.Add(bakGo, 0, 6); bakCard.Controls.Add(bakBody); var logCard = MakeCard(); var logBody = MakeBody(); logBody.RowCount = 2; logBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); logBody.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); logBody.Controls.Add(MakeHead("03", "运行控制台", "实时输出", null), 0, 0); _log = new TextBox { Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical, BorderStyle = BorderStyle.None, Dock = DockStyle.Fill, BackColor = Theme.LogBg, ForeColor = Theme.LogText, Font = Theme.Mono, Text = "等待操作…" }; logBody.Controls.Add(_log, 0, 1); logCard.Controls.Add(logBody); var left = new TableLayoutPanel(); left.Dock = DockStyle.Fill; left.BackColor = Color.Transparent; left.ColumnCount = 1; left.RowCount = 2; left.RowStyles.Add(new RowStyle(SizeType.Percent, 54F)); left.RowStyles.Add(new RowStyle(SizeType.Percent, 46F)); left.Controls.Add(connCard, 0, 0); left.Controls.Add(bakCard, 0, 1); grid.Controls.Add(left, 0, 0); grid.Controls.Add(logCard, 1, 0); Controls.Add(grid); Controls.Add(header); _poll = new System.Windows.Forms.Timer(); _poll.Interval = 1000; _poll.Tick += OnPoll; Load += delegate { LoadConfigToUi(); }; FormClosing += OnClosing; } static string PrepareWorkDir(string exeDir) { string serverScript = Path.Combine(exeDir, "backup-db.ps1"); string marker = Path.Combine(exeDir, Program.MarkerName); bool original = File.Exists(serverScript) && !File.Exists(marker); if (!original) { ExtractPackedFiles(exeDir); File.WriteAllText(marker, "packed", Encoding.ASCII); } return exeDir; } static void ExtractPackedFiles(string dir) { Assembly asm = Assembly.GetExecutingAssembly(); foreach (string name in PackedFiles) { Stream src = asm.GetManifestResourceStream(name); if (src == null) { continue; } using (src) using (FileStream fs = File.Create(Path.Combine(dir, name))) { src.CopyTo(fs); } } } static CardPanel MakeCard() { return new CardPanel { Dock = DockStyle.Fill }; } static TableLayoutPanel MakeBody() { var body = new TableLayoutPanel(); body.Dock = DockStyle.Fill; body.ColumnCount = 1; body.BackColor = Theme.Card; body.Padding = new Padding(10, 6, 12, 8); body.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); return body; } /// /// 卡片标题行:序号 + 标题 + 说明,右侧可放按钮。 /// static Panel MakeHead(string step, string title, string hint, Control right) { var head = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Card }; var st = MakeStep(step); st.Location = new Point(2, 8); var t = SectionLabel(title, 38, 8); head.Controls.Add(st); head.Controls.Add(t); if (!string.IsNullOrEmpty(hint)) { int titleW = TextRenderer.MeasureText(title, Theme.Section).Width; var h = new Label { Text = hint, Font = Theme.Caption, ForeColor = Theme.Muted, AutoSize = true, Location = new Point(46 + titleW, 12), BackColor = Theme.Card }; head.Controls.Add(h); } if (right != null) { right.Anchor = AnchorStyles.Top | AnchorStyles.Right; head.Controls.Add(right); EventHandler place = delegate { right.Location = new Point(Math.Max(120, head.Width - right.Width - 4), 4); }; head.Resize += place; place(head, EventArgs.Empty); } return head; } /// /// 标签在上、输入在下,两列等宽,避免右侧字段被挤没。 /// static Panel MakeField(string caption, Control input) { var box = new Panel(); box.Dock = DockStyle.Fill; box.BackColor = Theme.Card; box.Margin = new Padding(4, 2, 8, 2); box.Padding = new Padding(0, 0, 4, 2); var lb = new Label(); lb.Text = caption; lb.ForeColor = Theme.Muted; lb.Font = Theme.Caption; lb.Dock = DockStyle.Top; lb.Height = 20; lb.BackColor = Theme.Card; lb.TextAlign = ContentAlignment.BottomLeft; input.Dock = DockStyle.Fill; box.Controls.Add(input); box.Controls.Add(lb); return box; } static Label MakeStep(string no) { return new Label { Text = no, Font = new Font("Segoe UI", 8.5F, FontStyle.Bold), ForeColor = Theme.Ink, BackColor = Theme.Cyan, Size = new Size(28, 22), Location = new Point(0, 6), TextAlign = ContentAlignment.MiddleCenter }; } static void PaintMesh(object sender, PaintEventArgs e) { var c = sender as Control; if (c == null) { return; } using (var p = new Pen(Theme.Mesh)) { for (int x = 0; x < c.Width; x += 42) { e.Graphics.DrawLine(p, x, 0, x, c.Height); } for (int y = 0; y < c.Height; y += 42) { e.Graphics.DrawLine(p, 0, y, c.Width, y); } } } static Label SectionLabel(string text, int x, int y) { return new Label { Text = text, Font = Theme.Section, ForeColor = Theme.Text, AutoSize = true, Location = new Point(x, y), BackColor = Theme.Card }; } static Label FieldLabel(string text) { return new Label { Text = text, ForeColor = Theme.Muted, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, BackColor = Theme.Card }; } static TextBox MakeInput() { return new TextBox { Dock = DockStyle.Fill, BorderStyle = BorderStyle.FixedSingle, BackColor = Theme.Input, ForeColor = Theme.Text, Margin = new Padding(0) }; } static Button MakeButton(string text, bool primary) { var b = new Button(); b.Text = text; b.Width = 110; b.Height = 32; b.FlatStyle = FlatStyle.Flat; b.Cursor = Cursors.Hand; if (primary) { b.FlatAppearance.BorderSize = 0; b.BackColor = Theme.CyanDeep; b.ForeColor = Theme.Ink; b.MouseEnter += delegate { if (b.Enabled) { b.BackColor = Theme.Cyan; } }; b.MouseLeave += delegate { b.BackColor = Theme.CyanDeep; }; } else { b.FlatAppearance.BorderSize = 1; b.FlatAppearance.BorderColor = Color.FromArgb(70, 255, 255, 255); b.BackColor = Color.FromArgb(18, 22, 32); b.ForeColor = Theme.Text; b.MouseEnter += delegate { if (b.Enabled) { b.BackColor = Color.FromArgb(28, 36, 52); } }; b.MouseLeave += delegate { b.BackColor = Color.FromArgb(18, 22, 32); }; } return b; } static Button MakeGoldButton(string text) { var b = new Button(); b.Text = text; b.Width = 110; b.Height = 32; b.FlatStyle = FlatStyle.Flat; b.FlatAppearance.BorderSize = 0; b.BackColor = Theme.Cyan; b.ForeColor = Theme.Ink; b.Cursor = Cursors.Hand; b.MouseEnter += delegate { if (b.Enabled) { b.BackColor = Theme.CyanDeep; } }; b.MouseLeave += delegate { b.BackColor = Theme.Cyan; }; return b; } DbConfig ReadUiConfig() { var cfg = new DbConfig(); cfg.Host = (_host.Text ?? "").Trim(); int port; if (!int.TryParse((_port.Text ?? "").Trim(), out port)) { port = 3306; } cfg.Port = port; cfg.Database = (_database.Text ?? "").Trim(); cfg.User = (_user.Text ?? "").Trim(); cfg.Password = _password.Text ?? ""; return cfg; } void LoadConfigToUi() { DbConfig cfg = DbConfigStore.Load(_workDir); _host.Text = cfg.Host; _port.Text = cfg.Port.ToString(); _database.Text = cfg.Database; _user.Text = cfg.User; _password.Text = cfg.Password; } void OnTest(object sender, EventArgs e) { _btnTest.Enabled = false; SetCfg("正在测试连接,请稍候…", Theme.Warn); DbConfig cfg = ReadUiConfig(); ThreadPool.QueueUserWorkItem(delegate { try { string msg = MysqlUtil.TestConnection(cfg); BeginInvoke(new Action(delegate { SetCfg(msg, Theme.Ok); _btnTest.Enabled = true; })); } catch (Exception ex) { BeginInvoke(new Action(delegate { SetCfg(ex.Message, Theme.Bad); _btnTest.Enabled = true; })); } }); } void OnSave(object sender, EventArgs e) { try { DbConfig cfg = ReadUiConfig(); DbConfigStore.Save(_workDir, cfg); SetCfg("已保存 MySQL 配置", Theme.Ok); } catch (Exception ex) { SetCfg(ex.Message, Theme.Bad); } } void OnBrowse(object sender, EventArgs e) { using (var dlg = new FolderBrowserDialog()) { dlg.Description = "选择备份文件保存位置(可选手动插入的 U 盘或移动硬盘)"; dlg.ShowNewFolderButton = true; dlg.RootFolder = Environment.SpecialFolder.MyComputer; if (!string.IsNullOrEmpty(_selectedPath) && Directory.Exists(_selectedPath)) { dlg.SelectedPath = _selectedPath; } if (dlg.ShowDialog(this) != DialogResult.OK) { return; } try { _selectedPath = GetSafePath(dlg.SelectedPath); _picked.Text = " " + _selectedPath; _picked.ForeColor = Theme.Text; long free = GetFreeBytes(_selectedPath); if (free > 0) { _spaceHint.Text = "该磁盘剩余 " + Gb(free) + " GB。"; } SetRun("已选择保存位置", Theme.Ok); } catch (Exception ex) { SetRun(ex.Message, Theme.Bad); } } } void OnBackup(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(_selectedPath)) { SetRun("请先点「浏览文件夹」选择保存位置", Theme.Bad); return; } string script = Path.Combine(_workDir, "backup-db.ps1"); if (!File.Exists(script)) { SetRun("找不到 backup-db.ps1", Theme.Bad); return; } try { DbConfig cfg = ReadUiConfig(); DbConfigStore.Save(_workDir, cfg); string dir = GetSafePath(_selectedPath); long free = GetFreeBytes(dir); _dbBytes = MysqlUtil.GetDbSizeBytes(cfg); if (_dbBytes > 0 && free > 0 && free < (long)(_dbBytes * 1.2)) { throw new Exception("目标盘剩余空间不足:约需 " + Gb((long)(_dbBytes * 1.2)) + " GB,当前剩余 " + Gb(free) + " GB。请换更大的 U 盘/硬盘。"); } if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } int keep = _keepOld.Checked ? 7 : 0; string powershell = Path.Combine(Environment.SystemDirectory, @"WindowsPowerShell\v1.0\powershell.exe"); if (!File.Exists(powershell)) { powershell = "powershell.exe"; } _outFile = Path.Combine(Path.GetTempPath(), "mes_backup_ui_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".log"); string errFile = _outFile + ".err"; TryDelete(_outFile); TryDelete(errFile); var psi = new ProcessStartInfo(); psi.FileName = powershell; psi.Arguments = "-NoProfile -STA -ExecutionPolicy Bypass -File \"" + script + "\" -BackupDir \"" + dir + "\" -KeepDaysParam " + keep; psi.WorkingDirectory = _workDir; psi.UseShellExecute = false; psi.CreateNoWindow = true; psi.RedirectStandardOutput = true; psi.RedirectStandardError = true; psi.StandardOutputEncoding = Encoding.UTF8; psi.StandardErrorEncoding = Encoding.UTF8; _backupProc = new Process(); _backupProc.StartInfo = psi; _backupProc.EnableRaisingEvents = true; _backupProc.OutputDataReceived += OnBackupOutput; _backupProc.ErrorDataReceived += OnBackupOutput; if (!_backupProc.Start()) { throw new Exception("无法启动备份进程"); } _backupProc.BeginOutputReadLine(); _backupProc.BeginErrorReadLine(); _running = true; _startedAt = DateTime.Now; _selectedPath = dir; _btnBackup.Enabled = false; _bar.Style = ProgressBarStyle.Marquee; _bar.Value = 0; SetLog(new[] { "开始备份到: " + dir }); if (_dbBytes > 0) { AppendLog("库数据约 " + Gb(_dbBytes) + " GB,采用边导出边压缩,请耐心等待,勿拔盘"); } SetRun("正在备份,数据量大时可能需要较长时间...", Theme.Warn); _poll.Start(); } catch (Exception ex) { SetRun(ex.Message, Theme.Bad); _btnBackup.Enabled = true; } } void OnBackupOutput(object sender, DataReceivedEventArgs e) { if (string.IsNullOrEmpty(e.Data)) { return; } BeginInvoke(new Action(delegate { AppendLog(e.Data); })); } void OnPoll(object sender, EventArgs e) { if (!_running) { return; } long written; string fileName; GetWrittenBackup(_selectedPath, out written, out fileName); if (_dbBytes > 0 && written > 1024) { _bar.Style = ProgressBarStyle.Continuous; int pct = (int)Math.Max(2, Math.Min(99, written * 100 / _dbBytes)); _bar.Value = pct; } _progHint.Text = "已写入 " + Mb(written) + " MB" + (string.IsNullOrEmpty(fileName) ? "" : "(" + fileName + ")") + ",请勿拔盘"; Process p = _backupProc; bool exited = false; int code = -1; if (p == null) { exited = true; } else { try { p.Refresh(); exited = p.HasExited; if (exited) { try { p.WaitForExit(500); } catch { } try { code = p.ExitCode; } catch { code = -1; } } } catch { exited = true; } } if (!exited) { return; } _poll.Stop(); _running = false; _btnBackup.Enabled = true; _bar.Style = ProgressBarStyle.Continuous; bool fileOk = TestBackupFileOk(_selectedPath, _startedAt); bool success = (code == 0) || fileOk; if (success) { _bar.Value = 100; GetWrittenBackup(_selectedPath, out written, out fileName); _progHint.Text = "完成,文件约 " + Mb(written) + " MB:" + (fileName ?? ""); AppendLog("备份完成"); SetRun("备份完成", Theme.Ok); } else { string codeText = code < 0 ? "未知" : code.ToString(); string err = "备份失败,退出码 " + codeText; AppendLog(err); SetRun(err, Theme.Bad); } _backupProc = null; } void OnClosing(object sender, FormClosingEventArgs e) { if (_running) { DialogResult r = MessageBox.Show(this, "备份正在进行,关闭窗口会中断备份。确定退出吗?", "MES 数据库备份", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (r != DialogResult.Yes) { e.Cancel = true; return; } } _poll.Stop(); Process p = _backupProc; _backupProc = null; if (p != null) { try { if (!p.HasExited) { KillProcessTree(p.Id); } } catch { } try { p.Dispose(); } catch { } } } static void GetWrittenBackup(string dir, out long bytes, out string name) { bytes = 0; name = ""; if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir)) { return; } FileInfo pick = null; try { var files = new List(new DirectoryInfo(dir).GetFiles("mes_cloud_*.sql.gz*")); files.Sort(delegate(FileInfo a, FileInfo b) { return b.LastWriteTime.CompareTo(a.LastWriteTime); }); foreach (FileInfo f in files) { if (f.Name.IndexOf(".tmp", StringComparison.OrdinalIgnoreCase) < 0) { pick = f; break; } } if (pick == null && files.Count > 0) { pick = files[0]; } } catch { } if (pick != null) { name = pick.Name; bytes = pick.Length; } } static bool TestBackupFileOk(string dir, DateTime started) { if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir)) { return false; } FileInfo gz = null; try { foreach (FileInfo f in new DirectoryInfo(dir).GetFiles("mes_cloud_*.sql.gz")) { if (gz == null || f.LastWriteTime > gz.LastWriteTime) { gz = f; } } } catch { return false; } if (gz == null || gz.Length <= 100) { return false; } if (gz.LastWriteTime < started.AddMinutes(-2)) { return false; } return true; } static string GetSafePath(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new Exception("请先选择保存位置"); } string p = path.Trim().Trim('"'); if (!Regex.IsMatch(p, @"^[A-Za-z]:\\")) { throw new Exception("只允许本机磁盘路径"); } string full = Path.GetFullPath(p); if (Regex.IsMatch(full, @"^[A-Za-z]:\\?$")) { return char.ToUpperInvariant(full[0]) + @":\"; } return full.TrimEnd('\\'); } static long GetFreeBytes(string path) { try { string full = GetSafePath(path); string root = Path.GetPathRoot(full); var di = new DriveInfo(root); if (di.IsReady) { return di.AvailableFreeSpace; } } catch { } return 0; } static void KillProcessTree(int pid) { try { var psi = new ProcessStartInfo(); psi.FileName = "taskkill"; psi.Arguments = "/PID " + pid + " /T /F"; psi.CreateNoWindow = true; psi.UseShellExecute = false; using (Process k = Process.Start(psi)) { if (k != null) { k.WaitForExit(5000); } } } catch { } } static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } static string Gb(long n) { return (n / 1073741824.0).ToString("0.00"); } static string Mb(long n) { return (n / 1048576.0).ToString("0.0"); } void SetCfg(string text, Color color) { _cfgStatus.Text = text ?? ""; _cfgStatus.ForeColor = color; } void SetRun(string text, Color color) { _runStatus.Text = text ?? ""; _runStatus.ForeColor = color; } void SetLog(string[] lines) { _log.Text = (lines != null && lines.Length > 0) ? string.Join(Environment.NewLine, lines) : "等待操作…"; _log.SelectionStart = _log.TextLength; _log.ScrollToCaret(); } void AppendLog(string line) { if (_log.Text == "等待操作…" || _log.Text == "等待操作...") { _log.Text = ""; } if (_log.TextLength > 60000) { _log.Text = _log.Text.Substring(_log.TextLength - 40000); } _log.AppendText(line + Environment.NewLine); } } }