报告XP.ReportEngine:新增 IReportService接口封装完整报告生成流程,支持外部类库直接调用;新增 ReportRequest、ReportServiceResult 请求/响应模型;新增引擎预热机制;PDF 生成改为 Task.Run 后台线程执行,解决进度窗口和主窗口卡死问题;完善文档。

This commit is contained in:
QI Mingxuan
2026-05-14 19:29:11 +08:00
parent 11c69d03fb
commit 29bc8576af
13 changed files with 1485 additions and 318 deletions
+269
View File
@@ -0,0 +1,269 @@
# XP.ReportEngine 使用指南 | Usage Guidance
## 1. 概述
本文档说明如何在 XplorePlane 项目中使用 `XP.ReportEngine` 模块生成 PDF 检测报告。
模块提供两种调用方式:
- **推荐**:通过 `IReportService` 门面接口(一行调用,自动处理所有细节)
- **高级**:直接使用底层管线接口(`IReportGenerator``IReportDataAdapter` 等)
## 2. 前置条件
### 2.1 项目引用
在调用方项目的 `.csproj` 中添加项目引用:
```xml
<ProjectReference Include="..\XP.ReportEngine\XP.ReportEngine.csproj" />
```
### 2.2 模块注册
确保 `ReportEngineModule` 已在 `XP.App` 的模块目录中注册。模块初始化时会自动:
- 注册所有服务到 DI 容器
- 注册多语言资源到 Fallback Chain
- 后台执行引擎预热(字体加载 + JIT 编译)
### 2.3 配置文件
`App.config` 中添加报告引擎配置项,参见 `Documents/App.config.example`
## 3. 通过 IReportService 生成报告(推荐)
### 3.1 基本用法
```csharp
using XP.ReportEngine.Interfaces;
using XP.ReportEngine.Models;
public class InspectionService
{
private readonly IReportService _reportService;
public InspectionService(IReportService reportService)
{
_reportService = reportService;
}
public async Task<string> GenerateInspectionReport(
List<ProcessorOutput> processorOutputs,
string productName,
string operatorName)
{
var request = new ReportRequest
{
// 必填:处理器输出数据
ProcessorOutputs = processorOutputs,
// 必填:报告元数据
Metadata = new ReportMetadata
{
SampleName = productName,
OperatorName = operatorName,
InspectionDate = DateTime.Now,
Description = "BGA 焊球气泡率检测"
},
// 可选:文件名占位符参数(用于 FileNamePattern
FileNameParameters = new Dictionary<string, string>
{
["ProductName"] = productName,
["ProductCode"] = "PCBA-X100",
["WorkpieceSN"] = "SN20250001",
["DeviceId"] = "XP-CT-001",
["MachineId"] = "MC01",
["Result"] = "Pass"
}
};
var result = await _reportService.GenerateAsync(request);
if (result.IsSuccess)
{
return result.OutputFilePath; // PDF 文件路径
}
else
{
throw new Exception($"报告生成失败: {result.ErrorMessage}");
}
}
}
```
### 3.2 指定输出路径
```csharp
var request = new ReportRequest
{
ProcessorOutputs = outputs,
Metadata = metadata,
// 指定输出路径后,不再使用 ReportConfig 中的 OutputDirectory 和 FileNamePattern
OutputFilePath = @"D:\CustomPath\MyReport.pdf"
};
```
### 3.3 注入额外图像
```csharp
var request = new ReportRequest
{
ProcessorOutputs = outputs,
Metadata = metadata,
AdditionalImages = new Dictionary<string, ImageData>
{
// 工件整体图(首页显示)
["workpieceImage"] = new ImageData
{
SourceType = ImageSourceType.FilePath,
FilePath = @"D:\Images\workpiece.png"
},
// 自定义图像
["customImage"] = new ImageData
{
SourceType = ImageSourceType.Bytes,
Bytes = imageBytes
}
}
};
```
### 3.4 注入自定义属性
```csharp
var request = new ReportRequest
{
ProcessorOutputs = outputs,
Metadata = metadata,
// 自定义属性会合并到 ReportContext.Properties,可在模板中通过 ${key} 绑定
CustomProperties = new Dictionary<string, object>
{
["batchNumber"] = "BATCH-2025-001",
["inspectionStation"] = "Station-A"
}
};
```
## 4. ReportRequest 完整字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `ProcessorOutputs` | `List<ProcessorOutput>` | 是 | 处理器输出数据列表 |
| `Metadata` | `ReportMetadata` | 是 | 报告元数据 |
| `OutputFilePath` | `string` | 否 | 输出路径,为空时自动生成 |
| `FileNameParameters` | `Dictionary<string, string>` | 否 | 文件名占位符参数 |
| `AdditionalImages` | `Dictionary<string, ImageData>` | 否 | 额外图像数据 |
| `CustomProperties` | `Dictionary<string, object>` | 否 | 自定义属性 |
### ReportMetadata 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `ReportId` | `string` | 报告编号(为空时自动生成 RPT-yyyyMMdd-NNN |
| `InspectionDate` | `DateTime` | 检测日期 |
| `SampleName` | `string` | 样品/产品名称 |
| `OperatorName` | `string` | 操作员 |
| `Description` | `string` | 描述信息 |
### ProcessorOutput 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `ProcessorType` | `string` | 处理器类型标识 |
| `OutputData` | `Dictionary<string, object>` | 输出数据字典 |
| `AnnotatedImage` | `ImageData` | 关联的已标注图像 |
支持的 ProcessorType
- `LineMeasurementProcessor` — 线测量
- `BgaVoidRateProcessor` — BGA 气泡率
- `VoidMeasurementProcessor` — 空隙测量
- `FillRateProcessor` — 通孔填锡率
## 5. ReportServiceResult 结果说明
| 字段 | 类型 | 说明 |
|------|------|------|
| `IsSuccess` | `bool` | 是否成功 |
| `OutputFilePath` | `string` | 输出文件路径(成功时有值) |
| `ReportId` | `string` | 报告编号 |
| `ErrorMessage` | `string` | 错误信息(失败时有值) |
| `Exception` | `Exception` | 异常对象(失败时有值) |
## 6. 在 ViewModel 中使用(带 UI 交互)
如果需要进度条、文件对话框等 UI 交互,参考 `ReportDemoViewModel.cs` 的实现模式:
```csharp
// 在后台线程执行,避免阻塞 UI
var genResult = await Task.Run(() => _reportService.GenerateAsync(request));
```
关键点:
- `IReportService.GenerateAsync` 内部是 CPU 密集型操作(PDF 渲染)
- 必须用 `Task.Run` 推到线程池,否则会阻塞 UI 线程
- 进度条、对话框等 UI 逻辑留在 ViewModel 中
## 7. 高级用法:直接使用底层管线
适用于需要自定义管线某个阶段的场景:
```csharp
// 1. 数据适配
var adapter = container.Resolve<IReportDataAdapter>();
var context = adapter.Adapt(processorOutputs, metadata);
// 2. 手动修改上下文
context.Properties["customKey"] = "customValue";
context.Images["myImage"] = new ImageData { ... };
// 3. 调用管线生成
var generator = container.Resolve<IReportGenerator>();
var options = new ReportGenerationOptions
{
TemplatePath = "Templates/StandardReportTemplate.json",
OutputFilePath = @"D:\output.pdf",
Format = ReportOutputFormat.Pdf
};
var result = await generator.GenerateAsync(context, options);
```
## 8. 预热机制
模块启动时自动在后台执行预热,无需手动调用。如果需要在特定时机手动预热:
```csharp
var reportService = container.Resolve<IReportService>();
await reportService.WarmUpAsync();
```
预热内容:
- iText7 程序集加载
- BouncyCastle 加密提供程序注册
- 微软雅黑字体文件读取和解析(约 5-6 秒)
- JSON 模板反序列化 JIT 编译
- 管线各阶段代码 JIT 编译
预热完成后,首次正式生成报告不会有额外延迟。
## 9. 线程安全
`IReportService` 内部使用 `SemaphoreSlim(1, 1)` 互斥锁,确保:
- 预热与正式生成不会并发执行
- 多个并发的 `GenerateAsync` 调用会串行执行
这是因为 `ITextPdfRenderer` 的字体字段在每次渲染时重置,并发渲染会导致 iText7 的 `"belongs to other PDF document"` 错误。
## 10. 常见问题
### Q: 首次生成报告很慢?
A: 正常现象。首次需要加载字体(~5s)+ 字体子集化(~1.2s)。预热机制会在应用启动时后台完成字体加载,用户首次操作时只需等待子集化时间。
### Q: PDF 中图像显示为"无图像"占位矩形?
A: 检查 `ProcessorOutput.AnnotatedImage``ReportRequest.AdditionalImages` 中的图像路径是否正确、文件是否存在。
### Q: 报告中某些字段为空?
A: 检查 `ProcessorOutput.OutputData` 中的键名是否与 `ProcessorDataAdapter` 中的映射一致(区分大小写)。
### Q: 如何自定义报告模板?
A: 修改 `Templates/StandardReportTemplate.json`,参考 `Documents/XP.ReportEngineModelDefine.md` 中的模板结构定义。