Spring Boot中基于HTML发票/收据生成和下载功能

教程与源码:Spring Boot+Thymeleaf实现基于HTML发票/收据生成和下载功能

计费功能对于每个 SaaS 来说都是必不可少的,需要生成发票或收据。大多数架构都倾向于通过 API 调用来实现此功能,以获得单一实现、一致性和减少客户端负载等好处。使用 HTML 模板可以更轻松地坚持产品品牌。

在本文中,我将展示如何使用 Java Spring Boot 和 Thymeleaf 模板引擎从 HTML 模板生成 pdf 格式的订阅收据。

需要开发的关键要求包括:

  •  从 PDF 格式的 HTML 模板生成订阅收据。
  • .在单个 PDF 文件中包含一个或多个收据。
  •  创建 API 以下载收据。

 控制器(端点)下载生成的收据。

@RestController
@RequestMapping("api/v1/subscription")
public class SubscriptionController {
 
 private final SubscriptionService subscriptionService;
 
 public SubscriptionController(SubscriptionService subscriptionService) {
  this.subscriptionService = subscriptionService;
 }
 
 @GetMapping(
"/receipt-binary")
 public ResponseEntity<byte[]> downloadSubscriptionReceipt() {
  return subscriptionService.downloadSubscriptionReceipts();
 }
}

让我们定义缺失的 Subscription 和 SubscriptionService 类

public class Subscription implements Serializable {
 
 private Integer id;
 
 private String subscriptionPlan;
 
 private LocalDate startDate;
 
 private LocalDate endDate;
 
 private String description;
 
 private Double price;

//removed constructor, getters and setters due to length
}
@Service
public class SubscriptionService {

//Converts HTML string to byte array
 private byte[] generatePdfFromHtml(String html){
  ByteArrayOutputStream output = new ByteArrayOutputStream();
  HtmlConverter.convertToPdf(html, output);
  return output.toByteArray();
 }
}

我们将从基础开始构建 SubscriptionService 类,首先是辅助私人函数。 PDF 以字节数组的形式返回,我们需要使用 generatePdfFromHtml 函数将 HTML 字符串转换为字节数组。 在资源文件夹中创建名为 templates 的子文件夹,并添加名为 SubscriptionReceiptTemplate.html 的 HTML 模板。 在此下载模板 在需求 2 中,如果有多个订阅对象,我们需要确保每个收据都显示在一个新的 PDF 页面上。

我发现最好的解决办法是让模板接受订阅对象列表,即使只需要或只可用一个订阅(而不是分别创建收据并将它们合并到一个 PDF 文件中)。

<div class="invoice-box" data-th-each="subscription, iteration : ${subscriptions}">
.
.
.
<!--
Reduced due to length
Please download the file from Github
 -->
<div data-th-if=
"${iteration.index + 1 < subscriptions.size()}" style="page-break-after: always;"></div>

在上面的 Thymeleaf HTML 模板中,data-th-each 用于循环遍历订阅对象。 HTML 代码段的最后一行使用了条件内联 CSS,如果不是最后一次迭代,则每次迭代后都会中断页面。

我们首先需要将 Thymeleaf 模板转换为字符串,然后再转换为字节数组。 在 SubscriptionService 类中添加以下代码,将模板转换为字符串。

 private String parseSubscriptionTemplate(List<Subscription> subscriptions){
  
  ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
  templateResolver.setTemplateMode(TemplateMode.HTML);
  templateResolver.setSuffix(".html");

  Context context = new Context();
  Map<String, Object> templateVariables = Map.of(
   
"subscriptions", subscriptions,
   
"userFirstName", "Foo",
   
"userLastName","Bar",
   
"userCompanyName", "BarFoo Services Ltd");
  
  context.setVariables(templateVariables);
  
  SpringTemplateEngine templateEngine = new SpringTemplateEngine();
  templateEngine.setTemplateResolver(templateResolver);
  
  return templateEngine.process(
"templates/SubscriptionReceiptTemplate", context);
 }

在上面的代码中,我们传递了一个 Subscription 对象和用户值的列表,该列表将在运行时被模板使用。

ClassLoaderTemplateResolver 是 TemplateResolver 的一种类型,它有助于确定如何以及在何处查找模板。 在这种情况下,模板必须位于 classpath 中(即资源文件夹下)。 其他选项包括 FileTemplateResolver(如果需要将模板文件存储在其他地方)或 StringTemplateResolver(如果需要直接从数据库等来源传递字符串)。

现在,我们需要在 SubscriptionService 中添加以下代码,以完成 SubscriptionController 中引用的 downloadSubscriptionReceipts() 方法。

public ResponseEntity<byte[]> downloadSubscriptionReceipts(){
  
  List<Subscription> subscriptions = new ArrayList<>() {};
  subscriptions.add(
    new Subscription(1, "Gold Package", LocalDate.of(2024, 7, 9),
    LocalDate.of(2025, 7,9),
"Annual Subscription", 20000.0 ));
  
  String subscriptionPdfHtml = parseSubscriptionTemplate(subscriptions);
  
  byte[] pdf = generatePdfFromHtml(subscriptionPdfHtml);

  String fileName =
"subscription_receipt.pdf";
  
  HttpHeaders header = new HttpHeaders();
  header.add(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename="+fileName);
  header.add(
"Cache-Control", "no-cache, no-store, must-revalidate");
  header.add(
"Pragma", "no-cache");
  header.add(
"Expires", "0");
  
  return ResponseEntity.ok()
    .headers(header)
    .contentType(MediaType.APPLICATION_PDF)
    .body(pdf);
 }

最后,让我们运行解决方案并测试终端。

源码:Github repository