207 lines
8.6 KiB
C#
Raw Normal View History

using System.Globalization;
using System.Reflection;
using NPOI.SS.UserModel;
namespace YD_Event.Application.Common.Excel;
2026-10-10 09:04:52 +08:00
/// <summary>
/// 需要保留 Excel 物理行号的行模型。
/// </summary>
public interface IExcelRowLocation
{
int ExcelRowNumber { get; set; }
}
/// <summary>
/// Excel 表头列名映射特性,标注属性所对应的表头单元格文本
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class ExcelHeaderAttribute : Attribute
{
/// <summary>
/// 表头文本
/// </summary>
public string Name { get; }
public ExcelHeaderAttribute(string name)
{
Name = name;
}
}
/// <summary>
/// 基于 NPOI 的 Excel 读取器,统一支持 .xls(HSSF) 与 .xlsx(XSSF),按表头名称映射为对象
/// </summary>
public static class NpoiExcelReader
{
/// <summary>
/// 按文件流自动识别格式创建工作簿,调用方无需区分 .xls/.xlsx
/// </summary>
public static IWorkbook CreateWorkbook(Stream stream)
{
// 缓冲到内存,避免上传流不可寻址导致 NPOI 无法按文件头识别格式
var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
memoryStream.Position = 0;
return WorkbookFactory.Create(memoryStream);
}
/// <summary>
/// 读取指定名称的工作表
/// </summary>
public static List<T> ReadSheet<T>(IWorkbook workbook, string sheetName, int headerSearchRows = 10)
where T : new()
{
var sheet = workbook.GetSheet(sheetName)
?? throw new InvalidDataException($"缺少工作表:{sheetName}");
return ReadSheet<T>(sheet, headerSearchRows);
}
/// <summary>
/// 读取指定序号的工作表(序号从0开始)
/// </summary>
public static List<T> ReadSheet<T>(IWorkbook workbook, int sheetIndex, int headerSearchRows = 10)
where T : new()
{
if (sheetIndex < 0 || sheetIndex >= workbook.NumberOfSheets)
throw new InvalidDataException($"工作表序号超出范围:{sheetIndex}");
return ReadSheet<T>(workbook.GetSheetAt(sheetIndex), headerSearchRows);
}
/// <summary>
/// 读取工作表:在前若干行内定位表头行,按表头名称逐行映射为对象,整行为空则跳过
/// </summary>
public static List<T> ReadSheet<T>(ISheet sheet, int headerSearchRows = 10) where T : new()
{
var headerMap = BuildHeaderMap<T>();
var formatter = new DataFormatter();
var headerRow = LocateHeaderRow(sheet, headerMap.Keys, headerSearchRows, formatter)
?? throw new InvalidDataException($"工作表「{sheet.SheetName}」缺少表头");
// 列序号到属性的映射
var columnProperties = new Dictionary<int, PropertyInfo>();
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<T>();
for (var rowIndex = headerRow.RowNum + 1; rowIndex <= sheet.LastRowNum; rowIndex++)
{
var row = sheet.GetRow(rowIndex);
if (row == null) continue;
var item = new T();
2026-10-10 09:04:52 +08:00
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;
}
/// <summary>
/// 构建表头文本到属性的映射
/// </summary>
private static Dictionary<string, PropertyInfo> BuildHeaderMap<T>()
{
var map = new Dictionary<string, PropertyInfo>();
foreach (var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
var header = property.GetCustomAttribute<ExcelHeaderAttribute>();
if (header != null) map[header.Name] = property;
}
return map;
}
/// <summary>
/// 在表头搜索范围内,取命中映射表头数量最多的一行作为表头行
/// </summary>
private static IRow? LocateHeaderRow(ISheet sheet, ICollection<string> 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;
}
/// <summary>
/// 单元格值按目标属性类型转换;空白单元格对引用/可空类型返回 null,对非空值类型返回默认值
/// </summary>
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);
}
/// <summary>
/// 生成类型默认值:非空值类型返回 Activator 默认实例,其余返回 null
/// </summary>
private static object? CreateDefaultValue(Type targetType)
{
return targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null
? Activator.CreateInstance(targetType)
: null;
}
}