From c2d121d1855bf32211b52d44aa3557a87def2a65 Mon Sep 17 00:00:00 2001 From: chenshoulin Date: Sun, 1 Nov 2026 12:14:10 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=20README=20?= =?UTF-8?q?=E5=92=8C=E4=BB=A3=E7=A0=81=E4=BB=A5=E6=94=AF=E6=8C=81=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E6=A8=A1=E6=9D=BF=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md: 修改数据源描述,明确默认模板随程序发布,并更新操作流程中的按钮名称。 - ExcelService.cs: 重命名模板生成方法为 CopyTemplate,简化模板导出逻辑。 - MainForm.cs: 更新按钮文本和事件处理,改为导出默认模板。 - Config.cs 和 config.json: 添加 OriginalEstimateRef 字段以支持新功能。 - 删除不再使用的 build_template.ps1 脚本及相关模板部件。 --- README.md | 34 +++++-- TfsExcelImporter/Config.cs | 2 + TfsExcelImporter/ExcelService.cs | 97 +++++------------- TfsExcelImporter/MainForm.cs | 52 ++++------ TfsExcelImporter/TfsExcelImporter.csproj | 4 + TfsExcelImporter/config.json | 1 + templates/TFS导入模板.xlsx | Bin 4865 -> 10397 bytes templates/_parts/[Content_Types].xml | 8 -- templates/_parts/_rels/.rels | 4 - templates/_parts/xl/_rels/workbook.xml.rels | 5 - templates/_parts/xl/styles.xml | 8 -- templates/_parts/xl/workbook.xml | 6 -- templates/_parts/xl/worksheets/sheet1.xml | 41 -------- templates/build_template.ps1 | 104 -------------------- 14 files changed, 74 insertions(+), 292 deletions(-) delete mode 100644 templates/_parts/[Content_Types].xml delete mode 100644 templates/_parts/_rels/.rels delete mode 100644 templates/_parts/xl/_rels/workbook.xml.rels delete mode 100644 templates/_parts/xl/styles.xml delete mode 100644 templates/_parts/xl/workbook.xml delete mode 100644 templates/_parts/xl/worksheets/sheet1.xml delete mode 100644 templates/build_template.ps1 diff --git a/README.md b/README.md index ba056ad..52840df 100644 --- a/README.md +++ b/README.md @@ -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 或网络断开,不是代码问题;桌面版会对这类异常附加排查提示。 diff --git a/TfsExcelImporter/Config.cs b/TfsExcelImporter/Config.cs index 89f9eb9..e11c842 100644 --- a/TfsExcelImporter/Config.cs +++ b/TfsExcelImporter/Config.cs @@ -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; } = ""; diff --git a/TfsExcelImporter/ExcelService.cs b/TfsExcelImporter/ExcelService.cs index ce46e81..2faecb0 100644 --- a/TfsExcelImporter/ExcelService.cs +++ b/TfsExcelImporter/ExcelService.cs @@ -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; } } /// - /// 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 /// 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) + /// 把随程序发布的默认模板原样复制到目标路径。 + 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)) - { - int idx = Array.IndexOf(Headers, h) + 1; - if (idx <= lastCol) map[key] = idx; - } + if (map.ContainsKey(key)) continue; + int idx = Array.IndexOf(Headers, h) + 1; + if (idx > lastCol) continue; + // 该列已有别的表头占用(如旧版模板的 Created Date 列)时不套用位置,避免读错列 + bool taken = map.Values.Any(c => c == idx); + if (!taken) map[key] = idx; } return map; } diff --git a/TfsExcelImporter/MainForm.cs b/TfsExcelImporter/MainForm.cs index 0d8c87d..4767823 100644 --- a/TfsExcelImporter/MainForm.cs +++ b/TfsExcelImporter/MainForm.cs @@ -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 existing = null; @@ -612,25 +610,17 @@ namespace TfsExcelImporter if (completed != null && compRef != null) fields.Add(new KeyValuePair(compRef, completed)); - // Notes -> Description(该 Task 模板必填) - if (!string.IsNullOrWhiteSpace(row.Notes)) - fields.Add(new KeyValuePair("System.Description", row.Notes.Trim())); + // Original Estimate 默认取 Completed 的值 + if (completed != null && origEstRef != null) + fields.Add(new KeyValuePair(origEstRef, completed)); + + if (!string.IsNullOrWhiteSpace(row.Description)) + fields.Add(new KeyValuePair("System.Description", row.Description.Trim())); var logDate = ExcelService.ToIsoDateTime(row.LogDate); if (logDate != null && logRef != null) fields.Add(new KeyValuePair(logRef, logDate)); - // 系统日期字段:bypassRules 时尝试写入 - if (_chkBypass.Checked) - { - var created = ExcelService.ToIsoDateTime(row.CreatedDate); - if (created != null) - fields.Add(new KeyValuePair(createdRef, created)); - var closed = ExcelService.ToIsoDateTime(row.ClosedDate); - if (closed != null) - fields.Add(new KeyValuePair(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; diff --git a/TfsExcelImporter/TfsExcelImporter.csproj b/TfsExcelImporter/TfsExcelImporter.csproj index ce48ffc..35b37b4 100644 --- a/TfsExcelImporter/TfsExcelImporter.csproj +++ b/TfsExcelImporter/TfsExcelImporter.csproj @@ -26,6 +26,10 @@ PreserveNewest + + + PreserveNewest + diff --git a/TfsExcelImporter/config.json b/TfsExcelImporter/config.json index 3aaa39b..0f4b896 100644 --- a/TfsExcelImporter/config.json +++ b/TfsExcelImporter/config.json @@ -15,6 +15,7 @@ "FinancialProjectRef": "", "CompletedRef": "", "LogDateRef": "", + "OriginalEstimateRef": "", "LastExcelPath": "", "ParentPbiId": "" } diff --git a/templates/TFS导入模板.xlsx b/templates/TFS导入模板.xlsx index c5610adbd0620f1cc42620c7b60bf4d2657291f9..0a65aa9921231695b435d03f6d3d63c69bc5cd28 100644 GIT binary patch literal 10397 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 SADb2Vh4BXYBIix=s`r1|4m+s; literal 4865 zcmd^D&u<$=6n0BnL=ghiA|X!8id$vxu9KF;vRyTHoVZ18M1q6_sM@T@&XV=6wKGW* zPH+fqLjzPCAmvvh67>KihXQFJAx98GLLmMC4qn$e<%qz1@i;lN*jEyF2^7 z_ujnu-kX^nx$w}DlM(iNHuC!~`6t#6MpO9C?Hd_3c6WJi7$=iqJ5%(iE0*9!!l7yoJCkXn?9a&RR{n!aY~#Zq>_*% ztWBV@iepvIzU%(Cr2=uH4r{v>r83OwB*t)eSD;i-#^{jojY|X5~1x;;P+)p z8i^7Bcg^b;{p{(I-)#(}{Prsq@`aRW=L?D`Fj{v`9>$n#gE1_J+H6Se5};YNONgYz zAW>;1NPcf{FsA>MZk~7!;z`@%5YvE$uH)Lwm;HydEiu@jXyw?O4Jdy!H|eawl-UWrIel&{Ti8}X2nO`mA70)53W7=7Gc2Ht zr_G4a%=tfZwpCy}y0*I7Y8nl+pR>&jiXUVKv4Z2d;;U+%ljfm z>y&X-j8gLb_oRUk7Gc%Jf@V#bP#6?3?RKP-_zhuNHK0d`?p^w`-?UX3A zae;!7Eb4~Q(iEqfpd+2EsDvQf%)x`uxMvm4gq;J^D zR=Hvzf(l`%91rz^Re}(P4sVEu$-pKL>NhH@n?$ImFT5*MPv{pIhgKfbfOd8_`-o%+^RZGOeMIbXl?MeVcq zosI9R_vWjcx1FnB*Vo>1-utk2{UauD|Koek++FAGIcIev;Ec=C0iNYCOUQq*V$v3hC!Z&(_zv%o-~bURFR`fJPbC}3DzSE zijq)z6TM0pewc?36VCI5<4S-3^P$5!&x0^_)R%7TZr*U#Hl4W@=lVi@ZQZ$cduMSu zV7;A&Jsu5v_N1ZQ?~%~YvGJ)=b$iuWUI>_B1mo$H$nXF=i`SfmFP+t^VW=z* zwSRu`H1vhp%kiX}{F3{_U_+f?3h=8h5tkFP9EKn6gs1%M126}l=i>R4<3YNi99zR1 zOQyTYch^w54>)xAWQ6~*1vUe|U&yDIpn~9O)Vi3km7t#B7TD9H!`28+k=}+l2o3I`JgD|a%l`* - - - - - - - diff --git a/templates/_parts/_rels/.rels b/templates/_parts/_rels/.rels deleted file mode 100644 index ad3ced5..0000000 --- a/templates/_parts/_rels/.rels +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/templates/_parts/xl/_rels/workbook.xml.rels b/templates/_parts/xl/_rels/workbook.xml.rels deleted file mode 100644 index 45a0a46..0000000 --- a/templates/_parts/xl/_rels/workbook.xml.rels +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/templates/_parts/xl/styles.xml b/templates/_parts/xl/styles.xml deleted file mode 100644 index 75ce262..0000000 --- a/templates/_parts/xl/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/templates/_parts/xl/workbook.xml b/templates/_parts/xl/workbook.xml deleted file mode 100644 index c26af18..0000000 --- a/templates/_parts/xl/workbook.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/templates/_parts/xl/worksheets/sheet1.xml b/templates/_parts/xl/worksheets/sheet1.xml deleted file mode 100644 index 25ce4c8..0000000 --- a/templates/_parts/xl/worksheets/sheet1.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - -Title -State -Assigned To -FinancialProject -Completed -LogDate -Created Date -Closed Date -PBI Id -Notes - - -20261012 - 示例:货载报告解析并上传凭证受托报告相关内容 -To Do -李晓柯 -信托 - 上海信托 -5 -2026-10-12 -2026-10-30 11:14:41 - -219975 -示例行,导入前请删除 - - -20261009 示例:受托报告bug修复 -Done -陈卫宇 -信托 - 上海信托 -8 -2026-10-09 -2026-10-10 18:20:00 -2026-10-30 18:00:00 -219975 -示例行,导入前请删除 - - - diff --git a/templates/build_template.ps1 b/templates/build_template.ps1 deleted file mode 100644 index 614e135..0000000 --- a/templates/build_template.ps1 +++ /dev/null @@ -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)