using Microsoft.AspNetCore.Mvc; using PuppeteerSharp; using PuppeteerSharp.Media; using HtmlToPdfService.Api.Models; using HtmlToPdfService.Core.Pool; using HtmlToPdfService.Core.Services; using HtmlToPdfService.Core.Storage; namespace HtmlToPdfService.Api.Controllers; /// /// PDF 转换控制器 /// [ApiController] [Route("api/pdf")] public class PdfController : ControllerBase { private readonly IPdfService _pdfService; private readonly IBrowserPool _browserPool; private readonly IFileStorage _fileStorage; private readonly ILogger _logger; public PdfController( IPdfService pdfService, IBrowserPool browserPool, IFileStorage fileStorage, ILogger logger) { _pdfService = pdfService; _browserPool = browserPool; _fileStorage = fileStorage; _logger = logger; } /// /// 从 HTML 内容生成 PDF /// /// 转换请求 /// 取消令牌 /// PDF 文件 [HttpPost("convert/html")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task ConvertHtml( [FromBody] ConvertHtmlRequest request, CancellationToken cancellationToken) { if (!ModelState.IsValid) { return BadRequest(ModelState); } try { _logger.LogInformation("收到 HTML 转 PDF 请求"); // 构建 PDF 选项 PdfOptions? pdfOptions = null; if (request.Options != null) { pdfOptions = new PdfOptions { Format = ParsePaperFormat(request.Options.Format), Landscape = request.Options.Landscape ?? false, PrintBackground = request.Options.PrintBackground ?? true, MarginOptions = request.Options.Margin != null ? new MarginOptions { Top = request.Options.Margin.Top, Right = request.Options.Margin.Right, Bottom = request.Options.Margin.Bottom, Left = request.Options.Margin.Left } : null }; } // 执行转换 var result = await _pdfService.ConvertHtmlToPdfAsync( request.Html, pdfOptions, request.Callback?.Url, request.Callback?.Headers, request.Callback?.IncludePdfData, request.SaveLocal, cancellationToken); // 返回 PDF 文件 return File(result.PdfData!, "application/pdf", "document.pdf"); } catch (ArgumentException ex) { _logger.LogWarning(ex, "请求参数无效"); return BadRequest(new ProblemDetails { Status = StatusCodes.Status400BadRequest, Title = "请求参数无效", Detail = ex.Message }); } catch (TimeoutException ex) { _logger.LogError(ex, "转换超时"); return StatusCode(StatusCodes.Status503ServiceUnavailable, new ProblemDetails { Status = StatusCodes.Status503ServiceUnavailable, Title = "服务繁忙", Detail = ex.Message }); } catch (Exception ex) { _logger.LogError(ex, "HTML 转 PDF 失败"); return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails { Status = StatusCodes.Status500InternalServerError, Title = "转换失败", Detail = ex.Message }); } } /// /// 从 URL 生成 PDF /// /// 转换请求 /// 取消令牌 /// PDF 文件 [HttpPost("convert/url")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] public async Task ConvertUrl( [FromBody] ConvertUrlRequest request, CancellationToken cancellationToken) { if (!ModelState.IsValid) { return BadRequest(ModelState); } try { _logger.LogInformation("收到 URL 转 PDF 请求: {Url}", request.Url); // 构建 PDF 选项 PdfOptions? pdfOptions = null; if (request.Options != null) { pdfOptions = new PdfOptions { Format = ParsePaperFormat(request.Options.Format), Landscape = request.Options.Landscape ?? false, PrintBackground = request.Options.PrintBackground ?? true, MarginOptions = request.Options.Margin != null ? new MarginOptions { Top = request.Options.Margin.Top, Right = request.Options.Margin.Right, Bottom = request.Options.Margin.Bottom, Left = request.Options.Margin.Left } : null }; } // 解析等待条件 WaitUntilNavigation[]? waitUntil = null; if (!string.IsNullOrEmpty(request.WaitUntil)) { waitUntil = ParseWaitUntil(request.WaitUntil); } // 执行转换 var result = await _pdfService.ConvertUrlToPdfAsync( request.Url, pdfOptions, waitUntil, request.Timeout, request.Callback?.Url, request.Callback?.Headers, request.Callback?.IncludePdfData, request.SaveLocal, cancellationToken); // 返回 PDF 文件 return File(result.PdfData!, "application/pdf", "document.pdf"); } catch (ArgumentException ex) { _logger.LogWarning(ex, "请求参数无效"); return BadRequest(new ProblemDetails { Status = StatusCodes.Status400BadRequest, Title = "请求参数无效", Detail = ex.Message }); } catch (TimeoutException ex) { _logger.LogError(ex, "转换超时"); return StatusCode(StatusCodes.Status503ServiceUnavailable, new ProblemDetails { Status = StatusCodes.Status503ServiceUnavailable, Title = "服务繁忙", Detail = ex.Message }); } catch (Exception ex) { _logger.LogError(ex, "URL 转 PDF 失败"); return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails { Status = StatusCodes.Status500InternalServerError, Title = "转换失败", Detail = ex.Message }); } } /// /// 下载已生成的 PDF /// /// 请求 ID /// 取消令牌 /// PDF 文件 [HttpGet("download/{requestId}")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Download( string requestId, CancellationToken cancellationToken) { try { var pdfData = await _fileStorage.GetAsync(requestId, cancellationToken); if (pdfData == null) { return NotFound(new ProblemDetails { Status = StatusCodes.Status404NotFound, Title = "文件未找到", Detail = $"请求 ID: {requestId} 对应的 PDF 文件不存在或已过期" }); } return File(pdfData, "application/pdf", $"{requestId}.pdf"); } catch (Exception ex) { _logger.LogError(ex, "下载 PDF 失败: {RequestId}", requestId); return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails { Status = StatusCodes.Status500InternalServerError, Title = "下载失败", Detail = ex.Message }); } } /// /// 健康检查 /// /// 服务健康状态 [HttpGet("/health")] [ProducesResponseType(typeof(HealthResponse), StatusCodes.Status200OK)] public IActionResult Health() { try { var poolStatus = _browserPool.GetStatus(); var response = new HealthResponse { Status = "Healthy", Timestamp = DateTime.UtcNow, BrowserPool = new Models.BrowserPoolStatus { TotalInstances = poolStatus.TotalInstances, AvailableInstances = poolStatus.AvailableInstances, MaxInstances = poolStatus.MaxInstances }, Queue = new QueueStatus { CurrentTasks = poolStatus.InUseInstances, MaxConcurrent = poolStatus.MaxInstances } }; return Ok(response); } catch (Exception ex) { _logger.LogError(ex, "健康检查失败"); return Ok(new HealthResponse { Status = "Unhealthy", Timestamp = DateTime.UtcNow }); } } /// /// 解析纸张格式 /// private PaperFormat? ParsePaperFormat(string? format) { if (string.IsNullOrEmpty(format)) return null; return format.ToUpperInvariant() switch { "A3" => PaperFormat.A3, "A4" => PaperFormat.A4, "A5" => PaperFormat.A5, "LETTER" => PaperFormat.Letter, "LEGAL" => PaperFormat.Legal, "TABLOID" => PaperFormat.Tabloid, _ => null }; } /// /// 解析等待条件 /// 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 } }; } }