diff --git a/src/Dockerfile b/src/Dockerfile index 27eeb11..4081a37 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -8,6 +8,8 @@ COPY HtmlToPdfService.Api/HtmlToPdfService.Api.csproj HtmlToPdfService.Api/ COPY HtmlToPdfService.Core/HtmlToPdfService.Core.csproj HtmlToPdfService.Core/ COPY HtmlToPdfService.Queue/HtmlToPdfService.Queue.csproj HtmlToPdfService.Queue/ COPY HtmlToPdfService.Infrastructure/HtmlToPdfService.Infrastructure.csproj HtmlToPdfService.Infrastructure/ +COPY HtmlToPdfService.Client/HtmlToPdfService.Client.csproj HtmlToPdfService.Client/ +COPY HtmlToPdfService.Tests/HtmlToPdfService.Tests.csproj HtmlToPdfService.Tests/ # 还原 NuGet 包 RUN dotnet restore HtmlToPdfService.sln @@ -67,13 +69,14 @@ WORKDIR /app COPY --from=build /app/publish . # 创建存储目录 -RUN mkdir -p /app/files /app/logs && chmod -R 777 /app/files /app/logs +RUN mkdir -p /app/files /app/logs /app/config /app/chromium && chmod -R 777 /app/files /app/logs /app/config /app/chromium # 设置环境变量 ENV ASPNETCORE_URLS=http://+:5000 ENV ASPNETCORE_ENVIRONMENT=Production ENV DOTNET_RUNNING_IN_CONTAINER=true ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=false +ENV PUPPETEER_CACHE_DIR=/app/chromium # 暴露端口 EXPOSE 5000 diff --git a/src/HtmlToPdfService.Api/Program.cs b/src/HtmlToPdfService.Api/Program.cs index 9f1a5be..a6ad5f5 100644 --- a/src/HtmlToPdfService.Api/Program.cs +++ b/src/HtmlToPdfService.Api/Program.cs @@ -276,27 +276,20 @@ var logger = app.Services.GetRequiredService>(); // 仅当已初始化时才预热浏览器池 if (isInitialized) { - try - { - var browserPool = app.Services.GetRequiredService(); - - logger.LogInformation("步骤 1/2: 准备 Chromium 浏览器引擎..."); - var chromiumPath = await browserPool.EnsureChromiumReadyAsync( - progress => logger.LogInformation(" {Progress}", progress)); - - logger.LogInformation("步骤 2/2: 预热浏览器池..."); - await browserPool.WarmUpAsync(); - logger.LogInformation("浏览器池预热完成"); - - logger.LogInformation("========================================"); - logger.LogInformation("服务准备就绪!"); - logger.LogInformation("Swagger UI: http://localhost:5000/swagger"); - logger.LogInformation("========================================"); - } - catch (Exception ex) - { - logger.LogError(ex, "启动准备失败,服务可能无法正常工作"); - } + var browserPool = app.Services.GetRequiredService(); + + logger.LogInformation("步骤 1/2: 准备 Chromium 浏览器引擎..."); + var chromiumPath = await browserPool.EnsureChromiumReadyAsync( + progress => logger.LogInformation(" {Progress}", progress)); + + logger.LogInformation("步骤 2/2: 预热浏览器池..."); + await browserPool.WarmUpAsync(); + logger.LogInformation("浏览器池预热完成"); + + logger.LogInformation("========================================"); + logger.LogInformation("服务准备就绪!"); + logger.LogInformation("Swagger UI: http://localhost:5000/swagger"); + logger.LogInformation("========================================"); } else { diff --git a/src/HtmlToPdfService.Core/Pool/BrowserPool.cs b/src/HtmlToPdfService.Core/Pool/BrowserPool.cs index 51df97f..eaff74b 100644 --- a/src/HtmlToPdfService.Core/Pool/BrowserPool.cs +++ b/src/HtmlToPdfService.Core/Pool/BrowserPool.cs @@ -488,7 +488,37 @@ public class BrowserPool : IBrowserPool _logger.LogInformation(message); progress?.Invoke(message); - var browserFetcher = new BrowserFetcher(); + // 使用固定缓存目录,方便 Docker volume 映射持久化 + var cacheDir = Environment.GetEnvironmentVariable("PUPPETEER_CACHE_DIR") + ?? Path.Combine(AppContext.BaseDirectory, "chromium"); + Directory.CreateDirectory(cacheDir); + + var fetcherOptions = new BrowserFetcherOptions { Path = cacheDir }; + + // 支持通过环境变量配置 Chromium 下载代理 + var proxyUrl = Environment.GetEnvironmentVariable("CHROMIUM_DOWNLOAD_PROXY"); + if (!string.IsNullOrWhiteSpace(proxyUrl)) + { + _logger.LogInformation("使用代理下载 Chromium: {Proxy}", proxyUrl); + fetcherOptions.CustomFileDownload = async (url, destinationPath) => + { + var handler = new System.Net.Http.HttpClientHandler + { + Proxy = new System.Net.WebProxy(proxyUrl), + UseProxy = true + }; + using var httpClient = new System.Net.Http.HttpClient(handler); + httpClient.Timeout = TimeSpan.FromMinutes(10); + + using var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + using var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None); + await response.Content.CopyToAsync(fileStream); + }; + } + + var browserFetcher = new BrowserFetcher(fetcherOptions); var installedBrowsers = browserFetcher.GetInstalledBrowsers(); if (installedBrowsers.Any()) diff --git a/src/HtmlToPdfService.Core/Services/PuppeteerImageService.cs b/src/HtmlToPdfService.Core/Services/PuppeteerImageService.cs index a97d7f3..010bfb6 100644 --- a/src/HtmlToPdfService.Core/Services/PuppeteerImageService.cs +++ b/src/HtmlToPdfService.Core/Services/PuppeteerImageService.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PuppeteerSharp; +using HtmlToPdfService.Core.Configuration; using HtmlToPdfService.Core.Models; using HtmlToPdfService.Core.Options; using HtmlToPdfService.Core.Pool; @@ -13,18 +14,44 @@ namespace HtmlToPdfService.Core.Services; /// public class PuppeteerImageService : IImageService { - private readonly PdfServiceOptions _options; + private readonly IOptions _staticOptions; + private readonly IConfigurationManager? _configManager; private readonly ILogger _logger; private readonly IBrowserPool _browserPool; + /// + /// 获取当前有效配置(优先使用动态配置) + /// + private PdfServiceOptions _options + { + get + { + if (_configManager?.IsInitialized == true) + { + try + { + var userConfig = _configManager.GetConfigurationAsync().GetAwaiter().GetResult(); + return userConfig.ToPdfServiceOptions(); + } + catch + { + // 动态配置读取失败时回退到静态配置 + } + } + return _staticOptions.Value; + } + } + public PuppeteerImageService( IOptions options, ILogger logger, - IBrowserPool browserPool) + IBrowserPool browserPool, + IConfigurationManager? configManager = null) { - _options = options.Value; + _staticOptions = options; _logger = logger; _browserPool = browserPool; + _configManager = configManager; } /// diff --git a/src/HtmlToPdfService.Core/Services/PuppeteerPdfService.cs b/src/HtmlToPdfService.Core/Services/PuppeteerPdfService.cs index 65e7461..00e19f3 100644 --- a/src/HtmlToPdfService.Core/Services/PuppeteerPdfService.cs +++ b/src/HtmlToPdfService.Core/Services/PuppeteerPdfService.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PuppeteerSharp; using PuppeteerSharp.Media; +using HtmlToPdfService.Core.Configuration; using HtmlToPdfService.Core.Models; using HtmlToPdfService.Core.Options; using HtmlToPdfService.Core.Pool; @@ -14,18 +15,44 @@ namespace HtmlToPdfService.Core.Services; /// public class PuppeteerPdfService : IPdfService { - private readonly PdfServiceOptions _options; + private readonly IOptions _staticOptions; + private readonly IConfigurationManager? _configManager; private readonly ILogger _logger; private readonly IBrowserPool _browserPool; + /// + /// 获取当前有效配置(优先使用动态配置) + /// + private PdfServiceOptions _options + { + get + { + if (_configManager?.IsInitialized == true) + { + try + { + var userConfig = _configManager.GetConfigurationAsync().GetAwaiter().GetResult(); + return userConfig.ToPdfServiceOptions(); + } + catch + { + // 动态配置读取失败时回退到静态配置 + } + } + return _staticOptions.Value; + } + } + public PuppeteerPdfService( IOptions options, ILogger logger, - IBrowserPool browserPool) + IBrowserPool browserPool, + IConfigurationManager? configManager = null) { - _options = options.Value; + _staticOptions = options; _logger = logger; _browserPool = browserPool; + _configManager = configManager; } ///