refactor: 更新 README 和代码以支持默认模板导出功能

- README.md: 修改数据源描述,明确默认模板随程序发布,并更新操作流程中的按钮名称。
- ExcelService.cs: 重命名模板生成方法为 CopyTemplate,简化模板导出逻辑。
- MainForm.cs: 更新按钮文本和事件处理,改为导出默认模板。
- Config.cs 和 config.json: 添加 OriginalEstimateRef 字段以支持新功能。
- 删除不再使用的 build_template.ps1 脚本及相关模板部件。
This commit is contained in:
chenshoulin
2026-11-01 12:14:10 +08:00
parent 49d29133cc
commit c2d121d185
14 changed files with 74 additions and 292 deletions
+24 -10
View File
@@ -18,7 +18,7 @@ TFS 2013 工作项批量导入工具集,用于把 Excel/CSV 里的工时台账
| --- | --- | --- |
| 通信协议 | TFS Client OM(VS/Team Explorer 同款内部 SOAP) | REST `/_apis/wit/*` |
| 适用 | **本环境可用**。该 TFS 2013 实例只有 `/_apis/projects` 通,`wit/fields`、`wit/classificationnodes`、`wit/wiql` 全部 404 | 服务器开放了 wit REST 路由时使用 |
| 数据源 | `.xlsx`(内置模板) | `data/work_items.csv`(GBK/UTF-8 均可,需自备) |
| 数据源 | `.xlsx`(默认模板随程序发布) | `data/work_items.csv`(GBK/UTF-8 均可,需自备) |
| 界面 | 图形界面,预览后导入 | 命令行,`--dry-run` 先校验 |
Python 版在本环境会因 REST 路由 404 而失败,保留用于其它 TFS 实例或接口可用时的批量脚本化导入。
@@ -33,14 +33,12 @@ TFSImport/
├── data/
│ └── work_items.example.csv # 列结构与示例行(UTF-8 BOM);真实台账不入库
├── templates/
│ ├── TFS导入模板.xlsx # Excel 模板成品
│ ├── build_template.ps1 # 由 _parts 里的 XML 打包生成 xlsx
│ └── _parts/ # xlsx 的 OOXML 源文件
│ └── TFS导入模板.xlsx # 默认模板:csproj 随构建拷到 exe 目录,「导出Excel模板」原样复制它
└── TfsExcelImporter/ # C# WinForms 桌面版(主用)
├── TfsExcelImporter.csproj # net48 + ClosedXML + TFS ExtendedClient 14.*
├── MainForm.cs # 界面与导入/修 Area 流程
├── TfsClient.cs # TFS Client OM 封装
├── ExcelService.cs # 读 Excel、生成模板、时间/数值归一
├── ExcelService.cs # 读 Excel、导出默认模板、时间/数值归一
├── Config.cs # exe 同目录 config.json 读写
└── config.json # 桌面版配置
```
@@ -55,11 +53,13 @@ dotnet build TfsExcelImporter.sln -c Release
TfsExcelImporter\bin\Release\net48\TfsExcelImporter.exe
```
「导出Excel模板」复制的是 exe 同目录的 `TFS导入模板.xlsx`(构建时自动拷过去);单独分发 exe 时要带上这个文件,否则该按钮会报「未找到默认模板文件」。
操作流程:
1. 填「TFS 连接」区:URL、项目、工作项类型、认证方式与域账号;内网保持「使用系统代理」**不勾选**。
2. 点「加载迭代列表」,选择目标迭代。
3. 点「生成Excel模板」拿到表头,或「浏览...」选择已有 `.xlsx`;点「预览数据」核对。
3. 点「导出Excel模板」拿到默认模板,或「浏览...」选择已有 `.xlsx`;点「预览数据」核对。
4. 需要挂父项时填「父 PBI Id」(默认全部挂同一个;Excel 里加 `PBI Id` 列可逐行指定不同父项)。
5. 点「开始导入 TFS」,日志区逐行输出 `OK/SKIP/FAIL` 与工作项 ID。
6. 历史数据 Area 不对时用「按迭代修Area」批量纠正。
@@ -89,14 +89,28 @@ python tfs_import.py --input other.csv --limit 5
| `auth.mode` | `ntlm`(域账号,典型)/ `basic` / `default`(当前 Windows 凭据,仅桌面版) |
| `field_refs` | 语义名 → 字段引用名;自定义字段留空时按 `resolve_fields_by_display_name` 的显示名在服务器字段表里查(支持前缀模糊匹配,如 `Completed` → `Completed Work`) |
| `parent_title` / `parent_id` | 父工作项,二选一;按标题匹配不到会直接报错并提示改用 `parent_id` |
| `bypass_rules` | 是否尝试写入 `CreatedDate`/`ClosedDate` 等系统日期 |
| `bypass_rules` | 是否请求绕过字段校验规则;桌面版走 Client OM(14.x 无 `SaveFlags.BypassRules`),实际由服务器规则决定 |
| `skip_existing_titles` | 同项目/同类型/同迭代下按标题去重 |
> 配置里含明文密码,请勿提交到共享仓库;`config.json` 保持空值,凭据本地填写。
## Excel / CSV 列
`Title`、`State`、`Assigned To`、`FinancialProject`、`Completed`、`LogDate`、`Created Date`、`Closed Date`、`PBI Id`、`Notes`
桌面版 Excel 模板 = 仓库里的 `templates\TFS导入模板.xlsx`,界面「导出Excel模板」直接复制它:
`Title`、`State`、`Assigned To`、`FinancialProject`、`Completed`、`LogDate`、`PBI Id`、`Description`
写入 TFS 的对应关系:
| 模板列 | 目标字段 |
| --- | --- |
| `Description` | `System.Description` |
| `Completed` | 显示名 `Completed`(模糊匹配到 `Completed Work`)解析出的字段 |
| —(无列) | `Original Estimate` 取该行 `Completed` 的值;引用名留空则按显示名 `Original Estimate` 解析,可写在 `OriginalEstimateRef` 里固定 |
系统日期列(`Created Date` / `Closed Date`)已从模板移除:`System.CreatedDate` 由服务器按导入时间填写,`Microsoft.VSTS.Common.ClosedDate` 不再写入、保持为空。
Python 版仍读 `data/work_items.example.csv` 的旧列(含 `CreatedDate`、`ClosedDate`、`Notes`),未随模板调整。
真实台账含人员与项目信息,不入库;本地准备数据文件:
@@ -104,7 +118,7 @@ python tfs_import.py --input other.csv --limit 5
copy data\work_items.example.csv data\work_items.csv # Python 版默认读这个路径,已在 .gitignore 里
```
- 表头按名匹配,顺序不限,`AssignedTo` / `Assigned To` 等价;缺列自动按模板顺序兜底。
- 表头按名匹配,顺序不限,`AssignedTo` / `Assigned To` 等价;缺列按模板顺序兜底,但该列已被别的表头占用时不套用位置(旧版模板文件不会串列)。
- 空 `Title` 的行直接跳过;`State` 为空时用 `default_state`;未识别的列(如 `Group`)忽略。
- 日期支持 `yyyy-MM-dd[ / ][ HH:mm[:ss]]` 等格式,Excel 日期序列号(如 `46300`)会被还原成日期。
- CSV 从 Excel 另存时注意编码,脚本按 `utf-8-sig` → `gbk` 依次尝试。
@@ -116,7 +130,7 @@ copy data\work_items.example.csv data\work_items.csv # Python 版默认读这
- **wit REST 路由全 404**:TFS 2013 RTM/低 Update 不带 `/_apis/wit/*`,`Classification.asmx?WSDL` 也被禁 → 桌面版整体走 Client OM(NuGet `Microsoft.TeamFoundationServer.ExtendedClient` 钉 `14.*`,对应 VS2015 客户端代际;15.x 不兼容 TFS 2013)。
- **502 Bad Gateway**:TFS OM 内部用 `HttpWebRequest`,默认跟随 IE 系统代理,代理会拦内网 TFS。桌面版默认把 `WebRequest.DefaultWebProxy` 置空直连;确实要走代理时勾「使用系统代理」并带上 Windows 凭据过代理认证。
- **TF400276 / 关闭状态只读**:改已关闭工作项的 Area 会被拒;`FixAreaAsync` 走「重新打开 → 改 Area → 还原状态」的状态循环,中途失败回滚状态。
- **`System.CreatedDate` 写不进去**:OM 14.x 没有 `SaveFlags.BypassRules`(15.x 才加),该字段服务器托管;`Microsoft.VSTS.Common.ClosedDate` 在 `State=Done` 时尝试写入,校验或保存失败自动降级为普通保存,保证行不丢。
- **`System.CreatedDate` 写不进去**:OM 14.x 没有 `SaveFlags.BypassRules`(15.x 才加),该字段服务器托管;`Microsoft.VSTS.Common.ClosedDate` 在 `State=Done` 时按普通字段可尝试写入。模板已去掉日期列,所以导入不再走这条路——若某模板要求 `Done` 必填 Closed Date,需另加列或改回写日期。
- **孤立 Task 在积压工作页不可见**:TFS 2013 的 backlog/任务板按「父项 → 子任务」组织,没挂父链接的 Task 只能在查询里看到,所以导入必须带 `parent_title`/`parent_id`。
- **DNS 解析失败 / 超时**:多为 VPN 或网络断开,不是代码问题;桌面版会对这类异常附加排查提示。
+2
View File
@@ -36,6 +36,8 @@ namespace TfsExcelImporter
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; } = "";
+21 -74
View File
@@ -16,22 +16,23 @@ namespace TfsExcelImporter
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; }
public string Description { get; set; }
}
/// <summary>
/// Excel 读取与模板生成。
/// 模板列: Title | State | Assigned To | FinancialProject | Completed | LogDate | Created Date | Closed Date | PBI Id | Notes
/// Excel 读取与默认模板。模板内容(列宽、示例行、样式)只在
/// templates\TFS导入模板.xlsx 里维护,随构建复制到 exe 目录后原样导出。
/// 列: Title | State | Assigned To | FinancialProject | Completed | LogDate | PBI Id | Description
/// </summary>
public static class ExcelService
{
public const string TemplateFileName = "TFS导入模板.xlsx";
public static readonly string[] Headers =
{
"Title", "State", "Assigned To", "FinancialProject",
"Completed", "LogDate", "Created Date", "Closed Date", "PBI Id", "Notes"
"Completed", "LogDate", "PBI Id", "Description"
};
private static readonly string[] DateFormats =
@@ -41,68 +42,15 @@ namespace TfsExcelImporter
"yyyy-MM-dd", "yyyy/MM/dd"
};
// --------------------------------------------------------- 生成模板
// --------------------------------------------------------- 默认模板
public static void GenerateTemplate(string path)
/// <summary>把随程序发布的默认模板原样复制到目标路径。</summary>
public static void CopyTemplate(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 ?? "";
var src = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplateFileName);
if (!File.Exists(src))
throw new FileNotFoundException("未找到默认模板文件: " + src, src);
File.Copy(src, path, true);
}
// --------------------------------------------------------- 读取数据
@@ -132,10 +80,8 @@ namespace TfsExcelImporter
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")
Description = CellText(ws, r, headerMap, "Description")
});
}
}
@@ -151,7 +97,7 @@ namespace TfsExcelImporter
{
var text = ws.Cell(firstRow, c).GetString().Trim();
if (text.Length == 0) continue;
// 归一化:去掉空格/下划线,兼容 "Assigned To" / "AssignedTo" / "Created Date" 等
// 归一化:去掉空格/下划线,兼容 "Assigned To" / "AssignedTo" / "PBI Id" 等
var key = text.Replace(" ", "").Replace("_", "");
if (!map.ContainsKey(key)) map[key] = c;
}
@@ -159,11 +105,12 @@ namespace TfsExcelImporter
foreach (var h in Headers)
{
var key = h.Replace(" ", "");
if (!map.ContainsKey(key))
{
if (map.ContainsKey(key)) continue;
int idx = Array.IndexOf(Headers, h) + 1;
if (idx <= lastCol) map[key] = idx;
}
if (idx > lastCol) continue;
// 该列已有别的表头占用(如旧版模板的 Created Date 列)时不套用位置,避免读错列
bool taken = map.Values.Any(c => c == idx);
if (!taken) map[key] = idx;
}
return map;
}
+21 -31
View File
@@ -144,10 +144,10 @@ namespace TfsExcelImporter
_btnBrowse.Click += (s, e) => BrowseExcel();
grpImport.Controls.Add(_btnBrowse);
_btnTemplate.Text = "生成Excel模板";
_btnTemplate.Text = "导出Excel模板";
_btnTemplate.Location = new Point(760, 58); _btnTemplate.Size = new Size(110, 29);
_btnTemplate.Anchor = AnchorStyles.Top | AnchorStyles.Right;
_btnTemplate.Click += (s, e) => GenerateTemplate();
_btnTemplate.Click += (s, e) => ExportTemplate();
grpImport.Controls.Add(_btnTemplate);
_chkSkip.Text = "跳过已存在标题";
@@ -155,7 +155,7 @@ namespace TfsExcelImporter
_chkSkip.Anchor = AnchorStyles.Top | AnchorStyles.Right;
grpImport.Controls.Add(_chkSkip);
_chkBypass.Text = "bypassRules(可写系统日期)";
_chkBypass.Text = "bypassRules(绕过字段规则)";
_chkBypass.Location = new Point(886, 60); _chkBypass.Size = new Size(150, 24);
_chkBypass.Anchor = AnchorStyles.Top | AnchorStyles.Right;
grpImport.Controls.Add(_chkBypass);
@@ -357,25 +357,25 @@ namespace TfsExcelImporter
}
}
private void GenerateTemplate()
private void ExportTemplate()
{
using (var dlg = new SaveFileDialog())
{
dlg.Filter = "Excel 文件 (*.xlsx)|*.xlsx";
dlg.FileName = "TFS导入模板.xlsx";
dlg.Title = "保存 Excel 模板";
dlg.FileName = ExcelService.TemplateFileName;
dlg.Title = "导出默认模板";
if (dlg.ShowDialog(this) != DialogResult.OK) return;
try
{
ExcelService.GenerateTemplate(dlg.FileName);
Log("模板已生成: " + dlg.FileName);
MessageBox.Show(this, "模板已生成:\n" + dlg.FileName, "完成",
ExcelService.CopyTemplate(dlg.FileName);
Log("模板已导出: " + dlg.FileName);
MessageBox.Show(this, "模板已导出:\n" + dlg.FileName, "完成",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log("生成模板失败: " + ex.Message);
MessageBox.Show(this, "生成模板失败:\n" + ex.Message, "错误",
Log("导出模板失败: " + ex.Message);
MessageBox.Show(this, "导出模板失败:\n" + ex.Message, "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -402,9 +402,8 @@ namespace TfsExcelImporter
财务项目 = r.FinancialProject,
r.Completed,
LogDate = r.LogDate,
Created = r.CreatedDate,
Closed = r.ClosedDate,
r.Notes
r.PbiId,
r.Description
}).ToList();
_lblStatus.Text = $"已读取 {_rows.Count} 行";
Log($"读取 {path} 共 {_rows.Count} 行");
@@ -563,13 +562,12 @@ namespace TfsExcelImporter
var assignedRef = "System.AssignedTo";
var iterationRef = "System.IterationPath";
var areaRef = "System.AreaPath";
var createdRef = "System.CreatedDate";
var closedRef = "Microsoft.VSTS.Common.ClosedDate";
var finRef = TfsClient.ResolveRef(fieldMap, "FinancialProject", _cfg.FinancialProjectRef);
var compRef = TfsClient.ResolveRef(fieldMap, "Completed", _cfg.CompletedRef);
var logRef = TfsClient.ResolveRef(fieldMap, "LogDate", _cfg.LogDateRef);
Log($"字段: FinancialProject->{finRef ?? "(未找到)"} Completed->{compRef ?? "(未找到)"} LogDate->{logRef ?? "(未找到)"}");
var origEstRef = TfsClient.ResolveRef(fieldMap, "Original Estimate", _cfg.OriginalEstimateRef);
Log($"字段: FinancialProject->{finRef ?? "(未找到)"} Completed->{compRef ?? "(未找到)"} LogDate->{logRef ?? "(未找到)"} OriginalEstimate->{origEstRef ?? "(未找到)"}");
Log($"父PBI: '{(string.IsNullOrWhiteSpace(_cfg.ParentPbiId) ? "(未填)" : _cfg.ParentPbiId)}'——Excel 每行可用 PBI Id 列覆盖");
HashSet<string> existing = null;
@@ -612,25 +610,17 @@ namespace TfsExcelImporter
if (completed != null && compRef != null)
fields.Add(new KeyValuePair<string, object>(compRef, completed));
// Notes -> Description(该 Task 模板必填)
if (!string.IsNullOrWhiteSpace(row.Notes))
fields.Add(new KeyValuePair<string, object>("System.Description", row.Notes.Trim()));
// Original Estimate 默认取 Completed 的值
if (completed != null && origEstRef != null)
fields.Add(new KeyValuePair<string, object>(origEstRef, completed));
if (!string.IsNullOrWhiteSpace(row.Description))
fields.Add(new KeyValuePair<string, object>("System.Description", row.Description.Trim()));
var logDate = ExcelService.ToIsoDateTime(row.LogDate);
if (logDate != null && logRef != null)
fields.Add(new KeyValuePair<string, object>(logRef, logDate));
// 系统日期字段:bypassRules 时尝试写入
if (_chkBypass.Checked)
{
var created = ExcelService.ToIsoDateTime(row.CreatedDate);
if (created != null)
fields.Add(new KeyValuePair<string, object>(createdRef, created));
var closed = ExcelService.ToIsoDateTime(row.ClosedDate);
if (closed != null)
fields.Add(new KeyValuePair<string, object>(closedRef, closed));
}
// 父 PBI:行级 PBI Id 列优先,其次界面默认值
var pbiText = !string.IsNullOrWhiteSpace(row.PbiId) ? row.PbiId.Trim() : _cfg.ParentPbiId;
int? parentPbi = int.TryParse(pbiText, out var pbiNum) ? pbiNum : (int?)null;
+4
View File
@@ -26,6 +26,10 @@
<None Update="config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<!-- 默认导入模板:随 exe 一起发布,「导出Excel模板」原样复制这份文件 -->
<Content Include="..\templates\TFS导入模板.xlsx" Link="TFS导入模板.xlsx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
+1
View File
@@ -15,6 +15,7 @@
"FinancialProjectRef": "",
"CompletedRef": "",
"LogDateRef": "",
"OriginalEstimateRef": "",
"LastExcelPath": "",
"ParentPbiId": ""
}
Binary file not shown.
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
</Types>
-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
<borders count="1"><border/></borders>
<cellStyleXfs count="1"><xf/></cellStyleXfs>
<cellXfs count="1"><xf/></cellXfs>
</styleSheet>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets>
<sheet name="工作项" sheetId="1" r:id="rId1"/>
</sheets>
</workbook>
-41
View File
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1">
<c r="A1" t="inlineStr"><is><t>Title</t></is></c>
<c r="B1" t="inlineStr"><is><t>State</t></is></c>
<c r="C1" t="inlineStr"><is><t>Assigned To</t></is></c>
<c r="D1" t="inlineStr"><is><t>FinancialProject</t></is></c>
<c r="E1" t="inlineStr"><is><t>Completed</t></is></c>
<c r="F1" t="inlineStr"><is><t>LogDate</t></is></c>
<c r="G1" t="inlineStr"><is><t>Created Date</t></is></c>
<c r="H1" t="inlineStr"><is><t>Closed Date</t></is></c>
<c r="I1" t="inlineStr"><is><t>PBI Id</t></is></c>
<c r="J1" t="inlineStr"><is><t>Notes</t></is></c>
</row>
<row r="2">
<c r="A2" t="inlineStr"><is><t>20261012 - 示例:货载报告解析并上传凭证受托报告相关内容</t></is></c>
<c r="B2" t="inlineStr"><is><t>To Do</t></is></c>
<c r="C2" t="inlineStr"><is><t>李晓柯</t></is></c>
<c r="D2" t="inlineStr"><is><t>信托 - 上海信托</t></is></c>
<c r="E2" t="inlineStr"><is><t>5</t></is></c>
<c r="F2" t="inlineStr"><is><t>2026-10-12</t></is></c>
<c r="G2" t="inlineStr"><is><t>2026-10-30 11:14:41</t></is></c>
<c r="H2" t="inlineStr"><is><t></t></is></c>
<c r="I2" t="inlineStr"><is><t>219975</t></is></c>
<c r="J2" t="inlineStr"><is><t>示例行,导入前请删除</t></is></c>
</row>
<row r="3">
<c r="A3" t="inlineStr"><is><t>20261009 示例:受托报告bug修复</t></is></c>
<c r="B3" t="inlineStr"><is><t>Done</t></is></c>
<c r="C3" t="inlineStr"><is><t>陈卫宇</t></is></c>
<c r="D3" t="inlineStr"><is><t>信托 - 上海信托</t></is></c>
<c r="E3" t="inlineStr"><is><t>8</t></is></c>
<c r="F3" t="inlineStr"><is><t>2026-10-09</t></is></c>
<c r="G3" t="inlineStr"><is><t>2026-10-10 18:20:00</t></is></c>
<c r="H3" t="inlineStr"><is><t>2026-10-30 18:00:00</t></is></c>
<c r="I3" t="inlineStr"><is><t>219975</t></is></c>
<c r="J3" t="inlineStr"><is><t>示例行,导入前请删除</t></is></c>
</row>
</sheetData>
</worksheet>
-104
View File
@@ -1,104 +0,0 @@
# Build TFS import xlsx from XML parts as a stored (uncompressed) ZIP.
# Pure in-process .NET byte APIs: no external processes, no assembly loading.
# Output file name uses ASCII+char trick to stay encoding-safe.
$ErrorActionPreference = 'Stop'
$parts = Join-Path $PSScriptRoot '_parts'
$outName = 'TFS' + [char]0x5BFC + [char]0x5165 + [char]0x6A21 + [char]0x677F + '.xlsx'
$out = Join-Path $PSScriptRoot $outName
# ---------- CRC32 (IEEE 802.3) ----------
$tbl = New-Object 'System.Int64[]' 256
for ($i = 0; $i -lt 256; $i++) {
$c = [int64]$i
for ($k = 0; $k -lt 8; $k++) {
if (($c -band 1) -eq 1) { $c = (3988292384 -bxor ($c -shr 1)) } else { $c = ($c -shr 1) }
$c = $c -band 4294967295
}
$tbl[$i] = $c
}
function Get-Crc32([byte[]]$data) {
$crc = [int64]4294967295
foreach ($b in $data) {
$idx = [int](($crc -bxor [int64]$b) -band 255)
$crc = ($tbl[$idx] -bxor ($crc -shr 8)) -band 4294967295
}
return ($crc -bxor 4294967295)
}
# ---------- entry list (source relative path, zip entry name) ----------
$entries = @(
@{ src = '[Content_Types].xml'; zip = '[Content_Types].xml' },
@{ src = '_rels\.rels'; zip = '_rels/.rels' },
@{ src = 'xl\workbook.xml'; zip = 'xl/workbook.xml' },
@{ src = 'xl\_rels\workbook.xml.rels'; zip = 'xl/_rels/workbook.xml.rels' },
@{ src = 'xl\styles.xml'; zip = 'xl/styles.xml' },
@{ src = 'xl\worksheets\sheet1.xml'; zip = 'xl/worksheets/sheet1.xml' }
)
$ms = New-Object System.IO.MemoryStream
$central = New-Object System.Collections.ArrayList
foreach ($e in $entries) {
$srcPath = Join-Path $parts $e['src']
$bytes = [System.IO.File]::ReadAllBytes($srcPath)
$nameBytes = [System.Text.Encoding]::ASCII.GetBytes([string]$e['zip'])
$crc = Get-Crc32 $bytes
$offset = $ms.Position
$ms.Write([BitConverter]::GetBytes([uint32]0x04034b50), 0, 4) # local header signature
$ms.Write([BitConverter]::GetBytes([uint16]20), 0, 2) # version needed
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # flags
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # method: stored
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # mod time
$ms.Write([BitConverter]::GetBytes([uint16]33), 0, 2) # mod date 1980-01-01
$ms.Write([BitConverter]::GetBytes([uint32]$crc), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$bytes.Length), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$bytes.Length), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint16]$nameBytes.Length), 0, 2)
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # extra len
$ms.Write($nameBytes, 0, $nameBytes.Length)
$ms.Write($bytes, 0, $bytes.Length)
[void]$central.Add(@{ name = $nameBytes; crc = $crc; size = $bytes.Length; offset = $offset })
}
# ---------- central directory ----------
$cdStart = $ms.Position
foreach ($c in $central) {
$ms.Write([BitConverter]::GetBytes([uint32]0x02014b50), 0, 4) # central header signature
$ms.Write([BitConverter]::GetBytes([uint16]20), 0, 2) # made by
$ms.Write([BitConverter]::GetBytes([uint16]20), 0, 2) # version needed
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # flags
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # method
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # time
$ms.Write([BitConverter]::GetBytes([uint16]33), 0, 2) # date
$ms.Write([BitConverter]::GetBytes([uint32]$c['crc']), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$c['size']), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$c['size']), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint16]$c['name'].Length), 0, 2)
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # extra len
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # comment len
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # disk start
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # internal attrs
$ms.Write([BitConverter]::GetBytes([uint32]0), 0, 4) # external attrs
$ms.Write([BitConverter]::GetBytes([uint32]$c['offset']), 0, 4)
$ms.Write($c['name'], 0, $c['name'].Length)
}
$cdSize = $ms.Position - $cdStart
# ---------- end of central directory ----------
$ms.Write([BitConverter]::GetBytes([uint32]0x06054b50), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # disk number
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # cd disk
$ms.Write([BitConverter]::GetBytes([uint16]$central.Count), 0, 2)
$ms.Write([BitConverter]::GetBytes([uint16]$central.Count), 0, 2)
$ms.Write([BitConverter]::GetBytes([uint32]$cdSize), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint32]$cdStart), 0, 4)
$ms.Write([BitConverter]::GetBytes([uint16]0), 0, 2) # comment len
[System.IO.File]::WriteAllBytes($out, $ms.ToArray())
$ms.Dispose()
Write-Output ('CREATED ' + $out + ' size=' + (New-Object System.IO.FileInfo $out).Length)