d72b583319
[XP.ReportEngine] 模型与配置扩展:ReportOutputFormat 枚举:新增 Excel=1, Csv=2, Txt=3(显式序号,向后兼容);新增多格式相关配置加载。
345 lines
16 KiB
C#
345 lines
16 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
|
||
namespace XP.ReportEngine.Configs
|
||
{
|
||
/// <summary>
|
||
/// 报告引擎配置模型 | Report engine configuration model
|
||
/// </summary>
|
||
public class ReportConfig
|
||
{
|
||
/// <summary>
|
||
/// 报告输出文件夹路径 | Report output directory path
|
||
/// </summary>
|
||
public string OutputDirectory { get; set; } = Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
||
"XplorePlane", "Reports");
|
||
|
||
/// <summary>
|
||
/// 报告模板文件路径(相对或绝对)| Report template file path (relative or absolute)
|
||
/// </summary>
|
||
public string TemplatePath { get; set; } = @"Templates\StandardReportTemplate.json";
|
||
|
||
/// <summary>
|
||
/// 输出文件名模式,支持占位符 | Output file name pattern, supports placeholders
|
||
/// 支持的占位符 | Supported placeholders:
|
||
/// {ReportId} - 报告编号(如 RPT-20250512-001)
|
||
/// {CncProgram} - CNC 程序名称
|
||
/// {ProductName} - 产品名称
|
||
/// {ProductCode} - 产品类型码
|
||
/// {WorkpieceSN} - 工件 SN 码
|
||
/// {DeviceId} - 检测设备编号(本机)
|
||
/// {MachineId} - 生产机台号
|
||
/// {Date} - 日期(yyyyMMdd)
|
||
/// {Time} - 时间(HHmmss)
|
||
/// {Result} - 综合检测结论(Pass/Fail)
|
||
/// </summary>
|
||
public string FileNamePattern { get; set; } = "{ReportId}";
|
||
|
||
/// <summary>
|
||
/// 文件名重复时是否自动累加序号 | Whether to auto-increment suffix when file name duplicates
|
||
/// true: 重复时生成 filename(1).pdf, filename(2).pdf ...
|
||
/// false: 直接覆盖同名文件
|
||
/// </summary>
|
||
public bool AutoIncrementOnDuplicate { get; set; } = true;
|
||
|
||
/// <summary>
|
||
/// 生成后是否自动打开 PDF 阅读器 | Whether to auto-open PDF viewer after generation
|
||
/// </summary>
|
||
public bool AutoOpenAfterGenerate { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// 默认页面尺寸 | Default page size
|
||
/// </summary>
|
||
public string DefaultPageSize { get; set; } = "A4";
|
||
|
||
/// <summary>
|
||
/// 默认页面方向(Portrait / Landscape)| Default page orientation
|
||
/// </summary>
|
||
public string DefaultOrientation { get; set; } = "Portrait";
|
||
|
||
/// <summary>
|
||
/// 默认上边距(mm)| Default top margin (mm)
|
||
/// </summary>
|
||
public float MarginTop { get; set; } = 20f;
|
||
|
||
/// <summary>
|
||
/// 默认下边距(mm)| Default bottom margin (mm)
|
||
/// </summary>
|
||
public float MarginBottom { get; set; } = 20f;
|
||
|
||
/// <summary>
|
||
/// 默认左边距(mm)| Default left margin (mm)
|
||
/// </summary>
|
||
public float MarginLeft { get; set; } = 20f;
|
||
|
||
/// <summary>
|
||
/// 默认右边距(mm)| Default right margin (mm)
|
||
/// </summary>
|
||
public float MarginRight { get; set; } = 20f;
|
||
|
||
/// <summary>
|
||
/// 报告中显示的公司名称 | Company name displayed in report
|
||
/// </summary>
|
||
public string CompanyName { get; set; } = "海克斯康制造智能技术(青岛)有限公司";
|
||
|
||
/// <summary>
|
||
/// 公司 Logo 图片路径(可选,为空则不显示)| Company logo image path (optional, empty means no logo)
|
||
/// </summary>
|
||
public string CompanyLogo { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// 报告中显示的软件名称 | Software name displayed in report
|
||
/// </summary>
|
||
public string SoftwareName { get; set; } = "XplorePlane";
|
||
|
||
/// <summary>
|
||
/// 软件 Logo 图片路径(可选,为空则不显示)| Software logo image path (optional, empty means no logo)
|
||
/// </summary>
|
||
public string SoftwareLogo { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// 默认 Excel 模板文件路径(可选,相对或绝对)| Default Excel template file path (optional, relative or absolute)
|
||
/// 配置且指向有效 .xlsx 时走基于模板生成(需求 12);为空时走默认结构化生成(需求 11)
|
||
/// Configured and pointing to a valid .xlsx → template-based generation (Req 12); empty → default structured generation (Req 11)
|
||
/// </summary>
|
||
public string DefaultExcelTemplatePath { get; set; } = string.Empty;
|
||
|
||
/// <summary>
|
||
/// TXT/CSV 文本编码(可选)| Text encoding for TXT/CSV (optional)
|
||
/// 取值:Utf8Bom(默认)/ Utf8 / GB2312 / GBK | Values: Utf8Bom (default) / Utf8 / GB2312 / GBK
|
||
/// </summary>
|
||
public string TextEncoding { get; set; } = "Utf8Bom";
|
||
|
||
/// <summary>
|
||
/// 获取解析后的模板绝对路径 | Get resolved absolute template path
|
||
/// 如果 TemplatePath 是相对路径,则基于应用程序目录解析
|
||
/// If TemplatePath is relative, resolves based on application directory
|
||
/// </summary>
|
||
public string GetResolvedTemplatePath()
|
||
{
|
||
if (Path.IsPathRooted(TemplatePath))
|
||
{
|
||
return TemplatePath;
|
||
}
|
||
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplatePath);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取解析后的默认 Excel 模板绝对路径 | Get resolved absolute default Excel template path
|
||
/// 与 GetResolvedTemplatePath 采用相同的相对/绝对路径解析逻辑
|
||
/// Uses the same relative/absolute path resolution logic as GetResolvedTemplatePath
|
||
/// 当 DefaultExcelTemplatePath 为空时返回空字符串(表示未配置模板)
|
||
/// Returns empty string when DefaultExcelTemplatePath is empty (means no template configured)
|
||
/// </summary>
|
||
public string GetResolvedExcelTemplatePath()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(DefaultExcelTemplatePath))
|
||
{
|
||
return string.Empty;
|
||
}
|
||
if (Path.IsPathRooted(DefaultExcelTemplatePath))
|
||
{
|
||
return DefaultExcelTemplatePath;
|
||
}
|
||
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultExcelTemplatePath);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据文件名模式和上下文参数生成实际文件名 | Generate actual file name based on pattern and context parameters
|
||
/// </summary>
|
||
/// <param name="parameters">占位符参数字典 | Placeholder parameter dictionary</param>
|
||
/// <returns>生成的文件名(不含扩展名)| Generated file name (without extension)</returns>
|
||
public string ResolveFileName(Dictionary<string, string> parameters)
|
||
{
|
||
var fileName = FileNamePattern;
|
||
|
||
// 替换所有已知占位符 | Replace all known placeholders
|
||
if (parameters != null)
|
||
{
|
||
foreach (var kvp in parameters)
|
||
{
|
||
fileName = fileName.Replace($"{{{kvp.Key}}}", SanitizeFileName(kvp.Value ?? ""));
|
||
}
|
||
}
|
||
|
||
// 替换日期和时间(始终可用)| Replace date and time (always available)
|
||
fileName = fileName.Replace("{Date}", DateTime.Now.ToString("yyyyMMdd"));
|
||
fileName = fileName.Replace("{Time}", DateTime.Now.ToString("HHmmss"));
|
||
|
||
// 清理未被替换的占位符(替换为空)| Clean up unreplaced placeholders
|
||
fileName = System.Text.RegularExpressions.Regex.Replace(fileName, @"\{[^}]+\}", "");
|
||
|
||
// 移除连续的分隔符 | Remove consecutive separators
|
||
fileName = System.Text.RegularExpressions.Regex.Replace(fileName, @"[_\-]{2,}", "_");
|
||
fileName = fileName.Trim('_', '-');
|
||
|
||
return string.IsNullOrWhiteSpace(fileName) ? "Report" : fileName;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析最终输出文件完整路径(含重复累加逻辑)| Resolve final output file full path (with duplicate increment logic)
|
||
/// </summary>
|
||
/// <param name="parameters">占位符参数字典 | Placeholder parameter dictionary</param>
|
||
/// <param name="extension">文件扩展名(含点号,如 ".pdf")| File extension (with dot, e.g. ".pdf")</param>
|
||
/// <returns>最终输出文件完整路径 | Final output file full path</returns>
|
||
public string ResolveOutputFilePath(Dictionary<string, string> parameters, string extension = ".pdf")
|
||
{
|
||
var baseName = ResolveFileName(parameters);
|
||
var outputDir = OutputDirectory;
|
||
|
||
// 确保输出目录存在 | Ensure output directory exists
|
||
if (!Directory.Exists(outputDir))
|
||
{
|
||
Directory.CreateDirectory(outputDir);
|
||
}
|
||
|
||
var filePath = Path.Combine(outputDir, baseName + extension);
|
||
|
||
// 重复累加逻辑 | Duplicate increment logic
|
||
if (AutoIncrementOnDuplicate && File.Exists(filePath))
|
||
{
|
||
int counter = 1;
|
||
string newPath;
|
||
do
|
||
{
|
||
newPath = Path.Combine(outputDir, $"{baseName}({counter}){extension}");
|
||
counter++;
|
||
} while (File.Exists(newPath));
|
||
|
||
filePath = newPath;
|
||
}
|
||
|
||
return filePath;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 自动累加序号的上限 | Upper limit for auto-increment suffix
|
||
/// 需求 7.4/7.6:最多尝试至 9999,超过则视为无法生成唯一文件名。
|
||
/// Requirement 7.4/7.6: try up to 9999; beyond that a unique file name cannot be generated.
|
||
/// </summary>
|
||
public const int MaxDuplicateIncrement = 9999;
|
||
|
||
/// <summary>
|
||
/// 解析输出文件路径的结果(含成功标志与错误信息)| Result of resolving output file path (with success flag and error message)
|
||
/// 供报告服务在多格式编排时区分“路径已就绪”“文件已存在(不覆盖)”“累加上限”“路径非法”等情形。
|
||
/// Used by the report service during multi-format orchestration to distinguish
|
||
/// "path ready", "file exists (no overwrite)", "increment limit reached", and "illegal path".
|
||
/// </summary>
|
||
public sealed class OutputPathResolution
|
||
{
|
||
/// <summary>是否解析成功 | Whether the resolution succeeded</summary>
|
||
public bool IsSuccess { get; private set; }
|
||
|
||
/// <summary>解析得到的完整文件路径(成功时非空)| Resolved full file path (non-null on success)</summary>
|
||
public string FilePath { get; private set; }
|
||
|
||
/// <summary>错误信息(失败时非空)| Error message (non-null on failure)</summary>
|
||
public string ErrorMessage { get; private set; }
|
||
|
||
/// <summary>构造成功结果 | Build a success result</summary>
|
||
public static OutputPathResolution Success(string filePath)
|
||
=> new OutputPathResolution { IsSuccess = true, FilePath = filePath };
|
||
|
||
/// <summary>构造失败结果 | Build a failure result</summary>
|
||
public static OutputPathResolution Failure(string errorMessage)
|
||
=> new OutputPathResolution { IsSuccess = false, ErrorMessage = errorMessage };
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析最终输出文件完整路径并返回带错误信息的结果 | Resolve final output file full path and return a result with error info
|
||
/// 覆盖需求 7.4(累加不重复)、7.5(不覆盖时文件已存在错误)、7.6(累加上限错误)、7.8(路径非法错误)。
|
||
/// Covers Req 7.4 (increment without duplicate), 7.5 (file-exists error when not overwriting),
|
||
/// 7.6 (increment-limit error), 7.8 (illegal path error).
|
||
/// </summary>
|
||
/// <param name="parameters">占位符参数字典 | Placeholder parameter dictionary</param>
|
||
/// <param name="extension">文件扩展名(含点号,如 ".txt")| File extension (with dot, e.g. ".txt")</param>
|
||
/// <returns>解析结果 | Resolution result</returns>
|
||
public OutputPathResolution TryResolveOutputFilePath(Dictionary<string, string> parameters, string extension)
|
||
{
|
||
var baseName = ResolveFileName(parameters);
|
||
var outputDir = OutputDirectory;
|
||
|
||
// 需求 7.8:校验输出目录路径是否含非法字符 | Req 7.8: validate output directory path for illegal characters
|
||
if (string.IsNullOrWhiteSpace(outputDir) || outputDir.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
|
||
{
|
||
return OutputPathResolution.Failure(
|
||
$"输出目录路径非法: {outputDir} | Illegal output directory path: {outputDir}");
|
||
}
|
||
|
||
// 需求 7.8:校验最终文件名是否含非法字符(扩展名一并校验)| Req 7.8: validate file name (including extension) for illegal chars
|
||
var fileName = baseName + extension;
|
||
if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
|
||
{
|
||
return OutputPathResolution.Failure(
|
||
$"输出文件名非法: {fileName} | Illegal output file name: {fileName}");
|
||
}
|
||
|
||
// 确保输出目录存在 | Ensure output directory exists
|
||
try
|
||
{
|
||
if (!Directory.Exists(outputDir))
|
||
{
|
||
Directory.CreateDirectory(outputDir);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return OutputPathResolution.Failure(
|
||
$"无法创建输出目录: {outputDir},{ex.Message} | Cannot create output directory: {outputDir}, {ex.Message}");
|
||
}
|
||
|
||
var filePath = Path.Combine(outputDir, fileName);
|
||
|
||
if (File.Exists(filePath))
|
||
{
|
||
// 需求 7.5:不覆盖且文件已存在 → 错误 | Req 7.5: no overwrite and file exists → error
|
||
if (!AutoIncrementOnDuplicate)
|
||
{
|
||
return OutputPathResolution.Failure(
|
||
$"目标文件已存在且不允许覆盖: {filePath} | Target file already exists and overwrite is disabled: {filePath}");
|
||
}
|
||
|
||
// 需求 7.4/7.6:累加序号(最多至 9999),超上限 → 错误 | Req 7.4/7.6: increment (up to 9999); beyond limit → error
|
||
int counter = 1;
|
||
string newPath = null;
|
||
bool found = false;
|
||
for (; counter <= MaxDuplicateIncrement; counter++)
|
||
{
|
||
newPath = Path.Combine(outputDir, $"{baseName}({counter}){extension}");
|
||
if (!File.Exists(newPath))
|
||
{
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!found)
|
||
{
|
||
return OutputPathResolution.Failure(
|
||
$"累加序号已达上限({MaxDuplicateIncrement}),无法生成唯一文件名: {baseName}{extension} | " +
|
||
$"Increment limit ({MaxDuplicateIncrement}) reached, cannot generate a unique file name: {baseName}{extension}");
|
||
}
|
||
|
||
filePath = newPath;
|
||
}
|
||
|
||
return OutputPathResolution.Success(filePath);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理文件名中的非法字符 | Sanitize illegal characters in file name
|
||
/// </summary>
|
||
private static string SanitizeFileName(string name)
|
||
{
|
||
var invalidChars = Path.GetInvalidFileNameChars();
|
||
foreach (var c in invalidChars)
|
||
{
|
||
name = name.Replace(c, '_');
|
||
}
|
||
return name;
|
||
}
|
||
}
|
||
}
|