ABP VNext 报表:EPPlus DinkToPdf 多格式导出
ABP VNext 报表:EPPlus & DinkToPdf 多格式导出 🚀
📚 目录
- ABP VNext 报表:EPPlus & DinkToPdf 多格式导出 🚀
- 一、引言 ✨
- 二、环境与依赖 ⚙️
- 三、项目骨架 📁
- 项目结构高层流程
- 四、Excel 报表(EPPlus) 📊
- 4.1 许可设置与性能考量
- 4.2 构建 Excel 文件
- 五、PDF 报表(DinkToPdf + Razor) 📄
- 5.1 本机库部署与 DI 注册
- 5.2 Razor 模板与虚拟文件系统
- 5.3 渲染与返回
- 六、生产级优化 🔧
- 七、端到端流程 🔄
一、引言 ✨
TL;DR
- 🚀 在 ABP VNext 中集成 EPPlus,使用
ExcelPackage.License.SetNonCommercialOrganization
设置 Polyform 非商业许可,避免调试时抛出LicenseNotSetException
。 - 📄 使用
SynchronizedConverter
单例注入 DinkToPdf,确保多线程环境下转换任务队列化执行,提高稳定性。 - 🔄 结合
Volo.Abp.TextTemplating.Razor
渲染 Razor 模板,实现端到端 PDF 导出流程,模板作为嵌入资源管理。 - 💡 分层设计:ApplicationService 仅负责数据与字节流生成,Controller 专责返回文件;引入分布式缓存、后台作业与日志,打造可复用、高可用方案。
二、环境与依赖 ⚙️
- 平台:.NET 9 + ABP Framework 9.x
- EPPlus 8.x:需设置商业或非商业许可
ExcelPackage.License.SetNonCommercialOrganization("Your Organization");// 商业用途请使用 SetCommercial("<Your License Key>");
- DinkToPdf.Core + DinkToPdf.Native.*:跨平台需部署对应
libwkhtmltox.*
本机库,避免运行时因找不到 DLL 报错。 - Volo.Abp.TextTemplating.Razor:渲染 Razor 为字符串,需安装并在模块上声明依赖
- Volo.Abp.AspNetCore.Mvc:MVC/Razor 支持
三、项目骨架 📁
├─ Modules/ReportModule/
│ ├─ Application/
│ │ ├─ ReportManager.cs
│ │ └─ ReportAppService.cs
│ ├─ Domain/
│ │ └─ ReportDto.cs
│ └─ Web/Controllers/
│ └─ ReportController.cs
├─ ReportsTemplates/
│ ├─ InvoiceTemplate.cshtml (嵌入式资源 via VFS)
│ └─ Styles/report.css (嵌入式资源 via VFS)
└─ Program.cs├ builder.Services.AddRazorPages();├ builder.Services.AddSingleton<IConverter>(new SynchronizedConverter(new PdfTools()));├ ExcelPackage.License.SetNonCommercialOrganization("Your Organization");└ Configure<AbpVirtualFileSystemOptions>(opt =>opt.FileSets.AddEmbedded<ReportModule>("YourNamespace.ReportsTemplates"));
说明:通过 ABP 的虚拟文件系统(VFS)将模板与样式打包为嵌入资源,避免发布时路径错漏。
项目结构高层流程
四、Excel 报表(EPPlus) 📊
4.1 许可设置与性能考量
// Program.cs 顶部
ExcelPackage.License.SetNonCommercialOrganization("Your Organization");
-
EPPlus 8.x 在未设置许可时会抛出
LicenseNotSetException
,非商业项目使用上述 Polyform Noncommercial 模式。 -
海量数据场景:EPPlus 在内存中构建整个工作簿,导出百万行以上时易 OOM。推荐:
- 拆批导出、分文件下载
- 商业版 Streaming 模式或第三方流式库
💡Tips:可封装
IExcelPackageFactory
,统一许可与模板配置,便于单元测试与维护。
4.2 构建 Excel 文件
public class ReportManager
{private readonly IRepository<Invoice, Guid> _invoiceRepo;public ReportManager(IRepository<Invoice, Guid> invoiceRepo) => _invoiceRepo = invoiceRepo;public async Task<Stream> BuildInvoiceExcelAsync(Guid invoiceId){var dto = await _invoiceRepo.GetAsync(invoiceId);using var package = new ExcelPackage();var sheet = package.Workbook.Worksheets.Add("Invoice");// 标题sheet.Cells[1,1,1,5].Merge = true;sheet.Cells[1,1].Value = "发票";sheet.Cells[1,1].Style.Font.Size = 16;sheet.Cells[1,1].Style.Font.Bold = true;// 表头var headers = new[]{ "商品","数量","单价","总价","备注" };for (int i = 0; i < headers.Length; i++){var cell = sheet.Cells[2, i+1];cell.Value = headers[i];cell.Style.Font.Bold = true;cell.Style.Border.BorderAround(ExcelBorderStyle.Thin);}// 数据行int row = 3;foreach (var item in dto.Items){sheet.Cells[row,1].Value = item.ProductName;sheet.Cells[row,2].Value = item.Quantity;sheet.Cells[row,3].Value = item.UnitPrice;sheet.Cells[row,4].Formula = $"B{row}*C{row}";sheet.Cells[row,5].Value = item.Remark;row++;}// 列宽自适应if (sheet.Dimension != null)sheet.Cells[sheet.Dimension.Address].AutoFitColumns();var stream = new MemoryStream();await package.SaveAsAsync(stream);stream.Position = 0;return stream;}
}
五、PDF 报表(DinkToPdf + Razor) 📄
5.1 本机库部署与 DI 注册
csproj 确保 wkhtmltox
文件复制到输出目录:
<ItemGroup><None Include="wkhtmltox\**\*.*"><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></None>
</ItemGroup>
Program.cs 注入线程安全转换器:
builder.Services.AddSingleton<IConverter>(new SynchronizedConverter(new PdfTools()));
5.2 Razor 模板与虚拟文件系统
-
安装并依赖包:
abp add-package Volo.Abp.TextTemplating.Razor
-
在模块类添加依赖:
[DependsOn(typeof(AbpTextTemplatingRazorModule))] public class ReportModule : AbpModule { }
-
InvoiceTemplate.cshtml
(嵌入式资源)示例:
@model InvoiceDto
@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase<InvoiceDto>
<!DOCTYPE html>
<html>
<head><link href="Styles/report.css" rel="stylesheet" />
</head>
<body><header><h1>@L["Invoice"] - @Model.InvoiceNo</h1></header><table>…</table><footer>@L["PrintDate"]: @DateTime.Now:yyyy-MM-dd | @L["Page"]: [page]/[toPage]</footer>
</body>
</html>
5.3 渲染与返回
public class ReportManager
{// 注入 IConverter 与 IRazorRendererpublic async Task<byte[]> BuildInvoicePdfAsync(Guid invoiceId){var dto = await _invoiceRepo.GetAsync(invoiceId);var html = await _razorRenderer.RenderAsync("ReportsTemplates/InvoiceTemplate", dto);var doc = new HtmlToPdfDocument{GlobalSettings = {Orientation = Orientation.Portrait,PaperSize = PaperKind.A4,Margins = new MarginSettings { Top = 20, Bottom = 20 }},Objects = {new ObjectSettings {HtmlContent = html,WebSettings = { DefaultEncoding = "utf-8" },HeaderSettings= { Center = "发票", FontSize=9 },FooterSettings= { Right = "[page]/[toPage]", FontSize=9 }}}};return _converter.Convert(doc);}
}
💡Tips:确保静态资源(CSS/图片)可由 wkhtmltopdf 加载,可使用嵌入资源或绝对 URL。
六、生产级优化 🔧
- 分布式缓存:使用 ABP 扩展的
IDistributedCache
(Redis 实现)缓存热点报表,减少重复生成。 - 后台作业预生成:结合 ABP Background Jobs 模块异步预渲染大型报表,用户获取下载链接后再下载。
- 异常与日志:在
ReportManager
内部捕获关键异常,利用ILogger<ReportManager>
记录堆栈与上下文。 - 单元测试:将 Excel/PDF 生成逻辑拆分为独立服务,编写针对空数据、超大数据与异常场景的单元测试,确保兼容性与稳定性。
- 安全与输入校验:Razor 默认会对所有变量输出进行 HTML 编码,避免使用
Html.Raw
渲染未验证的用户输入;结合数据注解与输出编码防 XSS。 - 容器化与监控:在 ASP.NET Core 中集成 OpenTelemetry + Prometheus,暴露
/metrics
端点,并利用 Grafana 可视化监控,确保报表服务的可观测性与健壮性。
七、端到端流程 🔄
- 启动应用 → 登录 → 访问
/reports
- 点击“导出 Excel”或“生成 PDF”
- 浏览器下载并打开,验证分页、样式、页眉/页脚 🎉