如何同时返回合并PDF与失败发票列表的多值响应?
如何同时返回合并PDF和失败发票号码列表
我正在开发一个POST API,功能是接收发票号码列表,通过RestTemplate调用外部API获取每张发票的PDF文件,将成功获取的PDF合并为单个文件返回。但存在部分发票号码无效的情况,调用外部API无法获取对应PDF,希望同时返回合并后的PDF文件和包含失败发票号码的JSON对象,请教实现方案。
现有代码
POST API 代码
@PostMapping(value = "/gen-report") public ResponseEntity<byte[]> generateReport( @RequestHeader(value = HttpHeaders.AUTHORIZATION) String headerAuthorization) { byte[] res = null; List<String> failedInvoices = new ArrayList<>(); ResponseEntity<byte[]> response = null; ArrayList<RequestParameters> requests = new ArrayList<>(); RequestParameters rp1 = new RequestParameters("360", "3600382368", "N"); RequestParameters rp2 = new RequestParameters("360", "3600382367", "N"); requests.add(rp1); requests.add(rp2); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); List<byte[]> responses = new ArrayList<>(); for (RequestParameters parameter : requests) { MultiValueMap<String, String> map = mobileOrderReportService.genrateReportService(parameter); HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers); response = null; byte[] content = null; content = requestReportByInvoiceNumber(entity); if (content != null) { responses.add(content); } else { failedInvoices.add(parameter.getOrderNum()); } } try { res = mergePDF(responses); } catch (DocumentException ex) { Logger.getLogger(MobileOrderReportController.class.getName()).log(Level.SEVERE, null, ex); } catch (IOException ex) { Logger.getLogger(MobileOrderReportController.class.getName()).log(Level.SEVERE, null, ex); } headers.setContentType(MediaType.parseMediaType("application/pdf")); String filename = "pdf1.pdf"; headers.add("content-disposition", "inline;filename=" + filename); headers.setCacheControl("must-revalidate, post-check=0, pre-check=0"); response = new ResponseEntity<byte[]>(res, headers, HttpStatus.OK); return response; }
获取单张发票PDF的方法
该方法成功返回byte[],失败返回null:
public byte[] requestReportByInvoiceNumber(HttpEntity<MultiValueMap<String, String>> entity) { RestTemplate restTemplate = new RestTemplate(); ResponseEntity<byte[]> response = null; try { response = restTemplate.exchange(mobileOrderReportService.getUrl(), HttpMethod.POST, entity, byte[].class); byte[] content = response.getBody(); return content; } catch (RestClientException ex) { logger.error("request to UReport failed in requestReportByInvoiceNumber method !..."); return null; } }
PDF合并方法
public byte[] mergePDF(List<byte[]> pdfFilesAsByteArray) throws DocumentException, IOException { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); Document document = null; PdfCopy writer = null; for (byte[] pdfByteArray : pdfFilesAsByteArray) { try { PdfReader reader = new PdfReader(pdfByteArray); int numberOfPages = reader.getNumberOfPages(); if (document == null) { document = new Document(reader.getPageSizeWithRotation(1)); writer = new PdfCopy(document, outStream); // new document.open(); } PdfImportedPage page; for (int i = 0; i < numberOfPages;) { ++i; page = writer.getImportedPage(reader, i); writer.addPage(page); } } catch (Exception e) { e.printStackTrace(); } } document.close(); outStream.close(); return outStream.toByteArray(); }
实现方案
HTTP标准响应通常只能返回一种媒体类型,但可以通过以下两种方式实现同时返回文件和结构化数据:
方案1:使用Multipart响应(推荐)
Multipart响应可以在一个请求中包含多个部分,分别传输PDF文件和JSON格式的失败清单。修改后的API代码如下:
@PostMapping(value = "/gen-report", produces = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity<MultiValueMap<String, Object>> generateReport( @RequestHeader(value = HttpHeaders.AUTHORIZATION) String headerAuthorization) { List<String> failedInvoices = new ArrayList<>(); ArrayList<RequestParameters> requests = new ArrayList<>(); RequestParameters rp1 = new RequestParameters("360", "3600382368", "N"); RequestParameters rp2 = new RequestParameters("360", "3600382367", "N"); requests.add(rp1); requests.add(rp2); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); List<byte[]> responses = new ArrayList<>(); for (RequestParameters parameter : requests) { MultiValueMap<String, String> map = mobileOrderReportService.genrateReportService(parameter); HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers); byte[] content = requestReportByInvoiceNumber(entity); if (content != null) { responses.add(content); } else { failedInvoices.add(parameter.getOrderNum()); } } byte[] mergedPdf = null; try { mergedPdf = mergePDF(responses); } catch (DocumentException | IOException ex) { Logger.getLogger(MobileOrderReportController.class.getName()).log(Level.SEVERE, null, ex); } // 构建Multipart响应 MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); // 添加PDF文件部分 HttpHeaders pdfHeaders = new HttpHeaders(); pdfHeaders.setContentType(MediaType.APPLICATION_PDF); pdfHeaders.setContentDisposition(ContentDisposition.builder("attachment") .filename("merged_invoices.pdf") .build()); HttpEntity<byte[]> pdfPart = new HttpEntity<>(mergedPdf, pdfHeaders); body.add("merged_pdf", pdfPart); // 添加失败发票清单JSON部分 HttpHeaders jsonHeaders = new HttpHeaders(); jsonHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<List<String>> failedPart = new HttpEntity<>(failedInvoices, jsonHeaders); body.add("failed_invoices", failedPart); return new ResponseEntity<>(body, HttpStatus.OK); }
方案2:将失败信息存入响应头(适合失败数量少的场景)
如果失败发票数量不多,可以将失败清单转成JSON字符串存入自定义响应头,PDF作为响应体返回:
@PostMapping(value = "/gen-report") public ResponseEntity<byte[]> generateReport( @RequestHeader(value = HttpHeaders.AUTHORIZATION) String headerAuthorization) { byte[] res = null; List<String> failedInvoices = new ArrayList<>(); ArrayList<RequestParameters> requests = new ArrayList<>(); RequestParameters rp1 = new RequestParameters("360", "3600382368", "N"); RequestParameters rp2 = new RequestParameters("360", "3600382367", "N"); requests.add(rp1); requests.add(rp2); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); List<byte[]> responses = new ArrayList<>(); for (RequestParameters parameter : requests) { MultiValueMap<String, String> map = mobileOrderReportService.genrateReportService(parameter); HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers); byte[] content = requestReportByInvoiceNumber(entity); if (content != null) { responses.add(content); } else { failedInvoices.add(parameter.getOrderNum()); } } try { res = mergePDF(responses); } catch (DocumentException ex) { Logger.getLogger(MobileOrderReportController.class.getName()).log(Level.SEVERE, null, ex); } catch (IOException ex) { Logger.getLogger(MobileOrderReportController.class.getName()).log(Level.SEVERE, null, ex); } // 设置响应头 HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setContentType(MediaType.parseMediaType("application/pdf")); responseHeaders.add("content-disposition", "inline;filename=merged_invoices.pdf"); responseHeaders.setCacheControl("must-revalidate, post-check=0, pre-check=0"); // 将失败清单转成JSON存入自定义头 try { ObjectMapper objectMapper = new ObjectMapper(); String failedJson = objectMapper.writeValueAsString(failedInvoices); responseHeaders.add("X-Failed-Invoices", failedJson); } catch (JsonProcessingException e) { Logger.getLogger(MobileOrderReportController.class.getName()).log(Level.SEVERE, null, e); } return new ResponseEntity<>(res, responseHeaders, HttpStatus.OK); }
注意事项
- 方案1的Multipart响应需要客户端支持解析多部分数据,大部分HTTP客户端(如Postman、OkHttp)都支持。
- 方案2的响应头有长度限制,若失败发票数量多,JSON字符串过长可能导致头截断,不推荐这种场景使用。
- 确保
ObjectMapper已正确引入(Spring Boot默认已包含Jackson依赖)。
内容的提问来源于stack exchange,提问作者mohamed Saaad
相关产品推荐
相关产品推荐

