336 lines
11 KiB
C#
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 }
|
|
};
|
|
}
|
|
}
|
|
|