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

如何同时返回合并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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 05:20:27