diff --git a/TfsExcelImporter/Config.cs b/TfsExcelImporter/Config.cs
index 0cdc9c7..75fa873 100644
--- a/TfsExcelImporter/Config.cs
+++ b/TfsExcelImporter/Config.cs
@@ -1,5 +1,7 @@
using System;
using System.IO;
+using System.Security.Cryptography;
+using System.Text;
using Newtonsoft.Json;
namespace TfsExcelImporter
@@ -20,8 +22,43 @@ namespace TfsExcelImporter
// 认证固定 NTLM:用户名可写 user,也可写 DOMAIN\user
public string Username { get; set; } = "";
+
+ // 明文密码只存在于内存(UI 回显需要);落盘一律走 DPAPI 加密的 PasswordEnc。
+ // [JsonIgnore] 防止明文随 SerializeObject 写进 config.json。
+ [JsonIgnore]
public string Password { get; set; } = "";
+ // 兼容旧版明文键:反序列化时承接旧 config.json 的 "Password",下次 Save 自动转为加密存储。
+ // getter 恒返 null,序列化不再输出明文键。
+ [JsonProperty("Password", NullValueHandling = NullValueHandling.Ignore)]
+ private string LegacyPassword { get { return null; } set { if (!string.IsNullOrEmpty(value)) Password = value; } }
+
+ // DPAPI(CurrentUser) 加密后的密码,Base64;同机同用户可解,拷到别的机器/账户需重输。
+ [JsonProperty("PasswordEnc", NullValueHandling = NullValueHandling.Ignore)]
+ private string PasswordEnc { get; set; } = "";
+
+ // ------------------------------------------------------------- DPAPI
+
+ private static string Protect(string plain)
+ {
+ var bytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(plain), null, DataProtectionScope.CurrentUser);
+ return Convert.ToBase64String(bytes);
+ }
+
+ /// 解密失败返回 null(不抛异常,让调用方回退到空密码)。
+ private static string TryUnprotect(string cipher)
+ {
+ try
+ {
+ var bytes = ProtectedData.Unprotect(Convert.FromBase64String(cipher), null, DataProtectionScope.CurrentUser);
+ return Encoding.UTF8.GetString(bytes);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
// 旧版本有 BypassRules 开关,已移除:TFS 2013 对应的 Client OM 14.x 没有 SaveFlags.BypassRules
// (15.x 才加,而 15.x 不兼容 TFS 2013),勾选从未生效。旧 config.json 里的残留键由
// Newtonsoft 按默认 MissingMemberHandling.Ignore 忽略,不需要手工清理。
@@ -80,7 +117,16 @@ namespace TfsExcelImporter
if (File.Exists(ConfigPath))
{
var cfg = JsonConvert.DeserializeObject(File.ReadAllText(ConfigPath));
- if (cfg != null) return cfg;
+ if (cfg != null)
+ {
+ // 加密值优先于旧明文键;解密失败(换机/换账户/文件被改)留空,用户重输即可
+ if (!string.IsNullOrEmpty(cfg.PasswordEnc))
+ {
+ var plain = TryUnprotect(cfg.PasswordEnc);
+ if (plain != null) cfg.Password = plain;
+ }
+ return cfg;
+ }
}
}
catch (Exception ex)
@@ -92,6 +138,8 @@ namespace TfsExcelImporter
public void Save()
{
+ // 落盘前把内存中的明文加密;Password 本身带 [JsonIgnore] 不会写出
+ PasswordEnc = string.IsNullOrEmpty(Password) ? "" : Protect(Password);
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
try
{
diff --git a/TfsExcelImporter/MainForm.cs b/TfsExcelImporter/MainForm.cs
index a34f159..90fe9ae 100644
--- a/TfsExcelImporter/MainForm.cs
+++ b/TfsExcelImporter/MainForm.cs
@@ -39,7 +39,7 @@ namespace TfsExcelImporter
_btnLoadIterations.Click += async (s, e) => await LoadIterationsAsync();
_btnBrowse.Click += (s, e) => BrowseExcel();
_btnTemplate.Click += (s, e) => ExportTemplate();
- _btnPreview.Click += (s, e) => PreviewData();
+ _btnPreview.Click += async (s, e) => await PreviewDataAsync();
_btnImport.Click += async (s, e) => await ImportAsync();
_btnFixArea.Click += async (s, e) => await FixAreaAsync();
_btnLinkParent.Click += async (s, e) => await LinkParentAsync();
@@ -222,7 +222,7 @@ namespace TfsExcelImporter
}
// ------------------------------------------------------------- Excel
- private void BrowseExcel()
+ private async void BrowseExcel()
{
using (var dlg = new OpenFileDialog())
{
@@ -235,11 +235,11 @@ namespace TfsExcelImporter
if (dlg.ShowDialog(this) != DialogResult.OK) return;
_txtExcel.Text = dlg.FileName;
SaveUiToConfig();
- PreviewData();
+ await PreviewDataAsync();
}
}
- private void ExportTemplate()
+ private async void ExportTemplate()
{
using (var dlg = new SaveFileDialog())
{
@@ -253,7 +253,7 @@ namespace TfsExcelImporter
Log("模板已导出: " + dlg.FileName);
_txtExcel.Text = dlg.FileName;
SaveUiToConfig();
- PreviewData();
+ await PreviewDataAsync();
}
catch (Exception ex)
{
@@ -263,7 +263,10 @@ namespace TfsExcelImporter
}
}
- private void PreviewData()
+ // ------------------------------------------------------------- 预览
+ // ClosedXML 全量载入工作簿,大文件在 UI 线程读会冻结界面——放 Task.Run,
+ // 读期间 SetBusy 防止重复点击;await 后自动回到 UI 线程再更新表格。
+ private async Task PreviewDataAsync()
{
var path = _txtExcel.Text.Trim();
if (string.IsNullOrEmpty(path) || !File.Exists(path))
@@ -272,9 +275,10 @@ namespace TfsExcelImporter
Append("预览未执行:Excel 文件不存在", LogLevel.Warn);
return;
}
+ SetBusy(true);
try
{
- _rows = ExcelService.ReadRows(path);
+ _rows = await Task.Run(() => ExcelService.ReadRows(path));
_grid.DataSource = null;
_grid.DataSource = _rows.Select((r, i) => new
{
@@ -305,6 +309,10 @@ namespace TfsExcelImporter
ShowError(_errExcel, _fExcel, _cardData, "读取失败:" + FirstLine(ex.Message));
UpdateEmptyState();
}
+ finally
+ {
+ SetBusy(false);
+ }
}
// ------------------------------------------------------------- Area 修正
@@ -421,13 +429,15 @@ namespace TfsExcelImporter
}
if (_rows.Count == 0)
{
- try { _rows = ExcelService.ReadRows(path); }
+ SetBusy(true);
+ try { _rows = await Task.Run(() => ExcelService.ReadRows(path)); }
catch (Exception ex)
{
ShowError(_errExcel, _fExcel, _cardData, "读取失败:" + FirstLine(ex.Message));
Log("读取 Excel 失败: " + FirstLine(ex.Message));
return;
}
+ finally { SetBusy(false); }
}
if (_rows.Count == 0)
{
diff --git a/TfsExcelImporter/TfsClient.cs b/TfsExcelImporter/TfsClient.cs
index 073336c..a35b2e4 100644
--- a/TfsExcelImporter/TfsClient.cs
+++ b/TfsExcelImporter/TfsClient.cs
@@ -367,7 +367,9 @@ namespace TfsExcelImporter
{
return await Task.Run(() =>
{
- var wiql = "SELECT [System.Id] FROM WorkItems WHERE " +
+ // SELECT 必须带全循环里要读的字段:OM 对查询未包含的字段会逐条触发完整加载
+ // (每条一次服务器往返),只 SELECT Id 会让批量修 Area 退化成 N+1
+ var wiql = "SELECT [System.Id], [System.Title], [System.State], [System.AreaPath] FROM WorkItems WHERE " +
$"[System.TeamProject] = '{EscapeWiql(project)}' AND " +
$"[System.WorkItemType] = '{EscapeWiql(workItemType)}' AND " +
$"[System.IterationPath] = '{EscapeWiql(iterationPath)}' AND " +
diff --git a/TfsExcelImporter/TfsExcelImporter.csproj b/TfsExcelImporter/TfsExcelImporter.csproj
index 85dc59d..f236f4d 100644
--- a/TfsExcelImporter/TfsExcelImporter.csproj
+++ b/TfsExcelImporter/TfsExcelImporter.csproj
@@ -31,6 +31,11 @@
+
+
+
+
+
PreserveNewest