HtmlToPdf/mvp/HtmlToPdfService.Api/Controllers/PdfController.cs
2025-12-11 23:35:52 +08:00

336 lines
11 KiB
C#

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;
/// <summary>
/// PDF 转换控制器
/// </summary>
[ApiController]
[Route("api/pdf")]
public class PdfController : ControllerBase
{
private readonly IPdfService _pdfService;
private readonly IBrowserPool _browserPool;
private readonly IFileStorage _fileStorage;
private readonly ILogger<PdfController> _logger;
public PdfController(
IPdfService pdfService,
IBrowserPool browserPool,
IFileStorage fileStorage,
ILogger<PdfController> logger)
{
_pdfService = pdfService;
_browserPool = browserPool;
_fileStorage = fileStorage;
_logger = logger;
}
/// <summary>
/// 从 HTML 内容生成 PDF
/// </summary>
/// <param name="request">转换请求</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>PDF 文件</returns>
[HttpPost("convert/html")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> 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
});
}
}
/// <summary>
/// 从 URL 生成 PDF
/// </summary>
/// <param name="request">转换请求</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>PDF 文件</returns>
[HttpPost("convert/url")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> 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
});
}
}
/// <summary>
/// 下载已生成的 PDF
/// </summary>
/// <param name="requestId">请求 ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>PDF 文件</returns>
[HttpGet("download/{requestId}")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> 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
});
}
}
/// <summary>
/// 健康检查
/// </summary>
/// <returns>服务健康状态</returns>
[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
});
}
}
/// <summary>
/// 解析纸张格式
/// </summary>
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
};
}
/// <summary>
/// 解析等待条件
/// </summary>
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 }
};
}
}