From 20ea1bc70c72132f0cfb3635a2556dbcbec05c6d Mon Sep 17 00:00:00 2001 From: fuenmao <980740792@qq.com> Date: Tue, 15 Sep 2026 16:43:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=87=E4=BB=B6=E6=A8=A1?= =?UTF-8?q?=E7=89=88=E5=8D=A0=E4=BD=8D=E7=AC=A6=E9=94=99=E8=AF=AF=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ContractTemplate/DocxTemplateProcessor.cs | 67 +++++++++++++++++++ .../OnboardingContractTemplateController.cs | 5 ++ 2 files changed, 72 insertions(+) diff --git a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/DocxTemplateProcessor.cs b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/DocxTemplateProcessor.cs index 0927f21..66b3828 100644 --- a/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/DocxTemplateProcessor.cs +++ b/SimpleAdmin.Application/Services/Onboarding/ContractTemplate/DocxTemplateProcessor.cs @@ -35,6 +35,7 @@ public static class DocxTemplateProcessor foreach (var paragraph in document.Descendants(WordNamespace + "p")) { var text = string.Concat(paragraph.Descendants(WordNamespace + "t").Select(node => node.Value)); + ValidatePlaceholderSyntax(text); foreach (global::System.Text.RegularExpressions.Match match in PlaceholderRegex.Matches(text)) codes.Add(match.Groups["code"].Value.Trim()); } @@ -52,6 +53,72 @@ public static class DocxTemplateProcessor } } + /// + /// 占位符提取正则只会匹配正确格式,因此必须先主动检查错误格式, + /// 避免缺少花括号或变量编码写错时被静默忽略并继续创建版本。 + /// + private static void ValidatePlaceholderSyntax(string text) + { + if (string.IsNullOrEmpty(text)) + return; + + if (text.Contains("{{", StringComparison.Ordinal) + || text.Contains("}}", StringComparison.Ordinal)) + { + throw Oops.Bah( + "DOCX 占位符格式错误:检测到中文全角花括号。请使用英文半角双花括号,例如 {{employee.EmployeeName}}。"); + } + + if (text.Contains("{{{", StringComparison.Ordinal) + || text.Contains("}}}", StringComparison.Ordinal)) + { + throw Oops.Bah( + "DOCX 占位符格式错误:检测到多余的花括号。占位符必须使用成对的英文半角双花括号,例如 {{employee.EmployeeName}}。"); + } + + var searchStart = 0; + while (searchStart < text.Length) + { + var openIndex = text.IndexOf("{{", searchStart, StringComparison.Ordinal); + var closeIndex = text.IndexOf("}}", searchStart, StringComparison.Ordinal); + + if (closeIndex >= 0 && (openIndex < 0 || closeIndex < openIndex)) + { + throw Oops.Bah( + $"DOCX 占位符格式错误:{BuildPlaceholderPreview(text, closeIndex)} 缺少对应的起始符“{{{{”。正确格式示例:{{{{employee.EmployeeName}}}}。"); + } + + if (openIndex < 0) + return; + + closeIndex = text.IndexOf("}}", openIndex + 2, StringComparison.Ordinal); + if (closeIndex < 0) + { + throw Oops.Bah( + $"DOCX 占位符格式错误:{BuildPlaceholderPreview(text, openIndex)} 缺少结束符“}}}}”。正确格式示例:{{{{employee.EmployeeName}}}}。"); + } + + var placeholder = text.Substring(openIndex, closeIndex + 2 - openIndex); + var match = PlaceholderRegex.Match(placeholder); + if (!match.Success || match.Length != placeholder.Length) + { + throw Oops.Bah( + $"DOCX 占位符格式错误:“{placeholder}”。变量编码只能包含英文字母、数字、下划线、点和短横线,并且必须使用成对的英文半角双花括号。"); + } + + searchStart = closeIndex + 2; + } + } + + private static string BuildPlaceholderPreview(string text, int markerIndex) + { + const int maxLength = 60; + var preview = text[markerIndex..].Trim(); + if (preview.Length > maxLength) + preview = preview[..maxLength] + "…"; + return $"“{preview}”"; + } + /// /// 使用任务变量快照替换 DOCX 中的占位符,支持占位符跨多个 Word 文本节点。 /// diff --git a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs index d511b2e..04ebfe3 100644 --- a/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs +++ b/SimpleAdmin.Web.Core/Controllers/Application/Onboarding/OnboardingContractTemplateController.cs @@ -37,6 +37,11 @@ public class OnboardingContractTemplateController if (!string.Equals(suffix, ".docx", StringComparison.OrdinalIgnoreCase)) throw Oops.Bah("合同模板仅支持 DOCX 文件"); + await using var input = file.OpenReadStream(); + using var bytes = new MemoryStream(); + await input.CopyToAsync(bytes); + DocxTemplateProcessor.ExtractVariableCodes(bytes.ToArray()); + return await _fileService.UploadFile(SysDictConst.FILE_ENGINE_ALIYUN_OSS, file); }