using YD_Event.Application.Certificate.Dtos; using YD_Event.Core.Entities; using YD_Event.Core.Options; using SqlSugar; using Mapster; using OnceMi.AspNetCore.OSS; using Microsoft.Extensions.Options; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace YD_Event.Application.Certificate; /// /// 证书生成服务 /// public class CertificateGenerateService : IDynamicApiController, ITransient { private readonly ISqlSugarRepository _certificateRepository; private readonly ISqlSugarRepository _templateRepository; private readonly ISqlSugarRepository _elementRepository; private readonly IOSSService _ossService; private readonly IOptionsMonitor _ossOptionsMonitor; private readonly IIdGenerator _idGenerator; private readonly IWebHostEnvironment _webHostEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; public CertificateGenerateService( ISqlSugarRepository certificateRepository, ISqlSugarRepository templateRepository, ISqlSugarRepository elementRepository, IOSSService ossService, IOptionsMonitor ossOptionsMonitor, IIdGenerator idGenerator, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContextAccessor, ILogger logger) { _certificateRepository = certificateRepository; _templateRepository = templateRepository; _elementRepository = elementRepository; _ossService = ossService; _ossOptionsMonitor = ossOptionsMonitor; _idGenerator = idGenerator; _webHostEnvironment = webHostEnvironment; _httpContextAccessor = httpContextAccessor; _logger = logger; } /// /// 生成证书 /// [DisplayName("生成证书")] public async Task Generate(GenerateCertificateInput input) { // 1. 获取证书信息 var certificate = await _certificateRepository.GetByIdAsync(input.CertificateId); if (certificate == null) throw Oops.Bah("证书不存在"); if (certificate.Status != Core.Enum.AvailabilityStatus.Enable) throw Oops.Bah("证书已禁用"); // 2. 获取模板 (优先使用 input 指定的,否则使用证书关联的) CertificateTemplate template; if (input.TemplateId.HasValue) { template = await _templateRepository.GetByIdAsync(input.TemplateId.Value); } else { // 使用证书关联的模板 template = await _templateRepository.GetByIdAsync(certificate.TemplateId); } if (template == null) throw Oops.Bah("未找到有效证书模板"); // 3. 获取元素 var elements = await _elementRepository.AsQueryable() .Where(x => x.TemplateId == template.Id) .OrderBy(x => x.Sort) .ToListAsync(); // 4. 生成 PDF 内容 byte[] bgImageBytes; using (var httpClient = new HttpClient()) { bgImageBytes = await httpClient.GetByteArrayAsync(template.BackgroundImageUrl); } // 预生成文件名和路径,以便在生成 PDF 时使用 URL (如二维码) var options = _ossOptionsMonitor.CurrentValue; var now = DateTime.Today; string name = _idGenerator.Encode(_idGenerator.NewLong()); string extension = ".pdf"; string filePath = $"/{now.Year}/{now.Month:D2}/{now.Day:D2}/"; 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}"; } var document = Document.Create(container => { container.Page(page => { page.Size(new PageSize(template.Width, template.Height)); page.Margin(0); page.Content().Layers(layers => { layers.PrimaryLayer().Image(bgImageBytes).FitArea(); foreach (var element in elements) { DrawElement(layers, element, input.Data, finalUrl); } }); }); }); var pdfBytes = document.GeneratePdf(); // 5. 文件处理逻辑 (参照 FileService) if (!options.Enable) { // 本地存储 _logger.LogInformation("开始本地存储证书文件. Bucket: {Bucket}, Path: {Path}", options.Bucket, filePath); 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}"); await System.IO.File.WriteAllBytesAsync(fullPath, pdfBytes); _logger.LogInformation("证书文件本地存储成功. URL: {Url}", finalUrl); } else { // OSS 上传 _logger.LogInformation("开始上传证书文件到 OSS. Bucket: {Bucket}, Path: {Path}", options.Bucket, filePath); string fileName = $"{filePath}{name}{extension}"; using (var stream = new MemoryStream(pdfBytes)) { await _ossService.PutObjectAsync(options.Bucket, fileName, stream); } _logger.LogInformation("证书文件 OSS 上传成功. URL: {Url}", finalUrl); } return finalUrl; } private void DrawElement(LayersDescriptor layers, CertificateElement element, Dictionary data, string qrCodeUrl) { float x = element.X; float y = element.Y; float w = element.Width; float h = element.Height; var layer = layers.Layer() .TranslateX(x) .TranslateY(y) .Width(w) .Height(h); if (element.Type == "Text") { string text = element.FieldName; if (!string.IsNullOrEmpty(element.BindingKey) && data != null && data.ContainsKey(element.BindingKey)) { text = data[element.BindingKey]; } var isBold = element.StyleJson != null && element.StyleJson.Contains("\"bold\":true", StringComparison.OrdinalIgnoreCase); var isCenter = element.StyleJson != null && element.StyleJson.Contains("\"align\":\"center\"", StringComparison.OrdinalIgnoreCase); var isRight = element.StyleJson != null && element.StyleJson.Contains("\"align\":\"right\"", StringComparison.OrdinalIgnoreCase); var container = layer; if (isCenter) container = container.AlignCenter(); else if (isRight) container = container.AlignRight(); else container = container.AlignLeft(); container.Text(t => { var span = t.Span(text) .FontFamily(element.FontFamily) .FontSize(element.FontSize) .FontColor(element.Color ?? "#000000"); if (isBold) span.Bold(); // Fallback #pragma warning disable CS0618 span.Fallback(x => x.FontFamily("SimHei")); #pragma warning restore CS0618 }); } else if (element.Type == "QRCode") { string content = qrCodeUrl; if (!string.IsNullOrEmpty(element.BindingKey) && data != null && data.ContainsKey(element.BindingKey)) { content = data[element.BindingKey]; } // layer.Element(e => e.QrCode(content)); layer.Text("QR: " + content); } } }