YD_Event.Admin/YD_Event.Application/Certificate/CertificateGenerateService.cs
2026-04-15 17:26:50 +08:00

1171 lines
43 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using YD_Event.Application.Certificate.Dtos;
using YD_Event.Core.Entities;
using YD_Event.Core.Domain.Certificate;
using SqlSugar;
using Mapster;
using Microsoft.Extensions.Options;
using YD_Event.Core.Options;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
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;
using YD_Event.Application.Common;
namespace YD_Event.Application.Certificate;
/// <summary>
/// 证书生成服务
/// </summary>
public class CertificateGenerateService : IDynamicApiController, ITransient
{
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Certificate> _certificateRepository;
private readonly ISqlSugarRepository<CertificateTemplate> _templateRepository;
private readonly ISqlSugarRepository<CertificatePlaceholder> _placeholderRepository;
private readonly FileService _fileService;
private readonly IOptionsMonitor<OssConnectionOptions> _ossOptionsMonitor;
private readonly IIdGenerator _idGenerator;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<CertificateGenerateService> _logger;
private readonly DocxTemplateProcessor _docxProcessor;
private readonly QRCodeService _qrCodeService;
public CertificateGenerateService(
ISqlSugarRepository<YD_Event.Core.Entities.Certificate> certificateRepository,
ISqlSugarRepository<CertificateTemplate> templateRepository,
ISqlSugarRepository<CertificatePlaceholder> placeholderRepository,
FileService fileService,
IOptionsMonitor<OssConnectionOptions> ossOptionsMonitor,
IIdGenerator idGenerator,
IWebHostEnvironment webHostEnvironment,
IHttpContextAccessor httpContextAccessor,
ILogger<CertificateGenerateService> logger,
DocxTemplateProcessor docxProcessor,
QRCodeService qrCodeService)
{
_certificateRepository = certificateRepository;
_templateRepository = templateRepository;
_placeholderRepository = placeholderRepository;
_fileService = fileService;
_ossOptionsMonitor = ossOptionsMonitor;
_idGenerator = idGenerator;
_webHostEnvironment = webHostEnvironment;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_docxProcessor = docxProcessor;
_qrCodeService = qrCodeService;
}
/// <summary>
/// 生成PDF证书
/// </summary>
[DisplayName("生成PDF证书")]
public async Task<PdfCertificateGenerationResult> GeneratePdf(GeneratePdfCertificateInput input)
{
var stopwatch = Stopwatch.StartNew();
var result = new PdfCertificateGenerationResult
{
GeneratedTime = DateTime.Now,
IsSuccess = true
};
try
{
// 1. 获取证书信息
var certificate = await _certificateRepository.GetByIdAsync(input.CertificateId);
if (certificate == null)
{
result.IsSuccess = false;
result.ErrorMessage = "证书不存在";
return result;
}
if (certificate.Status != Core.Enum.AvailabilityStatus.Enable)
{
result.IsSuccess = false;
result.ErrorMessage = "证书已禁用";
return result;
}
// 2. 获取模板
CertificateTemplate template;
if (input.TemplateId.HasValue)
{
template = await _templateRepository.GetByIdAsync(input.TemplateId.Value);
}
else
{
template = await _templateRepository.GetByIdAsync(certificate.TemplateId);
}
if (template == null)
{
result.IsSuccess = false;
result.ErrorMessage = "未找到有效证书模板";
return result;
}
// 3. 检查模板类型
if (template.TemplateType != "Doc")
{
result.IsSuccess = false;
result.ErrorMessage = "指定的模板不是DOC类型";
return result;
}
// 4. 获取占位符配置
var placeholders = await _placeholderRepository.AsQueryable()
.Where(x => x.TemplateId == template.Id)
.OrderBy(x => x.Sort)
.ToListAsync();
// 5. 验证必填数据
var requiredPlaceholders = placeholders.Where(x => x.IsRequired).Select(x => x.PlaceholderKey).ToList();
var validationResult = _docxProcessor.ValidateTemplateData(requiredPlaceholders, input.Data);
if (!validationResult.IsValid)
{
result.IsSuccess = false;
result.ErrorMessage = $"缺少必填数据: {string.Join(", ", validationResult.MissingPlaceholders)}";
return result;
}
// 6. 下载DOC模板
using var httpClient = new HttpClient();
var templateStream = new MemoryStream(await httpClient.GetByteArrayAsync(template.DocxTemplateUrl));
// 7. 生成二维码数据(用于DOC模板中的占位符)
var qrCodeData = new Dictionary<string, string>();
if (input.GenerateQRCode)
{
var certificateUrl = GenerateCertificateUrl(input.CertificateId);
qrCodeData["CertificateQR"] = certificateUrl;
qrCodeData["VerificationQR"] = certificateUrl + "/verify";
}
// 8. 设置占位符映射(支持中文显示名 -> 英文键名)
var placeholderMapping = new Dictionary<string, string>();
foreach (var placeholder in placeholders)
{
if (!string.IsNullOrWhiteSpace(placeholder.DisplayName))
{
placeholderMapping[placeholder.DisplayName] = placeholder.PlaceholderKey;
}
}
_docxProcessor.SetPlaceholderMapping(placeholderMapping);
_logger.LogInformation("设置占位符映射完成. 映射数量: {Count}", placeholderMapping.Count);
// 9. 处理模板
var processedStream = _docxProcessor.ProcessTemplate(templateStream, input.Data, qrCodeData);
// 10. 先生成证书保存地址(预设的PDF地址)
var pdfUrl = GeneratePdfUrl(input.CertificateId, certificate, template);
// 11. 生成二维码图片(使用预设的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();
}
// 12. 转换为PDF(包含二维码)
var pdfStream = await ConvertDocToPdf(processedStream, qrCodeImage);
// 13. 保存PDF文件到预设地址
await SavePdfFileToUrl(pdfStream, pdfUrl);
// 14. 生成二维码图片(如果需要)
string qrCodeUrl = null;
if (input.GenerateQRCode)
{
qrCodeUrl = await GenerateQRCodeImage(input.CertificateId, input.QRCodeSize);
}
// 15. 设置结果
result.PdfUrl = pdfUrl;
result.QRCodeUrl = qrCodeUrl;
result.VerificationUrl = GenerateCertificateUrl(input.CertificateId);
result.Warnings.AddRange(validationResult.Warnings);
_logger.LogInformation("PDF证书生成成功. CertificateId: {CertificateId}, Url: {Url}",
input.CertificateId, pdfUrl);
}
catch (Exception ex)
{
result.IsSuccess = false;
result.ErrorMessage = ex.Message;
_logger.LogError(ex, "PDF证书生成失败. CertificateId: {CertificateId}", input.CertificateId);
}
finally
{
stopwatch.Stop();
result.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
}
return result;
}
/// <summary>
/// 批量生成PDF证书
/// </summary>
[DisplayName("批量生成PDF证书")]
public async Task<BatchPdfCertificateGenerationResult> BatchGeneratePdf(BatchGeneratePdfCertificateInput input)
{
var result = new BatchPdfCertificateGenerationResult
{
StartTime = DateTime.Now,
TotalCount = input.DataList.Count
};
_logger.LogInformation("开始批量生成PDF证书. Count: {Count}", input.DataList.Count);
foreach (var data in input.DataList)
{
var generateInput = new GeneratePdfCertificateInput
{
CertificateId = input.CertificateId,
TemplateId = input.TemplateId,
Data = data,
GenerateQRCode = input.GenerateQRCode,
QRCodeSize = input.QRCodeSize
};
var generateResult = await GeneratePdf(generateInput);
if (generateResult.IsSuccess)
{
result.SuccessResults.Add(generateResult);
}
else
{
result.FailedResults.Add(generateResult);
}
}
result.EndTime = DateTime.Now;
_logger.LogInformation("批量生成PDF证书完成. Total: {Total}, Success: {Success}, Failed: {Failed}",
result.TotalCount, result.SuccessCount, result.FailedCount);
return result;
}
/// <summary>
/// 转换DOC为PDF(使用LibreOffice,保持原始Word布局)
/// </summary>
private async Task<Stream> ConvertDocToPdf(Stream docStream, byte[] qrCodeImage = 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);
}
}
/// <summary>
/// 使用LibreOffice转换DOCX到PDF
/// </summary>
private async Task<Stream> 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);
}
}
}
/// <summary>
/// 使用QuestPDF作为后备方案
/// </summary>
private async Task<Stream> ConvertDocToPdfWithQuestPDF(Stream docStream, byte[] qrCodeImage = null)
{
_logger.LogInformation("使用QuestPDF作为后备方案");
// 重置流位置
docStream.Position = 0;
// 使用QuestPDF许可证
QuestPDF.Settings.License = LicenseType.Community;
// 解析DOCX内容
var documentData = await ExtractDocumentData(docStream);
// 添加二维码图片
if (qrCodeImage != null)
{
documentData.QRCodeImage = qrCodeImage;
documentData.QRCodeMimeType = "image/png";
}
// 生成PDF
var pdfStream = new MemoryStream();
await GeneratePdfWithQuestPDF(pdfStream, documentData);
pdfStream.Position = 0;
return pdfStream;
}
/// <summary>
/// 添加二维码到DOCX(简单实现,实际项目中需要完善)
/// </summary>
private async Task<Stream> AddQRCodeToDocx(Stream docStream, byte[] qrCodeImage)
{
// 注意:这里简化实现,实际项目中需要:
// 1. 查找DOCX中的二维码占位符
// 2. 将占位符替换为二维码图片
// 3. 正确处理图片大小和位置
// 临时方案:暂时不添加二维码到DOCX,使用QuestPDF的后备方案处理
_logger.LogWarning("添加二维码到DOCX功能待实现,将使用QuestPDF后备方案");
// 直接返回原始流,后续会使用QuestPDF处理
var resultStream = new MemoryStream();
docStream.Position = 0;
await docStream.CopyToAsync(resultStream);
resultStream.Position = 0;
return resultStream;
}
/// <summary>
/// 提取DOCX文档数据(包括段落、表格、背景图片等)
/// </summary>
private async Task<DocumentData> ExtractDocumentData(Stream docStream)
{
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<Paragraph>())
{
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-"))
{
_logger.LogDebug("跳过不应该的内容: {Text}", paragraphText);
continue;
}
var paragraphData = new ParagraphData
{
Text = paragraphText,
Style = DetermineParagraphStyle(paragraph),
FontSize = GetParagraphFontSize(paragraph),
Alignment = GetParagraphAlignment(paragraph),
IsBold = IsParagraphBold(paragraph),
FontColor = GetParagraphColor(paragraph)
};
// 尝试获取位置信息
GetParagraphPosition(paragraph, paragraphData);
_logger.LogDebug("提取段落: Text={Text}, X={X}, Y={Y}",
paragraphText, paragraphData.PositionX, paragraphData.PositionY);
documentData.Paragraphs.Add(paragraphData);
}
}
// 提取表格
foreach (var table in body.Elements<Table>())
{
var tableData = new TableData();
foreach (var row in table.Elements<TableRow>())
{
var rowData = new List<string>();
foreach (var cell in row.Elements<TableCell>())
{
var cellText = cell.InnerText?.Trim() ?? string.Empty;
rowData.Add(cellText);
}
if (rowData.Any())
{
tableData.Rows.Add(rowData);
}
}
if (tableData.Rows.Any())
{
documentData.Tables.Add(tableData);
}
}
}
return documentData;
}
/// <summary>
/// 提取背景图片
/// </summary>
private async Task ExtractBackgroundImage(WordprocessingDocument wordDoc, DocumentData documentData)
{
try
{
var mainPart = wordDoc.MainDocumentPart;
if (mainPart != null)
{
// 查找文档中的图片
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 (Exception 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 中字体大小是半磅为单位
}
}
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();
}
// 使用 Column 来放置所有内容,但主要依赖备用位置(基于标准证书模板)
page.Content().Column(column =>
{
// 首先根据段落信息进行精确定位
foreach (var paragraph in documentData.Paragraphs)
{
var fontSize = paragraph.FontSize ?? 14;
var isBold = paragraph.IsBold;
// 日志输出段落信息,便于调试
_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;
_logger.LogDebug("最终位置 - X={X}, Y={Y}", x, y);
// 使用 Padding 来定位文本
var textElement = column.Item().PaddingLeft(x).PaddingTop(y).Text(paragraph.Text)
.FontSize((float)fontSize)
.FontColor(QuestPDF.Helpers.Colors.Black)
.AlignLeft();
if (isBold)
{
textElement.Bold();
}
}
});
// 添加二维码到右下角(使用 Footer)
if (documentData.QRCodeImage != null)
{
float qrSize = 100;
_logger.LogDebug("添加二维码: Size={Size}", qrSize);
page.Footer().AlignRight().PaddingRight(30).PaddingBottom(30)
.Width(qrSize).Height(qrSize)
.Image(documentData.QRCodeImage);
}
});
});
// 生成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>
/// 获取LibreOffice可执行文件路径
/// </summary>
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";
}
/// <summary>
/// 保存PDF文件
/// </summary>
private async Task<string> SavePdfFile(Stream pdfStream, 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;
pdfStream.Position = 0;
if (!options.Enable)
{
// 本地存储
string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath);
string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir.TrimStart('/'));
if (!Directory.Exists(absoluteDir))
{
Directory.CreateDirectory(absoluteDir);
}
string fullPath = Path.Combine(absoluteDir, name);
// 预测最终 URL
var request = _httpContextAccessor.HttpContext?.Request;
if (request != null)
{
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}";
}
using var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
await pdfStream.CopyToAsync(fileStream);
_logger.LogInformation("PDF文件保存到本地: {FullPath}", fullPath);
}
else
{
// OSS 上传
string fileName = $"{filePath.TrimStart('/')}{name}";
finalUrl = await _fileService.UploadFileStreamAsync(fileName, pdfStream);
_logger.LogInformation("PDF文件上传到OSS: {FileName}", fileName);
}
return finalUrl;
}
/// <summary>
/// 生成二维码图片
/// </summary>
private async Task<string> GenerateQRCodeImage(long certificateId, int size)
{
try
{
var certificateUrl = GenerateCertificateUrl(certificateId);
using var qrStream = _qrCodeService.GenerateQRCode(certificateUrl, size);
return await SaveQRCodeImage(qrStream, certificateId, size);
}
catch (Exception ex)
{
_logger.LogError(ex, "生成二维码图片失败. CertificateId: {CertificateId}", certificateId);
return null;
}
}
/// <summary>
/// 保存二维码图片
/// </summary>
private async Task<string> SaveQRCodeImage(Stream qrStream, long certificateId, int size)
{
var options = _ossOptionsMonitor.CurrentValue;
var now = DateTime.Today;
string name = $"qr_{certificateId}_{_idGenerator.Encode(_idGenerator.NewLong())}";
string filePath = $"/{now.Year}/{now.Month:D2}/{now.Day:D2}/qrcodes/";
string extension = ".png";
string finalUrl = string.Empty;
qrStream.Position = 0;
if (!options.Enable)
{
string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath);
string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir.TrimStart('/'));
if (!Directory.Exists(absoluteDir))
{
Directory.CreateDirectory(absoluteDir);
}
string fullPath = Path.Combine(absoluteDir, $"{name}{extension}");
using var fileStream2 = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
await qrStream.CopyToAsync(fileStream2);
var request = _httpContextAccessor.HttpContext?.Request;
if (request != null)
{
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}{extension}";
}
}
else
{
// OSS 上传
string fileName = $"{filePath.TrimStart('/')}{name}{extension}";
finalUrl = await _fileService.UploadFileStreamAsync(fileName, qrStream);
}
return finalUrl;
}
/// <summary>
/// 生成证书URL
/// </summary>
private string GenerateCertificateUrl(long certificateId)
{
var request = _httpContextAccessor.HttpContext?.Request;
if (request == null) return string.Empty;
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.TrimStart('/')}{name}";
}
}
else
{
// OSS 上传
string fileName = $"{filePath.TrimStart('/')}{name}";
finalUrl = $"{options.Domain.TrimEnd('/')}/{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 _fileService.UploadFileStreamAsync(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>>();
}