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.File; namespace YD_Event.Application.Certificate; /// /// 证书生成服务 /// public class CertificateGenerateService : IDynamicApiController, ITransient { private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _templateRepository; private readonly ISqlSugarRepository _placeholderRepository; private readonly FileService _fileService; private readonly IOptionsMonitor _ossOptionsMonitor; private readonly IIdGenerator _idGenerator; private readonly IWebHostEnvironment _webHostEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; private readonly DocxTemplateProcessor _docxProcessor; private readonly QRCodeService _qrCodeService; public CertificateGenerateService( ISqlSugarRepository certificateRepository, ISqlSugarRepository templateRepository, ISqlSugarRepository placeholderRepository, FileService fileService, IOptionsMonitor ossOptionsMonitor, IIdGenerator idGenerator, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContextAccessor, ILogger 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; } /// /// 生成PDF证书 /// [DisplayName("生成PDF证书")] public async Task 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(); if (input.GenerateQRCode) { var certificateUrl = GenerateCertificateUrl(input.CertificateId); qrCodeData["CertificateQR"] = certificateUrl; qrCodeData["VerificationQR"] = certificateUrl + "/verify"; } // 8. 处理模板 var processedStream = _docxProcessor.ProcessTemplate(templateStream, input.Data, qrCodeData); // 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); // 12. 保存PDF文件到预设地址 await SavePdfFileToUrl(pdfStream, pdfUrl); // 12. 生成二维码图片(如果需要) string qrCodeUrl = null; if (input.GenerateQRCode) { qrCodeUrl = await GenerateQRCodeImage(input.CertificateId, input.QRCodeSize); } // 13. 设置结果 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; } /// /// 批量生成PDF证书 /// [DisplayName("批量生成PDF证书")] public async Task 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; } /// /// 转换DOC为PDF(使用QuestPDF和DocumentFormat.OpenXml) /// private async Task ConvertDocToPdf(Stream docStream, byte[] qrCodeImage = null) { try { _logger.LogInformation("开始DOCX转PDF转换"); // 重置流位置 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; _logger.LogInformation("DOCX转PDF完成,PDF大小: {Size} bytes", pdfStream.Length); return pdfStream; } catch (Exception ex) { _logger.LogError(ex, "DOCX转PDF失败"); throw new Exception("DOCX转PDF转换失败", ex); } } /// /// 提取DOCX文档数据(包括段落、表格、背景图片等) /// private async Task 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()) { var paragraphText = paragraph.InnerText?.Trim(); if (!string.IsNullOrEmpty(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()) { var tableData = new TableData(); foreach (var row in table.Elements()) { var rowData = new List(); foreach (var cell in row.Elements()) { 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; } /// /// 提取背景图片 /// private async Task ExtractBackgroundImage(WordprocessingDocument wordDoc, DocumentData documentData) { try { var mainPart = wordDoc.MainDocumentPart; if (mainPart != null) { // 查找文档中的图片 var imageParts = mainPart.GetPartsOfType().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(); 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, "提取背景图片失败"); } } /// /// 获取段落字体大小 /// private double? GetParagraphFontSize(Paragraph paragraph) { var runProperties = paragraph.Elements() .SelectMany(r => r.Elements()) .FirstOrDefault(); if (runProperties?.FontSize != null) { var fontSize = runProperties.FontSize.Val; if (double.TryParse(fontSize, out double size)) { return size / 2.0; // OpenXML 中字体大小是半磅为单位 } } return null; } /// /// 获取段落对齐方式 /// 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"; } /// /// 检查段落是否为粗体 /// private bool IsParagraphBold(Paragraph paragraph) { var runProperties = paragraph.Elements() .SelectMany(r => r.Elements()) .FirstOrDefault(); return runProperties?.Bold != null; } /// /// 获取段落字体颜色 /// private string GetParagraphColor(Paragraph paragraph) { var runProperties = paragraph.Elements() .SelectMany(r => r.Elements()) .FirstOrDefault(); if (runProperties?.Color != null) { var color = runProperties.Color.Val; if (!string.IsNullOrEmpty(color)) { return color; } } return "black"; } /// /// 获取段落位置信息 /// private void GetParagraphPosition(Paragraph paragraph, ParagraphData paragraphData) { try { var paragraphProperties = paragraph.ParagraphProperties; if (paragraphProperties != null) { _logger.LogDebug("处理段落: {Text}", paragraphData.Text); // 检查是否有绝对定位 var frameProperties = paragraphProperties.Elements().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().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, "获取段落位置信息失败"); } } /// /// 使用QuestPDF生成PDF /// 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; // 为了保持异步签名 } /// /// 根据段落内容确定文本位置(基于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); } /// /// 确定段落样式 /// 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"; } /// /// 获取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文件 /// private async Task 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; } /// /// 生成二维码图片 /// private async Task 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; } } /// /// 保存二维码图片 /// private async Task 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; } /// /// 生成证书URL /// 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); } /// /// 生成PDF保存地址 /// 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; } /// /// 保存PDF文件到指定URL /// 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); } } } /// /// 文档数据模型 /// public class DocumentData { public List Paragraphs { get; set; } = new List(); public List Tables { get; set; } = new List(); 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; } /// /// 段落数据模型 /// 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"; } /// /// 表格数据模型 /// public class TableData { public List> Rows { get; set; } = new List>(); }