报告XP.ReportEngine模块:根据项目设计搭建数据处理层,数据绑定器实现,PDF渲染和生成功能开发实现。

This commit is contained in:
QI Mingxuan
2026-05-11 19:18:46 +08:00
parent 1573a33a02
commit c5a390935e
11 changed files with 1634 additions and 61 deletions
+53 -1
View File
@@ -1,19 +1,71 @@
using Prism.Ioc; using Prism.Ioc;
using Prism.Modularity; using Prism.Modularity;
using XP.ReportEngine.Views; using System.Resources;
using XP.Common.Localization;
using XP.Common.Localization.Interfaces;
using XP.ReportEngine.Interfaces;
using XP.ReportEngine.Services;
namespace XP.ReportEngine namespace XP.ReportEngine
{ {
/// <summary>
/// 报告引擎模块 | Report Engine Module
/// Prism 模块入口,注册报告生成相关服务到 DI 容器
/// Prism module entry, registers report generation services to DI container
/// </summary>
[Module(ModuleName = "ReportEngineModule")]
public class ReportEngineModule : IModule public class ReportEngineModule : IModule
{ {
/// <summary>
/// 模块初始化 | Module initialization
/// 注册模块级多语言资源源 | Register module-level localization resource source
/// </summary>
public void OnInitialized(IContainerProvider containerProvider) public void OnInitialized(IContainerProvider containerProvider)
{ {
// 注册模块级多语言资源到 Fallback Chain | Register module-level localization resources to Fallback Chain
var localizationService = containerProvider.Resolve<ILocalizationService>();
var resourceManager = new ResourceManager(
"XP.ReportEngine.Resources.Resources",
typeof(ReportEngineModule).Assembly);
localizationService.RegisterResourceSource("XP.ReportEngine", resourceManager);
// 初始化 LocalizationHelper,使其通过 ILocalizationService 获取字符串(支持 Fallback Chain
// Initialize LocalizationHelper to use ILocalizationService for string lookup (supports Fallback Chain)
LocalizationHelper.Initialize(localizationService);
System.Console.WriteLine("[ReportEngineModule] 模块已初始化 | Module initialized");
} }
/// <summary>
/// 注册类型到 DI 容器 | Register types to DI container
/// </summary>
public void RegisterTypes(IContainerRegistry containerRegistry) public void RegisterTypes(IContainerRegistry containerRegistry)
{ {
// 注册报告生成器(瞬态)| Register report generator (transient)
containerRegistry.Register<IReportGenerator, PdfReportGenerator>();
// 注册报告生成器工厂(单例)| Register report generator factory (singleton)
containerRegistry.RegisterSingleton<IReportGeneratorFactory, ReportGeneratorFactory>();
// 注册模板引擎(瞬态)| Register template engine (transient)
containerRegistry.Register<ITemplateEngine, JsonTemplateEngine>();
// 注册数据绑定器(瞬态)| Register data binder (transient)
containerRegistry.Register<IDataBinder, ExpressionDataBinder>();
// 注册排版引擎(瞬态)| Register layout engine (transient)
containerRegistry.Register<ILayoutEngine, PageLayoutEngine>();
// 注册 PDF 渲染器(瞬态)| Register PDF renderer (transient)
containerRegistry.Register<IPdfRenderer, ITextPdfRenderer>();
// 注册数据适配器(瞬态)| Register data adapter (transient)
containerRegistry.Register<IReportDataAdapter, ProcessorDataAdapter>();
// 注册报告编号生成器(单例,维护每日计数器状态)| Register report ID generator (singleton, maintains daily counter state)
containerRegistry.RegisterSingleton<ReportIdGenerator>();
System.Console.WriteLine("[ReportEngineModule] 类型注册完成 | Type registration completed");
} }
} }
} }
@@ -58,4 +58,184 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<!-- Report metadata keys -->
<data name="Report_Title" xml:space="preserve">
<value>Inspection Report</value>
</data>
<data name="Report_Date" xml:space="preserve">
<value>Inspection Date</value>
</data>
<data name="Report_Sample" xml:space="preserve">
<value>Sample Name</value>
</data>
<data name="Report_Operator" xml:space="preserve">
<value>Operator</value>
</data>
<data name="Report_Summary" xml:space="preserve">
<value>Inspection Summary</value>
</data>
<data name="Report_Id" xml:space="preserve">
<value>Report ID</value>
</data>
<data name="Report_Description" xml:space="preserve">
<value>Description</value>
</data>
<!-- Classification keys -->
<data name="Classification_Pass" xml:space="preserve">
<value>PASS</value>
</data>
<data name="Classification_Fail" xml:space="preserve">
<value>FAIL</value>
</data>
<!-- Inspection type keys -->
<data name="Inspection_LineMeasurement" xml:space="preserve">
<value>Line Measurement</value>
</data>
<data name="Inspection_BgaVoidRate" xml:space="preserve">
<value>BGA Void Rate</value>
</data>
<data name="Inspection_VoidMeasurement" xml:space="preserve">
<value>Void Measurement</value>
</data>
<data name="Inspection_FillRate" xml:space="preserve">
<value>Via Fill Rate</value>
</data>
<!-- Table header keys -->
<data name="Table_Index" xml:space="preserve">
<value>No.</value>
</data>
<data name="Table_VoidRate" xml:space="preserve">
<value>Void Rate</value>
</data>
<data name="Table_Classification" xml:space="preserve">
<value>Classification</value>
</data>
<data name="Table_Area" xml:space="preserve">
<value>Area</value>
</data>
<data name="Table_AreaPercent" xml:space="preserve">
<value>Area %</value>
</data>
<data name="Table_CenterX" xml:space="preserve">
<value>Center X</value>
</data>
<data name="Table_CenterY" xml:space="preserve">
<value>Center Y</value>
</data>
<!-- Measurement keys -->
<data name="Measurement_Type" xml:space="preserve">
<value>Measurement Type</value>
</data>
<data name="Measurement_Distance" xml:space="preserve">
<value>Distance</value>
</data>
<data name="Measurement_Unit" xml:space="preserve">
<value>Unit</value>
</data>
<data name="Measurement_Angle" xml:space="preserve">
<value>Angle</value>
</data>
<data name="Measurement_FillRate" xml:space="preserve">
<value>Fill Rate</value>
</data>
<data name="Measurement_VoidRate" xml:space="preserve">
<value>Void Rate</value>
</data>
<data name="Measurement_Limit" xml:space="preserve">
<value>Limit</value>
</data>
<!-- Summary keys -->
<data name="Summary_TotalDefects" xml:space="preserve">
<value>Total Defects</value>
</data>
<data name="Summary_PassCount" xml:space="preserve">
<value>Pass Count</value>
</data>
<data name="Summary_FailCount" xml:space="preserve">
<value>Fail Count</value>
</data>
<data name="Summary_OverallResult" xml:space="preserve">
<value>Overall Result</value>
</data>
<!-- Image placeholder key -->
<data name="Image_NoImage" xml:space="preserve">
<value>No Image</value>
</data>
<!-- Page title keys -->
<data name="Page_Homepage" xml:space="preserve">
<value>Inspection Report</value>
</data>
<data name="Page_MetricData" xml:space="preserve">
<value>Measurement Data</value>
</data>
<data name="Page_BgaInspection" xml:space="preserve">
<value>BGA Solder Ball Inspection</value>
</data>
<data name="Page_VoidInspection" xml:space="preserve">
<value>Void Inspection</value>
</data>
<data name="Page_ViaFillInspection" xml:space="preserve">
<value>Via Fill Rate Inspection</value>
</data>
<!-- BGA inspection keys -->
<data name="Bga_Count" xml:space="preserve">
<value>Ball Count</value>
</data>
<data name="Bga_TotalArea" xml:space="preserve">
<value>Total Ball Area</value>
</data>
<data name="Bga_TotalVoidArea" xml:space="preserve">
<value>Total Void Area</value>
</data>
<data name="Bga_VoidLimit" xml:space="preserve">
<value>Void Rate Limit</value>
</data>
<data name="Bga_BallIndex" xml:space="preserve">
<value>Ball No.</value>
</data>
<!-- Void inspection keys -->
<data name="Void_RoiArea" xml:space="preserve">
<value>ROI Area</value>
</data>
<data name="Void_TotalArea" xml:space="preserve">
<value>Total Void Area</value>
</data>
<data name="Void_Count" xml:space="preserve">
<value>Void Count</value>
</data>
<data name="Void_MaxArea" xml:space="preserve">
<value>Max Void Area</value>
</data>
<data name="Void_Limit" xml:space="preserve">
<value>Void Rate Limit</value>
</data>
<!-- Via fill rate keys -->
<data name="Fill_Rate" xml:space="preserve">
<value>Fill Rate</value>
</data>
<data name="Fill_FullDistance" xml:space="preserve">
<value>Full Distance</value>
</data>
<data name="Fill_FillDistance" xml:space="preserve">
<value>Fill Distance</value>
</data>
<data name="Fill_THTLimit" xml:space="preserve">
<value>THT Limit</value>
</data>
<!-- Common field keys -->
<data name="Field_Point1" xml:space="preserve">
<value>Point 1</value>
</data>
<data name="Field_Point2" xml:space="preserve">
<value>Point 2</value>
</data>
<data name="Field_Result" xml:space="preserve">
<value>Result</value>
</data>
<data name="Field_InspectionType" xml:space="preserve">
<value>Inspection Type</value>
</data>
<data name="Field_Status" xml:space="preserve">
<value>Status</value>
</data>
</root> </root>
+180 -59
View File
@@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
@@ -117,4 +58,184 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<!-- 报告元数据键 | Report metadata keys -->
<data name="Report_Title" xml:space="preserve">
<value>检测报告</value>
</data>
<data name="Report_Date" xml:space="preserve">
<value>检测日期</value>
</data>
<data name="Report_Sample" xml:space="preserve">
<value>样品名称</value>
</data>
<data name="Report_Operator" xml:space="preserve">
<value>操作员</value>
</data>
<data name="Report_Summary" xml:space="preserve">
<value>检测摘要</value>
</data>
<data name="Report_Id" xml:space="preserve">
<value>报告编号</value>
</data>
<data name="Report_Description" xml:space="preserve">
<value>描述</value>
</data>
<!-- 分类结果键 | Classification keys -->
<data name="Classification_Pass" xml:space="preserve">
<value>通过</value>
</data>
<data name="Classification_Fail" xml:space="preserve">
<value>不通过</value>
</data>
<!-- 检测类型键 | Inspection type keys -->
<data name="Inspection_LineMeasurement" xml:space="preserve">
<value>距离测量</value>
</data>
<data name="Inspection_BgaVoidRate" xml:space="preserve">
<value>BGA 气泡率</value>
</data>
<data name="Inspection_VoidMeasurement" xml:space="preserve">
<value>空隙测量</value>
</data>
<data name="Inspection_FillRate" xml:space="preserve">
<value>通孔填锡率</value>
</data>
<!-- 表格列头键 | Table header keys -->
<data name="Table_Index" xml:space="preserve">
<value>序号</value>
</data>
<data name="Table_VoidRate" xml:space="preserve">
<value>气泡率</value>
</data>
<data name="Table_Classification" xml:space="preserve">
<value>分类结果</value>
</data>
<data name="Table_Area" xml:space="preserve">
<value>面积</value>
</data>
<data name="Table_AreaPercent" xml:space="preserve">
<value>面积百分比</value>
</data>
<data name="Table_CenterX" xml:space="preserve">
<value>中心 X</value>
</data>
<data name="Table_CenterY" xml:space="preserve">
<value>中心 Y</value>
</data>
<!-- 测量键 | Measurement keys -->
<data name="Measurement_Type" xml:space="preserve">
<value>测量类型</value>
</data>
<data name="Measurement_Distance" xml:space="preserve">
<value>距离</value>
</data>
<data name="Measurement_Unit" xml:space="preserve">
<value>单位</value>
</data>
<data name="Measurement_Angle" xml:space="preserve">
<value>角度</value>
</data>
<data name="Measurement_FillRate" xml:space="preserve">
<value>填锡率</value>
</data>
<data name="Measurement_VoidRate" xml:space="preserve">
<value>气泡率</value>
</data>
<data name="Measurement_Limit" xml:space="preserve">
<value>限值</value>
</data>
<!-- 摘要键 | Summary keys -->
<data name="Summary_TotalDefects" xml:space="preserve">
<value>总缺陷数</value>
</data>
<data name="Summary_PassCount" xml:space="preserve">
<value>通过数量</value>
</data>
<data name="Summary_FailCount" xml:space="preserve">
<value>不通过数量</value>
</data>
<data name="Summary_OverallResult" xml:space="preserve">
<value>总体结果</value>
</data>
<!-- 图像占位符键 | Image placeholder key -->
<data name="Image_NoImage" xml:space="preserve">
<value>无图像</value>
</data>
<!-- 页面标题键 | Page title keys -->
<data name="Page_Homepage" xml:space="preserve">
<value>检测报告首页</value>
</data>
<data name="Page_MetricData" xml:space="preserve">
<value>测量数据</value>
</data>
<data name="Page_BgaInspection" xml:space="preserve">
<value>BGA 焊球检测</value>
</data>
<data name="Page_VoidInspection" xml:space="preserve">
<value>空隙检测</value>
</data>
<data name="Page_ViaFillInspection" xml:space="preserve">
<value>通孔填锡检测</value>
</data>
<!-- BGA 检测键 | BGA inspection keys -->
<data name="Bga_Count" xml:space="preserve">
<value>焊球数量</value>
</data>
<data name="Bga_TotalArea" xml:space="preserve">
<value>焊球总面积</value>
</data>
<data name="Bga_TotalVoidArea" xml:space="preserve">
<value>气泡总面积</value>
</data>
<data name="Bga_VoidLimit" xml:space="preserve">
<value>气泡率限值</value>
</data>
<data name="Bga_BallIndex" xml:space="preserve">
<value>焊球序号</value>
</data>
<!-- 空隙检测键 | Void inspection keys -->
<data name="Void_RoiArea" xml:space="preserve">
<value>ROI 面积</value>
</data>
<data name="Void_TotalArea" xml:space="preserve">
<value>空隙总面积</value>
</data>
<data name="Void_Count" xml:space="preserve">
<value>空隙数量</value>
</data>
<data name="Void_MaxArea" xml:space="preserve">
<value>最大空隙面积</value>
</data>
<data name="Void_Limit" xml:space="preserve">
<value>空隙率限值</value>
</data>
<!-- 通孔填锡键 | Via fill rate keys -->
<data name="Fill_Rate" xml:space="preserve">
<value>填锡率</value>
</data>
<data name="Fill_FullDistance" xml:space="preserve">
<value>满填距离</value>
</data>
<data name="Fill_FillDistance" xml:space="preserve">
<value>填充距离</value>
</data>
<data name="Fill_THTLimit" xml:space="preserve">
<value>THT 限值</value>
</data>
<!-- 通用字段键 | Common field keys -->
<data name="Field_Point1" xml:space="preserve">
<value>起点</value>
</data>
<data name="Field_Point2" xml:space="preserve">
<value>终点</value>
</data>
<data name="Field_Result" xml:space="preserve">
<value>结果</value>
</data>
<data name="Field_InspectionType" xml:space="preserve">
<value>检测类型</value>
</data>
<data name="Field_Status" xml:space="preserve">
<value>状态</value>
</data>
</root> </root>
@@ -58,4 +58,184 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<!-- 报告元数据键 | Report metadata keys -->
<data name="Report_Title" xml:space="preserve">
<value>检测报告</value>
</data>
<data name="Report_Date" xml:space="preserve">
<value>检测日期</value>
</data>
<data name="Report_Sample" xml:space="preserve">
<value>样品名称</value>
</data>
<data name="Report_Operator" xml:space="preserve">
<value>操作员</value>
</data>
<data name="Report_Summary" xml:space="preserve">
<value>检测摘要</value>
</data>
<data name="Report_Id" xml:space="preserve">
<value>报告编号</value>
</data>
<data name="Report_Description" xml:space="preserve">
<value>描述</value>
</data>
<!-- 分类结果键 | Classification keys -->
<data name="Classification_Pass" xml:space="preserve">
<value>通过</value>
</data>
<data name="Classification_Fail" xml:space="preserve">
<value>不通过</value>
</data>
<!-- 检测类型键 | Inspection type keys -->
<data name="Inspection_LineMeasurement" xml:space="preserve">
<value>距离测量</value>
</data>
<data name="Inspection_BgaVoidRate" xml:space="preserve">
<value>BGA 气泡率</value>
</data>
<data name="Inspection_VoidMeasurement" xml:space="preserve">
<value>空隙测量</value>
</data>
<data name="Inspection_FillRate" xml:space="preserve">
<value>通孔填锡率</value>
</data>
<!-- 表格列头键 | Table header keys -->
<data name="Table_Index" xml:space="preserve">
<value>序号</value>
</data>
<data name="Table_VoidRate" xml:space="preserve">
<value>气泡率</value>
</data>
<data name="Table_Classification" xml:space="preserve">
<value>分类结果</value>
</data>
<data name="Table_Area" xml:space="preserve">
<value>面积</value>
</data>
<data name="Table_AreaPercent" xml:space="preserve">
<value>面积百分比</value>
</data>
<data name="Table_CenterX" xml:space="preserve">
<value>中心 X</value>
</data>
<data name="Table_CenterY" xml:space="preserve">
<value>中心 Y</value>
</data>
<!-- 测量键 | Measurement keys -->
<data name="Measurement_Type" xml:space="preserve">
<value>测量类型</value>
</data>
<data name="Measurement_Distance" xml:space="preserve">
<value>距离</value>
</data>
<data name="Measurement_Unit" xml:space="preserve">
<value>单位</value>
</data>
<data name="Measurement_Angle" xml:space="preserve">
<value>角度</value>
</data>
<data name="Measurement_FillRate" xml:space="preserve">
<value>填锡率</value>
</data>
<data name="Measurement_VoidRate" xml:space="preserve">
<value>气泡率</value>
</data>
<data name="Measurement_Limit" xml:space="preserve">
<value>限值</value>
</data>
<!-- 摘要键 | Summary keys -->
<data name="Summary_TotalDefects" xml:space="preserve">
<value>总缺陷数</value>
</data>
<data name="Summary_PassCount" xml:space="preserve">
<value>通过数量</value>
</data>
<data name="Summary_FailCount" xml:space="preserve">
<value>不通过数量</value>
</data>
<data name="Summary_OverallResult" xml:space="preserve">
<value>总体结果</value>
</data>
<!-- 图像占位符键 | Image placeholder key -->
<data name="Image_NoImage" xml:space="preserve">
<value>无图像</value>
</data>
<!-- 页面标题键 | Page title keys -->
<data name="Page_Homepage" xml:space="preserve">
<value>检测报告首页</value>
</data>
<data name="Page_MetricData" xml:space="preserve">
<value>测量数据</value>
</data>
<data name="Page_BgaInspection" xml:space="preserve">
<value>BGA 焊球检测</value>
</data>
<data name="Page_VoidInspection" xml:space="preserve">
<value>空隙检测</value>
</data>
<data name="Page_ViaFillInspection" xml:space="preserve">
<value>通孔填锡检测</value>
</data>
<!-- BGA 检测键 | BGA inspection keys -->
<data name="Bga_Count" xml:space="preserve">
<value>焊球数量</value>
</data>
<data name="Bga_TotalArea" xml:space="preserve">
<value>焊球总面积</value>
</data>
<data name="Bga_TotalVoidArea" xml:space="preserve">
<value>气泡总面积</value>
</data>
<data name="Bga_VoidLimit" xml:space="preserve">
<value>气泡率限值</value>
</data>
<data name="Bga_BallIndex" xml:space="preserve">
<value>焊球序号</value>
</data>
<!-- 空隙检测键 | Void inspection keys -->
<data name="Void_RoiArea" xml:space="preserve">
<value>ROI 面积</value>
</data>
<data name="Void_TotalArea" xml:space="preserve">
<value>空隙总面积</value>
</data>
<data name="Void_Count" xml:space="preserve">
<value>空隙数量</value>
</data>
<data name="Void_MaxArea" xml:space="preserve">
<value>最大空隙面积</value>
</data>
<data name="Void_Limit" xml:space="preserve">
<value>空隙率限值</value>
</data>
<!-- 通孔填锡键 | Via fill rate keys -->
<data name="Fill_Rate" xml:space="preserve">
<value>填锡率</value>
</data>
<data name="Fill_FullDistance" xml:space="preserve">
<value>满填距离</value>
</data>
<data name="Fill_FillDistance" xml:space="preserve">
<value>填充距离</value>
</data>
<data name="Fill_THTLimit" xml:space="preserve">
<value>THT 限值</value>
</data>
<!-- 通用字段键 | Common field keys -->
<data name="Field_Point1" xml:space="preserve">
<value>起点</value>
</data>
<data name="Field_Point2" xml:space="preserve">
<value>终点</value>
</data>
<data name="Field_Result" xml:space="preserve">
<value>结果</value>
</data>
<data name="Field_InspectionType" xml:space="preserve">
<value>检测类型</value>
</data>
<data name="Field_Status" xml:space="preserve">
<value>状态</value>
</data>
</root> </root>
@@ -58,4 +58,184 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<!-- 報告元數據鍵 | Report metadata keys -->
<data name="Report_Title" xml:space="preserve">
<value>檢測報告</value>
</data>
<data name="Report_Date" xml:space="preserve">
<value>檢測日期</value>
</data>
<data name="Report_Sample" xml:space="preserve">
<value>樣品名稱</value>
</data>
<data name="Report_Operator" xml:space="preserve">
<value>操作員</value>
</data>
<data name="Report_Summary" xml:space="preserve">
<value>檢測摘要</value>
</data>
<data name="Report_Id" xml:space="preserve">
<value>報告編號</value>
</data>
<data name="Report_Description" xml:space="preserve">
<value>描述</value>
</data>
<!-- 分類結果鍵 | Classification keys -->
<data name="Classification_Pass" xml:space="preserve">
<value>通過</value>
</data>
<data name="Classification_Fail" xml:space="preserve">
<value>不通過</value>
</data>
<!-- 檢測類型鍵 | Inspection type keys -->
<data name="Inspection_LineMeasurement" xml:space="preserve">
<value>距離測量</value>
</data>
<data name="Inspection_BgaVoidRate" xml:space="preserve">
<value>BGA 氣泡率</value>
</data>
<data name="Inspection_VoidMeasurement" xml:space="preserve">
<value>空隙測量</value>
</data>
<data name="Inspection_FillRate" xml:space="preserve">
<value>通孔填錫率</value>
</data>
<!-- 表格列頭鍵 | Table header keys -->
<data name="Table_Index" xml:space="preserve">
<value>序號</value>
</data>
<data name="Table_VoidRate" xml:space="preserve">
<value>氣泡率</value>
</data>
<data name="Table_Classification" xml:space="preserve">
<value>分類結果</value>
</data>
<data name="Table_Area" xml:space="preserve">
<value>面積</value>
</data>
<data name="Table_AreaPercent" xml:space="preserve">
<value>面積百分比</value>
</data>
<data name="Table_CenterX" xml:space="preserve">
<value>中心 X</value>
</data>
<data name="Table_CenterY" xml:space="preserve">
<value>中心 Y</value>
</data>
<!-- 測量鍵 | Measurement keys -->
<data name="Measurement_Type" xml:space="preserve">
<value>測量類型</value>
</data>
<data name="Measurement_Distance" xml:space="preserve">
<value>距離</value>
</data>
<data name="Measurement_Unit" xml:space="preserve">
<value>單位</value>
</data>
<data name="Measurement_Angle" xml:space="preserve">
<value>角度</value>
</data>
<data name="Measurement_FillRate" xml:space="preserve">
<value>填錫率</value>
</data>
<data name="Measurement_VoidRate" xml:space="preserve">
<value>氣泡率</value>
</data>
<data name="Measurement_Limit" xml:space="preserve">
<value>限值</value>
</data>
<!-- 摘要鍵 | Summary keys -->
<data name="Summary_TotalDefects" xml:space="preserve">
<value>總缺陷數</value>
</data>
<data name="Summary_PassCount" xml:space="preserve">
<value>通過數量</value>
</data>
<data name="Summary_FailCount" xml:space="preserve">
<value>不通過數量</value>
</data>
<data name="Summary_OverallResult" xml:space="preserve">
<value>總體結果</value>
</data>
<!-- 圖像佔位符鍵 | Image placeholder key -->
<data name="Image_NoImage" xml:space="preserve">
<value>無圖像</value>
</data>
<!-- 頁面標題鍵 | Page title keys -->
<data name="Page_Homepage" xml:space="preserve">
<value>檢測報告首頁</value>
</data>
<data name="Page_MetricData" xml:space="preserve">
<value>測量數據</value>
</data>
<data name="Page_BgaInspection" xml:space="preserve">
<value>BGA 焊球檢測</value>
</data>
<data name="Page_VoidInspection" xml:space="preserve">
<value>空隙檢測</value>
</data>
<data name="Page_ViaFillInspection" xml:space="preserve">
<value>通孔填錫檢測</value>
</data>
<!-- BGA 檢測鍵 | BGA inspection keys -->
<data name="Bga_Count" xml:space="preserve">
<value>焊球數量</value>
</data>
<data name="Bga_TotalArea" xml:space="preserve">
<value>焊球總面積</value>
</data>
<data name="Bga_TotalVoidArea" xml:space="preserve">
<value>氣泡總面積</value>
</data>
<data name="Bga_VoidLimit" xml:space="preserve">
<value>氣泡率限值</value>
</data>
<data name="Bga_BallIndex" xml:space="preserve">
<value>焊球序號</value>
</data>
<!-- 空隙檢測鍵 | Void inspection keys -->
<data name="Void_RoiArea" xml:space="preserve">
<value>ROI 面積</value>
</data>
<data name="Void_TotalArea" xml:space="preserve">
<value>空隙總面積</value>
</data>
<data name="Void_Count" xml:space="preserve">
<value>空隙數量</value>
</data>
<data name="Void_MaxArea" xml:space="preserve">
<value>最大空隙面積</value>
</data>
<data name="Void_Limit" xml:space="preserve">
<value>空隙率限值</value>
</data>
<!-- 通孔填錫鍵 | Via fill rate keys -->
<data name="Fill_Rate" xml:space="preserve">
<value>填錫率</value>
</data>
<data name="Fill_FullDistance" xml:space="preserve">
<value>滿填距離</value>
</data>
<data name="Fill_FillDistance" xml:space="preserve">
<value>填充距離</value>
</data>
<data name="Fill_THTLimit" xml:space="preserve">
<value>THT 限值</value>
</data>
<!-- 通用字段鍵 | Common field keys -->
<data name="Field_Point1" xml:space="preserve">
<value>起點</value>
</data>
<data name="Field_Point2" xml:space="preserve">
<value>終點</value>
</data>
<data name="Field_Result" xml:space="preserve">
<value>結果</value>
</data>
<data name="Field_InspectionType" xml:space="preserve">
<value>檢測類型</value>
</data>
<data name="Field_Status" xml:space="preserve">
<value>狀態</value>
</data>
</root> </root>
@@ -0,0 +1,134 @@
using System;
using System.IO;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XP.ReportEngine.Interfaces;
using XP.ReportEngine.Models;
namespace XP.ReportEngine.Services
{
/// <summary>
/// PDF 报告生成器实现 | PDF report generator implementation
/// 协调管线各阶段:模板加载 → 数据绑定 → 排版 → 渲染 → 保存
/// Orchestrates pipeline phases: template loading → data binding → layout → rendering → saving
/// </summary>
public class PdfReportGenerator : IReportGenerator
{
private readonly ILoggerService _logger;
private readonly ITemplateEngine _templateEngine;
private readonly IDataBinder _dataBinder;
private readonly ILayoutEngine _layoutEngine;
private readonly IPdfRenderer _pdfRenderer;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="logger">日志服务 | Logger service</param>
/// <param name="templateEngine">模板引擎 | Template engine</param>
/// <param name="dataBinder">数据绑定器 | Data binder</param>
/// <param name="layoutEngine">排版引擎 | Layout engine</param>
/// <param name="pdfRenderer">PDF 渲染器 | PDF renderer</param>
public PdfReportGenerator(
ILoggerService logger,
ITemplateEngine templateEngine,
IDataBinder dataBinder,
ILayoutEngine layoutEngine,
IPdfRenderer pdfRenderer)
{
_logger = logger?.ForModule<PdfReportGenerator>() ?? throw new ArgumentNullException(nameof(logger));
_templateEngine = templateEngine ?? throw new ArgumentNullException(nameof(templateEngine));
_dataBinder = dataBinder ?? throw new ArgumentNullException(nameof(dataBinder));
_layoutEngine = layoutEngine ?? throw new ArgumentNullException(nameof(layoutEngine));
_pdfRenderer = pdfRenderer ?? throw new ArgumentNullException(nameof(pdfRenderer));
}
/// <summary>
/// 异步生成 PDF 报告 | Generate PDF report asynchronously
/// 执行完整管线:模板加载 → 验证 → 数据绑定 → 排版计算 → PDF 渲染 → 文件保存(可选)
/// Executes full pipeline: template load → validate → data bind → layout → PDF render → save (optional)
/// </summary>
/// <param name="context">报告上下文数据 | Report context data</param>
/// <param name="options">生成选项 | Generation options</param>
/// <returns>生成结果 | Generation result</returns>
public async Task<ReportResult> GenerateAsync(ReportContext context, ReportGenerationOptions options)
{
try
{
_logger.Info("报告生成管线开始 | Report generation pipeline started");
// 阶段 1:加载模板 | Phase 1: Load template
_logger.Info("阶段 1:加载模板 | Phase 1: Loading template");
var template = _templateEngine.LoadTemplate(options.TemplatePath);
if (template == null)
{
var errorMsg = $"模板文件未找到: {options.TemplatePath}";
_logger.Error(null, "模板加载失败: {Path} | Template loading failed: {Path}", options.TemplatePath);
return ReportResult.Failure(errorMsg);
}
var validation = _templateEngine.Validate(template);
if (!validation.IsValid)
{
var errorMsg = $"模板验证失败: {validation.ErrorMessage}";
_logger.Error(null, "模板验证失败: {Message} | Template validation failed: {Message}", validation.ErrorMessage);
return ReportResult.Failure(errorMsg);
}
_logger.Info("阶段 1 完成:模板加载成功 | Phase 1 completed: Template loaded successfully");
// 阶段 2:数据绑定 | Phase 2: Data binding
_logger.Info("阶段 2:数据绑定 | Phase 2: Data binding");
var boundTemplate = _dataBinder.Bind(template, context);
_logger.Info("阶段 2 完成:数据绑定成功 | Phase 2 completed: Data binding successful");
// 阶段 3:排版计算 | Phase 3: Layout calculation
_logger.Info("阶段 3:排版计算 | Phase 3: Layout calculation");
var pages = _layoutEngine.CalculateLayout(boundTemplate, options);
_logger.Info("阶段 3 完成:排版计算成功,共 {PageCount} 页 | Phase 3 completed: Layout calculated, {PageCount} pages", pages.Count);
// 阶段 4PDF 渲染 | Phase 4: PDF rendering
_logger.Info("阶段 4PDF 渲染 | Phase 4: PDF rendering");
var stream = _pdfRenderer.Render(pages, options);
_logger.Info("阶段 4 完成:PDF 渲染成功 | Phase 4 completed: PDF rendering successful");
// 阶段 5:保存文件(可选)| Phase 5: Save file (optional)
if (!string.IsNullOrEmpty(options.OutputFilePath))
{
_logger.Info("阶段 5:保存文件 | Phase 5: Saving file");
await SaveToFileAsync(stream, options.OutputFilePath);
_logger.Info("阶段 5 完成:文件保存成功 {Path} | Phase 5 completed: File saved successfully {Path}", options.OutputFilePath);
}
_logger.Info("报告生成管线完成 | Report generation pipeline completed");
return ReportResult.Success(stream);
}
catch (Exception ex)
{
_logger.Error(ex, "报告生成失败 | Report generation failed: {Message}", ex.Message);
return ReportResult.Failure($"报告生成过程中发生错误: {ex.Message}", ex);
}
}
/// <summary>
/// 将 MemoryStream 保存到文件 | Save MemoryStream to file
/// </summary>
/// <param name="stream">PDF 内存流 | PDF memory stream</param>
/// <param name="filePath">输出文件路径 | Output file path</param>
private async Task SaveToFileAsync(MemoryStream stream, string filePath)
{
// 确保输出目录存在 | Ensure output directory exists
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 重置流位置后写入文件 | Reset stream position before writing to file
stream.Position = 0;
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
await stream.CopyToAsync(fileStream);
// 重置流位置以便后续使用 | Reset stream position for subsequent use
stream.Position = 0;
}
}
}
@@ -0,0 +1,472 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using XP.Common.Logging.Interfaces;
using XP.ReportEngine.Interfaces;
using XP.ReportEngine.Models;
namespace XP.ReportEngine.Services
{
/// <summary>
/// 处理器数据适配器实现 | Processor data adapter implementation
/// 将 XP.ImageProcessing 的 ProcessorOutput 转换为 ReportContext
/// Converts XP.ImageProcessing ProcessorOutput to ReportContext
/// </summary>
public class ProcessorDataAdapter : IReportDataAdapter
{
private readonly ILoggerService _logger;
// 处理器类型常量 | Processor type constants
private const string LineMeasurementProcessor = "LineMeasurementProcessor";
private const string BgaVoidRateProcessor = "BgaVoidRateProcessor";
private const string VoidMeasurementProcessor = "VoidMeasurementProcessor";
private const string FillRateProcessor = "FillRateProcessor";
public ProcessorDataAdapter(ILoggerService logger)
{
_logger = logger?.ForModule<ProcessorDataAdapter>() ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// 将处理器输出数据适配为报告上下文 | Adapt processor output data to report context
/// </summary>
public ReportContext Adapt(List<ProcessorOutput> processorOutputs, ReportMetadata metadata)
{
if (processorOutputs == null) throw new ArgumentNullException(nameof(processorOutputs));
if (metadata == null) throw new ArgumentNullException(nameof(metadata));
_logger.Info("开始数据适配,处理器数量: {Count} | Starting data adaptation, processor count: {Count}", processorOutputs.Count);
var context = new ReportContext
{
Metadata = metadata,
ResultGroups = new List<InspectionResultGroup>(),
Images = new Dictionary<string, ImageData>(),
Properties = new Dictionary<string, object>()
};
// 多处理器输出聚合逻辑:每个 ProcessorOutput 生成一个 InspectionResultGroup | Aggregation: each ProcessorOutput becomes one InspectionResultGroup
for (var i = 0; i < processorOutputs.Count; i++)
{
var output = processorOutputs[i];
if (output == null)
{
_logger.Warn("处理器输出为 null,索引: {Index},已跳过 | Processor output is null at index: {Index}, skipped", i);
continue;
}
var group = AdaptProcessorOutput(output, i);
context.ResultGroups.Add(group);
// 关联标注图像 | Associate annotated image
if (output.AnnotatedImage != null)
{
var imageKey = $"{output.ProcessorType}_{i}_annotated";
context.Images[imageKey] = output.AnnotatedImage;
}
}
_logger.Info("数据适配完成,生成 {Count} 个结果分组 | Data adaptation completed, generated {Count} result groups", context.ResultGroups.Count);
return context;
}
/// <summary>
/// 根据处理器类型分发适配逻辑 | Dispatch adaptation logic by processor type
/// </summary>
private InspectionResultGroup AdaptProcessorOutput(ProcessorOutput output, int index)
{
var sourceId = $"{output.ProcessorType}_{index}";
return output.ProcessorType switch
{
LineMeasurementProcessor => AdaptLineMeasurement(output, sourceId),
BgaVoidRateProcessor => AdaptBgaVoidRate(output, sourceId),
VoidMeasurementProcessor => AdaptVoidMeasurement(output, sourceId),
FillRateProcessor => AdaptFillRate(output, sourceId),
_ => AdaptGeneric(output, sourceId)
};
}
#region LineMeasurementProcessor | LineMeasurementProcessor Adaptation
/// <summary>
/// 适配线测量处理器输出 | Adapt line measurement processor output
/// 提取 MeasurementType、Point1、Point2、PixelDistance、ActualDistance、Unit、Angle
/// </summary>
private InspectionResultGroup AdaptLineMeasurement(ProcessorOutput output, string sourceId)
{
_logger.Debug("适配 LineMeasurementProcessor 输出,SourceId: {SourceId} | Adapting LineMeasurementProcessor output, SourceId: {SourceId}", sourceId);
var data = output.OutputData ?? new Dictionary<string, object>();
var group = new InspectionResultGroup
{
ProcessorType = LineMeasurementProcessor,
SourceId = sourceId,
Classification = string.Empty,
Data = new Dictionary<string, object>
{
["measurementType"] = GetValueOrDefault<string>(data, "MeasurementType", string.Empty),
["point1"] = GetValueOrDefault<object>(data, "Point1", null),
["point2"] = GetValueOrDefault<object>(data, "Point2", null),
["pixelDistance"] = GetValueOrDefault<double>(data, "PixelDistance", 0.0),
["actualDistance"] = GetValueOrDefault<double>(data, "ActualDistance", 0.0),
["unit"] = GetValueOrDefault<string>(data, "Unit", string.Empty),
["angle"] = GetValueOrDefault<double>(data, "Angle", 0.0)
},
TableRows = new List<Dictionary<string, object>>()
};
return group;
}
#endregion
#region BgaVoidRateProcessor | BgaVoidRateProcessor Adaptation
/// <summary>
/// 适配 BGA 气泡率处理器输出 | Adapt BGA void rate processor output
/// 提取 BgaCount、BgaBalls 列表转 TableRows、VoidRate、FillRate、TotalBgaArea、TotalVoidArea、Classification、VoidLimit
/// </summary>
private InspectionResultGroup AdaptBgaVoidRate(ProcessorOutput output, string sourceId)
{
_logger.Debug("适配 BgaVoidRateProcessor 输出,SourceId: {SourceId} | Adapting BgaVoidRateProcessor output, SourceId: {SourceId}", sourceId);
var data = output.OutputData ?? new Dictionary<string, object>();
var group = new InspectionResultGroup
{
ProcessorType = BgaVoidRateProcessor,
SourceId = sourceId,
Classification = GetValueOrDefault<string>(data, "Classification", string.Empty),
Data = new Dictionary<string, object>
{
["bgaCount"] = GetValueOrDefault<int>(data, "BgaCount", 0),
["voidRate"] = GetValueOrDefault<double>(data, "VoidRate", 0.0),
["fillRate"] = GetValueOrDefault<double>(data, "FillRate", 0.0),
["totalBgaArea"] = GetValueOrDefault<double>(data, "TotalBgaArea", 0.0),
["totalVoidArea"] = GetValueOrDefault<double>(data, "TotalVoidArea", 0.0),
["voidLimit"] = GetValueOrDefault<double>(data, "VoidLimit", 0.0)
},
TableRows = ConvertBgaBallsToTableRows(data)
};
return group;
}
/// <summary>
/// 将 BgaBalls 列表转换为表格行 | Convert BgaBalls list to table rows
/// 每个焊球一行,包含 index、voidRate、classification
/// </summary>
private List<Dictionary<string, object>> ConvertBgaBallsToTableRows(Dictionary<string, object> data)
{
var tableRows = new List<Dictionary<string, object>>();
var bgaBalls = GetListValue(data, "BgaBalls");
if (bgaBalls == null || bgaBalls.Count == 0)
{
return tableRows;
}
for (var i = 0; i < bgaBalls.Count; i++)
{
var ball = bgaBalls[i];
var row = new Dictionary<string, object>
{
["index"] = i + 1,
["voidRate"] = GetNestedValue<double>(ball, "VoidRate", 0.0),
["classification"] = GetNestedValue<string>(ball, "Classification", string.Empty)
};
tableRows.Add(row);
}
return tableRows;
}
#endregion
#region VoidMeasurementProcessor | VoidMeasurementProcessor Adaptation
/// <summary>
/// 适配空隙测量处理器输出 | Adapt void measurement processor output
/// 提取 RoiArea、TotalVoidArea、VoidRate、VoidLimit、VoidCount、MaxVoidArea、Classification、Voids 列表转 TableRows
/// </summary>
private InspectionResultGroup AdaptVoidMeasurement(ProcessorOutput output, string sourceId)
{
_logger.Debug("适配 VoidMeasurementProcessor 输出,SourceId: {SourceId} | Adapting VoidMeasurementProcessor output, SourceId: {SourceId}", sourceId);
var data = output.OutputData ?? new Dictionary<string, object>();
var group = new InspectionResultGroup
{
ProcessorType = VoidMeasurementProcessor,
SourceId = sourceId,
Classification = GetValueOrDefault<string>(data, "Classification", string.Empty),
Data = new Dictionary<string, object>
{
["roiArea"] = GetValueOrDefault<double>(data, "RoiArea", 0.0),
["totalVoidArea"] = GetValueOrDefault<double>(data, "TotalVoidArea", 0.0),
["voidRate"] = GetValueOrDefault<double>(data, "VoidRate", 0.0),
["voidLimit"] = GetValueOrDefault<double>(data, "VoidLimit", 0.0),
["voidCount"] = GetValueOrDefault<int>(data, "VoidCount", 0),
["maxVoidArea"] = GetValueOrDefault<double>(data, "MaxVoidArea", 0.0)
},
TableRows = ConvertVoidsToTableRows(data)
};
return group;
}
/// <summary>
/// 将 Voids 列表转换为表格行 | Convert Voids list to table rows
/// 每个空隙一行,包含 index、area、areaPercent、centerX、centerY
/// </summary>
private List<Dictionary<string, object>> ConvertVoidsToTableRows(Dictionary<string, object> data)
{
var tableRows = new List<Dictionary<string, object>>();
var voids = GetListValue(data, "Voids");
if (voids == null || voids.Count == 0)
{
return tableRows;
}
for (var i = 0; i < voids.Count; i++)
{
var voidItem = voids[i];
var row = new Dictionary<string, object>
{
["index"] = i + 1,
["area"] = GetNestedValue<double>(voidItem, "Area", 0.0),
["areaPercent"] = GetNestedValue<double>(voidItem, "AreaPercent", 0.0),
["centerX"] = GetNestedValue<double>(voidItem, "CenterX", 0.0),
["centerY"] = GetNestedValue<double>(voidItem, "CenterY", 0.0)
};
tableRows.Add(row);
}
return tableRows;
}
#endregion
#region FillRateProcessor | FillRateProcessor Adaptation
/// <summary>
/// 适配填锡率处理器输出 | Adapt fill rate processor output
/// 提取 FillRate、VoidRate、FullDistance、FillDistance、THTLimit、Classification、E1-E4 椭圆几何数据
/// </summary>
private InspectionResultGroup AdaptFillRate(ProcessorOutput output, string sourceId)
{
_logger.Debug("适配 FillRateProcessor 输出,SourceId: {SourceId} | Adapting FillRateProcessor output, SourceId: {SourceId}", sourceId);
var data = output.OutputData ?? new Dictionary<string, object>();
var group = new InspectionResultGroup
{
ProcessorType = FillRateProcessor,
SourceId = sourceId,
Classification = GetValueOrDefault<string>(data, "Classification", string.Empty),
Data = new Dictionary<string, object>
{
["fillRate"] = GetValueOrDefault<double>(data, "FillRate", 0.0),
["voidRate"] = GetValueOrDefault<double>(data, "VoidRate", 0.0),
["fullDistance"] = GetValueOrDefault<double>(data, "FullDistance", 0.0),
["fillDistance"] = GetValueOrDefault<double>(data, "FillDistance", 0.0),
["thtLimit"] = GetValueOrDefault<double>(data, "THTLimit", 0.0),
["e1"] = GetValueOrDefault<object>(data, "E1", null),
["e2"] = GetValueOrDefault<object>(data, "E2", null),
["e3"] = GetValueOrDefault<object>(data, "E3", null),
["e4"] = GetValueOrDefault<object>(data, "E4", null)
},
TableRows = new List<Dictionary<string, object>>()
};
return group;
}
#endregion
#region | Generic Adaptation
/// <summary>
/// 通用处理器适配(未知类型)| Generic processor adaptation (unknown type)
/// 将所有 OutputData 键值对直接映射到 Data 字典
/// </summary>
private InspectionResultGroup AdaptGeneric(ProcessorOutput output, string sourceId)
{
_logger.Warn("未知处理器类型: {ProcessorType},使用通用适配 | Unknown processor type: {ProcessorType}, using generic adaptation", output.ProcessorType);
var data = output.OutputData ?? new Dictionary<string, object>();
var group = new InspectionResultGroup
{
ProcessorType = output.ProcessorType ?? "Unknown",
SourceId = sourceId,
Classification = GetValueOrDefault<string>(data, "Classification", string.Empty),
Data = new Dictionary<string, object>(),
TableRows = new List<Dictionary<string, object>>()
};
// 将所有键值对转为小驼峰命名映射 | Map all key-value pairs with camelCase naming
foreach (var kvp in data)
{
var camelKey = ToCamelCase(kvp.Key);
group.Data[camelKey] = kvp.Value;
}
return group;
}
#endregion
#region | Helper Methods
/// <summary>
/// 从字典中获取值,缺失时使用默认值并记录警告 | Get value from dictionary, use default and log warning if missing
/// </summary>
private T GetValueOrDefault<T>(Dictionary<string, object> data, string key, T defaultValue)
{
if (data == null || !data.TryGetValue(key, out var value) || value == null)
{
_logger.Warn("处理器输出缺少键: {Key},使用默认值: {Default} | Processor output missing key: {Key}, using default: {Default}", key, defaultValue);
return defaultValue;
}
try
{
return ConvertValue<T>(value);
}
catch (Exception ex)
{
_logger.Warn("键 {Key} 的值类型转换失败: {Message},使用默认值 | Value type conversion failed for key {Key}: {Message}, using default", key, ex.Message);
return defaultValue;
}
}
/// <summary>
/// 从字典中获取列表值 | Get list value from dictionary
/// </summary>
private IList GetListValue(Dictionary<string, object> data, string key)
{
if (data == null || !data.TryGetValue(key, out var value) || value == null)
{
_logger.Warn("处理器输出缺少列表键: {Key},返回空列表 | Processor output missing list key: {Key}, returning empty list", key);
return null;
}
if (value is IList list)
{
return list;
}
_logger.Warn("键 {Key} 的值不是列表类型 | Value for key {Key} is not a list type", key);
return null;
}
/// <summary>
/// 从嵌套对象中获取属性值 | Get property value from nested object
/// </summary>
private T GetNestedValue<T>(object obj, string propertyName, T defaultValue)
{
if (obj == null) return defaultValue;
// 字典访问 | Dictionary access
if (obj is IDictionary<string, object> dict)
{
if (dict.TryGetValue(propertyName, out var dictValue) && dictValue != null)
{
try
{
return ConvertValue<T>(dictValue);
}
catch
{
return defaultValue;
}
}
return defaultValue;
}
// 反射访问 | Reflection access
var type = obj.GetType();
var propInfo = type.GetProperty(propertyName,
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase);
if (propInfo != null)
{
try
{
var value = propInfo.GetValue(obj);
if (value == null) return defaultValue;
return ConvertValue<T>(value);
}
catch
{
return defaultValue;
}
}
return defaultValue;
}
/// <summary>
/// 类型转换辅助方法 | Type conversion helper
/// </summary>
private T ConvertValue<T>(object value)
{
if (value == null) return default;
var targetType = typeof(T);
// 直接类型匹配 | Direct type match
if (value is T typedValue)
{
return typedValue;
}
// 处理 nullable 类型 | Handle nullable types
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
// object 类型直接返回 | Return directly for object type
if (underlyingType == typeof(object))
{
return (T)value;
}
// 数值类型转换 | Numeric type conversion
if (underlyingType == typeof(double))
{
return (T)(object)Convert.ToDouble(value);
}
if (underlyingType == typeof(int))
{
return (T)(object)Convert.ToInt32(value);
}
if (underlyingType == typeof(float))
{
return (T)(object)Convert.ToSingle(value);
}
if (underlyingType == typeof(long))
{
return (T)(object)Convert.ToInt64(value);
}
// 字符串转换 | String conversion
if (underlyingType == typeof(string))
{
return (T)(object)value.ToString();
}
// 通用转换 | General conversion
return (T)Convert.ChangeType(value, underlyingType);
}
/// <summary>
/// 将 PascalCase 转换为 camelCase | Convert PascalCase to camelCase
/// </summary>
private string ToCamelCase(string input)
{
if (string.IsNullOrEmpty(input)) return input;
if (input.Length == 1) return input.ToLowerInvariant();
return char.ToLowerInvariant(input[0]) + input.Substring(1);
}
#endregion
}
}
@@ -0,0 +1,53 @@
using System;
using XP.Common.Logging.Interfaces;
using XP.ReportEngine.Interfaces;
using XP.ReportEngine.Models;
namespace XP.ReportEngine.Services
{
/// <summary>
/// 报告生成器工厂实现 | Report generator factory implementation
/// 根据输出格式创建对应的报告生成器实例
/// Creates report generator instances based on output format
/// </summary>
public class ReportGeneratorFactory : IReportGeneratorFactory
{
private readonly ILoggerService _logger;
private readonly IReportGenerator _pdfReportGenerator;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="logger">日志服务 | Logger service</param>
/// <param name="pdfReportGenerator">PDF 报告生成器 | PDF report generator</param>
public ReportGeneratorFactory(ILoggerService logger, IReportGenerator pdfReportGenerator)
{
_logger = logger?.ForModule<ReportGeneratorFactory>() ?? throw new ArgumentNullException(nameof(logger));
_pdfReportGenerator = pdfReportGenerator ?? throw new ArgumentNullException(nameof(pdfReportGenerator));
}
/// <summary>
/// 根据输出格式创建生成器 | Create generator by output format
/// 当前仅支持 PDF 格式,未来可扩展 Excel/CSV
/// Currently only supports PDF format, extensible for Excel/CSV in the future
/// </summary>
/// <param name="format">输出格式 | Output format</param>
/// <returns>对应格式的报告生成器 | Report generator for the specified format</returns>
/// <exception cref="NotSupportedException">当请求不支持的格式时抛出 | Thrown when unsupported format is requested</exception>
public IReportGenerator Create(ReportOutputFormat format)
{
_logger.Info("创建报告生成器,格式: {Format} | Creating report generator, format: {Format}", format);
switch (format)
{
case ReportOutputFormat.Pdf:
return _pdfReportGenerator;
default:
var message = $"不支持的报告输出格式: {format} | Unsupported report output format: {format}";
_logger.Error(null, message);
throw new NotSupportedException(message);
}
}
}
}
@@ -0,0 +1,80 @@
using System;
namespace XP.ReportEngine.Services
{
/// <summary>
/// 报告编号生成器 | Report ID generator
/// 生成格式为 RPT-yyyyMMdd-NNN 的唯一报告编号
/// Generates unique report IDs in format: RPT-yyyyMMdd-NNN
/// 线程安全,每日自动重置计数器
/// Thread-safe with daily counter reset
/// </summary>
public class ReportIdGenerator
{
private readonly object _lock = new();
private int _dailyCounter;
private string _currentDate;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public ReportIdGenerator()
{
_currentDate = DateTime.Now.ToString("yyyyMMdd");
_dailyCounter = 0;
}
/// <summary>
/// 生成下一个唯一报告编号 | Generate next unique report ID
/// 格式:RPT-yyyyMMdd-NNN(如 RPT-20250101-001
/// Format: RPT-yyyyMMdd-NNN (e.g., RPT-20250101-001)
/// </summary>
/// <returns>唯一报告编号 | Unique report ID</returns>
public string GenerateNext()
{
lock (_lock)
{
var today = DateTime.Now.ToString("yyyyMMdd");
// 日期变更时重置计数器 | Reset counter when date changes
if (today != _currentDate)
{
_currentDate = today;
_dailyCounter = 0;
}
_dailyCounter++;
return $"RPT-{_currentDate}-{_dailyCounter:D3}";
}
}
/// <summary>
/// 根据指定时间戳生成报告编号 | Generate report ID with specified timestamp
/// 用于需要指定日期的场景(如补录报告)
/// Used for scenarios requiring specific dates (e.g., backfilling reports)
/// </summary>
/// <param name="timestamp">指定的时间戳 | Specified timestamp</param>
/// <returns>唯一报告编号 | Unique report ID</returns>
public string GenerateForDate(DateTime timestamp)
{
lock (_lock)
{
var dateStr = timestamp.ToString("yyyyMMdd");
// 如果指定日期与当前日期相同,使用当前计数器
// If specified date matches current date, use current counter
if (dateStr == _currentDate)
{
_dailyCounter++;
return $"RPT-{_currentDate}-{_dailyCounter:D3}";
}
// 如果指定日期与当前日期不同,更新日期并重置计数器
// If specified date differs from current date, update date and reset counter
_currentDate = dateStr;
_dailyCounter = 1;
return $"RPT-{_currentDate}-{_dailyCounter:D3}";
}
}
}
}
@@ -0,0 +1,115 @@
{
"document": {
"pageSize": "A4",
"orientation": "Portrait",
"margins": { "top": 20, "bottom": 20, "left": 20, "right": 20 }
},
"pages": [
{
"type": "homepage",
"elements": [
{ "type": "text", "content": "${loc:Report_Title}", "style": "title", "position": [60, 40], "positioning": "absolute" },
{ "type": "divider", "position": [0, 60], "size": [170, 1], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Id}${metadata.reportId}", "style": "body", "position": [0, 75], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Date}${formatDate(metadata.inspectionDate)}", "style": "body", "position": [0, 85], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Sample}${metadata.sampleName}", "style": "body", "position": [0, 95], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Operator}${metadata.operatorName}", "style": "body", "position": [0, 105], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Description}${metadata.description}", "style": "body", "position": [0, 115], "positioning": "absolute" },
{ "type": "divider", "position": [0, 130], "size": [170, 1], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Summary}", "style": "heading", "position": [0, 140], "positioning": "absolute" },
{
"type": "table", "dataKey": "summaryTable", "position": [0, 150], "size": [170, 60], "positioning": "absolute",
"columns": [
{ "header": "${loc:Field_InspectionType}", "field": "inspectionType", "width": 50, "align": "left" },
{ "header": "${loc:Field_Result}", "field": "classification", "width": 30, "align": "center" },
{ "header": "${loc:Field_Status}", "field": "status", "width": 30, "align": "center" }
]
}
]
},
{
"type": "metricData",
"elements": [
{ "type": "text", "content": "${loc:Page_MetricData}", "style": "heading", "positioning": "flow" },
{ "type": "divider", "size": [170, 1], "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_Type}${measurementType}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Field_Point1}${point1}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Field_Point2}${point2}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_Distance}${actualDistance} ${unit}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_Angle}${angle}°", "style": "body", "positioning": "flow" },
{ "type": "image", "dataKey": "lineMeasurementImage", "size": [150, 100], "border": true, "positioning": "flow" }
]
},
{
"type": "bgaInspection",
"elements": [
{ "type": "text", "content": "${loc:Page_BgaInspection}", "style": "heading", "positioning": "flow" },
{ "type": "divider", "size": [170, 1], "positioning": "flow" },
{ "type": "text", "content": "${loc:Bga_Count}${bgaCount}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_FillRate}${formatPercent(fillRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Bga_TotalArea}${formatNumber(totalBgaArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Bga_TotalVoidArea}${formatNumber(totalVoidArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Bga_VoidLimit}${formatPercent(voidLimit)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "bodyBold", "positioning": "flow" },
{ "type": "image", "dataKey": "bgaInspectionImage", "size": [150, 100], "border": true, "positioning": "flow" },
{
"type": "table", "dataKey": "bgaBallsTable", "positioning": "flow", "size": [170, 0],
"columns": [
{ "header": "${loc:Bga_BallIndex}", "field": "index", "width": 25, "align": "center" },
{ "header": "${loc:Table_VoidRate}", "field": "voidRate", "width": 40, "align": "center" },
{ "header": "${loc:Table_Area}", "field": "area", "width": 40, "align": "center" },
{ "header": "${loc:Table_Classification}", "field": "classification", "width": 35, "align": "center" }
]
}
]
},
{
"type": "voidInspection",
"elements": [
{ "type": "text", "content": "${loc:Page_VoidInspection}", "style": "heading", "positioning": "flow" },
{ "type": "divider", "size": [170, 1], "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_RoiArea}${formatNumber(roiArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_TotalArea}${formatNumber(totalVoidArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_Limit}${formatPercent(voidLimit)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_Count}${voidCount}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_MaxArea}${formatNumber(maxVoidArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "bodyBold", "positioning": "flow" },
{ "type": "image", "dataKey": "voidInspectionImage", "size": [150, 100], "border": true, "positioning": "flow" },
{
"type": "table", "dataKey": "voidsTable", "positioning": "flow", "size": [170, 0],
"columns": [
{ "header": "${loc:Table_Index}", "field": "index", "width": 20, "align": "center" },
{ "header": "${loc:Table_Area}", "field": "area", "width": 35, "align": "center" },
{ "header": "${loc:Table_AreaPercent}", "field": "areaPercent", "width": 35, "align": "center" },
{ "header": "${loc:Table_CenterX}", "field": "centerX", "width": 30, "align": "center" },
{ "header": "${loc:Table_CenterY}", "field": "centerY", "width": 30, "align": "center" }
]
}
]
},
{
"type": "viaFillInspection",
"elements": [
{ "type": "text", "content": "${loc:Page_ViaFillInspection}", "style": "heading", "positioning": "flow" },
{ "type": "divider", "size": [170, 1], "positioning": "flow" },
{ "type": "text", "content": "${loc:Fill_Rate}${formatPercent(fillRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Fill_FullDistance}${formatNumber(fullDistance, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Fill_FillDistance}${formatNumber(fillDistance, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Fill_THTLimit}${formatPercent(thtLimit)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "bodyBold", "positioning": "flow" },
{ "type": "image", "dataKey": "viaFillImage", "size": [150, 100], "border": true, "positioning": "flow" }
]
}
],
"styles": {
"title": { "font": "auto", "size": 24, "bold": true, "italic": false, "color": "#1a1a1a", "align": "center" },
"heading": { "font": "auto", "size": 16, "bold": true, "italic": false, "color": "#333333", "align": "left" },
"body": { "font": "auto", "size": 11, "bold": false, "italic": false, "color": "#333333", "align": "left" },
"bodyBold": { "font": "auto", "size": 11, "bold": true, "italic": false, "color": "#1a1a1a", "align": "left" },
"tableHeader": { "font": "auto", "size": 10, "bold": true, "italic": false, "color": "#ffffff", "align": "center", "backgroundColor": "#4472C4" },
"tableCell": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center" }
}
}
+6
View File
@@ -22,4 +22,10 @@
<EmbeddedResource Include="Fonts\*.otf" /> <EmbeddedResource Include="Fonts\*.otf" />
<EmbeddedResource Include="Fonts\*.ttf" /> <EmbeddedResource Include="Fonts\*.ttf" />
</ItemGroup> </ItemGroup>
<!-- 模板文件复制到输出目录 | Copy template files to output directory -->
<ItemGroup>
<Content Include="Templates\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project> </Project>