Files
XplorePlane/XP.ReportEngine/Services/ReportService.cs
T

391 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XP.ReportEngine.Configs;
using XP.ReportEngine.Interfaces;
using XP.ReportEngine.Models;
namespace XP.ReportEngine.Services
{
/// <summary>
/// 报告服务实现(门面)| Report service implementation (Facade)
/// 协调报告生成的完整流程,将 UI 无关的业务逻辑封装为可复用服务
/// Orchestrates the complete report generation workflow, encapsulating UI-independent business logic as a reusable service
/// </summary>
public class ReportService : IReportService
{
private readonly IReportGenerator _reportGenerator;
private readonly IReportDataAdapter _dataAdapter;
private readonly ILoggerService _logger;
private readonly ReportIdGenerator _reportIdGenerator;
private readonly ReportConfig _reportConfig;
/// <summary>
/// 生成互斥锁,防止并发渲染导致 iText7 字体对象跨文档引用错误
/// Generation mutex to prevent concurrent rendering causing iText7 cross-document font reference errors
/// </summary>
private readonly SemaphoreSlim _generateLock = new(1, 1);
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="reportGenerator">报告生成器 | Report generator</param>
/// <param name="dataAdapter">数据适配器 | Data adapter</param>
/// <param name="logger">日志服务 | Logger service</param>
/// <param name="reportIdGenerator">报告编号生成器 | Report ID generator</param>
/// <param name="reportConfig">报告配置 | Report config</param>
public ReportService(
IReportGenerator reportGenerator,
IReportDataAdapter dataAdapter,
ILoggerService logger,
ReportIdGenerator reportIdGenerator,
ReportConfig reportConfig)
{
_reportGenerator = reportGenerator ?? throw new ArgumentNullException(nameof(reportGenerator));
_dataAdapter = dataAdapter ?? throw new ArgumentNullException(nameof(dataAdapter));
_logger = logger?.ForModule<ReportService>() ?? throw new ArgumentNullException(nameof(logger));
_reportIdGenerator = reportIdGenerator ?? throw new ArgumentNullException(nameof(reportIdGenerator));
_reportConfig = reportConfig ?? throw new ArgumentNullException(nameof(reportConfig));
}
/// <summary>
/// 生成报告 | Generate report
/// </summary>
public async Task<ReportServiceResult> GenerateAsync(ReportRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
// 获取互斥锁,防止并发渲染 | Acquire mutex to prevent concurrent rendering
await _generateLock.WaitAsync();
try
{
_logger.Info("报告服务开始生成 | Report service starting generation");
// 步骤 1:生成报告编号 | Step 1: Generate report ID
var reportId = GenerateReportId(request);
_logger.Info("报告编号: {ReportId} | Report ID: {ReportId}", reportId);
// 步骤 2:确定输出路径 | Step 2: Determine output path
var outputPath = ResolveOutputPath(request, reportId);
_logger.Info("输出路径: {Path} | Output path: {Path}", outputPath);
// 步骤 3:数据适配 | Step 3: Data adaptation
var metadata = request.Metadata ?? new ReportMetadata();
if (string.IsNullOrEmpty(metadata.ReportId))
{
metadata.ReportId = reportId;
}
if (metadata.InspectionDate == default)
{
metadata.InspectionDate = DateTime.Now;
}
var context = _dataAdapter.Adapt(request.ProcessorOutputs ?? new List<ProcessorOutput>(), metadata);
_logger.Info("数据适配完成 | Data adaptation completed");
// 步骤 4:注入额外图像 | Step 4: Inject additional images
InjectAdditionalImages(context, request);
// 步骤 5:注入配置中的 Logo 和公司信息 | Step 5: Inject logo and company info from config
InjectConfigData(context);
// 步骤 6:注入自定义属性 | Step 6: Inject custom properties
InjectCustomProperties(context, request);
// 步骤 7:生成首页汇总表 | Step 7: Generate homepage summary table
context.Properties["summaryTable"] = CreateSummaryTableData(context);
// 步骤 8:调用管线生成 PDF | Step 8: Call pipeline to generate PDF
var templatePath = _reportConfig.GetResolvedTemplatePath();
var options = new ReportGenerationOptions
{
TemplatePath = templatePath,
OutputFilePath = outputPath,
Format = ReportOutputFormat.Pdf
};
var genResult = await _reportGenerator.GenerateAsync(context, options);
// 步骤 9:处理结果 | Step 9: Handle result
if (genResult.IsSuccess)
{
_logger.Info("报告生成成功: {Path} | Report generated successfully: {Path}", outputPath);
return ReportServiceResult.Success(outputPath, reportId);
}
else
{
_logger.Error(null, "报告生成失败: {Message} | Report generation failed: {Message}", genResult.ErrorMessage);
return ReportServiceResult.Failure(genResult.ErrorMessage, genResult.Exception);
}
}
catch (Exception ex)
{
_logger.Error(ex, "报告服务异常: {Message} | Report service exception: {Message}", ex.Message);
return ReportServiceResult.Failure($"报告生成过程中发生异常: {ex.Message}", ex);
}
finally
{
_generateLock.Release();
}
}
/// <summary>
/// 预热报告引擎 | Warm up report engine
/// 通过生成一个最小化的空白 PDF 来触发所有一次性初始化:
/// iText7 程序集加载、BouncyCastle 注册、字体子系统初始化、JIT 编译
/// </summary>
public async Task WarmUpAsync()
{
// 获取互斥锁,确保预热与正式生成不并发 | Acquire mutex to ensure warm-up doesn't overlap with generation
await _generateLock.WaitAsync();
try
{
_logger.Info("报告引擎预热开始 | Report engine warm-up started");
await Task.Run(() =>
{
// 加载模板(触发 JSON 反序列化 JIT| Load template (triggers JSON deserialization JIT)
var templatePath = _reportConfig.GetResolvedTemplatePath();
if (!File.Exists(templatePath))
{
_logger.Warn("预热跳过:模板文件不存在 {Path} | Warm-up skipped: template not found {Path}", templatePath);
return;
}
// 构建最小上下文 | Build minimal context
var context = new ReportContext
{
Metadata = new ReportMetadata
{
ReportId = "WARMUP",
InspectionDate = DateTime.Now,
SampleName = "WarmUp",
OperatorName = "System"
},
ResultGroups = new List<InspectionResultGroup>(),
Images = new Dictionary<string, ImageData>(),
Properties = new Dictionary<string, object>
{
["summaryTable"] = new List<Dictionary<string, object>>()
}
};
var options = new ReportGenerationOptions
{
TemplatePath = templatePath,
OutputFilePath = null, // 不保存文件 | Don't save file
Format = ReportOutputFormat.Pdf
};
// 执行完整管线(触发 iText7 初始化 + 字体加载 + JIT)
// Execute full pipeline (triggers iText7 init + font loading + JIT)
var result = _reportGenerator.GenerateAsync(context, options).GetAwaiter().GetResult();
// 释放预热产生的流 | Dispose warm-up stream
result.PdfStream?.Dispose();
});
_logger.Info("报告引擎预热完成 | Report engine warm-up completed");
}
catch (Exception ex)
{
// 预热失败不影响正常功能 | Warm-up failure doesn't affect normal functionality
_logger.Warn("报告引擎预热失败(不影响正常使用)| Report engine warm-up failed (doesn't affect normal usage): {Message}", ex.Message);
}
finally
{
_generateLock.Release();
}
}
#region | Private Methods
/// <summary>
/// 生成报告编号 | Generate report ID
/// 优先使用请求中已有的 ReportId,否则自动生成
/// Prefer existing ReportId from request, otherwise auto-generate
/// </summary>
private string GenerateReportId(ReportRequest request)
{
// 如果 Metadata 中已有 ReportId,直接使用 | If Metadata already has ReportId, use it directly
if (request.Metadata != null && !string.IsNullOrEmpty(request.Metadata.ReportId))
{
return request.Metadata.ReportId;
}
// 如果 FileNameParameters 中已有 ReportId,直接使用 | If FileNameParameters already has ReportId, use it
if (request.FileNameParameters != null &&
request.FileNameParameters.TryGetValue("ReportId", out var existingId) &&
!string.IsNullOrEmpty(existingId))
{
return existingId;
}
// 自动生成 | Auto-generate
return _reportIdGenerator.GenerateNext();
}
/// <summary>
/// 解析输出文件路径 | Resolve output file path
/// </summary>
private string ResolveOutputPath(ReportRequest request, string reportId)
{
// 如果请求中指定了输出路径,直接使用 | If output path specified in request, use it directly
if (!string.IsNullOrEmpty(request.OutputFilePath))
{
// 确保目录存在 | Ensure directory exists
var dir = Path.GetDirectoryName(request.OutputFilePath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
return request.OutputFilePath;
}
// 使用配置和参数自动生成路径 | Auto-generate path using config and parameters
var fileNameParams = request.FileNameParameters ?? new Dictionary<string, string>();
// 确保 ReportId 在参数中 | Ensure ReportId is in parameters
if (!fileNameParams.ContainsKey("ReportId"))
{
fileNameParams["ReportId"] = reportId;
}
return _reportConfig.ResolveOutputFilePath(fileNameParams);
}
/// <summary>
/// 注入额外图像到上下文 | Inject additional images into context
/// </summary>
private void InjectAdditionalImages(ReportContext context, ReportRequest request)
{
if (request.AdditionalImages == null || request.AdditionalImages.Count == 0)
return;
foreach (var kvp in request.AdditionalImages)
{
if (kvp.Value != null)
{
context.Images[kvp.Key] = kvp.Value;
_logger.Debug("注入额外图像: {Key} | Injected additional image: {Key}", kvp.Key);
}
}
}
/// <summary>
/// 注入配置中的 Logo 和公司信息 | Inject logo and company info from config
/// </summary>
private void InjectConfigData(ReportContext context)
{
// 注入公司名称 | Inject company name
if (!string.IsNullOrEmpty(_reportConfig.CompanyName))
{
context.Properties["CompanyName"] = _reportConfig.CompanyName;
}
// 注入软件名称 | Inject software name
if (!string.IsNullOrEmpty(_reportConfig.SoftwareName))
{
context.Properties["SoftwareName"] = _reportConfig.SoftwareName;
}
// 注入公司 Logo | Inject company logo
if (!string.IsNullOrEmpty(_reportConfig.CompanyLogo))
{
var logoPath = Path.IsPathRooted(_reportConfig.CompanyLogo)
? _reportConfig.CompanyLogo
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _reportConfig.CompanyLogo);
if (File.Exists(logoPath))
{
context.Images["companyLogo"] = new ImageData
{
SourceType = ImageSourceType.FilePath,
FilePath = logoPath
};
_logger.Info("公司 Logo 已加载: {Path} | Company logo loaded: {Path}", logoPath);
}
else
{
_logger.Warn("公司 Logo 文件不存在: {Path} | Company logo file not found: {Path}", logoPath);
}
}
// 注入软件 Logo | Inject software logo
if (!string.IsNullOrEmpty(_reportConfig.SoftwareLogo))
{
var softwareLogoPath = Path.IsPathRooted(_reportConfig.SoftwareLogo)
? _reportConfig.SoftwareLogo
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _reportConfig.SoftwareLogo);
if (File.Exists(softwareLogoPath))
{
context.Images["softwareLogo"] = new ImageData
{
SourceType = ImageSourceType.FilePath,
FilePath = softwareLogoPath
};
_logger.Info("软件 Logo 已加载: {Path} | Software logo loaded: {Path}", softwareLogoPath);
}
else
{
_logger.Warn("软件 Logo 文件不存在: {Path} | Software logo file not found: {Path}", softwareLogoPath);
}
}
}
/// <summary>
/// 注入自定义属性 | Inject custom properties
/// </summary>
private void InjectCustomProperties(ReportContext context, ReportRequest request)
{
if (request.CustomProperties == null || request.CustomProperties.Count == 0)
return;
foreach (var kvp in request.CustomProperties)
{
context.Properties[kvp.Key] = kvp.Value;
}
}
/// <summary>
/// 根据 ReportContext 中的结果分组生成首页汇总表数据
/// Generate homepage summary table data from ReportContext result groups
/// </summary>
private List<Dictionary<string, object>> CreateSummaryTableData(ReportContext context)
{
var rows = new List<Dictionary<string, object>>();
foreach (var group in context.ResultGroups)
{
var inspectionType = group.ProcessorType switch
{
"LineMeasurementProcessor" => "线测量 | Line Measurement",
"BgaVoidRateProcessor" => "BGA 气泡率检测 | BGA Void Rate",
"VoidMeasurementProcessor" => "空隙测量 | Void Measurement",
"FillRateProcessor" => "通孔填锡率 | Via Fill Rate",
_ => group.ProcessorType
};
var classification = string.IsNullOrEmpty(group.Classification) ? "N/A" : group.Classification;
var status = classification == "Pass" ? "[PASS] 合格" : classification == "Fail" ? "[FAIL] 不合格" : "—";
rows.Add(new Dictionary<string, object>
{
["inspectionType"] = inspectionType,
["classification"] = classification,
["status"] = status
});
}
return rows;
}
#endregion
}
}