chore: 首次簽入 Thinkyu ASP.NET 專案

- 加入 Visual Studio / ASP.NET .gitignore
- 排除建置輸出、IDE 設定、NuGet packages、大型 MSI 安裝檔
This commit is contained in:
2026-09-10 09:42:37 +08:00
commit 577060bc78
2496 changed files with 501389 additions and 0 deletions
+463
View File
@@ -0,0 +1,463 @@
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;
}
}
}
@@ -0,0 +1,88 @@
using System;
using System.Linq;
namespace Education
{
public class : ImportBase
{
public ()
{
columns.Add(new ImportColumn("編號", 10));
columns.Add(new ImportColumn("姓名", 10));
columns.Add(new ImportColumn("身分證字號", 16));
columns.Add(new ImportColumn("藝名網名", 10));
columns.Add(new ImportColumn("聯絡電話", 16));
columns.Add(new ImportColumn("電子郵件", 25));
columns.Add(new ImportColumn("戶籍地址_郵編", 10));
columns.Add(new ImportColumn("戶籍地址_縣市", 10));
columns.Add(new ImportColumn("戶籍地址_鄉鎮市區", 10));
columns.Add(new ImportColumn("戶籍地址_街道地址", 30));
columns.Add(new ImportColumn("居住地址_郵編", 10));
columns.Add(new ImportColumn("居住地址_縣市", 10));
columns.Add(new ImportColumn("居住地址_鄉鎮市區", 10));
columns.Add(new ImportColumn("居住地址_街道地址", 30));
columns.Add(new ImportColumn("職務內容", 100));
columns.Add(new ImportColumn("合作滿意度", 100));
columns.Add(new ImportColumn("窗口", 100));
columns.Add(new ImportColumn("是否接受邀約", 10, fieldName: "接受邀約", transformFunction: BoolTransform));
columns.Add(new ImportColumn("聯繫狀況與報價", 100));
columns.Add(new ImportColumn("備註", 100));
}
public override T SelectExisting<T>(T item)
{
var row = item as Item;
if (row == null)
return default(T);
var dac = new DAC_非講師(connection);
Item existing = null;
if (!String.IsNullOrEmpty(row.))
{
existing = dac.SelectOne(row.).FirstOrDefault();
}
else
{
existing = dac.Existing(row., row.);
}
if (existing != null && existing is T tExisting)
return tExisting;
return default(T);
}
public override void InsertOne<T>(T item)
{
var row = item as Item;
if (row == null)
return;
var dac = new DAC_非講師(connection);
dac.InsertOne(row);
}
public override void UpdateOne<T>(T newItem, T existingItem)
{
var newRow = newItem as Item;
var existingRow = existingItem as Item;
if (newRow == null || existingRow == null)
return;
var dac = new DAC_非講師(connection);
newRow. = existingRow.;
newRow.DB_APPNO = existingRow.DB_APPNO;
dac.UpdateOne(newRow);
}
public override string ImportRowErrorMessage<T>(T item, string errorMessage)
{
var row = item as Item;
if (row != null)
{
return $"姓名:{row.姓名},身分證字號:{row.身分證字號},錯誤原因:{errorMessage}";
}
else
{
return base.ImportRowErrorMessage(item, errorMessage);
}
}
}
}
+533
View File
@@ -0,0 +1,533 @@
using Newtonsoft.Json;
using NPOI.SS.Formula.Functions;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using WebLib;
namespace Education
{
/// <summary>
/// 領據匯入類別
/// </summary>
public class : ImportBase
{
public ()
{
// 定義欄位
columns.Add(new ImportColumn("活動日期", 12, required: true));
columns.Add(new ImportColumn("身分別", 10, required: true));
columns.Add(new ImportColumn("領款人姓名", 10, required: true));
columns.Add(new ImportColumn("身分證字號", 15));
columns.Add(new ImportColumn("憑證類別", 10, required: true));
columns.Add(new ImportColumn("計畫代號", 15, required: true));
columns.Add(new ImportColumn("活動地點", 20));
columns.Add(new ImportColumn("活動類別", 10));
columns.Add(new ImportColumn("付款方式", 10));
columns.Add(new ImportColumn("活動名稱/事由", 30, fieldName: "活動名稱事由"));
columns.Add(new ImportColumn("勞務費費用別", 15));
columns.Add(new ImportColumn("扣繳類別", 15));
columns.Add(new ImportColumn("單價", 10));
columns.Add(new ImportColumn("數量", 10));
columns.Add(new ImportColumn("單位", 10));
columns.Add(new ImportColumn("服務費", 10));
columns.Add(new ImportColumn("差旅票價資訊", 15, fieldName: "票價資訊"));
columns.Add(new ImportColumn("起點", 15));
columns.Add(new ImportColumn("訖點", 15));
columns.Add(new ImportColumn("捷運/公車", 10, fieldName: "捷運公車"));
columns.Add(new ImportColumn("火車", 10));
columns.Add(new ImportColumn("計程車", 10));
columns.Add(new ImportColumn("高鐵/飛機", 10, fieldName: "高鐵飛機"));
columns.Add(new ImportColumn("自行開車", 10));
columns.Add(new ImportColumn("過路費/停車費", 10, fieldName: "過路費停車費"));
columns.Add(new ImportColumn("住宿費", 10));
}
/// <summary>
/// 進行額外的資料驗證(欄位內容檢查)
/// </summary>
public List<string> ValidateContent(string filePath)
{
List<string> errors = new List<string>();
var workbook = LoadWorkbook(filePath);
var sheet = workbook.GetSheetAt(0);
// 從標頭列建立欄位名稱 → 欄位索引對照表
var columnMap = new Dictionary<string, int>();
var headerRow = 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;
}
}
// 差旅費數值欄位清單
var travelFeeColumns = new List<string> { "捷運/公車", "火車", "計程車", "高鐵/飛機", "自行開車", "過路費/停車費", "住宿費" };
var dac講師 = new DAC_講師(connection);
var dac非講師 = new DAC_非講師(connection);
for (int rowId = 1; rowId <= sheet.LastRowNum; rowId++)
{
var row = sheet.GetRow(rowId);
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;
// 驗證活動日期
if (!string.IsNullOrEmpty(cellValues["活動日期"]))
{
// 日期要能接受兩種:字串與 Excel 裡日期
if (!.IsValidDate(cellValues["活動日期"]))
{
errors.Add($"第 {rowId + 1} 列,欄位「活動日期」:日期格式不正確,值為「{cellValues[""]}」");
}
}
else
{
errors.Add($"第 {rowId + 1} 列,欄位「活動日期」:必填欄位不可為空");
}
// 驗證身分別
if (string.IsNullOrEmpty(cellValues["身分別"]))
{
errors.Add($"第 {rowId + 1} 列,欄位「身分別」:必填欄位不可為空");
}
else if (cellValues["身分別"] != "講師" && cellValues["身分別"] != "非講師")
{
errors.Add($"第 {rowId + 1} 列,欄位「身分別」:只允許「講師」或「非講師」,實際值為「{cellValues[""]}」");
}
// 驗證領款人姓名
if (string.IsNullOrEmpty(cellValues["領款人姓名"]))
{
errors.Add($"第 {rowId + 1} 列,欄位「領款人姓名」:必填欄位不可為空");
}
else
{
var _非講師編號 = string.Empty;
if (cellValues["身分別"] == "講師")
{
var t = dac講師.Select(false, cellValues["領款人姓名"], null, null)
.Where(x => string.Compare(x., cellValues["領款人姓名"]) == 0
&& string.Compare(x., cellValues["身分證字號"]) == 0)
.FirstOrDefault();
if (t != null)
_非講師編號 = t..ToString();
}
else if (cellValues["身分別"] == "非講師")
{
var t = dac非講師.Select(false, null, cellValues["領款人姓名"], null, null, null)
.Where(x => string.Compare(x., cellValues["領款人姓名"]) == 0
&& string.Compare(x., cellValues["身分證字號"]) == 0)
.FirstOrDefault();
if (t != null)
_非講師編號 = t.;
}
if (_非講師編號 == string.Empty)
{
errors.Add($"第 {rowId + 1} 列,欄位「領款人姓名」:不存在於系統內");
}
}
// 驗證憑證類別
if (string.IsNullOrEmpty(cellValues["憑證類別"]))
{
errors.Add($"第 {rowId + 1} 列,欄位「憑證類別」:必填欄位不可為空");
}
else if (!.IsValidProofType(cellValues["憑證類別"]))
{
errors.Add($"第 {rowId + 1} 列,欄位「憑證類別」:無效的值「{cellValues[""]}」,允許值為「領據」、「發票」、「收據」");
}
// 驗證計畫代號
if (string.IsNullOrEmpty(cellValues["計畫代號"]))
{
errors.Add($"第 {rowId + 1} 列,欄位「計畫代號」:必填欄位不可為空");
}
else
{
var planCode = cellValues["計畫代號"];
bool isTYCode = System.Text.RegularExpressions.Regex.IsMatch(planCode, @"^TY\d{4}$");
if (!isTYCode && !.IsValidProjectCode(planCode))
{
errors.Add($"第 {rowId + 1} 列,欄位「計畫代號」:計畫代號「{planCode}」不存在於系統中");
}
}
// 驗證付款方式(選填)
if (!string.IsNullOrEmpty(cellValues["付款方式"]) && !.IsValidPaymentMethod(cellValues["付款方式"]))
{
errors.Add($"第 {rowId + 1} 列,欄位「付款方式」:無效的值「{cellValues[""]}」,允許值為「匯款」、「支票」、「現金」");
}
// 驗證勞務費費用別(選填)
if (!string.IsNullOrEmpty(cellValues["勞務費費用別"]) && !.IsValidLaborExpenseType(cellValues["勞務費費用別"]))
{
errors.Add($"第 {rowId + 1} 列,欄位「勞務費費用別」:無效的值「{cellValues[""]}」");
}
// 驗證扣繳類別(選填)
if (!string.IsNullOrEmpty(cellValues["扣繳類別"]))
{
cellValues["扣繳類別"] = .MapValue(cellValues["扣繳類別"]);
if (!.IsValidWithholdingCategory(cellValues["扣繳類別"]))
{
errors.Add($"第 {rowId + 1} 列,欄位「扣繳類別」:無效的值「{cellValues[""]}」");
}
}
// 驗證扣繳類別於費用別的對應關係
if (cellValues["勞務費費用別"] == "演講費" || cellValues["勞務費費用別"] == "撰稿費" && cellValues["扣繳類別"] == "50")
{
errors.Add($"第 {rowId + 1} 列,欄位「扣繳類別」:扣繳類別只能是執行業務所得");
}
// 驗證單價、數量、服務費(選填)
int = 0;
decimal = 0;
bool = false;
if (!string.IsNullOrEmpty(cellValues["單價"]))
{
if (!.IsValidDecimal(cellValues["單價"]))
errors.Add($"第 {rowId + 1} 列,欄位「單價」:應為數值,實際值為「{cellValues[""]}」");
else
{
= DAC.GetInt32(cellValues["單價"]);
= true;
}
}
if (!string.IsNullOrEmpty(cellValues["數量"]))
{
if (!.IsValidDecimal(cellValues["數量"]))
errors.Add($"第 {rowId + 1} 列,欄位「數量」:應為數值,實際值為「{cellValues[""]}」");
else
{
= DAC.GetDecimal(cellValues["數量"]);
= true;
}
}
if (!string.IsNullOrEmpty(cellValues["服務費"]))
{
if (!.IsValidDecimal(cellValues["服務費"]))
errors.Add($"第 {rowId + 1} 列,欄位「服務費」:應為數值,實際值為「{cellValues[""]}」");
else
= true;
}
// 有勞務費金額但費用別或單位未填
if ( && string.IsNullOrEmpty(cellValues["勞務費費用別"]))
errors.Add($"第 {rowId + 1} 列,欄位「勞務費費用別」:已輸入金額,此欄位不可為空");
if ( && string.IsNullOrEmpty(cellValues["單位"]))
errors.Add($"第 {rowId + 1} 列,欄位「單位」:已輸入金額,此欄位不可為空");
// 合計金額驗證
int = (int)((decimal) * );
if (cellValues["扣繳類別"] == "50" && >= PublicVariable.) {
errors.Add($"第 {rowId + 1} 列:薪資不得超過基本工資 {PublicVariable.領據基本工資}(含),超過請拆成不同日期的領據");
}
if ((cellValues["扣繳類別"].StartsWith("9A") || cellValues["扣繳類別"].StartsWith("9B")) && >= PublicVariable.) {
errors.Add($"第 {rowId + 1} 列:執行業務所得不得超過 {PublicVariable.領據執行業務所得課稅門檻}(含),超過請拆成不同日期的領據");
}
// 驗證差旅費的數值欄位
foreach (var colTitle in travelFeeColumns)
{
if (cellValues.ContainsKey(colTitle) && !string.IsNullOrEmpty(cellValues[colTitle]) && !.IsValidDecimal(cellValues[colTitle]))
{
errors.Add($"第 {rowId + 1} 列,欄位「{colTitle}」:應為數值,實際值為「{cellValues[colTitle]}」");
}
}
}
return errors;
}
/// <summary>
/// 驗證重複資料
/// - 同一檔案內重複(允許匯入,但回傳警告)
/// - 本次匯入資料與資料庫現有資料重複的總筆數超過 5 筆(不允許匯入,回傳錯誤)
/// 重複條件:活動日期 + 領款人姓名 + 計畫代號 + 活動名稱/事由
/// </summary>
public void ValidateDuplicates(string filePath, DbConnection conn, out List<string> errors, out List<string> warnings)
{
errors = new List<string>();
warnings = new List<string>();
var workbook = LoadWorkbook(filePath);
var sheet = workbook.GetSheetAt(0);
// 建立欄位名稱 → 欄位索引對照表
var columnMap = new Dictionary<string, int>();
var headerRow = 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;
}
}
// 收集每一列的鍵值:key → List<int> Excel 顯示列號
var fileKeyRows = new Dictionary<string, List<int>>();
for (int rowId = 1; rowId <= sheet.LastRowNum; rowId++)
{
var row = sheet.GetRow(rowId);
if (row == null) break;
string GetCellValue(string colTitle)
{
if (!columnMap.ContainsKey(colTitle)) return "";
var c = row.GetCell(columnMap[colTitle]);
return c == null ? "" : excel.CellValueToString(c).Trim();
}
string Raw = GetCellValue("活動日期");
string = GetCellValue("領款人姓名");
string = GetCellValue("計畫代號");
string = GetCellValue("活動名稱/事由");
// 若整列為空則停止
if (string.IsNullOrEmpty(Raw) && string.IsNullOrEmpty() && string.IsNullOrEmpty())
break;
// 重複鍵需要四個欄位都有值才做比對
if (string.IsNullOrEmpty(Raw) || string.IsNullOrEmpty() || string.IsNullOrEmpty())
continue;
string key = string.Format("{0}|{1}|{2}|{3}", Raw, , , );
if (!fileKeyRows.ContainsKey(key))
fileKeyRows[key] = new List<int>();
fileKeyRows[key].Add(rowId + 1); // +1 因為 rowId=1 對應 Excel 第 2 列(含標頭)
}
// ── 1. 檔案內重複(警告,不阻擋)────────────────────────────────
foreach (var kv in fileKeyRows)
{
if (kv.Value.Count <= 1) continue;
string[] parts = kv.Key.Split('|');
warnings.Add(string.Format(
"檔案內重複:活動日期「{0}」、領款人「{1}」、計畫代號「{2}」、活動名稱事由「{3}」出現於第 {4} 列(共 {5} 筆)",
parts[0], parts[1], parts[2], parts[3],
string.Join("、", kv.Value.ConvertAll(r => r.ToString()).ToArray()),
kv.Value.Count));
}
// ── 2. 與資料庫現有資料比對 ──────────────────────────────────────
// 每個唯一鍵查一次 DB,累計所有重複筆數;每筆有重複的都顯示給使用者
var dac = new DAC_領據(conn);
int totalDbDuplicates = 0;
var dbDuplicateMessages = new List<string>();
foreach (var kv in fileKeyRows)
{
string[] parts = kv.Key.Split('|');
if (!DateTime.TryParse(parts[0], out DateTime )) continue;
string DB = parts[1];
string DB = parts[2];
string DB = parts[3];
int dbCount = dac.CountDuplicates(, DB, DB, DB);
if (dbCount <= 0) continue;
totalDbDuplicates += dbCount;
dbDuplicateMessages.Add(string.Format(
"與現有資料重複:活動日期「{0}」、領款人「{1}」、計畫代號「{2}」、活動名稱事由「{3}」已有 {4} 筆",
parts[0], DB, DB, DB, dbCount));
}
// 不論是否超過 5 筆,有重複的都要顯示;超過 5 筆則放入 errors 阻擋匯入
if (dbDuplicateMessages.Count > 0)
{
if (totalDbDuplicates > 5)
{
errors.Add(string.Format("與現有資料重複共 {0} 筆(超過 5 筆),不允許匯入:", totalDbDuplicates));
errors.AddRange(dbDuplicateMessages);
}
else
{
warnings.Add(string.Format("與現有資料重複共 {0} 筆:", totalDbDuplicates));
warnings.AddRange(dbDuplicateMessages);
}
}
}
public override void InsertOne<T>(T item)
{
var row = item as Item;
if (row == null)
return;
using (var ts = DAC.NewTransactionScope())
{
try
{
// 1. 插入領據資料
var dac領據 = new DAC_領據(connection);
var dac講師 = new DAC_講師(connection);
var dac非講師 = new DAC_非講師(connection);
var dacBPAA = new DAC_BPAA();
var Item = new Item();
var _非講師編號 = string.Empty;
if (row. == "講師")
{
var t = dac講師.Select(false, row., null, null)
.Where(x => string.Compare(x., row.) == 0
&& string.Compare(x., row.) == 0)
.FirstOrDefault();
if (t != null)
_非講師編號 = t..ToString();
}
else if (row. == "非講師")
{
var t = dac非講師.Select(false, null, row., null, null, null)
.Where(x => string.Compare(x., row.) == 0
&& string.Compare(x., row.) == 0)
.FirstOrDefault();
if (t != null)
_非講師編號 = t.;
}
if (_非講師編號 == string.Empty)
{
throw new Exception($"插入資料時發生錯誤: {row.領款人姓名} 不存在於系統內");
}
var dataTable = dacBPAA.GetBpaaWithTYPlusNone();
foreach (DataRow r in dataTable.Rows)
{
if (r["BPPYN"].ToString() == row.)
{
Item. = DAC.GetString(r["BPPYM"]);
}
}
Item. = row..Value;
Item. = row.;
Item._非講師編號 = _非講師編號;
Item. = row.;
Item. = row.;
Item. = row.;
Item. = row.;
Item. = row.;
Item. = row.;
Item. = row.;
Item. = row.;
Item. = dac領據.CurrentUserId;
dac領據.InsertOne(Item);
int = Item.;
// 2. 如果有勞務費資料,插入勞務費表
if (!string.IsNullOrEmpty(row.) || row..HasValue || row..HasValue || row..HasValue)
{
var dac勞務費 = new DAC_勞務費(connection);
var Item = new Item();
var a扣繳類別 = .MapValue(row.);
var = 0;
var result = new Services.().CalcTax((int)((decimal)row. * row.), a扣繳類別);
if (result.Success)
{
= DAC.GetInt32(result.Data);
}
Item. = ;
Item. = row.;
Item. = a扣繳類別;
Item. = row..HasValue ? row..Value : 0;
Item. = row..HasValue ? row..Value : 0;
Item. = row.;
Item. = row..HasValue ? row..Value : 0;
Item. = ;
dac勞務費.InsertOne(Item);
}
// 3. 如果有差旅費資料,插入差旅費表
if (!string.IsNullOrEmpty(row.) || row..HasValue || row..HasValue ||
row..HasValue || row..HasValue || row..HasValue ||
row..HasValue || row.宿.HasValue)
{
var dac差旅費 = new DAC_差旅費(connection);
var Item = new Item();
Item. = ;
Item. = _費用別.宿;
Item. = row.;
Item. = row.;
Item. = row.;
Item. = row..HasValue ? row..Value : 0;
Item. = row..HasValue ? row..Value : 0;
Item. = row..HasValue ? row..Value : 0;
Item. = row..HasValue ? row..Value : 0;
Item. = row..HasValue ? row..Value : 0;
Item. = row..HasValue ? row..Value : 0;
Item.宿 = row.宿.HasValue ? row.宿.Value : 0;
dac差旅費.InsertOne(Item);
}
ts.Complete();
}
catch (Exception ex)
{
throw new Exception($"插入資料時發生錯誤: {ex.Message}", ex);
}
}
}
public override string ImportRowErrorMessage<T>(T item, string errorMessage)
{
var row = item as Item;
if (row != null)
{
return $"領款人:{row.領款人姓名},憑證類別:{row.憑證類別},計畫代號:{row.計畫代號},錯誤原因:{errorMessage}";
}
else
{
return base.ImportRowErrorMessage(item, errorMessage);
}
}
}
}
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
namespace Education
{
/// <summary>
/// 領據匯入資料項目
/// </summary>
public class Item
{
public DateTime? { get; set; }
public string { get; set; }
public string { get; set; }
public string { get; set; }
public string { get; set; }
public string { get; set; }
public string { get; set; }
public string { get; set; }
public string { get; set; }
public string { get; set; }
public string { get; set; }
// 勞務費欄位
public string { get; set; }
public string { get; set; }
public int? { get; set; }
public decimal? { get; set; }
public string { get; set; }
public int? { get; set; }
// 差旅費欄位
public string { get; set; }
public string { get; set; }
public string { get; set; }
public int? { get; set; }
public int? { get; set; }
public int? { get; set; }
public int? { get; set; }
public int? { get; set; }
public int? { get; set; }
public int? 宿 { get; set; }
}
}