- 更新 README.md,强调 Area 路径为必填项,并增加加载 Area 列表的功能,确保用户在导入时不会遗漏必要信息。 - 修改 Config.cs 和 config.json,调整 SkipExistingTitles 默认值为 false,以便用户首次导入时能看到真实冲突和报错。 - 在 MainForm.cs 中添加加载 Area 列表的按钮,优化用户交互体验。 - 更新 ExcelService.cs,增强模板导出逻辑,确保用户能够更方便地使用自定义模板。 - 调整界面布局,确保 Area 路径与迭代路径的选择更加直观,提升整体用户体验。
169 lines
6.9 KiB
C#
169 lines
6.9 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using Newtonsoft.Json;
|
||
|
||
namespace TfsExcelImporter
|
||
{
|
||
/// <summary>
|
||
/// 程序配置。保存于 exe 同目录 config.json,界面修改后自动写回。
|
||
/// </summary>
|
||
public class AppConfig
|
||
{
|
||
public string TfsUrl { get; set; } = "http://pdtfs01.dev.goldenstand.cn:8080/tfs/DefaultCollection";
|
||
public string Project { get; set; } = "Tools_Dev";
|
||
public string WorkItemType { get; set; } = "Task";
|
||
|
||
// 目标迭代:完整迭代路径,如 Tools_Dev\2026年Sprint\2026年Sprint10
|
||
public string IterationPath { get; set; } = "";
|
||
// 可选:区域路径
|
||
public string AreaPath { get; set; } = "";
|
||
|
||
// 认证固定 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);
|
||
}
|
||
|
||
/// <summary>解密失败返回 null(不抛异常,让调用方回退到空密码)。</summary>
|
||
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 忽略,不需要手工清理。
|
||
// 默认不勾选:首次导入时希望先看到真实冲突/报错,而不是被静默跳过
|
||
public bool SkipExistingTitles { get; set; } = false;
|
||
public string DefaultState { get; set; } = "To Do";
|
||
|
||
// 自定义字段引用名;留空则按 DisplayName 自动解析
|
||
public string FinancialProjectRef { get; set; } = "";
|
||
public string CompletedRef { get; set; } = "";
|
||
public string LogDateRef { get; set; } = "";
|
||
// Original Estimate 导入时取 Completed 的值;引用名留空则按显示名解析
|
||
public string OriginalEstimateRef { get; set; } = "";
|
||
|
||
public string LastExcelPath { get; set; } = "";
|
||
|
||
// 父 Product Backlog Item:所有 Task 默认挂接的 PBI Id(如 219975);Excel 每行 PBI Id 列可覆盖
|
||
public string ParentPbiId { get; set; } = "";
|
||
|
||
// 便携优先:配置写在 exe 同目录(拷走 exe 即可带走配置);
|
||
// exe 目录只读(如放在 Program Files)时回退到 %AppData%\TfsExcelImporter。
|
||
private static string PortableConfigPath
|
||
{
|
||
get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"); }
|
||
}
|
||
|
||
private static string RoamingConfigPath
|
||
{
|
||
get
|
||
{
|
||
return Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||
"TfsExcelImporter", "config.json");
|
||
}
|
||
}
|
||
|
||
private static string ConfigPath
|
||
{
|
||
get
|
||
{
|
||
if (File.Exists(PortableConfigPath)) return PortableConfigPath;
|
||
if (File.Exists(RoamingConfigPath)) return RoamingConfigPath;
|
||
return PortableConfigPath;
|
||
}
|
||
}
|
||
|
||
/// <summary>当前实际使用的配置文件路径(界面提示用)。</summary>
|
||
public static string ConfigLocation
|
||
{
|
||
get { return ConfigPath; }
|
||
}
|
||
|
||
public static AppConfig Load()
|
||
{
|
||
try
|
||
{
|
||
if (File.Exists(ConfigPath))
|
||
{
|
||
var cfg = JsonConvert.DeserializeObject<AppConfig>(File.ReadAllText(ConfigPath));
|
||
if (cfg != null)
|
||
{
|
||
// 加密值优先于旧明文键;解密失败(换机/换账户/文件被改)留空,用户重输即可
|
||
if (!string.IsNullOrEmpty(cfg.PasswordEnc))
|
||
{
|
||
var plain = TryUnprotect(cfg.PasswordEnc);
|
||
if (plain != null) cfg.Password = plain;
|
||
}
|
||
return cfg;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine("读取 config.json 失败: " + ex.Message);
|
||
}
|
||
return new AppConfig();
|
||
}
|
||
|
||
public void Save()
|
||
{
|
||
// 落盘前把内存中的明文加密;Password 本身带 [JsonIgnore] 不会写出
|
||
PasswordEnc = string.IsNullOrEmpty(Password) ? "" : Protect(Password);
|
||
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
|
||
try
|
||
{
|
||
File.WriteAllText(ConfigPath, json);
|
||
return;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine("写入 config.json 失败: " + ex.Message);
|
||
}
|
||
|
||
// exe 目录不可写(只读介质 / Program Files)时落到用户配置目录
|
||
try
|
||
{
|
||
var fallback = RoamingConfigPath;
|
||
Directory.CreateDirectory(Path.GetDirectoryName(fallback));
|
||
File.WriteAllText(fallback, json);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine("写入用户配置目录失败: " + ex.Message);
|
||
}
|
||
}
|
||
}
|
||
}
|