Spring Boot中基於HTML發票/收據生成和下載功能

banq發表於2024-07-10

教程與原始碼:Spring Boot+Thymeleaf實現基於HTML發票/收據生成和下載功能

計費功能對於每個 SaaS 來說都是必不可少的,需要生成發票或收據。大多數架構都傾向於透過 API 呼叫來實現此功能,以獲得單一實現、一致性和減少客戶端負載等好處。使用 HTML 模板可以更輕鬆地堅持產品品牌。

在本文中,我將展示如何使用 Java Spring Boot 和 Thymeleaf 模板引擎從 HTML 模板生成 pdf 格式的訂閱收據。

需要開發的關鍵要求包括:

  •  從 PDF 格式的 HTML 模板生成訂閱收據。
  • .在單個 PDF 檔案中包含一個或多個收據。
  •  建立 API 以下載收據。

 控制器(端點)下載生成的收據。

@RestController
@RequestMapping(<font>"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;

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

//Converts HTML string to byte array<i>
 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=<font>"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(<font>".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, <font>"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

相關文章