diff --git a/src/YhCore.Service/Excel/ExcelHelper.cs b/src/YhCore.Service/Excel/ExcelHelper.cs
new file mode 100644
index 0000000..ab86c2a
--- /dev/null
+++ b/src/YhCore.Service/Excel/ExcelHelper.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using OfficeOpenXml;
+using OfficeOpenXml.Style;
+
+namespace YhCore.Service.Excel
+{
+ ///
+ /// EPPlus 多 Sheet 导出(移植自旧 Bsch.Common/DocPrint/ExcelHelper,
+ /// 保留 RenderToExcelBytes + ExportDynamicSheet:动态列、大标题、自定义列宽、打印优化)。
+ /// 注意:EPPlus 实际产出 xlsx 字节流,旧系统文件名沿用 .xls,Excel 可正常打开,保持原契约。
+ ///
+ public class ExcelHelper
+ {
+ /// 导出多 Sheet Excel 字节流(listdt/listsheetname/listtitle 按下标一一对应)
+ public static byte[] RenderToExcelBytes(string fileName, List listdt, List listsheetname, List listtitle, List listcolwidth = null)
+ {
+ try
+ {
+ var package = new ExcelPackage();
+ int n = 0;
+ foreach (var dt in listdt)
+ {
+ string sheetname = "Sheet" + (n + 1).ToString();
+ if (n < listsheetname.Count) sheetname = listsheetname[n];
+ string title = "";
+ if (n < listtitle.Count) title = listtitle[n];
+ var ws1 = package.Workbook.Worksheets.Add(sheetname);
+ ExportDynamicSheet(ws1, dt, title, "A", listcolwidth);
+ n++;
+ }
+ return package.GetAsByteArray();
+ }
+ catch (Exception) { return Array.Empty(); }
+ }
+
+ #region 动态列 Sheet 导出(通用方法)
+
+ ///
+ /// 导出 动态列 + 自定义表头 + 自定义页码 + 打印优化
+ ///
+ public static void ExportDynamicSheet(ExcelWorksheet ws, DataTable dt, string sheetTitle, string printMark, List listcolwidth = null)
+ {
+ // -------------------------- 行定义 --------------------------
+ int titleRow = 1; // 大标题行
+ int headerRow = 2; // 列标题行
+ int dataStartRow = 3; // 数据开始行
+
+ int colCount = dt.Columns.Count;
+ int rowCount = dt.Rows.Count;
+
+ // -------------------------- 1. 自定义大标题(合并行) --------------------------
+ if (sheetTitle != "")
+ {
+ ws.Cells[titleRow, 1].Value = sheetTitle;
+ ws.Cells[titleRow, 1, titleRow, colCount].Merge = true; // 合并单元格
+
+ // 标题样式
+ var titleStyle = ws.Cells[titleRow, 1].Style;
+ titleStyle.Font.Size = 12;
+ titleStyle.Font.Bold = true;
+ titleStyle.HorizontalAlignment = ExcelHorizontalAlignment.Center;
+ titleStyle.VerticalAlignment = ExcelVerticalAlignment.Center;
+ ws.Row(titleRow).Height = 32;
+ }
+ else
+ {
+ headerRow = 1; // 列标题行
+ dataStartRow = 2; // 数据开始行
+ }
+
+ // -------------------------- 2. 动态列 自动填充表头 --------------------------
+ for (int c = 0; c < colCount; c++)
+ {
+ ws.Cells[headerRow, c + 1].Value = dt.Columns[c].ColumnName;
+ }
+
+ // -------------------------- 3. 动态列 自动填充数据 --------------------------
+ for (int r = 0; r < rowCount; r++)
+ {
+ for (int c = 0; c < colCount; c++)
+ {
+ ws.Cells[dataStartRow + r, c + 1].Value = dt.Rows[r][c];
+ }
+ }
+
+ // -------------------------- 4. 自动换行 + 行高自适应(关键) --------------------------
+ var dataRange = ws.Cells[headerRow, 1, dataStartRow + rowCount - 1, colCount];
+ dataRange.Style.Font.Size = 10;
+ dataRange.Style.WrapText = true;
+ dataRange.Style.VerticalAlignment = ExcelVerticalAlignment.Center;
+ dataRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
+ // 加细边框
+ dataRange.Style.Border.Top.Style = ExcelBorderStyle.Thin;
+ dataRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
+ dataRange.Style.Border.Left.Style = ExcelBorderStyle.Thin;
+ dataRange.Style.Border.Right.Style = ExcelBorderStyle.Thin;
+
+ // 自动计算:所有列平均撑满 A4 宽度
+ if (listcolwidth != null)
+ {
+ int colnum = listcolwidth.Count > colCount ? colCount : listcolwidth.Count;
+ for (int i = 1; i <= colnum; i++)
+ {
+ ws.Column(i).Width = listcolwidth[i - 1];
+ }
+ }
+ else
+ {
+ double totalWidth = 58;
+ double perColWidth = (double)totalWidth / colCount;
+ for (int i = 1; i <= colCount; i++)
+ {
+ ws.Column(i).Width = perColWidth;
+ }
+ }
+
+ // 行高自适应(EPPlus 4.x 兼容)
+ for (int r = headerRow; r <= headerRow + rowCount - 1; r++)
+ {
+ ws.Row(r).CustomHeight = true;
+ }
+
+ // -------------------------- 5. 表头样式 --------------------------
+ var headerRange = ws.Cells[headerRow, 1, headerRow, colCount];
+ headerRange.Style.Font.Bold = true;
+ ws.Row(headerRow).Height = 25;
+
+ // -------------------------- 6. 打印:每页重复标题行 --------------------------
+ ws.PrinterSettings.RepeatRows = new ExcelAddress($"{headerRow}:{headerRow}");
+
+ // -------------------------- 7. 自定义页码(中文样式 + 字体大小) --------------------------
+ // &P=当前页 &N=总页 12号字体 居中
+ ws.HeaderFooter.OddFooter.CenteredText = "&14 第 &P 页 / 共 &N 页";
+
+ // -------------------------- 8. 打印格式优化 --------------------------
+ ws.PrinterSettings.PaperSize = ePaperSize.A4;
+ ws.PrinterSettings.Orientation = eOrientation.Portrait;
+
+ ws.PrinterSettings.LeftMargin = 0.4m;
+ ws.PrinterSettings.RightMargin = 0.4m;
+ ws.PrinterSettings.TopMargin = 0.8m;
+ ws.PrinterSettings.BottomMargin = 1.2m;
+ }
+ #endregion
+ }
+}
diff --git a/src/YhCore.Service/Excel/ExcelRender.cs b/src/YhCore.Service/Excel/ExcelRender.cs
new file mode 100644
index 0000000..a592cc7
--- /dev/null
+++ b/src/YhCore.Service/Excel/ExcelRender.cs
@@ -0,0 +1,404 @@
+using System;
+using System.Data;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using NPOI.HSSF.UserModel;
+using NPOI.SS.Formula.Eval;
+using NPOI.SS.UserModel;
+using NPOI.XSSF.UserModel;
+
+namespace YhCore.Service.Excel
+{
+ ///
+ /// 使用 NPOI 操作 Excel,无需 Office COM 组件(移植自旧 Bsch.Common/DocPrint/ExcelRender,
+ /// 仅保留 6 个 WYSHRole Excel 接口用到的导出/导入成员;控制器用 File() 包装下载)。
+ ///
+ public class ExcelRender
+ {
+ static ExcelRender()
+ {
+ // ExportDT 列宽按 GB2312 字节长计算:net5 需注册 CodePages 提供程序后才能 GetEncoding(936)
+ Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
+ }
+
+ #region DataTable 导出为 .xls 字节流
+
+ ///
+ /// DataTable 导出为 Excel 的 MemoryStream(HSSF .xls;65535 行自动分 sheet;列宽按 GB2312 字节长,上限 50)
+ ///
+ static MemoryStream ExportDT(DataTable dtSource, string strHeaderText)
+ {
+ HSSFWorkbook workbook = new HSSFWorkbook();
+ HSSFSheet sheet = workbook.CreateSheet() as HSSFSheet;
+
+ HSSFCellStyle dateStyle = workbook.CreateCellStyle() as HSSFCellStyle;
+ HSSFDataFormat format = workbook.CreateDataFormat() as HSSFDataFormat;
+ dateStyle.DataFormat = format.GetFormat("yyyy-mm-dd");
+
+ //取得列宽
+ int[] arrColWidth = new int[dtSource.Columns.Count];
+ foreach (DataColumn item in dtSource.Columns)
+ {
+ arrColWidth[item.Ordinal] = Encoding.GetEncoding(936).GetBytes(item.ColumnName.ToString()).Length;
+ }
+ for (int i = 0; i < dtSource.Rows.Count; i++)
+ {
+ for (int j = 0; j < dtSource.Columns.Count; j++)
+ {
+ int intTemp = Encoding.GetEncoding(936).GetBytes(dtSource.Rows[i][j].ToString()).Length;
+ if (intTemp > arrColWidth[j])
+ {
+ arrColWidth[j] = intTemp;
+ if (intTemp >= 50)
+ arrColWidth[j] = 50;
+ }
+ }
+ }
+ int rowIndex = 0;
+
+ foreach (DataRow row in dtSource.Rows)
+ {
+ #region 新建表,填充表头,填充列头,样式
+
+ if (rowIndex == 65535 || rowIndex == 0)
+ {
+ if (rowIndex != 0)
+ {
+ sheet = workbook.CreateSheet() as HSSFSheet;
+ }
+
+ #region 列头及样式
+
+ {
+ HSSFRow headerRow = sheet.CreateRow(0) as HSSFRow;
+
+ HSSFCellStyle headStyle = workbook.CreateCellStyle() as HSSFCellStyle;
+ headStyle.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Center;
+ HSSFFont font = workbook.CreateFont() as HSSFFont;
+ font.FontHeightInPoints = 10;
+ font.Boldweight = 700;
+ headStyle.SetFont(font);
+
+ foreach (DataColumn column in dtSource.Columns)
+ {
+ headerRow.CreateCell(column.Ordinal).SetCellValue(column.ColumnName);
+ headerRow.GetCell(column.Ordinal).CellStyle = headStyle;
+
+ //设置列宽
+ sheet.SetColumnWidth(column.Ordinal, (arrColWidth[column.Ordinal] + 1) * 256);
+ }
+ }
+
+ #endregion
+
+ rowIndex = 1;
+ }
+
+ #endregion
+
+ #region 填充内容
+
+ HSSFRow dataRow = sheet.CreateRow(rowIndex) as HSSFRow;
+ foreach (DataColumn column in dtSource.Columns)
+ {
+ HSSFCell newCell = dataRow.CreateCell(column.Ordinal) as HSSFCell;
+
+ string drValue = row[column].ToString();
+
+ switch (column.DataType.ToString())
+ {
+ case "System.String": //字符串类型
+ newCell.SetCellValue(drValue);
+ break;
+ case "System.DateTime": //日期类型
+ DateTime dateV;
+ DateTime.TryParse(drValue, out dateV);
+ newCell.SetCellValue(dateV);
+ newCell.CellStyle = dateStyle; //格式化显示
+ break;
+ case "System.Boolean": //布尔型
+ bool boolV = false;
+ bool.TryParse(drValue, out boolV);
+ newCell.SetCellValue(boolV);
+ break;
+ case "System.Int16": //整型
+ case "System.Int32":
+ case "System.Int64":
+ case "System.Byte":
+ int intV = 0;
+ int.TryParse(drValue, out intV);
+ newCell.SetCellValue(intV);
+ break;
+ case "System.Decimal": //浮点型
+ case "System.Double":
+ double doubV = 0;
+ double.TryParse(drValue, out doubV);
+ newCell.SetCellValue(doubV);
+ break;
+ case "System.DBNull": //空值处理
+ newCell.SetCellValue("");
+ break;
+ default:
+ newCell.SetCellValue("");
+ break;
+ }
+ }
+
+ #endregion
+
+ rowIndex++;
+ }
+ using (MemoryStream ms = new MemoryStream())
+ {
+ workbook.Write(ms);
+ ms.Flush();
+ ms.Position = 0;
+
+ return ms;
+ }
+ }
+
+ /// 导出 Excel 字节流(供 Service 层返回 byte[],控制器包 File 下载)
+ public static byte[] RenderToExcelBytes(DataTable table, string fileName)
+ {
+ try { return ExportDT(table, null).ToArray(); }
+ catch (Exception) { return Array.Empty(); }
+ }
+
+ #endregion
+
+ #region 读取 excel 到 DataTable
+
+ /// Excel 文档流是否有数据(默认第一个 sheet)
+ public static bool HasData(Stream excelFileStream, string fileExt)
+ {
+ return HasData(excelFileStream, 0, fileExt);
+ }
+
+ public static bool HasData(Stream excelFileStream, int sheetIndex, string fileExt)
+ {
+ using (excelFileStream)
+ {
+ IWorkbook workbook = null;
+ if (fileExt == ".xls")
+ {
+ workbook = new HSSFWorkbook(excelFileStream);
+ }
+ else if (fileExt == ".xlsx")
+ {
+ workbook = new XSSFWorkbook(excelFileStream);
+ }
+
+ {
+ if (workbook.NumberOfSheets > 0)
+ {
+ if (sheetIndex < workbook.NumberOfSheets)
+ {
+ ISheet sheet = workbook.GetSheetAt(sheetIndex);
+ {
+ return sheet.PhysicalNumberOfRows > 0;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Excel 文档流转换成 DataTable
+ ///
+ /// Excel 文档流
+ /// 表索引号,如第一个表为 0
+ /// 标题行索引号,如第一行为 0
+ public static DataTable RenderFromExcel(Stream excelFileStream, int sheetIndex, int headerRowIndex, string fileExt)
+ {
+ DataTable table = null;
+
+ using (excelFileStream)
+ {
+ IWorkbook workbook = null;
+ if (fileExt == ".xls")
+ {
+ workbook = new HSSFWorkbook(excelFileStream);
+ }
+ else if (fileExt == ".xlsx")
+ {
+ workbook = new XSSFWorkbook(excelFileStream);
+ }
+
+ ISheet sheet = workbook.GetSheetAt(sheetIndex);
+ {
+ table = ImportDt(sheet, headerRowIndex, true);
+ }
+ }
+ return table;
+ }
+
+ /// 将指定 sheet 中的数据导出到 datatable 中(第一行为列头)
+ static DataTable ImportDt(ISheet sheet, int HeaderRowIndex, bool needHeader)
+ {
+ DataTable table = new DataTable();
+ IRow headerRow;
+ int cellCount;
+ try
+ {
+ if (HeaderRowIndex < 0 || !needHeader)
+ {
+ headerRow = sheet.GetRow(0);
+ cellCount = headerRow.LastCellNum;
+
+ for (int i = headerRow.FirstCellNum; i <= cellCount; i++)
+ {
+ DataColumn column = new DataColumn(Convert.ToString(i));
+ table.Columns.Add(column);
+ }
+ }
+ else
+ {
+ headerRow = sheet.GetRow(HeaderRowIndex);
+ cellCount = headerRow.LastCellNum;
+
+ for (int i = headerRow.FirstCellNum; i < cellCount; i++)
+ {
+ if (headerRow.GetCell(i) == null)
+ {
+ if (table.Columns.IndexOf(Convert.ToString(i)) > 0)
+ {
+ DataColumn column = new DataColumn(Convert.ToString("重复列名" + i));
+ table.Columns.Add(column);
+ }
+ else
+ {
+ DataColumn column = new DataColumn(Convert.ToString(i));
+ table.Columns.Add(column);
+ }
+ }
+ else if (table.Columns.IndexOf(headerRow.GetCell(i).ToString()) > 0)
+ {
+ DataColumn column = new DataColumn(Convert.ToString("重复列名" + i));
+ table.Columns.Add(column);
+ }
+ else
+ {
+ DataColumn column = new DataColumn(headerRow.GetCell(i).ToString());
+ table.Columns.Add(column);
+ }
+ }
+ }
+ int rowCount = sheet.LastRowNum;
+ for (int i = (HeaderRowIndex + 1); i <= sheet.LastRowNum; i++)
+ {
+ try
+ {
+ IRow row;
+ if (sheet.GetRow(i) == null)
+ {
+ row = sheet.CreateRow(i);
+ }
+ else
+ {
+ row = sheet.GetRow(i);
+ }
+
+ DataRow dataRow = table.NewRow();
+
+ for (int j = row.FirstCellNum; j < cellCount; j++)
+ {
+ try
+ {
+ if (row.GetCell(j) != null)
+ {
+ switch (row.GetCell(j).CellType)
+ {
+ case CellType.String:
+ string str = row.GetCell(j).StringCellValue;
+ if (str != null && str.Length > 0)
+ {
+ dataRow[j] = str.ToString();
+ }
+ else
+ {
+ dataRow[j] = null;
+ }
+ break;
+ case CellType.Numeric:
+ if (DateUtil.IsCellDateFormatted(row.GetCell(j)))
+ {
+ dataRow[j] = DateTime.FromOADate(row.GetCell(j).NumericCellValue);
+ }
+ else
+ {
+ dataRow[j] = Convert.ToDouble(row.GetCell(j).NumericCellValue);
+ }
+ break;
+ case CellType.Boolean:
+ dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue);
+ break;
+ case CellType.Error:
+ dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue);
+ break;
+ case CellType.Formula:
+ switch (row.GetCell(j).CachedFormulaResultType)
+ {
+ case CellType.String:
+ string strFORMULA = row.GetCell(j).StringCellValue;
+ if (strFORMULA != null && strFORMULA.Length > 0)
+ {
+ dataRow[j] = strFORMULA.ToString();
+ }
+ else
+ {
+ dataRow[j] = null;
+ }
+ break;
+ case CellType.Numeric:
+ dataRow[j] = Convert.ToString(row.GetCell(j).NumericCellValue);
+ break;
+ case CellType.Boolean:
+ dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue);
+ break;
+ case CellType.Error:
+ dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue);
+ break;
+ default:
+ dataRow[j] = "";
+ break;
+ }
+ break;
+ default:
+ dataRow[j] = "";
+ break;
+ }
+ }
+ }
+ catch (Exception)
+ {
+ }
+ }
+ table.Rows.Add(dataRow);
+ }
+ catch (Exception)
+ {
+ }
+ }
+ }
+ catch (Exception)
+ {
+ }
+ return table;
+ }
+
+ #endregion
+
+ public static byte[] StreamToBytes(Stream stream)
+ {
+ byte[] bytes = new byte[stream.Length];
+ stream.Read(bytes, 0, bytes.Length);
+ // 设置当前流的位置为流的开始
+ stream.Seek(0, SeekOrigin.Begin);
+ return bytes;
+ }
+ }
+}
diff --git a/src/YhCore.Service/Pdf/NumberToChineseConverter.cs b/src/YhCore.Service/Pdf/NumberToChineseConverter.cs
new file mode 100644
index 0000000..98baac4
--- /dev/null
+++ b/src/YhCore.Service/Pdf/NumberToChineseConverter.cs
@@ -0,0 +1,123 @@
+using System;
+
+namespace YhCore.Service.Pdf
+{
+ /// 数字转中文大写金额(移植自旧 Bsch.Common/DocPrint/NumberToChineseConverter,审批单合计金额用)。
+ public class NumberToChineseConverter
+ {
+ // 中文大写数字
+ private static readonly char[] ChineseNumbers = { '零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖' };
+ // 中文大写单位
+ private static readonly string[] ChineseUnits = { "分", "角", "元", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿", "拾", "佰", "仟", "兆" };
+
+ ///
+ /// 将数字转换为中文大写金额
+ ///
+ /// 需要转换的数字(支持整数和小数,建议保留两位小数)
+ /// 中文大写金额字符串
+ public static string ToChineseUppercase(decimal number)
+ {
+ // 处理负数
+ bool isNegative = number < 0;
+ if (isNegative)
+ {
+ number = Math.Abs(number);
+ }
+
+ // 四舍五入保留两位小数
+ number = Math.Round(number, 2);
+ long integerPart = (long)number; // 整数部分
+ int decimalPart = (int)((number - integerPart) * 100); // 小数部分(角分)
+
+ // 处理零值
+ if (integerPart == 0 && decimalPart == 0)
+ {
+ return "零元整";
+ }
+
+ string result = string.Empty;
+
+ // 处理整数部分
+ if (integerPart > 0)
+ {
+ string integerStr = integerPart.ToString();
+ int length = integerStr.Length;
+
+ // 标记是否需要加零
+ bool needZero = false;
+
+ for (int i = 0; i < length; i++)
+ {
+ int digit = int.Parse(integerStr[i].ToString());
+ int unitIndex = length - i - 1 + 2; // 对应到ChineseUnits的索引(从元开始)
+
+ if (digit == 0)
+ {
+ needZero = true;
+ // 处理万、亿等单位,避免漏单位
+ if (unitIndex == 6 || unitIndex == 10)
+ {
+ result += ChineseUnits[unitIndex];
+ needZero = false;
+ }
+ }
+ else
+ {
+ if (needZero)
+ {
+ result += ChineseNumbers[0];
+ needZero = false;
+ }
+ result += ChineseNumbers[digit] + ChineseUnits[unitIndex];
+ }
+ }
+ }
+
+ // 处理小数部分(角分)
+ if (decimalPart > 0)
+ {
+ int jiao = decimalPart / 10; // 角
+ int fen = decimalPart % 10; // 分
+
+ if (jiao > 0)
+ {
+ result += ChineseNumbers[jiao] + ChineseUnits[1];
+ }
+ else if (integerPart > 0) // 有整数部分但角为0,需要加零
+ {
+ result += ChineseNumbers[0];
+ }
+
+ if (fen > 0)
+ {
+ result += ChineseNumbers[fen] + ChineseUnits[0];
+ }
+ }
+ else
+ {
+ // 无小数部分,加"整"
+ result += "整";
+ }
+
+ // 处理负数
+ if (isNegative)
+ {
+ result = "负" + result;
+ }
+
+ return result;
+ }
+
+ // 重载方法,支持double类型
+ public static string ToChineseUppercase(double number)
+ {
+ return ToChineseUppercase((decimal)number);
+ }
+
+ // 重载方法,支持int类型
+ public static string ToChineseUppercase(int number)
+ {
+ return ToChineseUppercase((decimal)number);
+ }
+ }
+}
diff --git a/src/YhCore.Service/Pdf/PdfHelper.cs b/src/YhCore.Service/Pdf/PdfHelper.cs
new file mode 100644
index 0000000..f3d2842
--- /dev/null
+++ b/src/YhCore.Service/Pdf/PdfHelper.cs
@@ -0,0 +1,102 @@
+// PDF helper(移植自旧 Bsch.Common/DocPrint/PdfHelper):
+// HTML→PDF 由 wkhtmltopdf 外部进程完成(原 iTextSharp XMLWorker 无 Core 版),
+// 多页合并用 iTextSharp.LGPLv2.Core;Base64→PNG 直接落盘。
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using iTextSharp.text;
+using iTextSharp.text.pdf;
+using Yh.Platform.Abstractions;
+using YhCore.Common.Config;
+
+namespace YhCore.Service.Pdf
+{
+ public class PdfHelper
+ {
+ private static string WkHtmlToPdfPath =>
+ string.IsNullOrEmpty(Config.WkHtmlToPdfPath) ? "wkhtmltopdf" : Config.WkHtmlToPdfPath;
+
+ // --enable-local-file-access 为 wkhtmltopdf 0.12.6 新增开关(签名图片用本地绝对路径
必需);
+ // 0.12.5 及以下(如 Debian 10 源)不认识该参数会报 Unknown long argument,按版本探测决定是否追加
+ private static readonly Lazy NeedsLocalFileAccessFlag = new Lazy(() =>
+ {
+ try
+ {
+ using var probe = Process.Start(new ProcessStartInfo(WkHtmlToPdfPath, "--version")
+ {
+ RedirectStandardOutput = true,
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ });
+ if (probe == null) return true;
+ string output = probe.StandardOutput.ReadToEnd();
+ probe.WaitForExit(10000);
+ var m = Regex.Match(output, @"(\d+)\.(\d+)\.(\d+)");
+ if (!m.Success) return true;
+ var version = new Version(int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value), int.Parse(m.Groups[3].Value));
+ return version >= new Version(0, 12, 6);
+ }
+ catch
+ {
+ return true;
+ }
+ });
+
+ /// 逐页 HTML→PDF 后合并为单个 PDF 字节流(DwonWYSQD 用)
+ public static byte[] ConvertHtmlTextToPdfByList(List htmlText, string HorV = "V")
+ => MergePdf(htmlText.Select(HtmlToPdf).ToList());
+
+ private static byte[] HtmlToPdf(string html)
+ {
+ var tmpIn = Path.GetTempFileName() + ".html";
+ var tmpOut = Path.GetTempFileName() + ".pdf";
+ try
+ {
+ File.WriteAllText(tmpIn, html, new UTF8Encoding(false));
+ string localFlag = NeedsLocalFileAccessFlag.Value ? " --enable-local-file-access" : "";
+ var psi = new ProcessStartInfo(WkHtmlToPdfPath,
+ $" --encoding utf-8{localFlag} --disable-smart-shrinking \"{tmpIn}\" \"{tmpOut}\"")
+ { CreateNoWindow = true, UseShellExecute = false };
+ using var proc = Process.Start(psi);
+ if (proc == null || !proc.WaitForExit(60000) || !File.Exists(tmpOut))
+ throw new DException("审批单 PDF 生成失败,请检查服务器是否已安装 wkhtmltopdf(或配置 wkhtmltopdf_path 指向其可执行文件)!");
+ return File.ReadAllBytes(tmpOut);
+ }
+ finally
+ {
+ if (File.Exists(tmpIn)) File.Delete(tmpIn);
+ if (File.Exists(tmpOut)) File.Delete(tmpOut);
+ }
+ }
+
+ public static byte[] MergePdf(List pdfBytes)
+ {
+ using var ms = new MemoryStream();
+ var doc = new Document();
+ var copy = new PdfCopy(doc, ms);
+ doc.Open();
+ foreach (var b in pdfBytes)
+ {
+ if (b == null || b.Length == 0) continue;
+ var reader = new PdfReader(b);
+ for (int i = 1; i <= reader.NumberOfPages; i++)
+ copy.AddPage(copy.GetImportedPage(reader, i));
+ copy.FreeReader(reader);
+ reader.Close();
+ }
+ doc.Close();
+ return ms.ToArray();
+ }
+
+ /// 签名 base64 → PNG 落盘(审批单 HTML 内嵌本地路径图片用)
+ public static void ConvertBase64ToPng(string base64String, string outputFilePath)
+ {
+ var bytes = Convert.FromBase64String(base64String);
+ File.WriteAllBytes(outputFilePath, bytes);
+ }
+ }
+}