diff --git a/XP.ReportEngine/ReportEngineModule.cs b/XP.ReportEngine/ReportEngineModule.cs index 1665670..aae3f36 100644 --- a/XP.ReportEngine/ReportEngineModule.cs +++ b/XP.ReportEngine/ReportEngineModule.cs @@ -1,19 +1,71 @@ using Prism.Ioc; 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 { + /// + /// 报告引擎模块 | Report Engine Module + /// Prism 模块入口,注册报告生成相关服务到 DI 容器 + /// Prism module entry, registers report generation services to DI container + /// + [Module(ModuleName = "ReportEngineModule")] public class ReportEngineModule : IModule { + /// + /// 模块初始化 | Module initialization + /// 注册模块级多语言资源源 | Register module-level localization resource source + /// public void OnInitialized(IContainerProvider containerProvider) { + // 注册模块级多语言资源到 Fallback Chain | Register module-level localization resources to Fallback Chain + var localizationService = containerProvider.Resolve(); + 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"); } + /// + /// 注册类型到 DI 容器 | Register types to DI container + /// public void RegisterTypes(IContainerRegistry containerRegistry) { + // 注册报告生成器(瞬态)| Register report generator (transient) + containerRegistry.Register(); + // 注册报告生成器工厂(单例)| Register report generator factory (singleton) + containerRegistry.RegisterSingleton(); + + // 注册模板引擎(瞬态)| Register template engine (transient) + containerRegistry.Register(); + + // 注册数据绑定器(瞬态)| Register data binder (transient) + containerRegistry.Register(); + + // 注册排版引擎(瞬态)| Register layout engine (transient) + containerRegistry.Register(); + + // 注册 PDF 渲染器(瞬态)| Register PDF renderer (transient) + containerRegistry.Register(); + + // 注册数据适配器(瞬态)| Register data adapter (transient) + containerRegistry.Register(); + + // 注册报告编号生成器(单例,维护每日计数器状态)| Register report ID generator (singleton, maintains daily counter state) + containerRegistry.RegisterSingleton(); + + System.Console.WriteLine("[ReportEngineModule] 类型注册完成 | Type registration completed"); } } -} \ No newline at end of file +} diff --git a/XP.ReportEngine/Resources/Resources.en-US.resx b/XP.ReportEngine/Resources/Resources.en-US.resx index 459ff73..68d1475 100644 --- a/XP.ReportEngine/Resources/Resources.en-US.resx +++ b/XP.ReportEngine/Resources/Resources.en-US.resx @@ -58,4 +58,184 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Inspection Report + + + Inspection Date + + + Sample Name + + + Operator + + + Inspection Summary + + + Report ID + + + Description + + + + PASS + + + FAIL + + + + Line Measurement + + + BGA Void Rate + + + Void Measurement + + + Via Fill Rate + + + + No. + + + Void Rate + + + Classification + + + Area + + + Area % + + + Center X + + + Center Y + + + + Measurement Type + + + Distance + + + Unit + + + Angle + + + Fill Rate + + + Void Rate + + + Limit + + + + Total Defects + + + Pass Count + + + Fail Count + + + Overall Result + + + + No Image + + + + Inspection Report + + + Measurement Data + + + BGA Solder Ball Inspection + + + Void Inspection + + + Via Fill Rate Inspection + + + + Ball Count + + + Total Ball Area + + + Total Void Area + + + Void Rate Limit + + + Ball No. + + + + ROI Area + + + Total Void Area + + + Void Count + + + Max Void Area + + + Void Rate Limit + + + + Fill Rate + + + Full Distance + + + Fill Distance + + + THT Limit + + + + Point 1 + + + Point 2 + + + Result + + + Inspection Type + + + Status + diff --git a/XP.ReportEngine/Resources/Resources.resx b/XP.ReportEngine/Resources/Resources.resx index 44e9f97..e248a76 100644 --- a/XP.ReportEngine/Resources/Resources.resx +++ b/XP.ReportEngine/Resources/Resources.resx @@ -1,64 +1,5 @@ - @@ -117,4 +58,184 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 检测报告 + + + 检测日期 + + + 样品名称 + + + 操作员 + + + 检测摘要 + + + 报告编号 + + + 描述 + + + + 通过 + + + 不通过 + + + + 距离测量 + + + BGA 气泡率 + + + 空隙测量 + + + 通孔填锡率 + + + + 序号 + + + 气泡率 + + + 分类结果 + + + 面积 + + + 面积百分比 + + + 中心 X + + + 中心 Y + + + + 测量类型 + + + 距离 + + + 单位 + + + 角度 + + + 填锡率 + + + 气泡率 + + + 限值 + + + + 总缺陷数 + + + 通过数量 + + + 不通过数量 + + + 总体结果 + + + + 无图像 + + + + 检测报告首页 + + + 测量数据 + + + BGA 焊球检测 + + + 空隙检测 + + + 通孔填锡检测 + + + + 焊球数量 + + + 焊球总面积 + + + 气泡总面积 + + + 气泡率限值 + + + 焊球序号 + + + + ROI 面积 + + + 空隙总面积 + + + 空隙数量 + + + 最大空隙面积 + + + 空隙率限值 + + + + 填锡率 + + + 满填距离 + + + 填充距离 + + + THT 限值 + + + + 起点 + + + 终点 + + + 结果 + + + 检测类型 + + + 状态 + diff --git a/XP.ReportEngine/Resources/Resources.zh-CN.resx b/XP.ReportEngine/Resources/Resources.zh-CN.resx index 459ff73..e248a76 100644 --- a/XP.ReportEngine/Resources/Resources.zh-CN.resx +++ b/XP.ReportEngine/Resources/Resources.zh-CN.resx @@ -58,4 +58,184 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 检测报告 + + + 检测日期 + + + 样品名称 + + + 操作员 + + + 检测摘要 + + + 报告编号 + + + 描述 + + + + 通过 + + + 不通过 + + + + 距离测量 + + + BGA 气泡率 + + + 空隙测量 + + + 通孔填锡率 + + + + 序号 + + + 气泡率 + + + 分类结果 + + + 面积 + + + 面积百分比 + + + 中心 X + + + 中心 Y + + + + 测量类型 + + + 距离 + + + 单位 + + + 角度 + + + 填锡率 + + + 气泡率 + + + 限值 + + + + 总缺陷数 + + + 通过数量 + + + 不通过数量 + + + 总体结果 + + + + 无图像 + + + + 检测报告首页 + + + 测量数据 + + + BGA 焊球检测 + + + 空隙检测 + + + 通孔填锡检测 + + + + 焊球数量 + + + 焊球总面积 + + + 气泡总面积 + + + 气泡率限值 + + + 焊球序号 + + + + ROI 面积 + + + 空隙总面积 + + + 空隙数量 + + + 最大空隙面积 + + + 空隙率限值 + + + + 填锡率 + + + 满填距离 + + + 填充距离 + + + THT 限值 + + + + 起点 + + + 终点 + + + 结果 + + + 检测类型 + + + 状态 + diff --git a/XP.ReportEngine/Resources/Resources.zh-TW.resx b/XP.ReportEngine/Resources/Resources.zh-TW.resx index 459ff73..0e12d4d 100644 --- a/XP.ReportEngine/Resources/Resources.zh-TW.resx +++ b/XP.ReportEngine/Resources/Resources.zh-TW.resx @@ -58,4 +58,184 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 檢測報告 + + + 檢測日期 + + + 樣品名稱 + + + 操作員 + + + 檢測摘要 + + + 報告編號 + + + 描述 + + + + 通過 + + + 不通過 + + + + 距離測量 + + + BGA 氣泡率 + + + 空隙測量 + + + 通孔填錫率 + + + + 序號 + + + 氣泡率 + + + 分類結果 + + + 面積 + + + 面積百分比 + + + 中心 X + + + 中心 Y + + + + 測量類型 + + + 距離 + + + 單位 + + + 角度 + + + 填錫率 + + + 氣泡率 + + + 限值 + + + + 總缺陷數 + + + 通過數量 + + + 不通過數量 + + + 總體結果 + + + + 無圖像 + + + + 檢測報告首頁 + + + 測量數據 + + + BGA 焊球檢測 + + + 空隙檢測 + + + 通孔填錫檢測 + + + + 焊球數量 + + + 焊球總面積 + + + 氣泡總面積 + + + 氣泡率限值 + + + 焊球序號 + + + + ROI 面積 + + + 空隙總面積 + + + 空隙數量 + + + 最大空隙面積 + + + 空隙率限值 + + + + 填錫率 + + + 滿填距離 + + + 填充距離 + + + THT 限值 + + + + 起點 + + + 終點 + + + 結果 + + + 檢測類型 + + + 狀態 + diff --git a/XP.ReportEngine/Services/PdfReportGenerator.cs b/XP.ReportEngine/Services/PdfReportGenerator.cs new file mode 100644 index 0000000..9eba04f --- /dev/null +++ b/XP.ReportEngine/Services/PdfReportGenerator.cs @@ -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 +{ + /// + /// PDF 报告生成器实现 | PDF report generator implementation + /// 协调管线各阶段:模板加载 → 数据绑定 → 排版 → 渲染 → 保存 + /// Orchestrates pipeline phases: template loading → data binding → layout → rendering → saving + /// + public class PdfReportGenerator : IReportGenerator + { + private readonly ILoggerService _logger; + private readonly ITemplateEngine _templateEngine; + private readonly IDataBinder _dataBinder; + private readonly ILayoutEngine _layoutEngine; + private readonly IPdfRenderer _pdfRenderer; + + /// + /// 构造函数 | Constructor + /// + /// 日志服务 | Logger service + /// 模板引擎 | Template engine + /// 数据绑定器 | Data binder + /// 排版引擎 | Layout engine + /// PDF 渲染器 | PDF renderer + public PdfReportGenerator( + ILoggerService logger, + ITemplateEngine templateEngine, + IDataBinder dataBinder, + ILayoutEngine layoutEngine, + IPdfRenderer pdfRenderer) + { + _logger = logger?.ForModule() ?? 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)); + } + + /// + /// 异步生成 PDF 报告 | Generate PDF report asynchronously + /// 执行完整管线:模板加载 → 验证 → 数据绑定 → 排版计算 → PDF 渲染 → 文件保存(可选) + /// Executes full pipeline: template load → validate → data bind → layout → PDF render → save (optional) + /// + /// 报告上下文数据 | Report context data + /// 生成选项 | Generation options + /// 生成结果 | Generation result + public async Task 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); + + // 阶段 4:PDF 渲染 | Phase 4: PDF rendering + _logger.Info("阶段 4:PDF 渲染 | 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); + } + } + + /// + /// 将 MemoryStream 保存到文件 | Save MemoryStream to file + /// + /// PDF 内存流 | PDF memory stream + /// 输出文件路径 | Output file path + 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; + } + } +} diff --git a/XP.ReportEngine/Services/ProcessorDataAdapter.cs b/XP.ReportEngine/Services/ProcessorDataAdapter.cs new file mode 100644 index 0000000..b525767 --- /dev/null +++ b/XP.ReportEngine/Services/ProcessorDataAdapter.cs @@ -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 +{ + /// + /// 处理器数据适配器实现 | Processor data adapter implementation + /// 将 XP.ImageProcessing 的 ProcessorOutput 转换为 ReportContext + /// Converts XP.ImageProcessing ProcessorOutput to ReportContext + /// + 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() ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 将处理器输出数据适配为报告上下文 | Adapt processor output data to report context + /// + public ReportContext Adapt(List 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(), + Images = new Dictionary(), + Properties = new Dictionary() + }; + + // 多处理器输出聚合逻辑:每个 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; + } + + /// + /// 根据处理器类型分发适配逻辑 | Dispatch adaptation logic by processor type + /// + 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 + + /// + /// 适配线测量处理器输出 | Adapt line measurement processor output + /// 提取 MeasurementType、Point1、Point2、PixelDistance、ActualDistance、Unit、Angle + /// + private InspectionResultGroup AdaptLineMeasurement(ProcessorOutput output, string sourceId) + { + _logger.Debug("适配 LineMeasurementProcessor 输出,SourceId: {SourceId} | Adapting LineMeasurementProcessor output, SourceId: {SourceId}", sourceId); + + var data = output.OutputData ?? new Dictionary(); + var group = new InspectionResultGroup + { + ProcessorType = LineMeasurementProcessor, + SourceId = sourceId, + Classification = string.Empty, + Data = new Dictionary + { + ["measurementType"] = GetValueOrDefault(data, "MeasurementType", string.Empty), + ["point1"] = GetValueOrDefault(data, "Point1", null), + ["point2"] = GetValueOrDefault(data, "Point2", null), + ["pixelDistance"] = GetValueOrDefault(data, "PixelDistance", 0.0), + ["actualDistance"] = GetValueOrDefault(data, "ActualDistance", 0.0), + ["unit"] = GetValueOrDefault(data, "Unit", string.Empty), + ["angle"] = GetValueOrDefault(data, "Angle", 0.0) + }, + TableRows = new List>() + }; + + return group; + } + + #endregion + + #region BgaVoidRateProcessor 适配 | BgaVoidRateProcessor Adaptation + + /// + /// 适配 BGA 气泡率处理器输出 | Adapt BGA void rate processor output + /// 提取 BgaCount、BgaBalls 列表转 TableRows、VoidRate、FillRate、TotalBgaArea、TotalVoidArea、Classification、VoidLimit + /// + private InspectionResultGroup AdaptBgaVoidRate(ProcessorOutput output, string sourceId) + { + _logger.Debug("适配 BgaVoidRateProcessor 输出,SourceId: {SourceId} | Adapting BgaVoidRateProcessor output, SourceId: {SourceId}", sourceId); + + var data = output.OutputData ?? new Dictionary(); + var group = new InspectionResultGroup + { + ProcessorType = BgaVoidRateProcessor, + SourceId = sourceId, + Classification = GetValueOrDefault(data, "Classification", string.Empty), + Data = new Dictionary + { + ["bgaCount"] = GetValueOrDefault(data, "BgaCount", 0), + ["voidRate"] = GetValueOrDefault(data, "VoidRate", 0.0), + ["fillRate"] = GetValueOrDefault(data, "FillRate", 0.0), + ["totalBgaArea"] = GetValueOrDefault(data, "TotalBgaArea", 0.0), + ["totalVoidArea"] = GetValueOrDefault(data, "TotalVoidArea", 0.0), + ["voidLimit"] = GetValueOrDefault(data, "VoidLimit", 0.0) + }, + TableRows = ConvertBgaBallsToTableRows(data) + }; + + return group; + } + + /// + /// 将 BgaBalls 列表转换为表格行 | Convert BgaBalls list to table rows + /// 每个焊球一行,包含 index、voidRate、classification + /// + private List> ConvertBgaBallsToTableRows(Dictionary data) + { + var tableRows = new List>(); + 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 + { + ["index"] = i + 1, + ["voidRate"] = GetNestedValue(ball, "VoidRate", 0.0), + ["classification"] = GetNestedValue(ball, "Classification", string.Empty) + }; + tableRows.Add(row); + } + + return tableRows; + } + + #endregion + + #region VoidMeasurementProcessor 适配 | VoidMeasurementProcessor Adaptation + + /// + /// 适配空隙测量处理器输出 | Adapt void measurement processor output + /// 提取 RoiArea、TotalVoidArea、VoidRate、VoidLimit、VoidCount、MaxVoidArea、Classification、Voids 列表转 TableRows + /// + private InspectionResultGroup AdaptVoidMeasurement(ProcessorOutput output, string sourceId) + { + _logger.Debug("适配 VoidMeasurementProcessor 输出,SourceId: {SourceId} | Adapting VoidMeasurementProcessor output, SourceId: {SourceId}", sourceId); + + var data = output.OutputData ?? new Dictionary(); + var group = new InspectionResultGroup + { + ProcessorType = VoidMeasurementProcessor, + SourceId = sourceId, + Classification = GetValueOrDefault(data, "Classification", string.Empty), + Data = new Dictionary + { + ["roiArea"] = GetValueOrDefault(data, "RoiArea", 0.0), + ["totalVoidArea"] = GetValueOrDefault(data, "TotalVoidArea", 0.0), + ["voidRate"] = GetValueOrDefault(data, "VoidRate", 0.0), + ["voidLimit"] = GetValueOrDefault(data, "VoidLimit", 0.0), + ["voidCount"] = GetValueOrDefault(data, "VoidCount", 0), + ["maxVoidArea"] = GetValueOrDefault(data, "MaxVoidArea", 0.0) + }, + TableRows = ConvertVoidsToTableRows(data) + }; + + return group; + } + + /// + /// 将 Voids 列表转换为表格行 | Convert Voids list to table rows + /// 每个空隙一行,包含 index、area、areaPercent、centerX、centerY + /// + private List> ConvertVoidsToTableRows(Dictionary data) + { + var tableRows = new List>(); + 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 + { + ["index"] = i + 1, + ["area"] = GetNestedValue(voidItem, "Area", 0.0), + ["areaPercent"] = GetNestedValue(voidItem, "AreaPercent", 0.0), + ["centerX"] = GetNestedValue(voidItem, "CenterX", 0.0), + ["centerY"] = GetNestedValue(voidItem, "CenterY", 0.0) + }; + tableRows.Add(row); + } + + return tableRows; + } + + #endregion + + #region FillRateProcessor 适配 | FillRateProcessor Adaptation + + /// + /// 适配填锡率处理器输出 | Adapt fill rate processor output + /// 提取 FillRate、VoidRate、FullDistance、FillDistance、THTLimit、Classification、E1-E4 椭圆几何数据 + /// + private InspectionResultGroup AdaptFillRate(ProcessorOutput output, string sourceId) + { + _logger.Debug("适配 FillRateProcessor 输出,SourceId: {SourceId} | Adapting FillRateProcessor output, SourceId: {SourceId}", sourceId); + + var data = output.OutputData ?? new Dictionary(); + var group = new InspectionResultGroup + { + ProcessorType = FillRateProcessor, + SourceId = sourceId, + Classification = GetValueOrDefault(data, "Classification", string.Empty), + Data = new Dictionary + { + ["fillRate"] = GetValueOrDefault(data, "FillRate", 0.0), + ["voidRate"] = GetValueOrDefault(data, "VoidRate", 0.0), + ["fullDistance"] = GetValueOrDefault(data, "FullDistance", 0.0), + ["fillDistance"] = GetValueOrDefault(data, "FillDistance", 0.0), + ["thtLimit"] = GetValueOrDefault(data, "THTLimit", 0.0), + ["e1"] = GetValueOrDefault(data, "E1", null), + ["e2"] = GetValueOrDefault(data, "E2", null), + ["e3"] = GetValueOrDefault(data, "E3", null), + ["e4"] = GetValueOrDefault(data, "E4", null) + }, + TableRows = new List>() + }; + + return group; + } + + #endregion + + #region 通用适配 | Generic Adaptation + + /// + /// 通用处理器适配(未知类型)| Generic processor adaptation (unknown type) + /// 将所有 OutputData 键值对直接映射到 Data 字典 + /// + 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(); + var group = new InspectionResultGroup + { + ProcessorType = output.ProcessorType ?? "Unknown", + SourceId = sourceId, + Classification = GetValueOrDefault(data, "Classification", string.Empty), + Data = new Dictionary(), + TableRows = new List>() + }; + + // 将所有键值对转为小驼峰命名映射 | 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 + + /// + /// 从字典中获取值,缺失时使用默认值并记录警告 | Get value from dictionary, use default and log warning if missing + /// + private T GetValueOrDefault(Dictionary 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(value); + } + catch (Exception ex) + { + _logger.Warn("键 {Key} 的值类型转换失败: {Message},使用默认值 | Value type conversion failed for key {Key}: {Message}, using default", key, ex.Message); + return defaultValue; + } + } + + /// + /// 从字典中获取列表值 | Get list value from dictionary + /// + private IList GetListValue(Dictionary 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; + } + + /// + /// 从嵌套对象中获取属性值 | Get property value from nested object + /// + private T GetNestedValue(object obj, string propertyName, T defaultValue) + { + if (obj == null) return defaultValue; + + // 字典访问 | Dictionary access + if (obj is IDictionary dict) + { + if (dict.TryGetValue(propertyName, out var dictValue) && dictValue != null) + { + try + { + return ConvertValue(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(value); + } + catch + { + return defaultValue; + } + } + + return defaultValue; + } + + /// + /// 类型转换辅助方法 | Type conversion helper + /// + private T ConvertValue(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); + } + + /// + /// 将 PascalCase 转换为 camelCase | Convert PascalCase to camelCase + /// + 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 + } +} diff --git a/XP.ReportEngine/Services/ReportGeneratorFactory.cs b/XP.ReportEngine/Services/ReportGeneratorFactory.cs new file mode 100644 index 0000000..c27623f --- /dev/null +++ b/XP.ReportEngine/Services/ReportGeneratorFactory.cs @@ -0,0 +1,53 @@ +using System; +using XP.Common.Logging.Interfaces; +using XP.ReportEngine.Interfaces; +using XP.ReportEngine.Models; + +namespace XP.ReportEngine.Services +{ + /// + /// 报告生成器工厂实现 | Report generator factory implementation + /// 根据输出格式创建对应的报告生成器实例 + /// Creates report generator instances based on output format + /// + public class ReportGeneratorFactory : IReportGeneratorFactory + { + private readonly ILoggerService _logger; + private readonly IReportGenerator _pdfReportGenerator; + + /// + /// 构造函数 | Constructor + /// + /// 日志服务 | Logger service + /// PDF 报告生成器 | PDF report generator + public ReportGeneratorFactory(ILoggerService logger, IReportGenerator pdfReportGenerator) + { + _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); + _pdfReportGenerator = pdfReportGenerator ?? throw new ArgumentNullException(nameof(pdfReportGenerator)); + } + + /// + /// 根据输出格式创建生成器 | Create generator by output format + /// 当前仅支持 PDF 格式,未来可扩展 Excel/CSV + /// Currently only supports PDF format, extensible for Excel/CSV in the future + /// + /// 输出格式 | Output format + /// 对应格式的报告生成器 | Report generator for the specified format + /// 当请求不支持的格式时抛出 | Thrown when unsupported format is requested + 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); + } + } + } +} diff --git a/XP.ReportEngine/Services/ReportIdGenerator.cs b/XP.ReportEngine/Services/ReportIdGenerator.cs new file mode 100644 index 0000000..4454246 --- /dev/null +++ b/XP.ReportEngine/Services/ReportIdGenerator.cs @@ -0,0 +1,80 @@ +using System; + +namespace XP.ReportEngine.Services +{ + /// + /// 报告编号生成器 | Report ID generator + /// 生成格式为 RPT-yyyyMMdd-NNN 的唯一报告编号 + /// Generates unique report IDs in format: RPT-yyyyMMdd-NNN + /// 线程安全,每日自动重置计数器 + /// Thread-safe with daily counter reset + /// + public class ReportIdGenerator + { + private readonly object _lock = new(); + private int _dailyCounter; + private string _currentDate; + + /// + /// 构造函数 | Constructor + /// + public ReportIdGenerator() + { + _currentDate = DateTime.Now.ToString("yyyyMMdd"); + _dailyCounter = 0; + } + + /// + /// 生成下一个唯一报告编号 | Generate next unique report ID + /// 格式:RPT-yyyyMMdd-NNN(如 RPT-20250101-001) + /// Format: RPT-yyyyMMdd-NNN (e.g., RPT-20250101-001) + /// + /// 唯一报告编号 | Unique report ID + 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}"; + } + } + + /// + /// 根据指定时间戳生成报告编号 | Generate report ID with specified timestamp + /// 用于需要指定日期的场景(如补录报告) + /// Used for scenarios requiring specific dates (e.g., backfilling reports) + /// + /// 指定的时间戳 | Specified timestamp + /// 唯一报告编号 | Unique report ID + 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}"; + } + } + } +} diff --git a/XP.ReportEngine/Templates/StandardReportTemplate.json b/XP.ReportEngine/Templates/StandardReportTemplate.json new file mode 100644 index 0000000..414249b --- /dev/null +++ b/XP.ReportEngine/Templates/StandardReportTemplate.json @@ -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" } + } +} diff --git a/XP.ReportEngine/XP.ReportEngine.csproj b/XP.ReportEngine/XP.ReportEngine.csproj index f26f2a6..4e322d0 100644 --- a/XP.ReportEngine/XP.ReportEngine.csproj +++ b/XP.ReportEngine/XP.ReportEngine.csproj @@ -22,4 +22,10 @@ + + + + PreserveNewest + +