diff --git a/YD_Event.Application/Certificate/CertificateGenerateService.cs b/YD_Event.Application/Certificate/CertificateGenerateService.cs index 40bb111..ce8e9a7 100644 --- a/YD_Event.Application/Certificate/CertificateGenerateService.cs +++ b/YD_Event.Application/Certificate/CertificateGenerateService.cs @@ -12,6 +12,7 @@ using System.Diagnostics; using System.ComponentModel.DataAnnotations; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; +using DocumentFormat.OpenXml.Drawing.Wordprocessing; using System.IO; using System.Linq; using QuestPDF.Fluent; @@ -179,8 +180,8 @@ public class CertificateGenerateService : IDynamicApiController, ITransient qrCodeImage = memoryStream.ToArray(); } - // 12. 转换为PDF(包含二维码) - var pdfStream = await ConvertDocToPdf(processedStream, qrCodeImage); + // 12. 转换为PDF(包含二维码和数据替换) + var pdfStream = await ConvertDocToPdf(processedStream, qrCodeImage, input.Data, qrCodeData, placeholderMapping); // 13. 保存PDF文件到预设地址 await SavePdfFileToUrl(pdfStream, pdfUrl); @@ -262,128 +263,11 @@ public class CertificateGenerateService : IDynamicApiController, ITransient } /// - /// 转换DOC为PDF(使用LibreOffice,保持原始Word布局) + /// 转换DOC为PDF(使用QuestPDF) /// - private async Task ConvertDocToPdf(Stream docStream, byte[] qrCodeImage = null) + private async Task ConvertDocToPdf(Stream docStream, byte[] qrCodeImage = null, Dictionary data = null, Dictionary qrCodeData = null, Dictionary placeholderMapping = null) { - try - { - _logger.LogInformation("开始DOCX转PDF转换(使用LibreOffice)"); - - // 重置流位置 - docStream.Position = 0; - - // 如果有二维码,先把二维码加到DOCX中 - if (qrCodeImage != null) - { - docStream = await AddQRCodeToDocx(docStream, qrCodeImage); - } - - // 使用LibreOffice转换 - var pdfStream = await ConvertDocToPdfWithLibreOffice(docStream); - - pdfStream.Position = 0; - _logger.LogInformation("DOCX转PDF完成,PDF大小: {Size} bytes", pdfStream.Length); - return pdfStream; - } - catch (Exception ex) - { - _logger.LogError(ex, "DOCX转PDF失败(LibreOffice),尝试使用QuestPDF后备方案"); - - // 如果LibreOffice失败,使用QuestPDF作为后备 - docStream.Position = 0; - return await ConvertDocToPdfWithQuestPDF(docStream, qrCodeImage); - } - } - - /// - /// 使用LibreOffice转换DOCX到PDF - /// - private async Task ConvertDocToPdfWithLibreOffice(Stream docStream) - { - var tempDir = Path.Combine(Path.GetTempPath(), "YD_Event_Certs_" + Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(tempDir); - - try - { - // 保存临时DOCX文件 - var docxPath = Path.Combine(tempDir, "template.docx"); - await using (var fileStream = File.Create(docxPath)) - { - await docStream.CopyToAsync(fileStream); - } - - // 使用LibreOffice转换 - var libreOfficePath = GetLibreOfficePath(); - var pdfPath = Path.Combine(tempDir, "output.pdf"); - - var arguments = $"--headless --convert-to pdf --outdir \"{tempDir}\" \"{docxPath}\""; - - _logger.LogInformation("启动LibreOffice: {Path} {Args}", libreOfficePath, arguments); - - var processInfo = new System.Diagnostics.ProcessStartInfo - { - FileName = libreOfficePath, - Arguments = arguments, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true - }; - - using var process = System.Diagnostics.Process.Start(processInfo); - if (process == null) - { - throw new Exception("无法启动LibreOffice进程"); - } - - await process.WaitForExitAsync(); - - if (process.ExitCode != 0) - { - var error = await process.StandardError.ReadToEndAsync(); - _logger.LogError("LibreOffice转换失败: {Error}", error); - throw new Exception($"LibreOffice转换失败: {error}"); - } - - // 读取生成的PDF - if (!File.Exists(pdfPath)) - { - throw new Exception("LibreOffice未生成PDF文件"); - } - - var pdfStream = new MemoryStream(); - await using (var fileStream = File.OpenRead(pdfPath)) - { - await fileStream.CopyToAsync(pdfStream); - } - - pdfStream.Position = 0; - return pdfStream; - } - finally - { - // 清理临时文件 - try - { - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "清理临时文件失败: {Path}", tempDir); - } - } - } - - /// - /// 使用QuestPDF作为后备方案 - /// - private async Task ConvertDocToPdfWithQuestPDF(Stream docStream, byte[] qrCodeImage = null) - { - _logger.LogInformation("使用QuestPDF作为后备方案"); + _logger.LogInformation("开始DOCX转PDF转换(使用QuestPDF)"); // 重置流位置 docStream.Position = 0; @@ -401,35 +285,97 @@ public class CertificateGenerateService : IDynamicApiController, ITransient documentData.QRCodeMimeType = "image/png"; } + // 替换占位符内容 + if (data != null) + { + ReplacePlaceholdersInDocumentData(documentData, data, qrCodeData, placeholderMapping); + } + // 生成PDF var pdfStream = new MemoryStream(); await GeneratePdfWithQuestPDF(pdfStream, documentData); pdfStream.Position = 0; + _logger.LogInformation("DOCX转PDF完成,PDF大小: {Size} bytes", pdfStream.Length); return pdfStream; } /// - /// 添加二维码到DOCX(简单实现,实际项目中需要完善) + /// 替换DocumentData中的占位符 /// - private async Task AddQRCodeToDocx(Stream docStream, byte[] qrCodeImage) + private void ReplacePlaceholdersInDocumentData(DocumentData documentData, Dictionary data, Dictionary qrCodeData, Dictionary placeholderMapping) { - // 注意:这里简化实现,实际项目中需要: - // 1. 查找DOCX中的二维码占位符 - // 2. 将占位符替换为二维码图片 - // 3. 正确处理图片大小和位置 + const string placeholderPattern = @"\{\{([^}]+)\}\}"; - // 临时方案:暂时不添加二维码到DOCX,使用QuestPDF的后备方案处理 - _logger.LogWarning("添加二维码到DOCX功能待实现,将使用QuestPDF后备方案"); + // 记录占位符映射信息(便于调试) + if (placeholderMapping != null && placeholderMapping.Count > 0) + { + _logger.LogDebug("占位符映射配置: {Mapping}", string.Join(", ", placeholderMapping.Select(kvp => $"{kvp.Key}->{kvp.Value}"))); + } - // 直接返回原始流,后续会使用QuestPDF处理 - var resultStream = new MemoryStream(); - docStream.Position = 0; - await docStream.CopyToAsync(resultStream); - resultStream.Position = 0; - return resultStream; + // 记录数据字典中的key(便于调试) + if (data != null && data.Count > 0) + { + _logger.LogDebug("数据字典中的key: {Keys}", string.Join(", ", data.Keys)); + } + + foreach (var paragraph in documentData.Paragraphs) + { + var originalText = paragraph.Text; + var processedText = originalText; + + // 使用正则表达式替换所有占位符 + processedText = System.Text.RegularExpressions.Regex.Replace(processedText, placeholderPattern, match => + { + var key = match.Groups[1].Value.Trim(); + _logger.LogDebug("处理占位符: {Key}", key); + + // 优先使用二维码数据 + if (qrCodeData != null && qrCodeData.ContainsKey(key)) + { + _logger.LogDebug(" -> 使用二维码数据: {Value}", qrCodeData[key]); + return qrCodeData[key]; + } + + // 尝试使用占位符映射(中文显示名 -> 英文键名) + string mappedKey = key; + if (placeholderMapping != null && placeholderMapping.ContainsKey(key)) + { + mappedKey = placeholderMapping[key]; + _logger.LogDebug(" -> 占位符映射: {Original} -> {Mapped}", key, mappedKey); + } + + // 使用普通数据(先尝试映射后的key,再尝试原始key) + if (data != null) + { + if (data.ContainsKey(mappedKey)) + { + _logger.LogDebug(" -> 使用数据字典(映射后): {Key} = {Value}", mappedKey, data[mappedKey]); + return data[mappedKey] ?? string.Empty; + } + + if (data.ContainsKey(key)) + { + _logger.LogDebug(" -> 使用数据字典(原始): {Key} = {Value}", key, data[key]); + return data[key] ?? string.Empty; + } + } + + // 返回原始占位符(未找到数据) + _logger.LogDebug(" -> 未找到数据,保留原始占位符"); + return match.Value; + }); + + if (originalText != processedText) + { + _logger.LogDebug("占位符替换: {Original} -> {Processed}", originalText, processedText); + paragraph.Text = processedText; + } + } } + + /// /// 提取DOCX文档数据(包括段落、表格、背景图片等) /// @@ -445,26 +391,32 @@ public class CertificateGenerateService : IDynamicApiController, ITransient if (body != null) { + // 首先处理 Drawing 对象(浮于文字上方的文本框) + ExtractDrawingObjects(body, documentData); + // 提取段落(包含位置信息) foreach (var paragraph in body.Elements()) { var paragraphText = paragraph.InnerText?.Trim(); if (!string.IsNullOrEmpty(paragraphText)) { - // 过滤掉不应该出现的内容 - if (paragraphText.Contains("CITATION") || - paragraphText.StartsWith("right-") || - paragraphText.StartsWith("left-") || - paragraphText.StartsWith("top-") || - paragraphText.StartsWith("bottom-")) + // 增强过滤逻辑,过滤掉不应该出现的内容 + if (ShouldSkipParagraph(paragraphText)) { _logger.LogDebug("跳过不应该的内容: {Text}", paragraphText); continue; } + // 清理段落文本,移除不需要的内容 + var cleanedText = CleanParagraphText(paragraphText); + if (string.IsNullOrEmpty(cleanedText)) + { + continue; + } + var paragraphData = new ParagraphData { - Text = paragraphText, + Text = cleanedText, Style = DetermineParagraphStyle(paragraph), FontSize = GetParagraphFontSize(paragraph), Alignment = GetParagraphAlignment(paragraph), @@ -476,7 +428,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient GetParagraphPosition(paragraph, paragraphData); _logger.LogDebug("提取段落: Text={Text}, X={X}, Y={Y}", - paragraphText, paragraphData.PositionX, paragraphData.PositionY); + cleanedText, paragraphData.PositionX, paragraphData.PositionY); documentData.Paragraphs.Add(paragraphData); } @@ -646,6 +598,342 @@ public class CertificateGenerateService : IDynamicApiController, ITransient return "black"; } + /// + /// 判断是否应该跳过该段落 + /// + private bool ShouldSkipParagraph(string text) + { + if (string.IsNullOrEmpty(text)) return true; + + // 过滤掉包含明显内部元数据的内容 + var lowerText = text.ToLower(); + + // 过滤引用相关内容 + if (lowerText.Contains("citation")) return true; + + // 过滤纯数字的定位信息 + if (System.Text.RegularExpressions.Regex.IsMatch(text, @"^\d+$")) return true; + + // 过滤定位前缀(left-, right-, top-, bottom-) + if (text.StartsWith("right-") || text.StartsWith("left-") || + text.StartsWith("top-") || text.StartsWith("bottom-")) return true; + + return false; + } + + /// + /// 清理段落文本,移除不需要的内容 + /// + private string CleanParagraphText(string text) + { + if (string.IsNullOrEmpty(text)) return text; + + var cleaned = text; + + // 移除连续的数字序列(定位信息) + cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"\d{6,}", ""); + + // 移除 left-xxx, right-xxx, top-xxx, bottom-xxx 这类定位信息 + cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"(left-|right-|top-|bottom-)\d+", ""); + + // 移除重复的占位符(如 {{单位}}00{{单位}}) + var placeholderPattern = @"\{\{([^}]+)\}\}"; + var matches = System.Text.RegularExpressions.Regex.Matches(cleaned, placeholderPattern); + if (matches.Count > 1) + { + // 保留第一个占位符,移除后续重复的 + var firstMatch = matches[0]; + cleaned = cleaned.Substring(0, firstMatch.Index + firstMatch.Length); + } + + // 移除残留的 "00" 这类分隔符 + cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"00+", ""); + + // 移除首尾空格 + cleaned = cleaned.Trim(); + + return cleaned; + } + + /// + /// 提取 Drawing 对象(浮于文字上方的文本框) + /// + private void ExtractDrawingObjects(Body body, DocumentData documentData) + { + try + { + // 查找所有包含 Drawing 的段落 + foreach (var paragraph in body.Elements()) + { + // 查找段落中的 Drawing 元素 + var drawings = paragraph.Descendants().ToList(); + + foreach (var drawing in drawings) + { + ExtractTextFromDrawing(drawing, documentData); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "提取 Drawing 对象失败"); + } + } + + /// + /// 从 Drawing 对象中提取文本和位置信息 + /// + private void ExtractTextFromDrawing(DocumentFormat.OpenXml.Wordprocessing.Drawing drawing, DocumentData documentData) + { + try + { + // 尝试从 Drawing 对象中提取文本框内容 + var textBoxContents = drawing.Descendants().ToList(); + + foreach (var textBoxContent in textBoxContents) + { + // 提取文本框中的所有段落 + var paragraphs = textBoxContent.Elements().ToList(); + + foreach (var paragraph in paragraphs) + { + var paragraphText = paragraph.InnerText?.Trim(); + if (!string.IsNullOrEmpty(paragraphText)) + { + // 增强过滤逻辑,过滤掉不应该出现的内容 + if (ShouldSkipParagraph(paragraphText)) + { + _logger.LogDebug("跳过 Drawing 对象中的内容: {Text}", paragraphText); + continue; + } + + // 清理段落文本,移除不需要的内容 + var cleanedText = CleanParagraphText(paragraphText); + if (string.IsNullOrEmpty(cleanedText)) + { + continue; + } + + var paragraphData = new ParagraphData + { + Text = cleanedText, + Style = DetermineParagraphStyle(paragraph), + FontSize = GetParagraphFontSize(paragraph), + Alignment = GetParagraphAlignment(paragraph), + IsBold = IsParagraphBold(paragraph), + FontColor = GetParagraphColor(paragraph) + }; + + // 尝试从 Drawing 对象中提取位置信息 + ExtractPositionFromDrawing(drawing, paragraphData); + + _logger.LogDebug("从 Drawing 对象提取段落: Text={Text}, X={X}, Y={Y}", + cleanedText, paragraphData.PositionX, paragraphData.PositionY); + + documentData.Paragraphs.Add(paragraphData); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "从 Drawing 对象提取文本失败"); + } + } + + /// + /// 从 Drawing 对象中提取位置信息 + /// + private void ExtractPositionFromDrawing(DocumentFormat.OpenXml.Wordprocessing.Drawing drawing, ParagraphData paragraphData) + { + try + { + _logger.LogDebug("开始解析 Drawing 对象位置信息"); + + var outerXml = drawing.OuterXml; + _logger.LogDebug("Drawing 对象原始 XML (前800字符): {Xml}", outerXml.Substring(0, Math.Min(800, outerXml.Length))); + + // 方法1: 查找 simplePos 属性(通常用于绝对定位) + ExtractSimplePosition(outerXml, paragraphData); + + // 方法2: 如果 simplePos 没有值,查找 positionOffset 元素 + if (paragraphData.PositionX == null || paragraphData.PositionY == null) + { + ExtractPositionOffset(outerXml, paragraphData); + } + + // 注意:不使用 distL, distT 等距离属性 + // 这些属性不是文本框在页面上的坐标,而是: + // - 要么是文本框的内边距(文本到边框的距离) + // - 要么是文本框到页面边距的距离(而不是绝对坐标) + // 我们目前使用写死定位方式,不需要依赖这些值 + + _logger.LogDebug("最终提取的位置 - X: {X}, Y: {Y}", paragraphData.PositionX, paragraphData.PositionY); + } + catch (Exception ex) + { + _logger.LogError(ex, "从 Drawing 对象提取位置信息失败"); + } + } + + /// + /// 从 XML 中提取 simplePos 属性 + /// + private void ExtractSimplePosition(string xml, ParagraphData paragraphData) + { + try + { + // 查找 simplePos:x 和 simplePos:y 属性 + var xMatch = System.Text.RegularExpressions.Regex.Match(xml, @"simplePos:x=[""'](\d+)[""']"); + var yMatch = System.Text.RegularExpressions.Regex.Match(xml, @"simplePos:y=[""'](\d+)[""']"); + + if (xMatch.Success) + { + if (long.TryParse(xMatch.Groups[1].Value, out long x)) + { + paragraphData.PositionX = EmuToPoints(x); + _logger.LogDebug("simplePos X (EMU): {EMU}, (点): {Points}", x, paragraphData.PositionX); + } + } + + if (yMatch.Success) + { + if (long.TryParse(yMatch.Groups[1].Value, out long y)) + { + paragraphData.PositionY = EmuToPoints(y); + _logger.LogDebug("simplePos Y (EMU): {EMU}, (点): {Points}", y, paragraphData.PositionY); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "提取 simplePos 失败"); + } + } + + /// + /// 从 XML 中提取 positionOffset 元素 + /// + private void ExtractPositionOffset(string xml, ParagraphData paragraphData) + { + try + { + // 分别提取 positionH 和 positionV 中的 posOffset 和 relativeFrom + // 这样可以更准确地获取 X 和 Y 坐标及其参照对象 + var positionHPattern = @"]*relativeFrom=[""']([^""']+)[""'][^>]*>(?:\s*]*>(\d+)\s*)"; + var positionHMatch = System.Text.RegularExpressions.Regex.Match( + xml, + positionHPattern, + System.Text.RegularExpressions.RegexOptions.Singleline); + + var positionVPattern = @"]*relativeFrom=[""']([^""']+)[""'][^>]*>(?:\s*]*>(\d+)\s*)"; + var positionVMatch = System.Text.RegularExpressions.Regex.Match( + xml, + positionVPattern, + System.Text.RegularExpressions.RegexOptions.Singleline); + + // 日志输出参照对象信息 + if (positionHMatch.Success) + { + _logger.LogDebug("positionH 参照对象: {RelativeFrom}", positionHMatch.Groups[1].Value); + } + if (positionVMatch.Success) + { + _logger.LogDebug("positionV 参照对象: {RelativeFrom}", positionVMatch.Groups[1].Value); + } + + if (positionHMatch.Success && paragraphData.PositionX == null) + { + if (long.TryParse(positionHMatch.Groups[2].Value, out long x)) + { + paragraphData.PositionX = EmuToPoints(x); + _logger.LogDebug("positionH posOffset (EMU): {EMU}, (点): {Points}", x, paragraphData.PositionX); + } + } + + if (positionVMatch.Success && paragraphData.PositionY == null) + { + if (long.TryParse(positionVMatch.Groups[2].Value, out long y)) + { + paragraphData.PositionY = EmuToPoints(y); + _logger.LogDebug("positionV posOffset (EMU): {EMU}, (点): {Points}", y, paragraphData.PositionY); + } + } + + // 如果上面的方法失败,尝试简单的正则表达式匹配 + if (!positionHMatch.Success || !positionVMatch.Success) + { + _logger.LogDebug("尝试备用方案:简单匹配 posOffset"); + var allPosOffsetMatches = System.Text.RegularExpressions.Regex.Matches(xml, @"]*>(\d+)"); + + if (allPosOffsetMatches.Count >= 2) + { + if (long.TryParse(allPosOffsetMatches[0].Groups[1].Value, out long x) && paragraphData.PositionX == null) + { + paragraphData.PositionX = EmuToPoints(x); + _logger.LogDebug("备用方案 - posOffset X (EMU): {EMU}, (点): {Points}", x, paragraphData.PositionX); + } + + if (long.TryParse(allPosOffsetMatches[1].Groups[1].Value, out long y) && paragraphData.PositionY == null) + { + paragraphData.PositionY = EmuToPoints(y); + _logger.LogDebug("备用方案 - posOffset Y (EMU): {EMU}, (点): {Points}", y, paragraphData.PositionY); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "提取 positionOffset 失败"); + } + } + + /// + /// 从 XML 中提取 distL, distT 等距离属性 + /// + private void ExtractDistanceAttributes(string xml, ParagraphData paragraphData) + { + try + { + // 查找 distL (left distance) 和 distT (top distance) + var distLMatch = System.Text.RegularExpressions.Regex.Match(xml, @"distL=[""'](\d+)[""']"); + var distTMatch = System.Text.RegularExpressions.Regex.Match(xml, @"distT=[""'](\d+)[""']"); + + if (distLMatch.Success && paragraphData.PositionX == null) + { + if (long.TryParse(distLMatch.Groups[1].Value, out long x)) + { + paragraphData.PositionX = EmuToPoints(x); + _logger.LogDebug("distL X (EMU): {EMU}, (点): {Points}", x, paragraphData.PositionX); + } + } + + if (distTMatch.Success && paragraphData.PositionY == null) + { + if (long.TryParse(distTMatch.Groups[1].Value, out long y)) + { + paragraphData.PositionY = EmuToPoints(y); + _logger.LogDebug("distT Y (EMU): {EMU}, (点): {Points}", y, paragraphData.PositionY); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "提取距离属性失败"); + } + } + + /// + /// EMU (English Metric Unit) 转换为点 + /// 1 英寸 = 914400 EMU + /// 1 英寸 = 72 点 + /// 所以:1 EMU = 72 / 914400 点 = 1 / 12700 点 + /// + private double EmuToPoints(long emu) + { + return emu / 12700.0; + } + /// /// 获取段落位置信息 /// @@ -745,10 +1033,10 @@ public class CertificateGenerateService : IDynamicApiController, ITransient .FitArea(); } - // 使用 Column 来放置所有内容,但主要依赖备用位置(基于标准证书模板) + // 使用 Column 来放置所有内容 page.Content().Column(column => { - // 首先根据段落信息进行精确定位 + // 根据段落信息进行定位(使用从Word文档提取的位置信息) foreach (var paragraph in documentData.Paragraphs) { var fontSize = paragraph.FontSize ?? 14; @@ -758,14 +1046,9 @@ public class CertificateGenerateService : IDynamicApiController, ITransient _logger.LogDebug("处理段落: Text={Text}, X={X}, Y={Y}, FontSize={FontSize}, Bold={IsBold}", paragraph.Text, paragraph.PositionX, paragraph.PositionY, fontSize, isBold); - // 使用内容判断位置(基于标准证书模板,这是最可靠的) - var (positionX, positionY) = GetTextPosition(paragraph); - - _logger.LogDebug("备用位置 - X={X}, Y={Y}", positionX, positionY); - - // 总是使用备用位置(基于标准证书模板) - float x = (float)positionX; - float y = (float)positionY; + // 直接使用从Word文档提取的位置信息 + float x = (float)(paragraph.PositionX ?? 50); + float y = (float)(paragraph.PositionY ?? 50); _logger.LogDebug("最终位置 - X={X}, Y={Y}", x, y); @@ -800,68 +1083,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient await Task.CompletedTask; // 为了保持异步签名 } - /// - /// 根据段落内容确定文本位置(基于A4页面210x297mm,坐标单位为点) - /// - private (double X, double Y) GetTextPosition(ParagraphData paragraph) - { - // 根据文本内容和样式判断位置 - var text = paragraph.Text; - - // 根据证书模板的布局进行定位(A4页面,左上角为原点) - // 调整坐标以更好地匹配证书模板 - if (text.Contains("第九届上海国际交互绳大奖赛")) - { - return (150, 80); // 顶部标题位置(居中) - } - else if (text.Contains("Double Dutch Contest Shanghai")) - { - return (150, 120); // 英文标题位置(居中) - } - else if (text.Contains("CERTIFICATE")) - { - return (250, 200); // 证书标题位置(居中) - } - else if (text.Contains("比赛证书")) - { - return (250, 240); // 中文证书标题位置(居中) - } - else if (text.Contains("参赛单位")) - { - return (100, 320); // 参赛单位标签位置 - } - else if (text.Contains("{{danwei}}")) - { - return (200, 320); // 参赛单位值位置 - } - else if (text.Contains("参赛运动员")) - { - return (100, 370); // 参赛运动员标签位置 - } - else if (text.Contains("{{name}}")) - { - return (200, 370); // 参赛运动员值位置 - } - else if (text.Contains("项目组别")) - { - return (100, 420); // 项目组别标签位置 - } - else if (text.Contains("名次")) - { - return (100, 470); // 名次标签位置 - } - else if (text.Contains("第九届上海国际交互绳大奖赛组委会")) - { - return (150, 550); // 底部组织机构位置(居中) - } - else if (text.Contains("二零二五年十一月")) - { - return (250, 590); // 日期位置(居中) - } - - // 默认位置 - return (50, 50); - } + /// /// 确定段落样式 @@ -892,40 +1114,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient return "normal"; } - /// - /// 获取LibreOffice可执行文件路径 - /// - private string GetLibreOfficePath() - { - var possiblePaths = new[] - { - // Windows - @"C:\Program Files\LibreOffice\program\soffice.exe", - @"C:\Program Files (x86)\LibreOffice\program\soffice.exe", - @"C:\Program Files\LibreOffice\program\libreoffice.exe", - @"C:\Program Files (x86)\LibreOffice\program\libreoffice.exe", - - // Linux - "/usr/bin/libreoffice", - "/usr/bin/soffice", - "/opt/libreoffice/program/soffice", - - // macOS - "/Applications/LibreOffice.app/Contents/MacOS/soffice" - }; - - foreach (var path in possiblePaths) - { - if (System.IO.File.Exists(path)) - { - _logger.LogInformation("找到LibreOffice: {Path}", path); - return path; - } - } - - // 如果找不到,尝试使用系统PATH中的命令 - return OperatingSystem.IsWindows() ? "soffice" : "libreoffice"; - } + /// /// 保存PDF文件 diff --git a/YD_Event.Application/Event/EventScoreService.cs b/YD_Event.Application/Event/EventScoreService.cs index f3ed590..128eb7f 100644 --- a/YD_Event.Application/Event/EventScoreService.cs +++ b/YD_Event.Application/Event/EventScoreService.cs @@ -618,20 +618,20 @@ public class EventScoreService : BaseService, ITransient { ["name"] = person.Name, ["姓名"] = person.Name, - ["group"] = person.GroupName, - ["组别"] = person.GroupName, - ["项目组别"] = person.GroupName, - ["project"] = result.ProjectTypeName, - ["项目"] = result.ProjectTypeName, + //["group"] = person.GroupName, + //["组别"] = person.GroupName, + ["项目组别"] = $"{person.ProjectName}-{person.GroupName}", + //["project"] = result.ProjectTypeName, + //["项目"] = result.ProjectTypeName, ["score"] = result.Score, ["成绩"] = result.Score, ["rank"] = result.Rank.ToString(), ["名次"] = GetRankText(result.Rank), ["danwei"] = person.ProjectName ?? "个人", - ["单位"] = person.ProjectName ?? "个人", - ["参赛单位"] = person.ProjectName ?? "个人" + ["单位"] = "上海跃动文化科技有限公司", + ["参赛单位"] = "上海跃动文化科技有限公司" }; - + // 添加日期信息 var now = DateTime.Now; data["year"] = now.Year.ToString(); diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index 8953b0f..0514590 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -802,9 +802,14 @@ 批量生成PDF证书 - + - 转换DOC为PDF(优先使用LibreOffice,后备使用QuestPDF) + 转换DOC为PDF(使用QuestPDF) + + + + + 替换DocumentData中的占位符 @@ -837,6 +842,54 @@ 获取段落字体颜色 + + + 判断是否应该跳过该段落 + + + + + 清理段落文本,移除不需要的内容 + + + + + 提取 Drawing 对象(浮于文字上方的文本框) + + + + + 从 Drawing 对象中提取文本和位置信息 + + + + + 从 Drawing 对象中提取位置信息 + + + + + 从 XML 中提取 simplePos 属性 + + + + + 从 XML 中提取 positionOffset 元素 + + + + + 从 XML 中提取 distL, distT 等距离属性 + + + + + EMU (English Metric Unit) 转换为点 + 1 英寸 = 914400 EMU + 1 英寸 = 72 点 + 所以:1 EMU = 72 / 914400 点 = 1 / 12700 点 + + 获取段落位置信息 @@ -847,21 +900,11 @@ 使用QuestPDF生成PDF - - - 根据段落内容确定文本位置(基于A4页面210x297mm,坐标单位为点) - - 确定段落样式 - - - 获取LibreOffice可执行文件路径 - - 保存PDF文件