// ============================================================ // 项目云窗体:把项目上传到会员服务器(项目云) / 从云下载还原 / 删除 // · 复用 主窗口.项目导入导出 的打包(BackupSQLiteDb+meta.json+zip)与还原(ImportProjectFromZip) // · 传输走 会员服务Host.PostBinary / DownloadToFile(服务器= shezhi\会员服务.config.xml 指向的会员服务器) // · 上传/下载按登录手机号隔离(服务器按手机号分目录存储) // UI 全部代码构建(免设计器 resx 资源化坑,照 PickProjectDialogMulti 模式) // ============================================================ using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.IO.Compression; using System.Text; using System.Threading; using System.Windows.Forms; namespace 成套报价软件 { public class 项目云窗体 : Form { private readonly MainForm _owner; private ListView _lv本地; // 本地项目(可勾选上传) private ListView _lv云端; // 云上项目(可勾选下载/删除) private Button _btn上传; private Button _btn刷新云; private Button _btn下载; private Button _btn删除云; private Label _lbl状态; private TextBox _txt手机号; public 项目云窗体(MainForm owner) { _owner = owner; 初始化UI(); this.BackColor = System.Drawing.Color.White; 填本地项目(); if (!string.IsNullOrWhiteSpace(MainForm.当前登录手机号)) { _txt手机号.Text = MainForm.当前登录手机号; 刷云端(); } } private void 初始化UI() { Text = "项目云(上传到会员服务器 / 下载还原)"; StartPosition = FormStartPosition.CenterParent; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; ClientSize = new Size(980, 560); Font = new Font("宋体", 9F); var lbl登录 = new Label { Text = "登录手机号:", Location = new Point(12, 14), Size = new Size(76, 20) }; Controls.Add(lbl登录); _txt手机号 = new TextBox { Location = new Point(90, 11), Size = new Size(140, 23), ReadOnly = true, BackColor = SystemColors.Control }; Controls.Add(_txt手机号); // ---- 左:本地项目 ---- var lbl本地 = new Label { Text = "本地项目(选中要上传的)", Location = new Point(12, 42), Size = new Size(300, 20) }; Controls.Add(lbl本地); _lv本地 = new ListView { Location = new Point(12, 64), Size = new Size(452, 300), View = View.Details, FullRowSelect = true, MultiSelect = true, HideSelection = false, }; _lv本地.Columns.Add("项目编号", 110); _lv本地.Columns.Add("项目名称", 170); _lv本地.Columns.Add("客户名称", 110); _lv本地.Columns.Add("项目日期", 60); Controls.Add(_lv本地); // ---- 右:云端项目 ---- var lbl云 = new Label { Text = "云端项目(会员服务器上,选中可下载/删除)", Location = new Point(516, 42), Size = new Size(320, 20) }; Controls.Add(lbl云); _lv云端 = new ListView { Location = new Point(516, 64), Size = new Size(452, 300), View = View.Details, FullRowSelect = true, MultiSelect = true, HideSelection = false, }; _lv云端.Columns.Add("项目编号", 110); _lv云端.Columns.Add("项目名称", 190); _lv云端.Columns.Add("上传时间", 80); _lv云端.Columns.Add("大小", 70); Controls.Add(_lv云端); // ---- 按钮(纯项目功能;库备份在主窗口 备份/恢复 按钮,不在这里) ---- _btn上传 = new Button { Text = "↑ 上传选中项目", Location = new Point(12, 474), Size = new Size(130, 30) }; _btn上传.Click += (s, e) => 上传选中(); Controls.Add(_btn上传); var btn全选本地 = new Button { Text = "全选本地", Location = new Point(150, 474), Size = new Size(80, 30) }; btn全选本地.Click += (s, e) => { foreach (ListViewItem it in _lv本地.Items) it.Selected = true; }; Controls.Add(btn全选本地); _btn刷新云 = new Button { Text = "刷新云端", Location = new Point(516, 474), Size = new Size(90, 30) }; _btn刷新云.Click += (s, e) => 刷云端(); Controls.Add(_btn刷新云); _btn下载 = new Button { Text = "↓ 下载还原选中项", Location = new Point(614, 474), Size = new Size(140, 30) }; _btn下载.Click += (s, e) => 下载还原选中(); Controls.Add(_btn下载); _btn删除云 = new Button { Text = "删除云端选中项", Location = new Point(762, 474), Size = new Size(130, 30), ForeColor = Color.Firebrick }; _btn删除云.Click += (s, e) => 删除云端选中(); Controls.Add(_btn删除云); _lbl状态 = new Label { Text = "就绪。", Location = new Point(12, 520), Size = new Size(956, 26) }; Controls.Add(_lbl状态); } private void 设状态(string s) { _lbl状态.Text = s; Application.DoEvents(); } /// 写 diag 日志(MainForm.写日志 静态入口)。 private static void Log(string s) { try { MainForm.写日志(s); } catch { } } private string 登录手机号 { get { string p = string.IsNullOrWhiteSpace(MainForm.当前登录手机号) ? (_txt手机号.Text ?? "").Trim() : MainForm.当前登录手机号.Trim(); return p; } } // ---------- 本地项目列表 ---------- private void 填本地项目() { try { _lv本地.Items.Clear(); var projects = _owner.ReadAllProjectsFromDatabaseForCloud(); foreach (var p in projects) { string bh = 取字段(p, "项目编号"); var it = new ListViewItem(bh); it.SubItems.Add(取字段(p, "项目名称")); it.SubItems.Add(取字段(p, "客户名称")); it.SubItems.Add(取字段(p, "项目日期")); _lv本地.Items.Add(it); } } catch (Exception ex) { Log("[项目云] 读本地项目失败: " + ex.Message); } } private static string 取字段(Dictionary d, string k) { return d != null && d.ContainsKey(k) && d[k] != null ? d[k].ToString() : ""; } // ---------- 上传 ---------- private void 上传选中() { string 手机号 = 登录手机号; if (手机号.Length == 0 || string.IsNullOrEmpty(MainForm.当前登录密码)) { MessageBox.Show(this, "请先登录会员(新版协议需登录换取云操作凭证)。", "项目云", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (_lv本地.SelectedItems.Count == 0) { MessageBox.Show(this, "请先在左侧选中要上传的项目。", "项目云", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var 选中 = new List(); foreach (ListViewItem it in _lv本地.SelectedItems) 选中.Add(it.Text); _btn上传.Enabled = false; try { int ok = 0; for (int i = 0; i < 选中.Count; i++) { 设状态("正在上传 " + (i + 1) + "/" + 选中.Count + ": " + 选中[i] + " ..."); if (上传单个(手机号, 选中[i])) ok++; } 设状态("上传完成:成功 " + ok + "/" + 选中.Count + "。"); 刷云端(); } catch (Exception ex) { Log("[项目云] 上传异常: " + ex.Message); 设状态("上传失败: " + ex.Message); MessageBox.Show(this, "上传失败:\n" + ex.Message, "项目云", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _btn上传.Enabled = true; } } /// 每日自动上传用的公开入口(内部走 登录态,单独线程调用须自带 Invoke 保护)。 public bool 上传单个公开(string pid) { return 上传单个(登录手机号, pid); } /// 单库备份静态入口(每日自动上传用;不碰 UI,显式传手机号)。 /// 库清单收集在 主窗口.母排与备份.cs 收集云端库清单() 的同款逻辑——此处独立列一份避免跨窗体引用。 public static bool 备份单个库静态(MainForm owner, string 手机号, string 云编号, string 相对路径) { try { string 本地 = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, 相对路径); if (!File.Exists(本地)) return false; string tempDir = Path.Combine(Path.GetTempPath(), "成套报价_库传_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { string 打包文件 = Path.Combine(tempDir, Path.GetFileName(相对路径)); if (相对路径.EndsWith(".db", StringComparison.OrdinalIgnoreCase)) owner.BackupSQLiteDbForCloud(本地, 打包文件); else File.Copy(本地, 打包文件, true); byte[] zip; using (var ms = new MemoryStream()) { using (var ar = new ZipArchive(ms, ZipArchiveMode.Create, true)) ar.CreateEntryFromFile(打包文件, Path.GetFileName(打包文件), CompressionLevel.Optimal); zip = ms.ToArray(); } var 头 = new StringBuilder(); 头.Append("{").Append("\"手机号\":\"").Append(owner.EscapeJsonStringForCloud(手机号)).Append("\",") .Append("\"编号\":\"").Append(owner.EscapeJsonStringForCloud(云编号)).Append("\",") .Append("\"名称\":\"").Append(owner.EscapeJsonStringForCloud(云编号.Substring(4) + "|" + 相对路径)).Append("\",") .Append("\"时间\":\"").Append(DateTime.Now.ToString("yyyy-MM-dd")).Append("\"}"); string resp = 会员服务Host.PostBinary("/api/proj/upload", 头.ToString(), zip); return resp != null && resp.IndexOf("\"ok\":true", StringComparison.OrdinalIgnoreCase) >= 0; } finally { try { Directory.Delete(tempDir, true); } catch { } } } catch { return false; } } /// 打包并上传单个项目。复用导出的 BackupSQLiteDb+meta.json+zip 链路。 private bool 上传单个(string 手机号, string pid) { string dbPath = _owner.GetQuoteDbPathForCloud(pid); if (!File.Exists(dbPath)) { Log("[项目云] 跳过(库不存在): " + pid); return false; } var meta = _owner.ReadProjectMetaDataForCloud(pid); string projName = meta != null && meta.ContainsKey("项目名称") && meta["项目名称"] != null ? meta["项目名称"].ToString() : ""; string tempDir = Path.Combine(Path.GetTempPath(), "成套报价_云传_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); try { string dbFile = Path.Combine(tempDir, "项目" + _owner.MakeSafeFileNameForCloud(pid) + ".db"); _owner.BackupSQLiteDbForCloud(dbPath, dbFile); var sb = new StringBuilder(); sb.Append("{\n \"项目编号\": \"").Append(_owner.EscapeJsonStringForCloud(pid)).Append("\",\n \"字段\": {"); bool first = true; if (meta != null) { foreach (var kv in meta) { if (first) first = false; else sb.Append(","); sb.Append("\n \"").Append(_owner.EscapeJsonStringForCloud(kv.Key)).Append("\": \"") .Append(_owner.EscapeJsonStringForCloud((kv.Value ?? "").ToString())).Append("\""); } } sb.Append("\n }\n}"); File.WriteAllText(Path.Combine(tempDir, "meta.json"), sb.ToString(), new UTF8Encoding(false)); // ★ 打包到内存流(不用 CreateFromDirectory+ReadAllBytes:临时 zip 文件句柄 // 释放不及时,紧接着读会报"正由另一进程使用") byte[] zip; using (var ms = new MemoryStream()) { using (var ar = new ZipArchive(ms, ZipArchiveMode.Create, true)) { foreach (var f in Directory.GetFiles(tempDir)) { ar.CreateEntryFromFile(f, Path.GetFileName(f), CompressionLevel.Optimal); } } zip = ms.ToArray(); } var 头 = new StringBuilder(); 头.Append("{").Append("\"手机号\":\"").Append(_owner.EscapeJsonStringForCloud(手机号)).Append("\",") .Append("\"编号\":\"").Append(_owner.EscapeJsonStringForCloud(pid)).Append("\",") .Append("\"名称\":\"").Append(_owner.EscapeJsonStringForCloud(projName)).Append("\",") .Append("\"时间\":\"").Append(DateTime.Now.ToString("yyyy-MM-dd")).Append("\"}"); string resp = 会员服务Host.PostBinary("/api/proj/upload", 头.ToString(), zip); bool ok = resp != null && resp.IndexOf("\"ok\":true", StringComparison.OrdinalIgnoreCase) >= 0; Log("[项目云] 上传 " + pid + "(" + zip.Length + "B): " + resp); return ok; } finally { try { Directory.Delete(tempDir, true); } catch { } } } // ---------- 云端列表 ---------- private void 刷云端() { string 手机号 = 登录手机号; if (手机号.Length == 0) { 设状态("未登录,输入/登录手机号后点\"刷新云端\"。"); return; } try { string q = Uri.EscapeDataString("手机号") + "=" + Uri.EscapeDataString(手机号); string resp = 会员服务Host.Get("/api/proj/list?" + q); _lv云端.Items.Clear(); if (resp == null || resp.IndexOf("\"ok\":true", StringComparison.OrdinalIgnoreCase) < 0) { 设状态("云端列表获取失败: " + (resp ?? "")); return; } // 解析 [{编号,名称,时间,大小}] —— 项目名/值用轻量解析(照 ParseSimpleJsonFields 思路) // ★ 过滤掉 LIB_ 开头的库备份条目(企业信息/方案库等在 备份/恢复 按钮的云端流程里管,不混进项目列表) var 项目们 = 解析项目数组(resp); int 库条目数 = 0; foreach (var p in 项目们) { if (p.编号 != null && p.编号.StartsWith("LIB_", StringComparison.Ordinal)) { 库条目数++; continue; } var it = new ListViewItem(p.编号); it.SubItems.Add(p.名称); it.SubItems.Add(p.时间); it.SubItems.Add(格式大小(p.大小)); it.Tag = p.编号; _lv云端.Items.Add(it); } 设状态("云端共 " + _lv云端.Items.Count + " 个项目" + (库条目数 > 0 ? "(另有 " + 库条目数 + " 个库备份,在 备份/恢复 里管理)" : "") + "。"); } catch (Exception ex) { Log("[项目云] 刷云端失败: " + ex.Message); 设状态("云端列表获取失败: " + ex.Message); } } private class 云项目 { public string 编号, 名称, 时间; public long 大小; } /// 解析 /api/proj/list 的 项目:[{编号,名称,时间,大小}] 数组(中文键,轻量逐对象扫描)。 private static List<云项目> 解析项目数组(string json) { var 出 = new List<云项目>(); if (string.IsNullOrEmpty(json)) return 出; int i = json.IndexOf('['); if (i < 0) return 出; int 深 = 0, 对象起 = -1; var 字段 = new Dictionary(); for (; i < json.Length; i++) { char c = json[i]; if (c == '{') { if (深 == 0) { 对象起 = i; 字段.Clear(); } 深++; } else if (c == '}') { 深--; if (深 == 0 && 对象起 >= 0) { string obj = json.Substring(对象起, i - 对象起 + 1); 出.Add(new 云项目 { 编号 = 取json串(obj, "编号"), 名称 = 取json串(obj, "名称"), 时间 = 取json串(obj, "时间"), 大小 = 取json数(obj, "大小"), }); 对象起 = -1; } } } return 出; } /// 从单个 JSON 对象文本里取字符串字段(处理 \uXXXX 与常规转义)。 private static string 取json串(string obj, string key) { int k = obj.IndexOf("\"" + key + "\"", StringComparison.Ordinal); if (k < 0) return ""; int 冒 = obj.IndexOf(':', k + key.Length + 2); if (冒 < 0) return ""; int 起 = obj.IndexOf('"', 冒 + 1); if (起 < 0) return ""; var sb = new StringBuilder(); for (int i = 起 + 1; i < obj.Length; i++) { char c = obj[i]; if (c == '\\' && i + 1 < obj.Length) { char n = obj[++i]; if (n == 'u' && i + 4 < obj.Length) { sb.Append((char)Convert.ToInt32(obj.Substring(i + 1, 4), 16)); i += 4; } else sb.Append(n == 'n' ? '\n' : n); } else if (c == '"') break; else sb.Append(c); } return sb.ToString(); } private static long 取json数(string obj, string key) { int k = obj.IndexOf("\"" + key + "\"", StringComparison.Ordinal); if (k < 0) return 0; int 冒 = obj.IndexOf(':', k + key.Length + 2); if (冒 < 0) return 0; long v; int e = 冒 + 1; while (e < obj.Length && (char.IsDigit(obj[e]) || obj[e] == '-')) e++; long.TryParse(obj.Substring(冒 + 1, e - 冒 - 1), out v); return v; } private static string 格式大小(long b) { if (b >= 1048576) return (b / 1048576d).ToString("0.#") + "M"; if (b >= 1024) return (b / 1024d).ToString("0.#") + "K"; return b + "B"; } // ---------- 下载还原 ---------- private void 下载还原选中() { string 手机号 = 登录手机号; if (手机号.Length == 0) { MessageBox.Show(this, "请先登录会员(新版协议需登录换取云操作凭证)。", "项目云", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (_lv云端.SelectedItems.Count == 0) { MessageBox.Show(this, "请先在右侧选中要下载的项目。", "项目云", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var 选中 = new List(); foreach (ListViewItem it in _lv云端.SelectedItems) 选中.Add((string)it.Tag ?? it.Text); _btn下载.Enabled = false; int ok = 0; var 新号 = new List(); try { for (int i = 0; i < 选中.Count; i++) { 设状态("正在下载 " + (i + 1) + "/" + 选中.Count + ": " + 选中[i] + " ..."); string q = Uri.EscapeDataString("手机号") + "=" + Uri.EscapeDataString(手机号) + "&" + Uri.EscapeDataString("编号") + "=" + Uri.EscapeDataString(选中[i]); string zipPath = Path.Combine(Path.GetTempPath(), "成套报价_云取_" + Guid.NewGuid().ToString("N") + ".zip"); try { 会员服务Host.DownloadToFile("/api/proj/download?" + q, zipPath); string newPid = _owner.ImportProjectFromZipForCloud(zipPath); if (newPid != null) { ok++; 新号.Add(newPid); } } finally { try { File.Delete(zipPath); } catch { } } } 设状态("下载还原完成:成功 " + ok + "/" + 选中.Count + (新号.Count > 0 ? "(新编号: " + string.Join(", ", 新号.ToArray()) + ")" : "")); MessageBox.Show(this, "下载还原完成:成功 " + ok + "/" + 选中.Count + "。" + (新号.Count > 0 ? "\n新项目编号: " + string.Join(", ", 新号.ToArray()) : ""), "项目云", MessageBoxButtons.OK, MessageBoxIcon.Information); 填本地项目(); } catch (Exception ex) { Log("[项目云] 下载异常: " + ex.Message); 设状态("下载失败: " + ex.Message); MessageBox.Show(this, "下载失败:\n" + ex.Message, "项目云", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { _btn下载.Enabled = true; } } // ---------- 删除云端 ---------- private void 删除云端选中() { string 手机号 = 登录手机号; if (手机号.Length == 0 || string.IsNullOrEmpty(MainForm.当前登录密码)) { MessageBox.Show(this, "请先登录会员(删除云端项目需登录凭证)。", "项目云", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (_lv云端.SelectedItems.Count == 0) { MessageBox.Show(this, "请先在右侧选中要删除的云端项目。", "项目云", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var 选中 = new List(); foreach (ListViewItem it in _lv云端.SelectedItems) 选中.Add((string)it.Tag ?? it.Text); if (MessageBox.Show(this, "确定删除云端上的 " + 选中.Count + " 个项目?\n(本地项目不受影响,删除后不可恢复)", "项目云", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return; _btn删除云.Enabled = false; try { int ok = 0; string 失败原因 = ""; for (int i = 0; i < 选中.Count; i++) { 设状态("正在删除 " + (i + 1) + "/" + 选中.Count + ": " + 选中[i] + " ..."); // body 只带 手机号+编号;密码由 会员服务Host.Post 自动补当前登录密码 // (服务端 v1.5.1 起删除接口要求 手机号+密码+编号,漏带密码会报"手机号或密码错误") string body = "{\"手机号\":\"" + _owner.EscapeJsonStringForCloud(手机号) + "\",\"编号\":\"" + _owner.EscapeJsonStringForCloud(选中[i]) + "\"}"; string resp = 会员服务Host.Post("/api/proj/delete", body); if (resp != null && resp.IndexOf("\"ok\":true", StringComparison.OrdinalIgnoreCase) >= 0) ok++; else if (失败原因.Length == 0) 失败原因 = 选中[i] + ": " + (resp ?? "无响应"); // 服务端建议逐条间隔 200ms,避免触发限速 if (i < 选中.Count - 1) System.Threading.Thread.Sleep(200); } 设状态("云端删除完成:成功 " + ok + "/" + 选中.Count + (失败原因.Length > 0 ? "。失败:" + 失败原因 : "。")); Log("[项目云] 云端删除 " + ok + "/" + 选中.Count + (失败原因.Length > 0 ? " 失败:" + 失败原因 : "")); 刷云端(); } catch (Exception ex) { Log("[项目云] 删除异常: " + ex.Message); 设状态("删除失败: " + ex.Message); } finally { _btn删除云.Enabled = true; } } } /// 每日自动云上传(后台静默):每天首次进软件触发一次,全部项目覆盖上传。 internal static class 项目云自动上传 { private static System.Windows.Forms.Timer _timer; /// 写 diag 日志。 private static void Log(string s) { try { MainForm.写日志(s); } catch { } } /// 库文件清单(与 主窗口.母排与备份.cs 收集云端库清单 同款;方案库目录动态收集)。 private static System.Collections.Generic.List> 收集库清单() { var 出 = new System.Collections.Generic.List>(); string baseDir = AppDomain.CurrentDomain.BaseDirectory; 出.Add(new System.Collections.Generic.KeyValuePair("LIB_铜排库", @"sjk\advancedDataGridView8Table.db")); 出.Add(new System.Collections.Generic.KeyValuePair("LIB_铝排库", @"sjk\advancedDataGridView8Table_LMY.db")); 出.Add(new System.Collections.Generic.KeyValuePair("LIB_用户库", @"sjk\advancedDataGridView7Table.db")); 出.Add(new System.Collections.Generic.KeyValuePair("LIB_柜体库", @"sjk\advancedDataGridView10Table.db")); 出.Add(new System.Collections.Generic.KeyValuePair("LIB_fak方案库(含公式表)", @"sjk\fak.db")); 出.Add(new System.Collections.Generic.KeyValuePair("LIB_企业信息", @"shezhi\企业信息.config.xml")); string 方案目录 = Path.Combine(baseDir, "sjk", "方案库"); if (Directory.Exists(方案目录)) { foreach (var f in Directory.GetFiles(方案目录, "*.db")) 出.Add(new System.Collections.Generic.KeyValuePair( "LIB_方案_" + Path.GetFileNameWithoutExtension(f), "sjk\\方案库\\" + Path.GetFileName(f))); } return 出; } public static void 启动(MainForm owner) { if (_timer != null) return; _timer = new System.Windows.Forms.Timer { Interval = 60000 }; // 每分钟看一眼 _timer.Tick += (s, e) => { try { // 远程模式才自动传(本地 127.0.0.1 模式不传);未登录或无密码(新协议要求)不传 if (!会员服务Host.是远程) return; if (string.IsNullOrWhiteSpace(MainForm.当前登录手机号)) return; if (string.IsNullOrEmpty(MainForm.当前登录密码)) { Log("[项目云自动] 无登录密码,跳过(请重新登录)"); return; } string today = DateTime.Now.ToString("yyyy-MM-dd"); string flag = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); Directory.CreateDirectory(flag); flag = Path.Combine(flag, "项目云自动_" + MainForm.当前登录手机号 + ".txt"); if (File.Exists(flag) && File.ReadAllText(flag).Trim() == today) return; // 今天已传 // 后台线程执行,失败静默(只记 diag) var 项目们 = owner.ReadAllProjectsFromDatabaseForCloud(); var ids = new List(); foreach (var p in 项目们) { string bh = p.ContainsKey("项目编号") && p["项目编号"] != null ? p["项目编号"].ToString() : ""; if (bh.Length > 0) ids.Add(bh); } // 库文件清单(方案库/用户库/柜体库/铜排铝排库/企业信息)一并自动上传 var 库们 = 收集库清单(); File.WriteAllText(flag, today); if (ids.Count == 0 && 库们.Count == 0) return; var 窗 = new 项目云窗体(owner); ThreadPool.QueueUserWorkItem(delegate { try { int ok = 0, 总 = ids.Count + 库们.Count; foreach (var pid in ids) { try { if ((bool)窗.Invoke(new Func(窗.上传单个公开), pid)) ok++; } catch { } } foreach (var kv in 库们) { try { if (项目云窗体.备份单个库静态(owner, MainForm.当前登录手机号, kv.Key, kv.Value)) ok++; } catch { } } Log("[项目云自动] 每日自动上传完成: " + ok + "/" + 总); } catch (Exception ex) { Log("[项目云自动] 失败: " + ex.Message); } finally { try { 窗.Invoke((Action)窗.Dispose); } catch { } } }); } catch (Exception ex) { Log("[项目云自动] 异常: " + ex.Message); } }; _timer.Start(); } } }