using System; using System.IO; using System.Security.Cryptography; using System.Text; using Newtonsoft.Json; namespace TfsExcelImporter { /// /// 程序配置。保存于 exe 同目录 config.json,界面修改后自动写回。 /// 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); } /// 解密失败返回 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 忽略,不需要手工清理。 public bool SkipExistingTitles { get; set; } = true; 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; } } /// 当前实际使用的配置文件路径(界面提示用)。 public static string ConfigLocation { get { return ConfigPath; } } public static AppConfig Load() { try { if (File.Exists(ConfigPath)) { var cfg = JsonConvert.DeserializeObject(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); } } } }