← 中文资源

简体中文版本

构建 PDF 发票生成器

一个完整的 PDF 发票生成器:数据模型、合计计算、内嵌字体、PDF 输出,以及断言金额与页数的测试。

阅读英文原文

项目概述

本项目实现 PDF 发票生成器:从结构化数据渲染可打印的合法发票,以整数最小货币单位计算小计、税额与合计,并在输入不一致时明确失败。发票是有法律形态的文档,用浮点取整或静默丢行比不生成更糟。

架构与关键决策

发票模型校验行项目并以整数最小单位计算各项金额,使文档中的每个数字都可回溯到校验步骤;布局模块在固定坐标摆放页眉、表格与页脚,把分页决策与金额运算分离。取整只在税额计算处应用一次,因此合计是已取整部分之和而非未取整之和的取整。字体嵌入使输出在任何机器上一致,且字体与生成代码一起版本化,保证多年后重打印与原件逐字节一致。

实施与测试

渲染已知发票断言页数与小计文本与计算值一致;零行发票断言抛错;行金额与其数量单价不符断言拒绝;并断言行金额之和等于小计、小计加税额等于合计。

限制与后续

  • 固定发票版式,不支持任意模板或品牌化。
  • 每张发票单一税规则,多辖区需更细模型。
  • 无数字签名或盖章,真实性依赖存储访问控制。

代码示例

pnpm init
pnpm add pdf-lib
pnpm add -D typescript tsx vitest
pdf-invoice-generator/
├── src/model.ts
├── src/totals.ts
├── src/layout.ts
├── src/render.ts
├── tests/invoice.test.ts
└── package.json
if (!items.length) throw new Error('invoice needs at least one line item');
const subtotal = items.reduce((s, i) => s + i.unitCents * i.qty, 0);
const tax = Math.round(subtotal * rateBps / 10000);
const page = doc.addPage([595, 842]); // A4
page.drawText(invoice.number, { x: 50, y: 800 });
if (y < 80) { y = startNewPage(doc); }
const font = await doc.embedFont(await fs.readFile('font.ttf'));
pnpm vitest run