590 lines
30 KiB
C#
590 lines
30 KiB
C#
// ============================================================
|
|
// 项目云窗体:把项目上传到会员服务器(项目云) / 从云下载还原 / 删除
|
|
// · 复用 主窗口.项目导入导出 的打包(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(); }
|
|
|
|
/// <summary>写 diag 日志(MainForm.写日志 静态入口)。</summary>
|
|
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<string, object> 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<string>();
|
|
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; }
|
|
}
|
|
|
|
/// <summary>每日自动上传用的公开入口(内部走 登录态,单独线程调用须自带 Invoke 保护)。</summary>
|
|
public bool 上传单个公开(string pid)
|
|
{
|
|
return 上传单个(登录手机号, pid);
|
|
}
|
|
|
|
/// <summary>单库备份静态入口(每日自动上传用;不碰 UI,显式传手机号)。
|
|
/// 库清单收集在 主窗口.母排与备份.cs 收集云端库清单() 的同款逻辑——此处独立列一份避免跨窗体引用。</summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>打包并上传单个项目。复用导出的 BackupSQLiteDb+meta.json+zip 链路。</summary>
|
|
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 大小; }
|
|
|
|
/// <summary>解析 /api/proj/list 的 项目:[{编号,名称,时间,大小}] 数组(中文键,轻量逐对象扫描)。</summary>
|
|
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<string, string>();
|
|
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 出;
|
|
}
|
|
|
|
/// <summary>从单个 JSON 对象文本里取字符串字段(处理 \uXXXX 与常规转义)。</summary>
|
|
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<string>();
|
|
foreach (ListViewItem it in _lv云端.SelectedItems) 选中.Add((string)it.Tag ?? it.Text);
|
|
|
|
_btn下载.Enabled = false;
|
|
int ok = 0; var 新号 = new List<string>();
|
|
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<string>();
|
|
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; }
|
|
}
|
|
}
|
|
|
|
/// <summary>每日自动云上传(后台静默):每天首次进软件触发一次,全部项目覆盖上传。</summary>
|
|
internal static class 项目云自动上传
|
|
{
|
|
private static System.Windows.Forms.Timer _timer;
|
|
|
|
/// <summary>写 diag 日志。</summary>
|
|
private static void Log(string s)
|
|
{
|
|
try { MainForm.写日志(s); } catch { }
|
|
}
|
|
|
|
/// <summary>库文件清单(与 主窗口.母排与备份.cs 收集云端库清单 同款;方案库目录动态收集)。</summary>
|
|
private static System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, string>> 收集库清单()
|
|
{
|
|
var 出 = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, string>>();
|
|
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
出.Add(new System.Collections.Generic.KeyValuePair<string, string>("LIB_铜排库", @"sjk\advancedDataGridView8Table.db"));
|
|
出.Add(new System.Collections.Generic.KeyValuePair<string, string>("LIB_铝排库", @"sjk\advancedDataGridView8Table_LMY.db"));
|
|
出.Add(new System.Collections.Generic.KeyValuePair<string, string>("LIB_用户库", @"sjk\advancedDataGridView7Table.db"));
|
|
出.Add(new System.Collections.Generic.KeyValuePair<string, string>("LIB_柜体库", @"sjk\advancedDataGridView10Table.db"));
|
|
出.Add(new System.Collections.Generic.KeyValuePair<string, string>("LIB_fak方案库(含公式表)", @"sjk\fak.db"));
|
|
出.Add(new System.Collections.Generic.KeyValuePair<string, string>("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<string, string>(
|
|
"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<string>();
|
|
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<string, bool>(窗.上传单个公开), 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();
|
|
}
|
|
}
|
|
}
|