git忽略文件

This commit is contained in:
马梓彭 2026-03-05 15:36:37 +08:00
parent d8a1e8654b
commit 62bb7903a0
3 changed files with 640 additions and 312 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
.vscode
bin
obj
wwwroot
*.jfm
*.dbmdl
*.user

View File

@ -12,8 +12,12 @@ using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.ComponentModel.DataAnnotations;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using System.IO;
using System.Linq;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace YD_Event.Application.Certificate;
@ -138,7 +142,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
using var httpClient = new HttpClient();
var templateStream = new MemoryStream(await httpClient.GetByteArrayAsync(template.DocxTemplateUrl));
// 7. 生成二维码数据
// 7. 生成二维码数据(用于DOC模板中的占位符)
var qrCodeData = new Dictionary<string, string>();
if (input.GenerateQRCode)
{
@ -150,20 +154,33 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
// 8. 处理模板
var processedStream = _docxProcessor.ProcessTemplate(templateStream, input.Data, qrCodeData);
// 9. 转换为PDF
var pdfStream = await ConvertDocToPdf(processedStream);
// 9. 先生成证书保存地址(预设的PDF地址)
var pdfUrl = GeneratePdfUrl(input.CertificateId, certificate, template);
// 10. 生成二维码图片(使用预设的PDF地址)
byte[] qrCodeImage = null;
if (input.GenerateQRCode)
{
using var qrStream = _qrCodeService.GenerateQRCode(pdfUrl, input.QRCodeSize);
using var memoryStream = new MemoryStream();
qrStream.CopyTo(memoryStream);
qrCodeImage = memoryStream.ToArray();
}
// 11. 转换为PDF(包含二维码)
var pdfStream = await ConvertDocToPdf(processedStream, qrCodeImage);
// 10. 保存PDF文件
var pdfUrl = await SavePdfFile(pdfStream, certificate, template);
// 12. 保存PDF文件到预设地址
await SavePdfFileToUrl(pdfStream, pdfUrl);
// 11. 生成二维码图片(如果需要)
// 12. 生成二维码图片(如果需要)
string qrCodeUrl = null;
if (input.GenerateQRCode)
{
qrCodeUrl = await GenerateQRCodeImage(input.CertificateId, input.QRCodeSize);
}
// 12. 设置结果
// 13. 设置结果
result.PdfUrl = pdfUrl;
result.QRCodeUrl = qrCodeUrl;
result.VerificationUrl = GenerateCertificateUrl(input.CertificateId);
@ -233,358 +250,493 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
}
/// <summary>
/// 转换DOC为PDF(使用DocumentFormat.OpenXml解析并生成PDF)
/// 转换DOC为PDF(使用QuestPDF和DocumentFormat.OpenXml)
/// </summary>
private async Task<Stream> ConvertDocToPdf(Stream docStream)
private async Task<Stream> ConvertDocToPdf(Stream docStream, byte[] qrCodeImage = null)
{
try
{
_logger.LogInformation("开始DOC转PDF转换");
_logger.LogInformation("开始DOCX转PDF转换");
var pdfStream = new MemoryStream();
string content = "";
// 重置流位置
docStream.Position = 0;
try
{
// 尝试解析DOCX文件
content = await ExtractDocxContent(docStream);
_logger.LogInformation("DOCX解析成功,内容长度: {Length}", content.Length);
}
catch (Exception ex)
{
_logger.LogError(ex, "DOCX解析失败,尝试直接读取");
// 如果解析失败,直接读取流内容
docStream.Position = 0;
using var reader = new StreamReader(docStream, System.Text.Encoding.UTF8);
content = await reader.ReadToEndAsync();
}
// 使用QuestPDF许可证
QuestPDF.Settings.License = LicenseType.Community;
// 如果内容为空,使用默认内容
if (string.IsNullOrWhiteSpace(content))
// 解析DOCX内容
var documentData = await ExtractDocumentData(docStream);
// 添加二维码图片
if (qrCodeImage != null)
{
content = "Certificate Content\n\nThis is a certificate generated from DOC template.\n\nTemplate processing completed successfully.";
documentData.QRCodeImage = qrCodeImage;
documentData.QRCodeMimeType = "image/png";
}
// 生成PDF
await GeneratePdfFromContent(pdfStream, content);
var pdfStream = new MemoryStream();
await GeneratePdfWithQuestPDF(pdfStream, documentData);
pdfStream.Position = 0;
_logger.LogInformation("DOC转PDF完成,PDF大小: {Size} bytes", pdfStream.Length);
_logger.LogInformation("DOCX转PDF完成,PDF大小: {Size} bytes", pdfStream.Length);
return pdfStream;
}
catch (Exception ex)
{
_logger.LogError(ex, "DOC转PDF失败,使用备用方案");
return await GenerateSimplePdf(docStream);
_logger.LogError(ex, "DOCX转PDF失败");
throw new Exception("DOCX转PDF转换失败", ex);
}
}
/// <summary>
/// 提取DOCX文件内容
/// 提取DOCX文档数据(包括段落、表格、背景图片等)
/// </summary>
private async Task<string> ExtractDocxContent(Stream docStream)
private async Task<DocumentData> ExtractDocumentData(Stream docStream)
{
var content = new System.Text.StringBuilder();
var documentData = new DocumentData();
using var wordprocessingDocument = WordprocessingDocument.Open(docStream, false);
var body = wordprocessingDocument.MainDocumentPart?.Document?.Body;
// 提取背景图片
await ExtractBackgroundImage(wordprocessingDocument, documentData);
if (body != null)
{
// 提取所有段落文本
foreach (var paragraph in body.Elements<DocumentFormat.OpenXml.Wordprocessing.Paragraph>())
// 提取段落(包含位置信息)
foreach (var paragraph in body.Elements<Paragraph>())
{
var paragraphText = paragraph.InnerText?.Trim();
if (!string.IsNullOrEmpty(paragraphText))
{
content.AppendLine(paragraphText);
var paragraphData = new ParagraphData
{
Text = paragraphText,
Style = DetermineParagraphStyle(paragraph),
FontSize = GetParagraphFontSize(paragraph),
Alignment = GetParagraphAlignment(paragraph),
IsBold = IsParagraphBold(paragraph),
FontColor = GetParagraphColor(paragraph)
};
// 尝试获取位置信息
GetParagraphPosition(paragraph, paragraphData);
documentData.Paragraphs.Add(paragraphData);
}
}
// 提取表格内容
foreach (var table in body.Elements<DocumentFormat.OpenXml.Wordprocessing.Table>())
// 提取表格
foreach (var table in body.Elements<Table>())
{
foreach (var row in table.Elements<DocumentFormat.OpenXml.Wordprocessing.TableRow>())
var tableData = new TableData();
foreach (var row in table.Elements<TableRow>())
{
var rowText = new System.Text.StringBuilder();
foreach (var cell in row.Elements<DocumentFormat.OpenXml.Wordprocessing.TableCell>())
var rowData = new List<string>();
foreach (var cell in row.Elements<TableCell>())
{
var cellText = cell.InnerText?.Trim();
if (!string.IsNullOrEmpty(cellText))
{
rowText.Append(cellText + " | ");
}
var cellText = cell.InnerText?.Trim() ?? string.Empty;
rowData.Add(cellText);
}
if (rowText.Length > 0)
if (rowData.Any())
{
content.AppendLine(rowText.ToString().TrimEnd(" | ".ToCharArray()));
tableData.Rows.Add(rowData);
}
}
if (tableData.Rows.Any())
{
documentData.Tables.Add(tableData);
}
}
}
return content.ToString();
return documentData;
}
/// <summary>
/// 从内容生成PDF
/// 提取背景图片
/// </summary>
private async Task GeneratePdfFromContent(Stream pdfStream, string content)
private async Task ExtractBackgroundImage(WordprocessingDocument wordDoc, DocumentData documentData)
{
using var writer = new StreamWriter(pdfStream, System.Text.Encoding.ASCII, leaveOpen: true);
// PDF头部
await writer.WriteAsync("%PDF-1.4\n");
// 对象1:Catalog
await writer.WriteAsync("1 0 obj\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync("/Type /Catalog\n");
await writer.WriteAsync("/Pages 2 0 R\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("endobj\n");
// 对象2:Pages
await writer.WriteAsync("2 0 obj\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync("/Type /Pages\n");
await writer.WriteAsync("/Kids [3 0 R]\n");
await writer.WriteAsync("/Count 1\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("endobj\n");
// 对象3:Page
await writer.WriteAsync("3 0 obj\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync("/Type /Page\n");
await writer.WriteAsync("/Parent 2 0 R\n");
await writer.WriteAsync("/MediaBox [0 0 595 842]\n");
await writer.WriteAsync("/Resources <<\n");
await writer.WriteAsync("/Font <<\n");
await writer.WriteAsync("/F1 <<\n");
await writer.WriteAsync("/Type /Font\n");
await writer.WriteAsync("/Subtype /Type1\n");
await writer.WriteAsync("/BaseFont /Arial\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("/Contents 4 0 R\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("endobj\n");
// 准备文本内容
var lines = content.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var textContent = new System.Text.StringBuilder();
var yPosition = 750;
// 添加标题
textContent.Append("BT /F1 16 Tf 50 750 Td (Certificate) Tj ET\n");
yPosition -= 30;
// 添加内容行
foreach (var line in lines.Take(20)) // 限制行数避免PDF过大
{
var cleanLine = line.Replace("(", "\\(").Replace(")", "\\)").Replace("\r", "").Trim();
if (cleanLine.Length > 0)
{
// 处理长行
if (cleanLine.Length > 70)
{
cleanLine = cleanLine.Substring(0, 70) + "...";
}
// 处理非ASCII字符
var asciiLine = ConvertToAscii(cleanLine);
textContent.Append($"BT /F1 12 Tf 50 {yPosition} Td ({asciiLine}) Tj ET\n");
yPosition -= 18; // 行间距
// 如果位置太低,停止添加
if (yPosition < 50) break;
}
}
var contentText = textContent.ToString();
var contentBytes = System.Text.Encoding.ASCII.GetBytes(contentText);
// 对象4:Content Stream
await writer.WriteAsync("4 0 obj\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync($"/Length {contentBytes.Length}\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("stream\n");
await writer.FlushAsync();
// 直接写入字节到流
pdfStream.Write(contentBytes, 0, contentBytes.Length);
await writer.WriteAsync("\nendstream\n");
await writer.WriteAsync("endobj\n");
// 记录当前位置用于xref
var currentPos = pdfStream.Length;
// 交叉引用表
var xrefOffset = pdfStream.Length;
await writer.WriteAsync("xref\n");
await writer.WriteAsync("0 5\n");
await writer.WriteAsync("0000000000 65535 f\n");
await writer.WriteAsync("0000000010 00000 n\n");
await writer.WriteAsync("0000000079 00000 n\n");
await writer.WriteAsync("0000000173 00000 n\n");
await writer.WriteAsync($"000000{currentPos.ToString().PadLeft(6, '0')} 00000 n\n");
// Trailer
await writer.WriteAsync("trailer\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync("/Size 5\n");
await writer.WriteAsync("/Root 1 0 R\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("startxref\n");
await writer.WriteAsync($"{xrefOffset}\n");
await writer.WriteAsync("%%EOF\n");
await writer.FlushAsync();
}
/// <summary>
/// 转换文本为ASCII兼容格式
/// </summary>
private string ConvertToAscii(string text)
{
var result = new System.Text.StringBuilder();
foreach (char c in text)
{
if (c < 128) // ASCII字符
{
result.Append(c);
}
else
{
// 非ASCII字符转换为占位符
result.Append("?");
}
}
return result.ToString();
}
/// <summary>
/// 生成简单的PDF作为备用方案
/// </summary>
private async Task<Stream> GenerateSimplePdf(Stream docStream)
{
_logger.LogInformation("使用备用PDF生成方案");
var pdfStream = new MemoryStream();
try
{
// 重置流位置
docStream.Position = 0;
// 读取DOC内容
string content;
try
var mainPart = wordDoc.MainDocumentPart;
if (mainPart != null)
{
using var reader = new StreamReader(docStream, System.Text.Encoding.UTF8);
content = await reader.ReadToEndAsync();
// 查找文档中的图片
var imageParts = mainPart.GetPartsOfType<ImagePart>().ToList();
if (imageParts.Any())
{
// 尝试获取第一个图片作为背景图片
var firstImage = imageParts.FirstOrDefault();
if (firstImage != null)
{
using var imageStream = firstImage.GetStream(FileMode.Open, FileAccess.Read);
using var memoryStream = new MemoryStream();
await imageStream.CopyToAsync(memoryStream);
documentData.BackgroundImage = memoryStream.ToArray();
documentData.BackgroundImageMimeType = firstImage.ContentType;
_logger.LogInformation("成功提取背景图片,类型: {Type}, 大小: {Size} bytes",
documentData.BackgroundImageMimeType, documentData.BackgroundImage.Length);
}
}
// 如果没有找到图片,尝试从页眉页脚中查找
if (documentData.BackgroundImage == null)
{
var headerParts = mainPart.HeaderParts;
foreach (var headerPart in headerParts)
{
var headerImages = headerPart.GetPartsOfType<ImagePart>();
foreach (var image in headerImages)
{
using var imageStream = image.GetStream(FileMode.Open, FileAccess.Read);
using var memoryStream = new MemoryStream();
await imageStream.CopyToAsync(memoryStream);
documentData.BackgroundImage = memoryStream.ToArray();
documentData.BackgroundImageMimeType = image.ContentType;
_logger.LogInformation("从页眉中提取背景图片,类型: {Type}, 大小: {Size} bytes",
documentData.BackgroundImageMimeType, documentData.BackgroundImage.Length);
break;
}
if (documentData.BackgroundImage != null) break;
}
}
}
catch
{
// 如果无法读取DOC内容,使用默认内容
content = "Original document content could not be read.";
}
// 创建简单的PDF内容
using var writer = new StreamWriter(pdfStream, System.Text.Encoding.UTF8, leaveOpen: true);
await writer.WriteAsync("%PDF-1.4\n");
await writer.WriteAsync("1 0 obj\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync("/Type /Catalog\n");
await writer.WriteAsync("/Pages 2 0 R\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("endobj\n");
await writer.WriteAsync("2 0 obj\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync("/Type /Pages\n");
await writer.WriteAsync("/Kids [3 0 R]\n");
await writer.WriteAsync("/Count 1\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("endobj\n");
await writer.WriteAsync("3 0 obj\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync("/Type /Page\n");
await writer.WriteAsync("/Parent 2 0 R\n");
await writer.WriteAsync("/MediaBox [0 0 595 842]\n");
await writer.WriteAsync("/Resources <<\n");
await writer.WriteAsync("/Font <<\n");
await writer.WriteAsync("/F1 <<\n");
await writer.WriteAsync("/Type /Font\n");
await writer.WriteAsync("/Subtype /Type1\n");
await writer.WriteAsync("/BaseFont /Arial\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync($"/Contents 4 0 R\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("endobj\n");
// 简化的内容(只显示基本信息)
var displayContent = content.Length > 0
? content.Substring(0, Math.Min(500, content.Length))
: "Document content is empty or could not be read.";
var simpleContent = "Certificate Generated\n\nNote: This is a simplified PDF generated\nbecause LibreOffice is not available.\n\nOriginal document content:\n" + displayContent;
var contentBytes = System.Text.Encoding.UTF8.GetBytes(simpleContent);
await writer.WriteAsync("4 0 obj\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync($"/Length {contentBytes.Length}\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("stream\n");
await writer.FlushAsync();
pdfStream.Write(contentBytes, 0, contentBytes.Length);
await writer.WriteAsync("\nendstream\n");
await writer.WriteAsync("endobj\n");
await writer.WriteAsync("xref\n");
await writer.WriteAsync("0 5\n");
await writer.WriteAsync("0000000000 65535 f\n");
await writer.WriteAsync("0000000010 00000 n\n");
await writer.WriteAsync("0000000079 00000 n\n");
await writer.WriteAsync("0000000173 00000 n\n");
// 计算正确的偏移量
var xrefOffset = pdfStream.Length;
await writer.WriteAsync($"000000{Math.Max(300, contentBytes.Length + 200).ToString().PadLeft(6, '0')} 00000 n\n");
await writer.WriteAsync("trailer\n");
await writer.WriteAsync("<<\n");
await writer.WriteAsync("/Size 5\n");
await writer.WriteAsync("/Root 1 0 R\n");
await writer.WriteAsync(">>\n");
await writer.WriteAsync("startxref\n");
await writer.WriteAsync($"{xrefOffset}\n");
await writer.WriteAsync("%%EOF\n");
await writer.FlushAsync();
_logger.LogInformation("备用PDF生成完成,大小: {Size} bytes", pdfStream.Length);
}
catch (Exception ex)
{
_logger.LogError(ex, "备用PDF生成也失败了");
throw new Exception("PDF生成完全失败", ex);
_logger.LogWarning(ex, "提取背景图片失败");
}
}
/// <summary>
/// 获取段落字体大小
/// </summary>
private double? GetParagraphFontSize(Paragraph paragraph)
{
var runProperties = paragraph.Elements<Run>()
.SelectMany(r => r.Elements<RunProperties>())
.FirstOrDefault();
if (runProperties?.FontSize != null)
{
var fontSize = runProperties.FontSize.Val;
if (double.TryParse(fontSize, out double size))
{
return size / 2.0; // OpenXML 中字体大小是半磅为单位
}
}
pdfStream.Position = 0;
return pdfStream;
return null;
}
/// <summary>
/// 获取段落对齐方式
/// </summary>
private string GetParagraphAlignment(Paragraph paragraph)
{
var paragraphProperties = paragraph.ParagraphProperties;
if (paragraphProperties?.Justification != null)
{
var justification = paragraphProperties.Justification.Val?.ToString().ToLower();
return justification switch
{
"center" => "center",
"right" => "right",
"both" => "justify",
_ => "left"
};
}
return "left";
}
/// <summary>
/// 检查段落是否为粗体
/// </summary>
private bool IsParagraphBold(Paragraph paragraph)
{
var runProperties = paragraph.Elements<Run>()
.SelectMany(r => r.Elements<RunProperties>())
.FirstOrDefault();
return runProperties?.Bold != null;
}
/// <summary>
/// 获取段落字体颜色
/// </summary>
private string GetParagraphColor(Paragraph paragraph)
{
var runProperties = paragraph.Elements<Run>()
.SelectMany(r => r.Elements<RunProperties>())
.FirstOrDefault();
if (runProperties?.Color != null)
{
var color = runProperties.Color.Val;
if (!string.IsNullOrEmpty(color))
{
return color;
}
}
return "black";
}
/// <summary>
/// 获取段落位置信息
/// </summary>
private void GetParagraphPosition(Paragraph paragraph, ParagraphData paragraphData)
{
try
{
var paragraphProperties = paragraph.ParagraphProperties;
if (paragraphProperties != null)
{
_logger.LogDebug("处理段落: {Text}", paragraphData.Text);
// 检查是否有绝对定位
var frameProperties = paragraphProperties.Elements<FrameProperties>().FirstOrDefault();
if (frameProperties != null)
{
_logger.LogDebug("找到FrameProperties");
// 提取位置信息
if (frameProperties.Width != null)
{
var widthStr = frameProperties.Width.Value.ToString();
_logger.LogDebug("Width: {Width}", widthStr);
if (uint.TryParse(widthStr, out uint width))
{
paragraphData.PositionX = width;
_logger.LogDebug("设置PositionX: {X}", width);
}
}
if (frameProperties.Height != null)
{
var heightStr = frameProperties.Height.Value.ToString();
_logger.LogDebug("Height: {Height}", heightStr);
if (uint.TryParse(heightStr, out uint height))
{
paragraphData.PositionY = height;
_logger.LogDebug("设置PositionY: {Y}", height);
}
}
}
else
{
_logger.LogDebug("未找到FrameProperties");
}
// 如果没有绝对定位,检查段落缩进
if (paragraphData.PositionX == null)
{
var indentation = paragraphProperties.Elements<Indentation>().FirstOrDefault();
if (indentation != null && indentation.Left != null)
{
var leftStr = indentation.Left.Value.ToString();
_logger.LogDebug("Left Indentation: {Indent}", leftStr);
if (uint.TryParse(leftStr, out uint leftIndent))
{
paragraphData.PositionX = leftIndent / 20; // 转换为点
_logger.LogDebug("通过缩进设置PositionX: {X}", paragraphData.PositionX);
}
}
}
// 检查段落间距
var spacingBetweenLines = paragraphProperties.SpacingBetweenLines;
if (spacingBetweenLines != null)
{
_logger.LogDebug("SpacingBetweenLines: {Spacing}", spacingBetweenLines.ToString());
}
}
_logger.LogDebug("最终位置 - X: {X}, Y: {Y}", paragraphData.PositionX, paragraphData.PositionY);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取段落位置信息失败");
}
}
/// <summary>
/// 使用QuestPDF生成PDF
/// </summary>
private async Task GeneratePdfWithQuestPDF(Stream pdfStream, DocumentData documentData)
{
var questDocument = QuestPDF.Fluent.Document.Create(container =>
{
container.Page(page =>
{
page.Margin(0);
page.Size(PageSizes.A4);
page.DefaultTextStyle(x => x.FontSize(12).FontFamily(QuestPDF.Helpers.Fonts.Calibri));
// 添加背景图片
if (documentData.BackgroundImage != null)
{
page.Background()
.Image(documentData.BackgroundImage)
.FitArea();
}
// 使用相对定位来放置文本
page.Content().Column(column =>
{
// 根据段落信息进行精确定位
foreach (var paragraph in documentData.Paragraphs)
{
var fontSize = paragraph.FontSize ?? 12;
var isBold = paragraph.IsBold;
// 使用从DOCX中提取的实际位置信息
var positionX = paragraph.PositionX ?? 50; // 默认左边距
var positionY = paragraph.PositionY ?? 50; // 默认顶部边距
// 跳过无效的文本内容
if (string.IsNullOrWhiteSpace(paragraph.Text) ||
paragraph.Text.Contains("left-") ||
paragraph.Text.Contains("right-") ||
paragraph.Text.Contains("top-") ||
paragraph.Text.Contains("bottom-"))
{
continue;
}
// 创建文本元素,使用原始位置
var textElement = column.Item().PaddingTop((float)positionY).PaddingLeft((float)positionX).Text(paragraph.Text)
.FontSize((float)fontSize)
.FontColor(QuestPDF.Helpers.Colors.Black);
// 如果需要粗体,应用到同一个元素
if (isBold)
{
textElement.Bold();
}
}
// 添加二维码到右下角
if (documentData.QRCodeImage != null)
{
column.Item().PaddingTop(500).PaddingRight(50).AlignRight().Width(80).Height(80).Image(documentData.QRCodeImage);
}
});
page.Footer()
.AlignCenter()
.Text(x =>
{
x.Span("生成时间: ").FontSize(8).FontColor(QuestPDF.Helpers.Colors.Grey.Darken1);
x.CurrentPageNumber().FontSize(8).FontColor(QuestPDF.Helpers.Colors.Grey.Darken1);
});
});
});
// 生成PDF到流
questDocument.GeneratePdf(pdfStream);
await Task.CompletedTask; // 为了保持异步签名
}
/// <summary>
/// 根据段落内容确定文本位置(基于A4页面210x297mm,坐标单位为点)
/// </summary>
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);
}
/// <summary>
/// 确定段落样式
/// </summary>
private string DetermineParagraphStyle(Paragraph paragraph)
{
// 检查是否为标题
var paragraphProperties = paragraph.ParagraphProperties;
if (paragraphProperties != null)
{
var style = paragraphProperties.ParagraphStyleId?.Val?.Value?.ToLower();
if (style != null)
{
if (style.Contains("heading") || style.Contains("title"))
{
return "heading";
}
}
}
// 根据文本特征判断
var text = paragraph.InnerText?.Trim() ?? string.Empty;
if (text.Length < 50 && text.Length > 0)
{
return "subheading";
}
return "normal";
}
/// <summary>
@ -608,15 +760,15 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
// macOS
"/Applications/LibreOffice.app/Contents/MacOS/soffice"
};
/*
foreach (var path in possiblePaths)
{
if (File.Exists(path))
if (System.IO.File.Exists(path))
{
_logger.LogInformation("找到LibreOffice: {Path}", path);
return path;
}
}*/
}
// 如果找不到,尝试使用系统PATH中的命令
return OperatingSystem.IsWindows() ? "soffice" : "libreoffice";
@ -761,4 +913,124 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
var baseUrl = $"{request.Scheme}://{request.Host.Value}";
return _qrCodeService.GenerateCertificateUrl(certificateId, baseUrl);
}
/// <summary>
/// 生成PDF保存地址
/// </summary>
private string GeneratePdfUrl(long certificateId, YD_Event.Core.Entities.Certificate certificate, CertificateTemplate template)
{
var options = _ossOptionsMonitor.CurrentValue;
var now = DateTime.Today;
string name = $"certificate_{_idGenerator.Encode(_idGenerator.NewLong())}.pdf";
string filePath = $"/certificates/{now.Year}/{now.Month:D2}/{now.Day:D2}/";
string finalUrl = string.Empty;
if (!options.Enable)
{
// 本地存储
string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath);
// 预测最终 URL
var request = _httpContextAccessor.HttpContext?.Request;
if (request != null)
{
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir}{name}";
}
}
else
{
// OSS 上传
string fileName = $"{filePath}{name}";
// 预测最终 URL
string bucketPrefix = (options.Provider == OSSProvider.Minio) ? $"/{options.Bucket}" : string.Empty;
finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}";
}
return finalUrl;
}
/// <summary>
/// 保存PDF文件到指定URL
/// </summary>
private async Task SavePdfFileToUrl(Stream pdfStream, string targetUrl)
{
var options = _ossOptionsMonitor.CurrentValue;
// 从URL中解析文件路径
var uri = new Uri(targetUrl);
var path = uri.AbsolutePath;
// 移除域名部分,获取相对路径
string relativePath = path;
if (path.StartsWith("/"))
{
relativePath = path.Substring(1);
}
pdfStream.Position = 0;
if (!options.Enable)
{
// 本地存储
string localDir = Path.GetDirectoryName(relativePath);
string fileName = Path.GetFileName(relativePath);
string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir);
if (!Directory.Exists(absoluteDir))
{
Directory.CreateDirectory(absoluteDir);
}
string fullPath = Path.Combine(absoluteDir, fileName);
using var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
await pdfStream.CopyToAsync(fileStream);
_logger.LogInformation("PDF文件保存到本地: {FullPath}", fullPath);
}
else
{
// OSS 上传
await _ossService.PutObjectAsync(options.Bucket, relativePath, pdfStream);
_logger.LogInformation("PDF文件上传到OSS: {FileName}", relativePath);
}
}
}
/// <summary>
/// 文档数据模型
/// </summary>
public class DocumentData
{
public List<ParagraphData> Paragraphs { get; set; } = new List<ParagraphData>();
public List<TableData> Tables { get; set; } = new List<TableData>();
public byte[] BackgroundImage { get; set; } = null;
public string BackgroundImageMimeType { get; set; } = string.Empty;
public byte[] QRCodeImage { get; set; } = null;
public string QRCodeMimeType { get; set; } = string.Empty;
}
/// <summary>
/// 段落数据模型
/// </summary>
public class ParagraphData
{
public string Text { get; set; } = string.Empty;
public string Style { get; set; } = "normal"; // normal, heading, subheading
public double? PositionX { get; set; } = null;
public double? PositionY { get; set; } = null;
public double? FontSize { get; set; } = null;
public string Alignment { get; set; } = "left"; // left, center, right
public bool IsBold { get; set; } = false;
public string FontColor { get; set; } = "black";
}
/// <summary>
/// 表格数据模型
/// </summary>
public class TableData
{
public List<List<string>> Rows { get; set; } = new List<List<string>>();
}

View File

@ -319,29 +319,59 @@
批量生成PDF证书
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.ConvertDocToPdf(System.IO.Stream)">
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.ConvertDocToPdf(System.IO.Stream,System.Byte[])">
<summary>
转换DOC为PDF(使用DocumentFormat.OpenXml解析并生成PDF)
转换DOC为PDF(使用QuestPDF和DocumentFormat.OpenXml)
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.ExtractDocxContent(System.IO.Stream)">
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.ExtractDocumentData(System.IO.Stream)">
<summary>
提取DOCX文件内容
提取DOCX文档数据(包括段落、表格、背景图片等)
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GeneratePdfFromContent(System.IO.Stream,System.String)">
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.ExtractBackgroundImage(DocumentFormat.OpenXml.Packaging.WordprocessingDocument,YD_Event.Application.Certificate.DocumentData)">
<summary>
从内容生成PDF
提取背景图片
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.ConvertToAscii(System.String)">
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GetParagraphFontSize(DocumentFormat.OpenXml.Wordprocessing.Paragraph)">
<summary>
转换文本为ASCII兼容格式
获取段落字体大小
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GenerateSimplePdf(System.IO.Stream)">
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GetParagraphAlignment(DocumentFormat.OpenXml.Wordprocessing.Paragraph)">
<summary>
生成简单的PDF作为备用方案
获取段落对齐方式
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.IsParagraphBold(DocumentFormat.OpenXml.Wordprocessing.Paragraph)">
<summary>
检查段落是否为粗体
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GetParagraphColor(DocumentFormat.OpenXml.Wordprocessing.Paragraph)">
<summary>
获取段落字体颜色
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GetParagraphPosition(DocumentFormat.OpenXml.Wordprocessing.Paragraph,YD_Event.Application.Certificate.ParagraphData)">
<summary>
获取段落位置信息
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GeneratePdfWithQuestPDF(System.IO.Stream,YD_Event.Application.Certificate.DocumentData)">
<summary>
使用QuestPDF生成PDF
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GetTextPosition(YD_Event.Application.Certificate.ParagraphData)">
<summary>
根据段落内容确定文本位置(基于A4页面210x297mm,坐标单位为点)
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.DetermineParagraphStyle(DocumentFormat.OpenXml.Wordprocessing.Paragraph)">
<summary>
确定段落样式
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GetLibreOfficePath">
@ -369,6 +399,31 @@
生成证书URL
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GeneratePdfUrl(System.Int64,YD_Event.Core.Entities.Certificate,YD_Event.Core.Entities.CertificateTemplate)">
<summary>
生成PDF保存地址
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.SavePdfFileToUrl(System.IO.Stream,System.String)">
<summary>
保存PDF文件到指定URL
</summary>
</member>
<member name="T:YD_Event.Application.Certificate.DocumentData">
<summary>
文档数据模型
</summary>
</member>
<member name="T:YD_Event.Application.Certificate.ParagraphData">
<summary>
段落数据模型
</summary>
</member>
<member name="T:YD_Event.Application.Certificate.TableData">
<summary>
表格数据模型
</summary>
</member>
<member name="T:YD_Event.Application.Certificate.CertificateService">
<summary>
证书管理服务