using System.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PuppeteerSharp; using PuppeteerSharp.Media; using HtmlToPdfService.Core.Models; using HtmlToPdfService.Core.Options; using HtmlToPdfService.Core.Pool; using HtmlToPdfService.Core.Storage; namespace HtmlToPdfService.Core.Services; /// /// Puppeteer PDF 转换服务实现 /// public class PuppeteerPdfService : IPdfService { private readonly PdfServiceOptions _options; private readonly ILogger _logger; private readonly IBrowserPool _browserPool; private readonly IFileStorage _fileStorage; private readonly ICallbackService _callbackService; public PuppeteerPdfService( IOptions options, ILogger logger, IBrowserPool browserPool, IFileStorage fileStorage, ICallbackService callbackService) { _options = options.Value; _logger = logger; _browserPool = browserPool; _fileStorage = fileStorage; _callbackService = callbackService; } /// /// 将 HTML 内容转换为 PDF /// public async Task ConvertHtmlToPdfAsync( string html, PdfOptions? options = null, string? callbackUrl = null, Dictionary? callbackHeaders = null, bool? includePdfInCallback = null, bool? saveLocal = null, CancellationToken cancellationToken = default) { var requestId = Guid.NewGuid().ToString(); var startTime = DateTime.UtcNow; var stopwatch = Stopwatch.StartNew(); _logger.LogInformation("开始转换 HTML to PDF, RequestId: {RequestId}", requestId); IBrowser? browser = null; IPage? page = null; try { // 验证 HTML 内容大小 var htmlSize = System.Text.Encoding.UTF8.GetByteCount(html); if (htmlSize > _options.Conversion.MaxHtmlSize) { throw new ArgumentException( $"HTML 内容过大: {htmlSize} bytes,最大允许: {_options.Conversion.MaxHtmlSize} bytes"); } // 从浏览器池获取实例 browser = await _browserPool.AcquireAsync(cancellationToken); _logger.LogDebug("获取浏览器实例成功, RequestId: {RequestId}", requestId); // 创建新页面 page = await browser.NewPageAsync(); // 设置 HTML 内容 await page.SetContentAsync(html, new NavigationOptions { Timeout = _options.Conversion.DefaultTimeout, WaitUntil = ParseWaitUntil(_options.Conversion.DefaultWaitUntil) }); // 生成 PDF var pdfOptions = BuildPdfOptions(options); var pdfData = await page.PdfDataAsync(pdfOptions); stopwatch.Stop(); _logger.LogInformation("HTML 转换成功, RequestId: {RequestId}, 耗时: {Duration}ms, 大小: {Size} bytes", requestId, stopwatch.ElapsedMilliseconds, pdfData.Length); // 构建结果 var result = new ConversionResult { RequestId = requestId, Success = true, PdfData = pdfData, FileSize = pdfData.Length, Duration = stopwatch.ElapsedMilliseconds, StartTime = startTime, CompleteTime = DateTime.UtcNow }; // 保存本地副本 var shouldSaveLocal = saveLocal ?? _options.Storage.SaveLocalCopy; if (shouldSaveLocal) { var (filePath, downloadUrl) = await _fileStorage.SaveAsync(requestId, pdfData, cancellationToken); result.LocalFilePath = filePath; result.DownloadUrl = downloadUrl; } // 发送回调 await SendCallbackIfNeededAsync( result, "html", html, options, callbackUrl, callbackHeaders, includePdfInCallback); return result; } catch (Exception ex) { stopwatch.Stop(); _logger.LogError(ex, "HTML 转换失败, RequestId: {RequestId}, 耗时: {Duration}ms", requestId, stopwatch.ElapsedMilliseconds); var result = new ConversionResult { RequestId = requestId, Success = false, Duration = stopwatch.ElapsedMilliseconds, StartTime = startTime, CompleteTime = DateTime.UtcNow, ErrorMessage = ex.Message, ExceptionDetails = ex.ToString() }; // 发送失败回调 await SendCallbackIfNeededAsync( result, "html", html, options, callbackUrl, callbackHeaders, includePdfInCallback); throw; } finally { // 关闭页面 if (page != null) { await page.CloseAsync(); } // 归还浏览器到池中 if (browser != null) { _browserPool.Release(browser); } } } /// /// 将 URL 转换为 PDF /// public async Task ConvertUrlToPdfAsync( string url, PdfOptions? options = null, WaitUntilNavigation[]? waitUntil = null, int? timeout = null, string? callbackUrl = null, Dictionary? callbackHeaders = null, bool? includePdfInCallback = null, bool? saveLocal = null, CancellationToken cancellationToken = default) { var requestId = Guid.NewGuid().ToString(); var startTime = DateTime.UtcNow; var stopwatch = Stopwatch.StartNew(); _logger.LogInformation("开始转换 URL to PDF, RequestId: {RequestId}, URL: {Url}", requestId, url); IBrowser? browser = null; IPage? page = null; try { // 验证 URL if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) { throw new ArgumentException($"无效的 URL: {url}"); } // 从浏览器池获取实例 browser = await _browserPool.AcquireAsync(cancellationToken); _logger.LogDebug("获取浏览器实例成功, RequestId: {RequestId}", requestId); // 创建新页面 page = await browser.NewPageAsync(); // 导航到 URL var navigationOptions = new NavigationOptions { Timeout = timeout ?? _options.Conversion.DefaultTimeout, WaitUntil = waitUntil ?? ParseWaitUntil(_options.Conversion.DefaultWaitUntil) }; await page.GoToAsync(url, navigationOptions); // 生成 PDF var pdfOptions = BuildPdfOptions(options); var pdfData = await page.PdfDataAsync(pdfOptions); stopwatch.Stop(); _logger.LogInformation("URL 转换成功, RequestId: {RequestId}, 耗时: {Duration}ms, 大小: {Size} bytes", requestId, stopwatch.ElapsedMilliseconds, pdfData.Length); // 构建结果 var result = new ConversionResult { RequestId = requestId, Success = true, PdfData = pdfData, FileSize = pdfData.Length, Duration = stopwatch.ElapsedMilliseconds, StartTime = startTime, CompleteTime = DateTime.UtcNow }; // 保存本地副本 var shouldSaveLocal = saveLocal ?? _options.Storage.SaveLocalCopy; if (shouldSaveLocal) { var (filePath, downloadUrl) = await _fileStorage.SaveAsync(requestId, pdfData, cancellationToken); result.LocalFilePath = filePath; result.DownloadUrl = downloadUrl; } // 发送回调 await SendCallbackIfNeededAsync( result, "url", url, options, callbackUrl, callbackHeaders, includePdfInCallback); return result; } catch (Exception ex) { stopwatch.Stop(); _logger.LogError(ex, "URL 转换失败, RequestId: {RequestId}, URL: {Url}, 耗时: {Duration}ms", requestId, url, stopwatch.ElapsedMilliseconds); var result = new ConversionResult { RequestId = requestId, Success = false, Duration = stopwatch.ElapsedMilliseconds, StartTime = startTime, CompleteTime = DateTime.UtcNow, ErrorMessage = ex.Message, ExceptionDetails = ex.ToString() }; // 发送失败回调 await SendCallbackIfNeededAsync( result, "url", url, options, callbackUrl, callbackHeaders, includePdfInCallback); throw; } finally { // 关闭页面 if (page != null) { await page.CloseAsync(); } // 归还浏览器到池中 if (browser != null) { _browserPool.Release(browser); } } } /// /// 构建 PDF 选项 /// private PdfOptions BuildPdfOptions(PdfOptions? customOptions) { var defaultOptions = _options.DefaultPdfOptions; return new PdfOptions { Format = customOptions?.Format ?? ParsePaperFormat(defaultOptions.Format), Landscape = customOptions?.Landscape ?? defaultOptions.Landscape, PrintBackground = customOptions?.PrintBackground ?? defaultOptions.PrintBackground, PreferCSSPageSize = customOptions?.PreferCSSPageSize ?? defaultOptions.PreferCSSPageSize, MarginOptions = customOptions?.MarginOptions ?? new PuppeteerSharp.Media.MarginOptions { Top = defaultOptions.Margin.Top, Right = defaultOptions.Margin.Right, Bottom = defaultOptions.Margin.Bottom, Left = defaultOptions.Margin.Left } }; } /// /// 解析纸张格式 /// private PaperFormat ParsePaperFormat(string format) { return format.ToUpperInvariant() switch { "A3" => PaperFormat.A3, "A4" => PaperFormat.A4, "A5" => PaperFormat.A5, "LETTER" => PaperFormat.Letter, "LEGAL" => PaperFormat.Legal, "TABLOID" => PaperFormat.Tabloid, _ => PaperFormat.A4 }; } /// /// 解析等待条件 /// private WaitUntilNavigation[] ParseWaitUntil(string waitUntil) { return waitUntil.ToLowerInvariant() switch { "load" => new[] { WaitUntilNavigation.Load }, "domcontentloaded" => new[] { WaitUntilNavigation.DOMContentLoaded }, "networkidle0" => new[] { WaitUntilNavigation.Networkidle0 }, "networkidle2" => new[] { WaitUntilNavigation.Networkidle2 }, _ => new[] { WaitUntilNavigation.Networkidle2 } }; } /// /// 发送回调(如果需要) /// private async Task SendCallbackIfNeededAsync( ConversionResult result, string sourceType, string sourceContent, object? options, string? callbackUrl, Dictionary? callbackHeaders, bool? includePdfInCallback) { // 确定回调 URL(请求级优先于全局配置) var effectiveCallbackUrl = callbackUrl ?? _options.Callback.DefaultUrl; if (string.IsNullOrEmpty(effectiveCallbackUrl)) { return; } // 确定是否包含 PDF 数据 var shouldIncludePdf = includePdfInCallback ?? _options.Callback.IncludePdfData; // 构建回调负载 var payload = new CallbackPayload { RequestId = result.RequestId, Status = result.Success ? "success" : "failed", Timestamp = DateTime.UtcNow, Duration = result.Duration, Source = new CallbackSource { Type = sourceType, Content = sourceContent, Options = options } }; if (result.Success) { payload.Result = new CallbackResult { FileSize = result.FileSize, DownloadUrl = result.DownloadUrl, PdfBase64 = shouldIncludePdf && result.PdfData != null ? Convert.ToBase64String(result.PdfData) : null, ExpiresAt = result.DownloadUrl != null ? DateTime.UtcNow.AddHours(_options.Storage.RetentionHours) : null }; } else { payload.Error = new CallbackError { Code = "CONVERSION_FAILED", Message = result.ErrorMessage ?? "未知错误", Details = result.ExceptionDetails }; } // 发送回调 await _callbackService.SendCallbackAsync( effectiveCallbackUrl, payload, callbackHeaders); } }