前言:電商系統的 PDF 需求
OMS 系統需要產生各種 PDF 文件:
| 文件類型 | 用途 | 特殊需求 |
|---|---|---|
| 出貨標籤 | 貼在包裹上 | 條碼、收件人資訊 |
| 訂單明細 | 放在包裹內 | 商品清單、金額 |
| 撿貨單 | 倉庫作業 | 商品位置、數量 |
| 對帳單 | 商戶結算 | 表格、統計 |
解決方案:Builder 模式
使用範例(先看效果)
byte[] pdf = PdfBuilder.create()
.title(“出貨單”)
.separator()
.text(“訂單編號:ORD-2024-001234”)
.text(“出貨日期:2024-03-18”)
.newLine()
.subtitle(“商品明細”)
.table(orderItems)
.newLine()
.subtitle(“收件資訊”)
.text(“收件人:王小明”)
.text(“電話:0912-345-678”)
.text(“地址:台北市信義區信義路五段7號”)
.newPage()
.image(barcodeBytes)
.build();
PdfBuilder 實作
核心結構
private List<Element> elements = new ArrayList<>();
private FontConfig fontConfig;
// 工廠方法
public static PdfBuilder create() {
return new PdfBuilder(FontConfig.defaultConfig());
}
public static PdfBuilder create(FontConfig config) {
return new PdfBuilder(config);
}
private PdfBuilder(FontConfig config) {
this.fontConfig = config;
}
}
文字方法
* 標題(最大字體)
*/
public PdfBuilder title(String text) {
elements.add(new TextElement(text, fontConfig.getTitleFont()));
return this; // 回傳 this 支援鏈式呼叫
}
/**
* 副標題
*/
public PdfBuilder subtitle(String text) {
elements.add(new TextElement(text, fontConfig.getSubtitleFont()));
return this;
}
/**
* 一般文字
*/
public PdfBuilder text(String text) {
elements.add(new TextElement(text, fontConfig.getBodyFont()));
return this;
}
/**
* 粗體文字
*/
public PdfBuilder boldText(String text) {
elements.add(new TextElement(text, fontConfig.getBoldFont()));
return this;
}
版面控制
* 換行
*/
public PdfBuilder newLine() {
elements.add(new NewLineElement());
return this;
}
/**
* 分隔線
*/
public PdfBuilder separator() {
elements.add(new SeparatorElement());
return this;
}
/**
* 換頁
*/
public PdfBuilder newPage() {
elements.add(new NewPageElement());
return this;
}
表格支援
* 新增表格
* @param data 二維資料,第一列為標題
*/
public PdfBuilder table(List<List<String>> data) {
elements.add(new TableElement(data, TableStyle.BORDERED));
return this;
}
/**
* 新增表格(指定樣式)
*/
public PdfBuilder table(List<List<String>> data, TableStyle style) {
elements.add(new TableElement(data, style));
return this;
}
圖片與條碼
* 新增圖片
*/
public PdfBuilder image(byte[] imageBytes) {
elements.add(new ImageElement(imageBytes));
return this;
}
/**
* 新增條碼(自動產生)
*/
public PdfBuilder barcode(String content) {
byte[] barcodeImage = BarcodeGenerator.generate(content);
elements.add(new ImageElement(barcodeImage));
return this;
}
輸出 PDF
* 產生 PDF byte 陣列
*/
public byte[] build() {
ByteArrayOutputStream output = new ByteArrayOutputStream();
Document document = new Document(PageSize.A4);
try {
PdfWriter.getInstance(document, output);
document.open();
for (Element element : elements) {
if (element instanceof NewPageElement) {
document.newPage();
} else {
document.add(element.render());
}
}
document.close();
} catch (DocumentException e) {
throw new PdfGenerationException(“PDF 產生失敗”, e);
}
return output.toByteArray();
}
/**
* 直接寫入檔案
*/
public void toFile(String filename) {
byte[] pdf = build();
try (FileOutputStream fos = new FileOutputStream(filename)) {
fos.write(pdf);
} catch (IOException e) {
throw new PdfGenerationException(“檔案寫入失敗”, e);
}
}
中文字型支援
private static final String FONT_PATH = “/fonts/NotoSansTC-Regular.ttf”;
private BaseFont baseFont;
public static FontConfig defaultConfig() {
return new FontConfig();
}
private FontConfig() {
try {
// 載入支援中文的字型
baseFont = BaseFont.createFont(
FONT_PATH,
BaseFont.IDENTITY_H, // Unicode 支援
BaseFont.EMBEDDED // 嵌入字型
);
} catch (Exception e) {
throw new RuntimeException(“字型載入失敗”, e);
}
}
public Font getTitleFont() {
return new Font(baseFont, 24, Font.BOLD);
}
public Font getSubtitleFont() {
return new Font(baseFont, 18, Font.BOLD);
}
public Font getBodyFont() {
return new Font(baseFont, 12, Font.NORMAL);
}
public Font getBoldFont() {
return new Font(baseFont, 12, Font.BOLD);
}
}
完整範例:出貨單
// 準備商品明細表格
List<List<String>> items = new ArrayList<>();
items.add(List.of(“商品”, “數量”, “單價”, “小計”));
for (OrderItem item : order.getItems()) {
items.add(List.of(
item.getName(),
String.valueOf(item.getQuantity()),
formatPrice(item.getPrice()),
formatPrice(item.getSubtotal())
));
}
items.add(List.of(“合計”, “”, “”, formatPrice(order.getTotal())));
// 產生 PDF
return PdfBuilder.create()
// 標題區
.title(“出貨單”)
.separator()
.text(“訂單編號:” + order.getOrderId())
.text(“出貨日期:” + LocalDate.now())
.newLine()
// 條碼
.barcode(order.getOrderId())
.newLine()
// 商品明細
.subtitle(“商品明細”)
.table(items)
.newLine()
// 收件資訊
.subtitle(“收件資訊”)
.text(“收件人:” + order.getReceiverName())
.text(“電話:” + order.getReceiverPhone())
.text(“地址:” + order.getReceiverAddress())
.build();
}
總結
| 設計 | 效果 |
|---|---|
| Builder 模式 | 鏈式呼叫,程式碼簡潔 |
| 流式 API | 像寫 HTML 一樣直覺 |
| 字型抽象 | 換字型只改一處 |
| 元件化 | 表格、圖片、條碼都是元件 |
為什麼不用其他方案?
| 方案 | 優點 | 缺點 | 結論 |
|---|---|---|---|
| HTML 轉 PDF | 會 HTML 就會用 | 排版難控制、分頁問題 | 簡單報表可用 |
| Word 範本 | 業務人員能改 | 需要額外軟體、格式問題 | 不推薦 |
| JasperReports | 功能強大 | 學習曲線陡、設計器難用 | 複雜報表可考慮 |
| iText + Builder | 程式碼控制、可測試 | 要寫程式碼 | 工程師友好 |
實戰踩坑
預設字型不支援中文,產出的 PDF 全是方框。要嵌入支援中文的字型(如 Noto Sans TC)。第一次載入字型要 2-3 秒,後來改成應用程式啟動時預載。
蝦皮、Momo、Yahoo 的出貨單長得不一樣。最初想做成一模一樣,後來發現不可能(平台會檢查格式)。解法:只統一「我們自己的出貨單」,平台的標籤用平台 API 下載。
一次產生 500 張出貨單,記憶體直接爆。解法:改成串流處理,產一張輸出一張,不要全部放在記憶體。
系列導航
| ◀ 上一篇 HTTP 客戶端 |
📚 返回目錄 | 下一篇 ▶ JSON 序列化 |
發佈留言