教程与源码: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