马梓彭 2c972df6d5 删除MiniExcel统一采用NPOI
对接完善打分系统工作台
2026-09-04 17:15:03 +08:00

197 lines
8.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Globalization;
using System.Reflection;
using NPOI.SS.UserModel;
namespace YD_Event.Application.Common.Excel;
/// <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();
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;
}
}