Files
chenshoulin 14f5e32262 feat: 增强数据导入功能与用户界面优化
- 更新 README.md,强调 Area 路径为必填项,并增加加载 Area 列表的功能,确保用户在导入时不会遗漏必要信息。
- 修改 Config.cs 和 config.json,调整 SkipExistingTitles 默认值为 false,以便用户首次导入时能看到真实冲突和报错。
- 在 MainForm.cs 中添加加载 Area 列表的按钮,优化用户交互体验。
- 更新 ExcelService.cs,增强模板导出逻辑,确保用户能够更方便地使用自定义模板。
- 调整界面布局,确保 Area 路径与迭代路径的选择更加直观,提升整体用户体验。
2026-11-02 10:40:52 +08:00

169 lines
6.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}
}