chore: 初始化 TFS 2013 工作项批量导入工具集

- TfsExcelImporter: WinForms 桌面版,走 TFS Client OM(本环境 wit REST 路由 404,OM 是唯一可行通道)
- tfs_import.py: REST 版命令行导入器,供 wit 接口可用的 TFS 实例使用
- templates: Excel 导入模板及其 OOXML 源部件
- README: 两套工具定位、配置、环境坑点与验证方式
- 真实台账与本机工作笔记不入库,仅提交 work_items.example.csv 说明列结构
This commit is contained in:
chenshoulin
2026-11-01 11:21:20 +08:00
commit 8d1af0d11c
23 changed files with 2423 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using ClosedXML.Excel;
namespace TfsExcelImporter
{
/// <summary>一行待导入的工作项数据(列顺序与 Excel 模板一致)。</summary>
public class WorkItemRow
{
public string Title { get; set; }
public string State { get; set; }
public string AssignedTo { get; set; }
public string FinancialProject { get; set; }
public string Completed { get; set; }
public string LogDate { get; set; }
public string CreatedDate { get; set; }
public string ClosedDate { get; set; }
public string PbiId { get; set; }
public string Notes { get; set; }
}
/// <summary>
/// Excel 读取与模板生成。
/// 模板列: Title | State | Assigned To | FinancialProject | Completed | LogDate | Created Date | Closed Date | PBI Id | Notes
/// </summary>
public static class ExcelService
{
public static readonly string[] Headers =
{
"Title", "State", "Assigned To", "FinancialProject",
"Completed", "LogDate", "Created Date", "Closed Date", "PBI Id", "Notes"
};
private static readonly string[] DateFormats =
{
"yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss",
"yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm",
"yyyy-MM-dd", "yyyy/MM/dd"
};
// --------------------------------------------------------- 生成模板
public static void GenerateTemplate(string path)
{
using (var wb = new XLWorkbook())
{
var ws = wb.Worksheets.Add("工作项");
for (int i = 0; i < Headers.Length; i++)
{
var cell = ws.Cell(1, i + 1);
cell.Value = Headers[i];
cell.Style.Font.Bold = true;
cell.Style.Fill.BackgroundColor = XLColor.LightGray;
}
// 示例行(To Do / Done 各一,帮助理解格式)
var sample1 = new WorkItemRow
{
Title = "20261012 - 示例:货载报告解析并上传凭证受托报告相关内容",
State = "To Do",
AssignedTo = "李晓柯",
FinancialProject = "信托 - 上海信托",
Completed = "5",
LogDate = "2026-10-12",
CreatedDate = "2026-10-30 11:14:41",
ClosedDate = "",
Notes = "示例行,导入前请删除"
};
var sample2 = new WorkItemRow
{
Title = "20261009 示例:受托报告bug修复",
State = "Done",
AssignedTo = "陈卫宇",
FinancialProject = "信托 - 上海信托",
Completed = "8",
LogDate = "2026-10-09",
CreatedDate = "2026-10-10 18:20:00",
ClosedDate = "2026-10-30 18:00:00",
Notes = "示例行,导入前请删除"
};
WriteRow(ws, 2, sample1);
WriteRow(ws, 3, sample2);
ws.Columns().AdjustToContents();
ws.SheetView.FreezeRows(1);
wb.SaveAs(path);
}
}
private static void WriteRow(IXLWorksheet ws, int r, WorkItemRow row)
{
ws.Cell(r, 1).Value = row.Title ?? "";
ws.Cell(r, 2).Value = row.State ?? "";
ws.Cell(r, 3).Value = row.AssignedTo ?? "";
ws.Cell(r, 4).Value = row.FinancialProject ?? "";
ws.Cell(r, 5).Value = row.Completed ?? "";
ws.Cell(r, 6).Value = row.LogDate ?? "";
ws.Cell(r, 7).Value = row.CreatedDate ?? "";
ws.Cell(r, 8).Value = row.ClosedDate ?? "";
ws.Cell(r, 9).Value = row.PbiId ?? "";
ws.Cell(r, 10).Value = row.Notes ?? "";
}
// --------------------------------------------------------- 读取数据
/// <summary>读取 Excel(或同格式 CSV 由用户另存为 xlsx)数据行,跳过空标题行。</summary>
public static List<WorkItemRow> ReadRows(string path)
{
var rows = new List<WorkItemRow>();
using (var wb = new XLWorkbook(path))
{
var ws = wb.Worksheets.First();
var range = ws.RangeUsed();
if (range == null) return rows;
// 按表头名定位列(顺序不限),找不到的列按 Headers 顺序兜底
var headerMap = MapHeaders(ws, range);
for (int r = range.FirstRow().RowNumber() + 1; r <= range.LastRow().RowNumber(); r++)
{
var title = CellText(ws, r, headerMap, "Title");
if (string.IsNullOrWhiteSpace(title)) continue;
rows.Add(new WorkItemRow
{
Title = title.Trim(),
State = CellText(ws, r, headerMap, "State"),
AssignedTo = CellText(ws, r, headerMap, "Assigned To"),
FinancialProject = CellText(ws, r, headerMap, "FinancialProject"),
Completed = CellText(ws, r, headerMap, "Completed"),
LogDate = CellText(ws, r, headerMap, "LogDate"),
CreatedDate = CellText(ws, r, headerMap, "Created Date"),
ClosedDate = CellText(ws, r, headerMap, "Closed Date"),
PbiId = CellText(ws, r, headerMap, "PBI Id"),
Notes = CellText(ws, r, headerMap, "Notes")
});
}
}
return rows;
}
private static Dictionary<string, int> MapHeaders(IXLWorksheet ws, IXLRange range)
{
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var firstRow = range.FirstRow().RowNumber();
int lastCol = range.LastColumn().ColumnNumber();
for (int c = 1; c <= lastCol; c++)
{
var text = ws.Cell(firstRow, c).GetString().Trim();
if (text.Length == 0) continue;
// 归一化:去掉空格/下划线,兼容 "Assigned To" / "AssignedTo" / "Created Date" 等
var key = text.Replace(" ", "").Replace("_", "");
if (!map.ContainsKey(key)) map[key] = c;
}
// 兜底:模板标准列名
foreach (var h in Headers)
{
var key = h.Replace(" ", "");
if (!map.ContainsKey(key))
{
int idx = Array.IndexOf(Headers, h) + 1;
if (idx <= lastCol) map[key] = idx;
}
}
return map;
}
private static string CellText(IXLWorksheet ws, int row, Dictionary<string, int> map, string header)
{
int col;
if (!map.TryGetValue(header.Replace(" ", ""), out col)) return "";
var cell = ws.Cell(row, col);
if (cell.IsEmpty()) return "";
if (cell.DataType == XLDataType.DateTime)
return cell.GetDateTime().ToString("yyyy-MM-dd HH:mm:ss");
if (cell.DataType == XLDataType.Number)
{
double d;
if (double.TryParse(cell.GetString(), out d))
{
// 处理 Excel 日期序列号(如 46300.0)
if (d > 20000 && d < 80000 && Math.Abs(d - Math.Floor(d)) < 0.001)
{
try { return DateTime.FromOADate(d).ToString("yyyy-MM-dd HH:mm:ss"); }
catch { }
}
return d.ToString("0.####");
}
}
return cell.GetString().Trim();
}
// --------------------------------------------------------- 时间转换
/// <summary>把单元格文本转成 TFS 接受的 ISO 时间;空值返回 null;解析失败抛异常。</summary>
public static string ToIsoDateTime(string value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
value = value.Trim();
foreach (var fmt in DateFormats)
{
DateTime dt;
if (DateTime.TryParseExact(value, fmt, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out dt))
return dt.ToString("yyyy-MM-ddTHH:mm:ss");
}
DateTime any;
if (DateTime.TryParse(value, out any))
return any.ToString("yyyy-MM-ddTHH:mm:ss");
throw new FormatException("无法解析时间: " + value);
}
/// <summary>Completed 数值:整数去小数点,非法值返回 null。</summary>
public static object ToNumber(string value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
double d;
if (!double.TryParse(value.Trim(), out d))
throw new FormatException("无法解析数值: " + value);
return Math.Abs(d - Math.Floor(d)) < 0.0001 ? (object)(int)Math.Floor(d) : d;
}
}
}