391 lines
17 KiB
C#
391 lines
17 KiB
C#
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
|
||
}
|
||
}
|