using System.Globalization; using System.Reflection; using NPOI.SS.UserModel; namespace YD_Event.Application.Common.Excel; /// /// 需要保留 Excel 物理行号的行模型。 /// public interface IExcelRowLocation { int ExcelRowNumber { get; set; } } /// /// Excel 表头列名映射特性,标注属性所对应的表头单元格文本 /// [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public sealed class ExcelHeaderAttribute : Attribute { /// /// 表头文本 /// public string Name { get; } public ExcelHeaderAttribute(string name) { Name = name; } } /// /// 基于 NPOI 的 Excel 读取器,统一支持 .xls(HSSF) 与 .xlsx(XSSF),按表头名称映射为对象 /// public static class NpoiExcelReader { /// /// 按文件流自动识别格式创建工作簿,调用方无需区分 .xls/.xlsx /// public static IWorkbook CreateWorkbook(Stream stream) { // 缓冲到内存,避免上传流不可寻址导致 NPOI 无法按文件头识别格式 var memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); memoryStream.Position = 0; return WorkbookFactory.Create(memoryStream); } /// /// 读取指定名称的工作表 /// public static List ReadSheet(IWorkbook workbook, string sheetName, int headerSearchRows = 10) where T : new() { var sheet = workbook.GetSheet(sheetName) ?? throw new InvalidDataException($"缺少工作表:{sheetName}"); return ReadSheet(sheet, headerSearchRows); } /// /// 读取指定序号的工作表(序号从0开始) /// public static List ReadSheet(IWorkbook workbook, int sheetIndex, int headerSearchRows = 10) where T : new() { if (sheetIndex < 0 || sheetIndex >= workbook.NumberOfSheets) throw new InvalidDataException($"工作表序号超出范围:{sheetIndex}"); return ReadSheet(workbook.GetSheetAt(sheetIndex), headerSearchRows); } /// /// 读取工作表:在前若干行内定位表头行,按表头名称逐行映射为对象,整行为空则跳过 /// public static List ReadSheet(ISheet sheet, int headerSearchRows = 10) where T : new() { var headerMap = BuildHeaderMap(); var formatter = new DataFormatter(); var headerRow = LocateHeaderRow(sheet, headerMap.Keys, headerSearchRows, formatter) ?? throw new InvalidDataException($"工作表「{sheet.SheetName}」缺少表头"); // 列序号到属性的映射 var columnProperties = new Dictionary(); foreach (var headerCell in headerRow.Cells) { var headerText = formatter.FormatCellValue(headerCell).Trim(); if (headerMap.TryGetValue(headerText, out var property)) columnProperties[headerCell.ColumnIndex] = property; } var result = new List(); for (var rowIndex = headerRow.RowNum + 1; rowIndex <= sheet.LastRowNum; rowIndex++) { var row = sheet.GetRow(rowIndex); if (row == null) continue; var item = new T(); if (item is IExcelRowLocation rowLocation) rowLocation.ExcelRowNumber = rowIndex + 1; var hasValue = false; foreach (var (columnIndex, property) in columnProperties) { var cell = row.GetCell(columnIndex); // 以单元格原始文本是否为空判断整行是否有数据,避免值类型默认值0误判 var rawText = cell == null ? null : formatter.FormatCellValue(cell).Trim(); if (!string.IsNullOrWhiteSpace(rawText)) hasValue = true; var value = ConvertCellValue(cell, property.PropertyType, formatter); property.SetValue(item, value); } // 整行没有任何有效数据时视为空行跳过 if (hasValue) result.Add(item); } return result; } /// /// 构建表头文本到属性的映射 /// private static Dictionary BuildHeaderMap() { var map = new Dictionary(); foreach (var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var header = property.GetCustomAttribute(); if (header != null) map[header.Name] = property; } return map; } /// /// 在表头搜索范围内,取命中映射表头数量最多的一行作为表头行 /// private static IRow? LocateHeaderRow(ISheet sheet, ICollection headers, int searchRows, DataFormatter formatter) { var lastRow = Math.Min(sheet.LastRowNum, sheet.FirstRowNum + searchRows - 1); IRow? matched = null; var maxHits = 0; for (var index = sheet.FirstRowNum; index <= lastRow; index++) { var row = sheet.GetRow(index); if (row == null) continue; var hits = row.Cells.Count(cell => headers.Contains(formatter.FormatCellValue(cell).Trim())); if (hits > maxHits) { maxHits = hits; matched = row; } } return maxHits > 0 ? matched : null; } /// /// 单元格值按目标属性类型转换;空白单元格对引用/可空类型返回 null,对非空值类型返回默认值 /// private static object? ConvertCellValue(ICell? cell, Type targetType, DataFormatter formatter) { var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; var text = cell == null ? null : formatter.FormatCellValue(cell).Trim(); if (string.IsNullOrWhiteSpace(text)) return CreateDefaultValue(targetType); if (underlyingType == typeof(string)) return text; // 数值、日期型单元格优先取强类型值,避免格式化文本的千分位、小数位影响解析 if (cell is { CellType: CellType.Numeric }) { if (DateUtil.IsCellDateFormatted(cell) && underlyingType == typeof(DateTime) && cell.DateCellValue is { } cellDate) return cellDate; var number = cell.NumericCellValue; if (underlyingType == typeof(long)) return (long)Math.Round(number); if (underlyingType == typeof(int)) return (int)Math.Round(number); if (underlyingType == typeof(short)) return (short)Math.Round(number); if (underlyingType == typeof(byte)) return (byte)Math.Round(number); if (underlyingType == typeof(decimal)) return (decimal)number; if (underlyingType == typeof(double)) return number; if (underlyingType == typeof(float)) return (float)number; } if (underlyingType == typeof(long) && long.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var longValue)) return longValue; if (underlyingType == typeof(int) && int.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var intValue)) return intValue; if (underlyingType == typeof(decimal) && decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var decimalValue)) return decimalValue; if (underlyingType == typeof(double) && double.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue)) return doubleValue; if (underlyingType == typeof(DateTime) && DateTime.TryParse(text, CultureInfo.CurrentCulture, DateTimeStyles.None, out var dateTimeValue)) return dateTimeValue; if (underlyingType == typeof(bool) && bool.TryParse(text, out var boolValue)) return boolValue; if (underlyingType.IsEnum && Enum.TryParse(underlyingType, text, out var enumValue)) return enumValue; // 无法转换时不抛异常中断整表导入:值类型给默认值,引用/可空类型给 null return CreateDefaultValue(targetType); } /// /// 生成类型默认值:非空值类型返回 Activator 默认实例,其余返回 null /// private static object? CreateDefaultValue(Type targetType) { return targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null ? Activator.CreateInstance(targetType) : null; } }