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
@@ -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));
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);
}
}
}
}