Files
thinkyu/branches/1150807/Import/ImportBase.cs
T
sryang 577060bc78 chore: 首次簽入 Thinkyu ASP.NET 專案
- 加入 Visual Studio / ASP.NET .gitignore
- 排除建置輸出、IDE 設定、NuGet packages、大型 MSI 安裝檔
2026-09-10 09:42:37 +08:00

463 lines
17 KiB
C#

using NPOI.HSSF.UserModel;
using NPOI.SS.Formula.Functions;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.IO;
using System.Linq;
using System.Web;
using WebLib;
namespace Education
{
public class ImportBase
{
protected WebLib.Excel excel;
protected List<ImportColumn> columns;
private static string importFilePath = "~/DocUpload/Import";
protected DbConnection connection;
protected static List<string> trueValues = new List<string>() { "Y", "V", "T", "1", "YES", "TRUE" };
public const double MinOADate = 43831.0; // 2020/01/01
public const double MaxOADate = 2958465.0; // 9999/12/31
public ImportBase()
{
excel = new WebLib.Excel();
columns = new List<ImportColumn>();
}
public DbConnection Connection
{
get { return connection; }
set { connection = value; }
}
public static string ImportFilePath
{
get
{
var fullPath = HttpContext.Current.Server.MapPath(importFilePath);
if (!Directory.Exists(fullPath))
{
Directory.CreateDirectory(fullPath);
}
return fullPath;
}
}
public MemoryStream ImportExample()
{
var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("匯入範例");
excel.Sheet = sheet;
int col = 0;
foreach (var column in columns)
{
excel.SetCellValue(0, col, column.Title);
excel.SetColumnWidthInChars(col, column.Width);
col++;
}
MemoryStream ms = new MemoryStream();
workbook.Write(ms);
return ms;
}
public IWorkbook LoadWorkbook(string filePath)
{
using (FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
if (Path.GetExtension(filePath).ToLower() == ".xls")
{
return new HSSFWorkbook(file);
}
else if (Path.GetExtension(filePath).ToLower() == ".xlsx")
{
return new XSSFWorkbook(file);
}
else
{
throw new Exception("不支援的檔案格式");
}
}
}
public List<string> Validate(string filePath, Type type, out int rowCount)
{
// 1. 開啟 Excel 檔案
var workbook = LoadWorkbook(filePath);
// 2. 讀取第一個工作表
var sheet = workbook.GetSheetAt(0);
// 3. 驗證欄位名稱
var headerRow = sheet.GetRow(0);
var errors = ValidateColumns(headerRow, out var columnMap);
// 4. 驗證每一列資料
rowCount = 0;
for (int rowId = 1; rowId <= sheet.LastRowNum; rowId++)
{
var row = sheet.GetRow(rowId);
if (row == null)
{
break;
}
var rowErrors = ValidateRow(row, type, columnMap, out bool stop);
foreach (var err in rowErrors)
{
errors.Add($"第 {rowId} 列:{err}");
}
rowCount++;
if (stop)
{
break;
}
}
if (rowCount == 0)
{
errors.Add("沒有任何資料列");
}
// 5. 回傳錯誤訊息清單
return errors;
}
public List<string> ValidateColumns(IRow headerRow, out Dictionary<string, int> columnMap)
{
List<string> errors = new List<string>();
columnMap = new Dictionary<string, int>();
// 建立標題 → 欄位索引的對照表
if (headerRow != null)
{
for (int i = 0; i <= headerRow.LastCellNum; i++)
{
var cell = headerRow.GetCell(i);
if (cell == null) continue;
string title = cell.ToString().Trim();
if (!string.IsNullOrEmpty(title) && !columnMap.ContainsKey(title))
{
columnMap[title] = i;
}
}
}
// 確認每個必要欄位都存在於標頭列
foreach (var column in columns)
{
if (!columnMap.ContainsKey(column.Title))
{
errors.Add($"找不到欄位「{column.Title}」");
}
}
return errors;
}
public List<string> ValidateRow(IRow dataRow, Type type, Dictionary<string, int> columnMap, out bool stop)
{
stop = false;
List<string> errors = new List<string>();
// 以欄位名稱取得每欄的值
var cellValues = new Dictionary<string, string>();
foreach (var column in columns)
{
string cellValue = "";
if (columnMap.ContainsKey(column.Title))
{
var cell = dataRow.GetCell(columnMap[column.Title]);
cellValue = cell == null ? "" : excel.CellValueToString(cell);
}
cellValues[column.Title] = cellValue;
}
// 若所有欄位皆為空則回傳停止旗標
bool allEmpty = true;
foreach (var v in cellValues.Values)
{
if (!string.IsNullOrEmpty(v)) { allEmpty = false; break; }
}
if (allEmpty)
{
stop = true;
return errors;
}
foreach (var column in columns)
{
string cellValue = cellValues[column.Title];
if (column.Required && string.IsNullOrEmpty(cellValue))
{
errors.Add($"欄位「{column.Title}」為必填欄位");
}
// 應用 TransformFunction(如果設定)
if (column.TransformFunction != null)
{
cellValue = column.TransformFunction(cellValue)?.ToString() ?? "";
}
if (!string.IsNullOrEmpty(cellValue))
{
var property = type.GetProperty(column.FieldName);
if (property != null)
{
try
{
var targetType = property.PropertyType;
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
targetType = Nullable.GetUnderlyingType(targetType);
}
if (targetType == typeof(DateTime))
{
ParseExcelDate(cellValue);
}
else
{
Convert.ChangeType(cellValue, targetType);
}
}
catch
{
var typeName = property.PropertyType.Name;
if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var underlying = Nullable.GetUnderlyingType(property.PropertyType);
typeName = $"{underlying.Name}(可空)";
}
errors.Add($"欄位「{column.Title}」的值「{cellValue}」應該是 {typeName} 類型".Replace("<", "&lt;").Replace(">", "&gt;"));
}
}
}
}
return errors;
}
public List<T> ReadRows<T>(string filePath, int beginRow = 1) where T : new()
{
var workbook = LoadWorkbook(filePath);
excel.Sheet = workbook.GetSheetAt(0);
var result = new List<T>();
// 從標頭列建立欄位名稱 → 欄位索引對照表
var columnMap = new Dictionary<string, int>();
var headerRow = excel.Sheet.GetRow(0);
if (headerRow != null)
{
for (int i = 0; i <= headerRow.LastCellNum; i++)
{
var cell = headerRow.GetCell(i);
if (cell == null) continue;
string title = cell.ToString().Trim();
if (!string.IsNullOrEmpty(title) && !columnMap.ContainsKey(title))
{
columnMap[title] = i;
}
}
}
for (int r = beginRow; r <= excel.Sheet.LastRowNum; r++)
{
var row = excel.Sheet.GetRow(r);
if (row == null)
{
break;
}
// 以欄位名稱取得每欄的值
var cellValues = new Dictionary<string, string>();
foreach (var column in columns)
{
string cellValue = "";
if (columnMap.ContainsKey(column.Title))
{
var cell = row.GetCell(columnMap[column.Title]);
cellValue = cell == null ? "" : excel.CellValueToString(cell);
}
cellValues[column.Title] = cellValue;
}
// 若所有欄位皆為空則停止
bool allEmpty = true;
foreach (var v in cellValues.Values)
{
if (!string.IsNullOrEmpty(v)) { allEmpty = false; break; }
}
if (allEmpty)
{
break;
}
var item = new T();
foreach (var column in columns)
{
string cellValue = cellValues[column.Title];
// 假設 T 有對應的屬性名稱與欄位標題相同
var property = typeof(T).GetProperty(column.FieldName);
if (property != null)
{
object valueToSet = null;
if (column.TransformFunction != null)
{
valueToSet = column.TransformFunction(cellValue);
}
else
{
if (string.IsNullOrEmpty(cellValue))
{
if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
valueToSet = null;
}
else
{
// 對於非 Nullable 類型,如果為空則跳過設定
continue;
}
}
else
{
var targetType = property.PropertyType;
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
targetType = Nullable.GetUnderlyingType(targetType);
}
if (targetType == typeof(DateTime))
{
valueToSet = ParseExcelDate(cellValue);
}
else
{
valueToSet = Convert.ChangeType(cellValue, targetType);
}
}
}
property.SetValue(item, valueToSet, null);
}
}
result.Add(item);
}
return result;
}
private static DateTime ParseExcelDate(string value)
{
if (DateTime.TryParse(value, out DateTime dt))
{
return dt;
}
if (double.TryParse(value, out double d) && d >= MinOADate && d <= MaxOADate)
{
return DateTime.FromOADate(d);
}
throw new FormatException($"無法將「{value}」轉換為日期");
}
public virtual T SelectExisting<T>(T item) where T : new()
{
throw new NotImplementedException("請在子類別中實作此方法以選取現有資料");
}
public virtual void InsertOne<T>(T item) where T : new()
{
throw new NotImplementedException("請在子類別中實作此方法以新增資料");
}
public virtual void UpdateOne<T>(T newItem, T existingItem) where T : new()
{
throw new NotImplementedException("請在子類別中實作此方法以更新資料");
}
public virtual string ImportRowErrorMessage<T>(T item, string errorMessage) where T : new()
{
return $"資料 {item.ToString()} 匯入失敗,錯誤原因:{errorMessage}";
}
public bool Import<T>(string filePath,
out string message,
int beginRow = 1) where T : new()
{
var importErrors = new List<string>();
message = "";
var insertCount = 0;
var updateCount = 0;
var rows = ReadRows<T>(filePath, beginRow);
if (rows.Count == 0)
{
importErrors.Add("匯入檔案中沒有任何資料");
return false;
}
using (var ts = DAC.NewTransactionScope())
{
foreach (var row in rows)
{
try
{
var existing = SelectExisting(row);
if (existing == null)
{
InsertOne(row);
insertCount++;
}
else
{
UpdateOne(row, existing);
updateCount++;
}
}
catch (Exception ex)
{
importErrors.Add(ImportRowErrorMessage(row, ex.Message));
}
}
}
if (importErrors.Count == 0)
{
message = $"資料匯入成功,共新增 {insertCount} 筆資料,修改 {updateCount} 筆資料。";
return true;
}
else
{
message = $"資料匯入完成,共新增 {insertCount} 筆資料,修改 {updateCount} 筆資料,但有以下錯誤:<br/>" + string.Join("<br/>", importErrors.ToArray());
return false;
}
}
/// <summary>
/// 真假值轉換函數,把代表真值的字串轉換成 1,否則轉換成 0
/// </summary>
protected TransformFunction BoolTransform = (input) =>
{
if (string.IsNullOrEmpty(input)) return 0;
var inputStr = input.ToUpper();
return trueValues.Contains(inputStr) ? 1 : 0;
};
}
// 新增委派定義
public delegate object TransformFunction(string input);
public class ImportColumn
{
public string Title { get; set; }
public int Width { get; set; }
public string FieldName { get; set; }
public bool Required { get; set; }
public TransformFunction TransformFunction { get; set; }
public ImportColumn(string title, int width, bool required = false, string fieldName = null, TransformFunction transformFunction = null)
{
Title = title;
Width = width;
Required = required;
FieldName = fieldName ?? title;
TransformFunction = transformFunction;
}
}
}