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文件