证书只有在人们信任它时才有意义。当学习者在求职申请中附上 PDF 时,招聘经理应该能够在几秒钟内确认其真实性。Laravel 为你提供了构建这种信任的工具,而无需将控制权交给第三方平台。你可以端到端地拥有品牌、数据和验证链。

目标很简单:生成一份打印时看起来很专业的 PDF,包含一个可扫描的 QR 码用于即时验证,并存储一条无法被猜测或伪造的唯一记录。以下是实现方法。

从数据库层开始

不要为证书使用自增 ID。像 8473 这样的 ID 很容易被猜到。有人可能会在你的验证页面上循环尝试数字,从而抓取你发行过的每一个凭证。相反,在创建证书记录时生成一个 UUID。

在你的迁移(migration)中,将 UUID 存储为字符串。当用户完成课程时,触发一个事件,创建一个与用户和课程关联的 Certificate 模型。将验证端点与任何已认证的仪表板完全分开。像 /verify/{uuid} 这样的公共路由应该查找记录并显示接收者姓名、课程标题和完成日期。如果 UUID 不存在,则返回 404。无需额外的登录。

这一设计选择保护了隐私并防止了枚举攻击。

设计 Blade 模板

你的证书设计位于标准的 Blade 视图中,但你必须以不同于普通网页的方式对待它。PDF 渲染引擎的行为与浏览器不同。外部样式表是不可靠的,因为 PDF 转换器可能无法在相同的上下文中获取它们。请务必仅使用内联 CSS。

以物理尺寸进行思考。如果你想要横向的证书,请直接设置容器的宽度和高度:

<div style="width: 11in; height: 8.5in; position: relative; padding: 40px; font-family: Georgia, serif;">

在该容器内使用绝对定位来放置签名、印章和边框。Web 安全字体是最稳妥的选择,尽管如果你足够小心,某些 PDF 引擎允许嵌入字体。除非你确定你的渲染引擎支持,否则请避免过度依赖 CSS Grid 或高级 Flexbox。为了获得广泛的兼容性,传统的 table 布局仍然可以解决现代 CSS 在 PDF 引擎内无法保证的对齐问题。

保持标记(markup)简洁。HTML 越简单,转换器出现布局损坏而让你措手不及的可能性就越小。

添加 QR 码

安装 Simple QRCode 包。你需要一个直接指向验证页面的 QR 码。在你的 Blade 模板中,像这样渲染它:

<img src="{!! QrCode::size(150)->generate(route('certificates.verify', $certificate->uuid)) !!}" style="position: absolute; bottom: 40px; right: 40px;">

当雇主用手机扫描该代码时,他们会进入你的 Laravel 应用并看到实时记录。这种从物理到数字的桥梁正是使证书可验证的关键。仅仅一份精美的 PDF 无法证明任何事情。QR 码让信任变得即时且直观。

选择 PDF 引擎

Laravel 开发人员通常会选择三条路径之一。每种路径都有真实的权衡。

DomPDF 完全在 PHP 中运行。通过 Composer 安装它,将你 Blade 渲染的 HTML 传递给它,然后保存输出。它不需要额外的服务器软件,如果你使用的是共享主机,这非常有吸引力。缺点是 CSS 支持。像 Flexbox 和 Grid 这样的现代布局工具要么仅部分支持,要么会失效。自定义 Web 字体可能很棘手,复杂的背景也经常渲染错误。如果你的证书设计比较保守,DomPDF 可以胜任。如果你需要精确度,它会让你感到沮丧。

Browsershot 采用了不同的方法。它使用 Puppeteer 来驱动一个无头 Chrome 实例,按照浏览器看到的样子精确渲染你的 HTML,并导出 PDF。因为它使用的是真实的 Chrome,你的 Tailwind 样式、自定义字体和响应式布局都能完美转换。难点在于服务器设置。你需要安装 Node.js、Puppeteer 和 Chrome。在某些平台上,你必须禁用 Chrome 沙箱或仔细管理内存,以防止进程停滞。如果你可以控制自己的服务器并需要像素级的设计还原,Browsershot 是无可比拟的。

HTML to PDF API 是第三种方案。你只需将渲染后的 Blade HTML 作为字符串,通过 POST 请求发送到外部服务,即可接收返回的 PDF。其实际优势在于零服务端依赖:你无需安装 Chrome,也无需调试 PHP 字体方面的兼容性问题。你最终会得到一个格式正确的矢量 PDF。虽然这些服务大多按文档计费,但对于许多应用场景而言,其运维的简便性远超其成本。如果你希望在无需系统管理员介入的情况下获得高质量输出,这通常是实现生产环境部署最快的路径。

完整工作流

一旦选定了引擎,整个流水线就非常清晰了。课程完成事件应当触发一个监听器,执行以下操作:

  1. 生成一个 UUID 并创建 Certificate 记录。
  2. 使用 view('certificates.pdf', compact('certificate'))->render() 将 Blade 视图渲染为 HTML 字符串。
  3. 将该 HTML 字符串传递给您选择的 PDF 服务或包。
  4. 将生成的文件保存到 storage/app/certificates/ 下的一个逻辑路径中,例如 certificates/2024/06/uuid.pdf
  5. 将文件附加到通知中,并自动通过电子邮件发送给学员。

Laravel 的通知系统可以非常优雅地处理电子邮件。如果渲染过程超过一秒,请务必将生成任务放入队列。你不希望学员在页面加载时还要等待 PDF 生成。

当用户下载文件时,他们会得到一个可以在任何查看器中打开的标准 PDF。当他们打印时,矢量文本依然清晰锐利。当雇主扫描二维码时,您的 Laravel 应用会根据数据库验证 UUID 并显示证书详情。

为什么矢量输出是必不可少的

某些 PDF 生成方法会产生位图(rasterized)输出。这意味着每一段文本都变成了一张图片。一旦放大,字母就会变得模糊,文件体积也会激增,且屏幕阅读器和搜索引擎都无法提取其中的文本。

请务必确认您选择的方法生成的是真正的矢量 PDF。文本应该是可以选中的,且在 400% 缩放时线条依然保持锐利。您的学员理应获得一份具有正式感的文档,而不是一张保存为 PDF 的截图。

矢量输出还能降低您的存储成本。一页矢量文本可能仅重 50 KB,而同样的一页高分辨率图像可能超过 2 MB。在大规模应用时,这种差异至关重要。

核心要点

在内部构建这一套流程,不仅仅是为了节省 SaaS 证书平台的费用,更是为了公信力。当学员可以打印一份文档,递到办公桌上,并让陌生人通过手机瞬间完成验证时,你便给了他们一份持久的凭证。你掌控设计,你掌控数据。信任属于您的品牌,而非供应商的二级域名。

在您自己的 Laravel 应用中,您是如何处理证书生成和验证的?欢迎在评论区分享您的经验。