using YD_Event.Application.Certificate.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Certificate; using SqlSugar; using Mapster; using OnceMi.AspNetCore.OSS; 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 System.IO; using System.Linq; namespace YD_Event.Application.Certificate; /// /// 证书生成服务 /// public class CertificateGenerateService : IDynamicApiController, ITransient { private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _templateRepository; private readonly ISqlSugarRepository _placeholderRepository; private readonly IOSSService _ossService; 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, IOSSService ossService, IOptionsMonitor ossOptionsMonitor, IIdGenerator idGenerator, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContextAccessor, ILogger logger, DocxTemplateProcessor docxProcessor, QRCodeService qrCodeService) { _certificateRepository = certificateRepository; _templateRepository = templateRepository; _placeholderRepository = placeholderRepository; _ossService = ossService; _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. 生成二维码数据 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 pdfStream = await ConvertDocToPdf(processedStream); // 10. 保存PDF文件 var pdfUrl = await SavePdfFile(pdfStream, certificate, template); // 11. 生成二维码图片(如果需要) string qrCodeUrl = null; if (input.GenerateQRCode) { qrCodeUrl = await GenerateQRCodeImage(input.CertificateId, input.QRCodeSize); } // 12. 设置结果 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(使用DocumentFormat.OpenXml解析并生成PDF) /// private async Task ConvertDocToPdf(Stream docStream) { try { _logger.LogInformation("开始DOC转PDF转换"); var pdfStream = new MemoryStream(); string content = ""; 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(); } // 如果内容为空,使用默认内容 if (string.IsNullOrWhiteSpace(content)) { content = "Certificate Content\n\nThis is a certificate generated from DOC template.\n\nTemplate processing completed successfully."; } // 生成PDF await GeneratePdfFromContent(pdfStream, content); pdfStream.Position = 0; _logger.LogInformation("DOC转PDF完成,PDF大小: {Size} bytes", pdfStream.Length); return pdfStream; } catch (Exception ex) { _logger.LogError(ex, "DOC转PDF失败,使用备用方案"); return await GenerateSimplePdf(docStream); } } /// /// 提取DOCX文件内容 /// private async Task ExtractDocxContent(Stream docStream) { var content = new System.Text.StringBuilder(); using var wordprocessingDocument = WordprocessingDocument.Open(docStream, false); var body = wordprocessingDocument.MainDocumentPart?.Document?.Body; if (body != null) { // 提取所有段落文本 foreach (var paragraph in body.Elements()) { var paragraphText = paragraph.InnerText?.Trim(); if (!string.IsNullOrEmpty(paragraphText)) { content.AppendLine(paragraphText); } } // 提取表格内容 foreach (var table in body.Elements()) { foreach (var row in table.Elements()) { var rowText = new System.Text.StringBuilder(); foreach (var cell in row.Elements()) { var cellText = cell.InnerText?.Trim(); if (!string.IsNullOrEmpty(cellText)) { rowText.Append(cellText + " | "); } } if (rowText.Length > 0) { content.AppendLine(rowText.ToString().TrimEnd(" | ".ToCharArray())); } } } } return content.ToString(); } /// /// 从内容生成PDF /// private async Task GeneratePdfFromContent(Stream pdfStream, string content) { 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(); } /// /// 转换文本为ASCII兼容格式 /// 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(); } /// /// 生成简单的PDF作为备用方案 /// private async Task GenerateSimplePdf(Stream docStream) { _logger.LogInformation("使用备用PDF生成方案"); var pdfStream = new MemoryStream(); try { // 重置流位置 docStream.Position = 0; // 读取DOC内容 string content; try { using var reader = new StreamReader(docStream, System.Text.Encoding.UTF8); content = await reader.ReadToEndAsync(); } 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); } pdfStream.Position = 0; return pdfStream; } /// /// 获取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 (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); 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}{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}{name}"; await _ossService.PutObjectAsync(options.Bucket, fileName, pdfStream); // 预测最终 URL string bucketPrefix = (options.Provider == OSSProvider.Minio) ? $"/{options.Bucket}" : string.Empty; finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}"; _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; // 预测最终 URL if (!options.Enable) { string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath); var request = _httpContextAccessor.HttpContext?.Request; if (request != null) { finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir}{name}{extension}"; } } else { string bucketPrefix = (options.Provider == OSSProvider.Minio) ? $"/{options.Bucket}" : string.Empty; string fileName = $"{filePath}{name}{extension}"; finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}"; } qrStream.Position = 0; if (!options.Enable) { // 本地存储 string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath); string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir); 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); } else { // OSS 上传 string fileName = $"{filePath}{name}{extension}"; await _ossService.PutObjectAsync(options.Bucket, 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); } }