You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多POST请求分块上传文件,如何合并生成单个PDF?

问题描述

前端应用以分块方式发送多个POST请求上传文件(例如每块包含5个文件,10个文件分2次请求发送),当前后端代码会为每个请求独立生成PDF,需要修改为整合所有请求中的文件生成单个PDF。

解决方案:多阶段上传+会话管理

核心思路是先将所有分块上传的文件暂存在服务器临时目录,待全部上传完成后再统一合并生成PDF。具体分为三个步骤:初始化上传会话、分块上传文件、生成最终PDF。

1. 实现临时上传会话管理服务

创建服务用于管理上传会话的创建、文件存储、文件获取和清理:

public class TempUploadSessionService
{
    private readonly ILogger<TempUploadSessionService> _logger;
    private readonly string _tempRootDir;

    public TempUploadSessionService(ILogger<TempUploadSessionService> logger)
    {
        _logger = logger;
        _tempRootDir = Path.Combine(Path.GetTempPath(), "PdfUploadSessions");
        Directory.CreateDirectory(_tempRootDir);
    }

    // 创建新的上传会话,返回会话ID
    public string CreateSession()
    {
        var sessionId = Guid.NewGuid().ToString();
        var sessionDir = Path.Combine(_tempRootDir, sessionId);
        Directory.CreateDirectory(sessionDir);
        return sessionId;
    }

    // 将上传的文件保存到对应会话目录
    public async Task SaveFilesToSession(string sessionId, IFormFileCollection files)
    {
        var sessionDir = Path.Combine(_tempRootDir, sessionId);
        if (!Directory.Exists(sessionDir))
            throw new InvalidOperationException("会话不存在或已过期");

        foreach (var file in files)
        {
            // 用GUID重命名文件避免冲突
            var tempFileName = $"{Guid.NewGuid()}_{Path.GetFileName(file.FileName)}";
            var tempFilePath = Path.Combine(sessionDir, tempFileName);
            
            using var fileStream = new FileStream(tempFilePath, FileMode.Create);
            await file.CopyToAsync(fileStream);
        }
    }

    // 获取会话下所有临时文件路径
    public List<string> GetSessionFiles(string sessionId)
    {
        var sessionDir = Path.Combine(_tempRootDir, sessionId);
        if (!Directory.Exists(sessionDir))
            throw new InvalidOperationException("会话不存在或已过期");

        return Directory.GetFiles(sessionDir).ToList();
    }

    // 清理会话的临时文件
    public void CleanupSession(string sessionId)
    {
        var sessionDir = Path.Combine(_tempRootDir, sessionId);
        if (Directory.Exists(sessionDir))
        {
            try
            {
                Directory.Delete(sessionDir, recursive: true);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "清理会话 {SessionId} 临时文件失败", sessionId);
            }
        }
    }
}

2. 修改Controller接口

替换原有的单个生成PDF接口,新增三个接口处理多阶段上传:

[ApiController]
[Route("api/pdf")]
public class PdfController : ControllerBase
{
    private readonly ILogger<PdfController> _logger;
    private readonly IPdfGeneratorService _pdfGeneratorService;
    private readonly TempUploadSessionService _uploadSessionService;

    public PdfController(ILogger<PdfController> logger, 
                         IPdfGeneratorService pdfGeneratorService, 
                         TempUploadSessionService uploadSessionService)
    {
        _logger = logger;
        _pdfGeneratorService = pdfGeneratorService;
        _uploadSessionService = uploadSessionService;
    }

    // 初始化上传会话
    [HttpPost("init-upload")]
    public IActionResult InitUpload()
    {
        var sessionId = _uploadSessionService.CreateSession();
        return Ok(new { SessionId = sessionId });
    }

    // 分块上传文件
    [HttpPost("upload-files")]
    public async Task<IActionResult> UploadFiles([FromQuery] string sessionId)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(sessionId))
                return BadRequest("会话ID不能为空");

            if (Request.Form.Files.Count == 0)
                return BadRequest("未上传任何文件");

            await _uploadSessionService.SaveFilesToSession(sessionId, Request.Form.Files);
            return Ok("文件块上传成功");
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(ex.Message);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "文件块上传失败");
            return StatusCode(StatusCodes.Status500InternalServerError, "文件上传失败");
        }
    }

    // 生成最终合并PDF
    [HttpPost("generate-final-pdf")]
    public async Task<IActionResult> GenerateFinalPdf([FromQuery] string sessionId,
                                                      [FromQuery] string colorMode,
                                                      [FromQuery] string? textOrPath)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(sessionId))
                return BadRequest("会话ID不能为空");

            var tempFilePaths = _uploadSessionService.GetSessionFiles(sessionId);
            if (!tempFilePaths.Any())
                return BadRequest("会话中无可用文件");

            // 批量生成单页PDF
            var pdfPageTasks = tempFilePaths.Select(path => 
                Task.Run(() => _pdfGeneratorService.GeneratePdfFromSvgPath(path, colorMode, textOrPath))).ToList();
            
            await Task.WhenAll(pdfPageTasks);
            var pdfPages = pdfPageTasks.Select(t => t.Result).ToList();

            // 合并所有PDF页
            var finalPdfBytes = MergePages.MergePdfPages(pdfPages);

            // 清理临时文件
            _uploadSessionService.CleanupSession(sessionId);

            return File(new MemoryStream(finalPdfBytes), "application/pdf", "merged-output.pdf");
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(ex.Message);
        }
        catch (NoFileProviderException ex)
        {
            _logger.LogError(ex.Message);
            return BadRequest(ex.Message);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "生成合并PDF失败");
            return StatusCode(StatusCodes.Status500InternalServerError, "生成PDF失败");
        }
    }
}

3. 前端配合修改

  • 先调用POST /api/pdf/init-upload获取会话ID
  • 分块上传每个文件组时,携带sessionId参数调用POST /api/pdf/upload-files
  • 所有文件块上传完成后,携带sessionId、colorMode、textOrPath调用POST /api/pdf/generate-final-pdf获取最终PDF

注意事项

  • 临时文件清理:添加定时任务(如Hangfire)清理超过有效期的会话(例如24小时未完成的会话),避免占用服务器磁盘空间
  • 会话安全性:可在会话中关联用户身份(如JWT Token中的用户ID),防止恶意用户访问他人的上传会话
  • 资源限制:在上传接口添加单文件大小、单会话文件总数的限制,避免服务器资源被耗尽

内容的提问来源于stack exchange,提问作者Tommy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 16:35:01