Laravel 导出报价单 PDF:从 Blade 模板到文件下载

报价单导出看似只是把一组字段放进 PDF,真正容易卡住的是三件事:模板和数据没有分开、中文输出不符合预期,以及下载响应、访问权限和文件版本没有接好。
本文用一张教学报价单说明接入顺序。

ComPDF负责把HTML或模板转换为PDF;报价数据、权限、文件命名和下载响应仍由Laravel应用负责。
先把报价数据和模板分开
不要在控制器里拼一整段HTML。先准备结构化数据,再交给Blade模板渲染:
$quote = [
'number' => 'Q-2026-271',
'customer' => '教学供应商丙',
'currency' => 'CNY',
'items' => [
['name' => '可调支架', 'quantity' => 7, 'unit_price' => 45.00],
],
];
return view('quotes.pdf', compact('quote'));
在 resources/views/quotes/pdf.blade.php 中,模板只负责展示已经校验过的数据:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<style>body{font-family: sans-serif} table{width:100%;border-collapse:collapse} th,td{border:1px solid #bbb;padding:8px} .total{text-align:right}</style>
</head>
<body>
<h1>报价单 {{ $quote['number'] }}</h1>
<p>客户:{{ $quote['customer'] }} 币种:{{ $quote['currency'] }}</p>
<table>
<thead><tr><th>项目</th><th>数量</th><th>单价</th><th>小计</th></tr></thead>
<tbody>
@foreach ($quote['items'] as $item)
<tr>
<td>{{ $item['name'] }}</td>
<td>{{ $item['quantity'] }}</td>
<td>{{ number_format($item['unit_price'], 2) }}</td>
<td>{{ number_format($item['quantity'] * $item['unit_price'], 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</body>
</html>
这个模板示例只演示字段和明细循环。生产环境还要按项目需要处理税额、舍入、页眉页脚和多页表格。
金额、数量和币种由应用校验。模板只负责展示,不能把“总价正确”交给视图自行猜测。
用Blade输出HTML,再调用HTML转PDF
Blade文档默认会转义变量,适合输出报价客户名称和明细。循环生成明细行时,仍应在保存报价前完成字段校验。
渲染好的HTML可交给ComPDF HTML转PDF接口。同步请求返回结果地址,文档列出的下载有效期为24小时,因此应及时下载。下面使用单份、小文件的同步调用;批量任务另按异步模式组织。
先在 .env 中配置 COMPDF_API_KEY,再在 config/services.php 的返回数组中加入配置。不要把密钥写进Blade或浏览器代码。
'compdf' => ['key' => env('COMPDF_API_KEY')],
以下片段放在已完成登录和报价查看权限检查的控制器方法里。方法中的 $quote 使用前面已经校验过的数据,HTTP请求使用Laravel提供的 Http 门面:
use Illuminate\Support\Facades\Http;
// 放入控制器方法内部:
$html = view('quotes.pdf', compact('quote'))->render();
$filename = 'Q-2026-271.pdf'; // 教学文件名;实际项目用已校验的编号
$key = config('services.compdf.key');
abort_unless(is_string($key) && $key !== '', 503, '尚未配置PDF服务');
$response = Http::timeout(120)
->withHeaders(['x-api-key' => $key])
->attach('file', $html, 'quote.html')
->post('https://api-server.compdf.com/server/v2/process/html/pdf', [
'language' => 2,
])->throw()->json();
$data = $response['data'] ?? [];
$url = $data['downloadUrl'] ?? null;
abort_unless(
(string) ($response['code'] ?? '') === '200'
&& ($data['status'] ?? '') === 'success'
&& is_string($url) && $url !== '',
502, '未取得PDF结果,请检查转换状态'
);
// 本例只下载本次转换响应中的HTTPS地址,不接受浏览器传入任意URL。
abort_unless(parse_url($url, PHP_URL_SCHEME) === 'https', 502, '结果地址无效');
$pdf = Http::timeout(60)->get($url)->throw()->body();
abort_unless(strncmp($pdf, '%PDF-', 5) === 0, 502, '结果不是PDF文件');
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="'.$filename.'"',
]);
调用失败或超时会抛出异常,应由项目统一处理并向用户显示“生成失败,请重试”,不要在异常分支里返回“成功”。%PDF-只检查文件头,不能证明版式和金额正确。大文件应下载到受控临时文件并返回文件响应,避免把整个文件读入内存;这不属于本例的单份小文件路径。
中文输出先在云转换结果里确认
本例调用云API,Laravel服务器安装字体并不意味着云端生成服务能读取它;Java/.NET SDK的本地字体注册方法也不能直接套进这个请求。HTML转PDF接口当前没有列出上传字体的参数,不应在代码中擅自添加。
先把一条中文名称、一条数字编号和金额放入模板,生成后重新打开PDF,检查中文显示、搜索和复制。若需要固定品牌字体、特殊字形或严格排版,先向服务方确认当前云端支持与资源加载方式,再决定是否改用能够自行配置字体资源的生成SDK。不要仅因开发机浏览器能显示,就认定云端输出一定一致。
下载前再做一次业务检查
生成PDF后、返回下载响应前检查:
- 报价编号与当前记录一致;
- 客户、币种、数量和金额来自同一版本数据;
- 生成结果确实是PDF;
- 当前用户有权下载这份报价;
- 生成失败时返回可理解的错误,不显示“已完成”。
如果需要保留历史报价,不要用同一个文件名覆盖旧文件;把报价版本和生成参数写入自己的记录。ComPDF处理文档生成,版本、权限、存储和下载链接仍由Laravel应用决定。
适合怎样继续接入
文件量少、模板固定时,可以先用一份教学报价验证HTML布局、中文字体和下载响应;模板多、字段经常变化时,再把模板版本、数据校验和失败重试独立出来。先用一份报价完成“生成→下载→重新打开→核对编号、金额和中文”这四步,再考虑批量生成或存储策略。
先用接口验证一份报价模板能否得到预期PDF;若需要更细的版式和字体控制,再了解PDF生成方式。确认布局和下载都符合要求后,再接入项目自己的报价版本与权限规则。
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
推荐文章: