多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
相关产品推荐
相关产品推荐

