领码方案|Windows 下 PLT → PDF 转换服务超级完整版:异步、权限、进度

发布于:2025-09-13 ⋅ 阅读:(25) ⋅ 点赞:(0)

摘要

面向 Windows 平台,使用 ASP.NET Core Web API 结合 Ghostscript.NET 库,实现 PLT(HPGL)→PDF 的纯库调用转换,无需外部进程。支持同步与异步模式,采用 JWT+RBAC 进行权限治理,任务状态存储于 Redis,后台服务并行处理并通过 SSE/WebSocket 或轮询实时推送进度。集成 iText7 对生成 PDF 添加半透明水印,输出可插拔至本地、S3、Azure Blob 等存储。容器化部署于 Windows Containers 或 Kubernetes,结合 HPA 弹性伸缩与 Serilog+Prometheus+Grafana 可观测性,满足高并发、安全、可运维的文件转换需求。

关键字

  • Ghostscript.NET
  • 异步转换
  • 权限治理
  • 进度推送
  • Redis 存储
  • Kubernetes 部署

一、组件安装与环境准备

  • 操作系统:Windows 10/11 或 Windows Server 2019/2022
  • .NET 环境:安装 .NET 7.0 SDK 与对应 ASP.NET Core Runtime
  • Ghostscript(含 PLT/HPGL 支持)
    • 安装 Ghostscript for Windows,确保包含 gsdll32.dll/gsdll64.dll
    • 将安装目录(例如 C:\Program Files\gs\gs9.xxx\bin)加入系统 PATH
  • NuGet 依赖:
    Install-Package Ghostscript.NET
    Install-Package Ghostscript.NET.Rasterizer   # 可选:预览
    Install-Package StackExchange.Redis
    Install-Package itext7                       # 或 PdfSharpCore
    Install-Package AWSSDK.S3                    # 可选:S3 存储
    Install-Package Azure.Storage.Blobs          # 可选:Azure Blob
    Install-Package Hangfire.Core Hangfire.AspNetCore  # 可选:任务调度
    
  • Redis 服务:安装 Memurai 或社区版 Redis for Windows,启动并确认 localhost:6379 可访问
  • 开发工具:Visual Studio 2022 或 VS Code + C# 扩展

二、核心 HTTP 接口

  1. POST /plt/upload

    • 参数:IFormFile filestring projectIdstring mode = "async"
    • 返回:sync→{ downloadUrl };async→{ taskId }
  2. GET /plt/status/{taskId}

    • 返回:{ taskId, status, progress, outputName, message }
  3. GET /plt/download/{fileName}

    • 下载生成的 PDF
  4. GET /plt/list

    • 参数:projectId, page, size
    • 返回:历史转换记录列表
  5. POST /plt/uploadConverted

    • 上传外部已转换 PDF,返回 { url }
  6. GET /auth/check

    • 校验当前用户对项目的 convert/download 权限

三、同步与异步模式

  • 同步(sync)

    • 上传后在当前请求里调用 Ghostscript.NET 直接转换并水印
    • 阻塞等待完成,返回下载链接
  • 异步(async)

    • 上传后立即返回 taskId
    • 后台由 BackgroundService 或 Hangfire 从队列取任务执行
    • 前端通过轮询或 SSE/WebSocket 获取实时进度

四、权限治理

  • 鉴权:JWT + OAuth2 公钥验证
  • 授权:Policy-Based Authorization + RBAC + 项目域校验
  • 中间件/过滤器解析 Token,校验用户角色、projectId 与操作类型
  • 未授权访问返回 HTTP 403

五、任务状态管理与存储

  • 领域模型 TaskStatusDto

    • TaskIdStatus(PENDING/PROCESSING/DONE/FAILED)
    • Progress (0–100)、FileNameOutputNameMessage
  • 接口 ITaskStatusStore

    Task CreateAsync(TaskStatusDto status);
    Task UpdateAsync(string taskId, Action<TaskStatusDto> update);
    Task<TaskStatusDto?> GetAsync(string taskId);
    ValueTask<(string taskId, string inputPath, string outputPath)> DequeueAsync(CancellationToken ct);
    
  • Redis 实现:StackExchange.Redis 保存 JSON,设置 TTL 自动清理

  • 存储输出:本地目录 / AWS S3 / Azure Blob / MinIO 插件化替换


六、PLT → PDF 转换与水印(无外部进程)

1. 转换接口定义

public interface IPltToPdfConverter
{
    Task ConvertAsync(
        string inputPath,
        string outputPath,
        IProgress<(int percent, string message)> progress,
        CancellationToken cancellationToken);
}

2. Ghostscript.NET 实现

using Ghostscript.NET;
using Ghostscript.NET.GhostscriptProcessing;

public class GhostscriptNetConverter : IPltToPdfConverter
{
    private readonly GhostscriptVersionInfo _versionInfo;

    public GhostscriptNetConverter()
    {
        _versionInfo = GhostscriptVersionInfo.GetLastInstalledVersion()
            ?? throw new InvalidOperationException("未检测到 Ghostscript 安装");
    }

    public Task ConvertAsync(
        string inputPath,
        string outputPath,
        IProgress<(int, string)> progress,
        CancellationToken cancellationToken) =>
        Task.Run(() =>
        {
            progress?.Report((10, "初始化 Ghostscript"));
            var switches = new[]
            {
                "-q", "-dNOPAUSE", "-dBATCH", "-dSAFER",
                "-sDEVICE=pdfwrite",
                $"-sOutputFile={outputPath}",
                inputPath
            };
            using var processor = new GhostscriptProcessor(_versionInfo, true);
            processor.StartProcessing(
                switches,
                new GhostscriptProcessorTextDelegate(line =>
                {
                    progress?.Report((50, "转换进行中…"));
                })
            );
            progress?.Report((100, "转换完成"));
        }, cancellationToken);
}

3. PDF 水印服务

using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;

public class PdfWatermarkService
{
    public void ApplyWatermark(string pdfPath, string watermarkText)
    {
        var tmpPath = pdfPath + ".tmp";
        using var reader = new PdfReader(pdfPath);
        using var writer = new PdfWriter(tmpPath);
        using var pdf    = new PdfDocument(reader, writer);
        for (int i = 1; i <= pdf.GetNumberOfPages(); i++)
        {
            var canvas = new PdfCanvas(pdf.GetPage(i));
            canvas.SetFontAndSize(PdfFontFactory.CreateFont(), 36)
                  .SetFillColorGray(0.5f)
                  .BeginText()
                  .MoveText(200, 400)
                  .ShowText(watermarkText)
                  .EndText();
        }
        pdf.Close();
        File.Replace(tmpPath, pdfPath, null);
    }
}

七、后台任务与进度推送

public class PltConversionJob : BackgroundService
{
    private readonly IPltToPdfConverter _converter;
    private readonly ITaskStatusStore  _store;
    private readonly PdfWatermarkService _watermarker;

    public PltConversionJob(
        IPltToPdfConverter converter,
        ITaskStatusStore store,
        PdfWatermarkService watermarker)
    {
        _converter   = converter;
        _store       = store;
        _watermarker = watermarker;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var (taskId, input, output) = await _store.DequeueAsync(stoppingToken);
            await _store.UpdateAsync(taskId, s => { s.Status = "PROCESSING"; s.Progress = 0; s.Message = "开始转换"; });
            try
            {
                await _converter.ConvertAsync(input, output,
                    new Progress<(int, string)>(p =>
                        _store.UpdateAsync(taskId, s => { s.Progress = p.Item1; s.Message = p.Item2; })),
                    stoppingToken);
                _watermarker.ApplyWatermark(output, "CONFIDENTIAL");
                await _store.UpdateAsync(taskId, s =>
                {
                    s.Status     = "DONE";
                    s.Progress   = 100;
                    s.OutputName = Path.GetFileName(output);
                    s.Message    = "完成";
                });
            }
            catch (Exception ex)
            {
                await _store.UpdateAsync(taskId, s => { s.Status = "FAILED"; s.Message = ex.Message; });
            }
        }
    }
}

八、配置示例(appsettings.json)

{
  "Plt": {
    "TempDirectory": "C:\\plt\\tmp",
    "OutputDirectory": "C:\\plt\\out"
  },
  "Storage": {
    "Type": "Local",
    "Local": { "RootPath": "C:\\plt\\out" }
  },
  "Redis": { "Configuration": "localhost:6379" },
  "Async": { "MaxConcurrentTasks": 8, "QueueCapacity": 200 },
  "Jwt": { "Authority": "https://auth.example.com", "Audience": "plt-api" },
  "Watermark": { "Enabled": true, "Text": "CONFIDENTIAL", "Opacity": 0.15, "FontSize": 36 },
  "Security": { "RateLimitQps": 50, "MaxUploadMb": 50 }
}

九、前端集成

  • 技术栈:Vue/React + Axios
  • 上传:FormData POST /plt/upload
  • 实时进度:setInterval/plt/status/{taskId} 或 SSE/WebSocket
  • 下载:window.location.href = '/plt/download/' + outputName

十、部署与运维

  • 容器化:Windows Containers(或 Linux 容器亦可加载 Ghostscript DLL)
  • Kubernetes:Windows 节点组 + ConfigMap/Secret + PVC
  • 弹性伸缩:HPA 按 CPU 与队列长度自动扩容
  • 安全:容器非管理员运行;Ghostscript 使用 -dSAFER;上传文件安全扫描
  • 监控:Serilog + Prometheus + Grafana,关注 QPS、成功率、P95、队列长度、失败原因 Top N

十一、常见问题与优化

  • 进度不精准:结合文件规模或并行分片估算
  • I/O 瓶颈:将临时目录挂载到 RAM Disk
  • 多格式支持:扩展输出为 PDF/A、PNG、SVG
  • 无服务器化:小文件可迁移至 Azure Functions/AWS Lambda
  • 水印增强:动态二维码、PDF 数字签名
  • CI/CD:GitHub Actions + Azure DevOps 压测
  • 前端体验:可视化进度条、失败自动重试、历史记录查看

更多架构细节与实践优化,欢迎交流!


网站公告

今日签到

点亮在社区的每一天
去签到