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

386 lines
14 KiB
C#
Raw Permalink 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 NPOI.SS.UserModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Text.RegularExpressions;
namespace 報到系統
{
public class FileUploadHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
try
{
string action = context.Request.QueryString["action"];
if (action == "ProcessImportFile")
{
ProcessImportFile(context);
}
else
{
SendJsonResponse(context, new BaseResponse
{
Success = false,
Message = "無效的操作"
});
}
}
catch (Exception ex)
{
SendJsonResponse(context, new BaseResponse
{
Success = false,
Message = "處理請求發生錯誤:" + ex.Message
});
}
}
/// <summary>
/// 處理匯入檔案 - 讀取 Excel 檔案並進行驗證,使用 NPOI
/// </summary>
private void ProcessImportFile(HttpContext context)
{
try
{
string courseIDStr = context.Request.QueryString["courseID"];
if (!int.TryParse(courseIDStr, out int courseID) || courseID <= 0)
{
SendJsonResponse(context, new BaseResponse
{
Success = false,
Message = "活動編號為必填欄位"
});
return;
}
// 檢查是否有上傳檔案
if (context.Request.Files.Count == 0)
{
SendJsonResponse(context, new BaseResponse
{
Success = false,
Message = "沒有上傳檔案"
});
return;
}
HttpPostedFile file = context.Request.Files[0];
if (file.ContentLength == 0)
{
SendJsonResponse(context, new BaseResponse
{
Success = false,
Message = "檔案為空"
});
return;
}
// 驗證檔案格式
string fileName = Path.GetFileName(file.FileName);
if (!fileName.ToLower().EndsWith(".xlsx") && !fileName.ToLower().EndsWith(".xls"))
{
SendJsonResponse(context, new BaseResponse
{
Success = false,
Message = "只支援 .xlsx 或 .xls 格式的 Excel 檔案"
});
return;
}
// 使用 NPOI 讀取 Excel 檔案
IWorkbook workbook = WorkbookFactory.Create(file.InputStream);
ISheet sheet = workbook.GetSheetAt(0);
List<Dictionary<string, object>> previewData = new List<Dictionary<string, object>>();
List<string> validationErrors = new List<string>();
int headerRowIndex = -1;
// 偵測表頭並讀取資料
for (int rowIndex = 0; rowIndex <= sheet.LastRowNum; rowIndex++)
{
IRow row = sheet.GetRow(rowIndex);
if (row == null) continue;
// 欄位順序: 序號(0), 學員姓名(1), 手機號(2), 身分別(5), 身分證字號(6), 餐點(7), 電子郵件(8), 失業給付認定次數(9), 性別(10)
string indexValue = GetCellValue(row.GetCell(0))?.Trim();
string nameValue = GetCellValue(row.GetCell(1))?.Trim();
string mobileValue = GetCellValue(row.GetCell(2))?.Trim();
string idTypeValue = GetCellValue(row.GetCell(5))?.Trim();
string idNumberValue = GetCellValue(row.GetCell(6))?.Trim();
string mealTypeValue = GetCellValue(row.GetCell(7))?.Trim();
string emailValue = GetCellValue(row.GetCell(8))?.Trim();
string lossJobTimesValue = GetCellValue(row.GetCell(9))?.Trim();
string genderValue = GetCellValue(row.GetCell(10))?.Trim();
// 偵測表頭 - 檢查第1欄和第2欄
if (headerRowIndex < 0 && IsHeaderRow(indexValue, nameValue))
{
headerRowIndex = rowIndex;
continue;
}
// 跳過表頭之前的行
if (headerRowIndex < 0) continue;
// 跳過空行
if (string.IsNullOrEmpty(nameValue) && string.IsNullOrEmpty(mobileValue))
continue;
// 驗證資料
List<string> rowErrors = new List<string>();
int dataRowIndex = rowIndex - headerRowIndex;
if (string.IsNullOrEmpty(nameValue?.Trim()))
{
rowErrors.Add($"第 {dataRowIndex} 列:姓名為必填欄位");
validationErrors.Add(rowErrors[rowErrors.Count - 1]);
}
else if (nameValue.Length > 100)
{
rowErrors.Add($"第 {dataRowIndex} 列:姓名最長 100 個字元");
validationErrors.Add(rowErrors[rowErrors.Count - 1]);
}
if (string.IsNullOrEmpty(mobileValue?.Trim()))
{
rowErrors.Add($"第 {dataRowIndex} 列:手機號為必填欄位");
validationErrors.Add(rowErrors[rowErrors.Count - 1]);
}
else
{
// 清洗資料,只保留數字
mobileValue = Regex.Replace(mobileValue, @"\D", "");
if (!Regex.IsMatch(mobileValue.Trim(), @"^09\d{8}$"))
{
rowErrors.Add($"第 {dataRowIndex} 列:手機號格式必須為 09 開頭的 10 位數字");
validationErrors.Add(rowErrors[rowErrors.Count - 1]);
}
}
// 驗證身分證字號(如果有填寫)
if (!string.IsNullOrEmpty(idNumberValue?.Trim()))
{
if (!ValidateIDNumber(idNumberValue.ToUpper().Trim()))
{
rowErrors.Add($"第 {dataRowIndex} 列:身分證字號無效(格式應為:大寫英文字母 + 9位數字,且檢查碼需正確)");
validationErrors.Add(rowErrors[rowErrors.Count - 1]);
}
}
// 驗證電子郵件(如果有填寫)
if (!string.IsNullOrEmpty(emailValue?.Trim()))
{
if (!ValidateEmail(emailValue.Trim()))
{
rowErrors.Add($"第 {dataRowIndex} 列:電子郵件格式不正確(例如:user@example.com");
validationErrors.Add(rowErrors[rowErrors.Count - 1]);
}
}
// 驗證失業給付認定次數(如果有填寫)
if (!string.IsNullOrEmpty(lossJobTimesValue?.Trim()))
{
if (!int.TryParse(lossJobTimesValue, out int i))
{
rowErrors.Add($"第 {dataRowIndex} 列:失業給付認定次數必須為數字");
validationErrors.Add(rowErrors[rowErrors.Count - 1]);
}
if (i < 0)
{
rowErrors.Add($"第 {dataRowIndex} 列:失業給付認定次數必須大於或等於 0");
validationErrors.Add(rowErrors[rowErrors.Count - 1]);
}
}
var rowData = new Dictionary<string, object>();
rowData["Name"] = nameValue;
rowData["Mobile"] = mobileValue;
rowData["IDType"] = idTypeValue;
rowData["IDNumber"] = idNumberValue?.ToUpper();
rowData["MealType"] = mealTypeValue;
rowData["Email"] = emailValue;
rowData["LossJobTimes"] = lossJobTimesValue;
rowData["Gender"] = genderValue;
rowData["SignUpType"] = "預約";
rowData["Errors"] = rowErrors;
rowData["IsValid"] = rowErrors.Count == 0;
previewData.Add(rowData);
}
workbook.Close();
int validRowCount = 0;
foreach (var item in previewData)
{
if ((bool)item["IsValid"])
{
validRowCount++;
}
}
SendJsonResponse(context, new BaseResponse
{
Success = true,
Message = "檔案讀取成功",
Data = new
{
PreviewData = previewData,
ValidationErrors = validationErrors,
ValidRowCount = validRowCount,
TotalRowCount = previewData.Count
}
});
}
catch (Exception ex)
{
SendJsonResponse(context, new BaseResponse
{
Success = false,
Message = "處理匯入檔案發生錯誤:" + ex.Message
});
}
}
/// <summary>
/// 驗證中華民國身分證字號
/// 規則:第一碼為英文字母(A-Z),後接九位數字
/// 使用檢查碼算法驗證
/// </summary>
private bool ValidateIDNumber(string idNumber)
{
if (string.IsNullOrEmpty(idNumber))
return false;
idNumber = idNumber.Trim().ToUpper();
// 檢查長度
if (idNumber.Length != 10)
return false;
// 第一碼必須是英文字母
if (idNumber[0] < 'A' || idNumber[0] > 'Z')
return false;
// 後九碼必須是數字
for (int i = 1; i < 10; i++)
{
if (idNumber[i] < '0' || idNumber[i] > '9')
return false;
}
// 驗證檢查碼
// 字母對應的數字
int[] letterValues = {
10, 11, 12, 13, 14, 15, 16, 17, 34, 18, 19, 20, 21,
22, 35, 23, 24, 25, 26, 27, 28, 29, 32, 30, 31, 33
};
int code = letterValues[idNumber[0] - 'A'];
int sum = code / 10 + (code % 10) * 9;
int[] weights = { 8, 7, 6, 5, 4, 3, 2, 1 };
for (int i = 1; i <= 8; i++)
{
sum += (idNumber[i] - '0') * weights[i - 1];
}
sum += idNumber[9] - '0';
return sum % 10 == 0;
}
/// <summary>
/// 驗證電子郵件格式
/// </summary>
private bool ValidateEmail(string email)
{
if (string.IsNullOrEmpty(email))
return true;
return System.Text.RegularExpressions.Regex.IsMatch(
email.Trim(),
@"^[^\s@]+@[^\s@]+\.[^\s@]+$");
}
/// <summary>
/// 取得單元格的值(文字)
/// </summary>
private string GetCellValue(ICell cell)
{
if (cell == null) return "";
try
{
switch (cell.CellType)
{
case CellType.String:
return cell.StringCellValue;
case CellType.Numeric:
return cell.NumericCellValue.ToString();
case CellType.Boolean:
return cell.BooleanCellValue.ToString();
default:
return "";
}
}
catch
{
return "";
}
}
/// <summary>
/// 判斷是否為表頭行
/// </summary>
private bool IsHeaderRow(string col1, string col2)
{
// col1 是序號, col2 是學員姓名
string[] col1Keywords = { "序號", "編號", "index", "Index", "#" };
string[] col2Keywords = { "姓名", "名字", "name", "Name", "學員" };
bool isCol1Header = col1Keywords.Any(keyword =>
(col1 ?? "").ToLower().Contains(keyword.ToLower()));
bool isCol2Header = col2Keywords.Any(keyword =>
(col2 ?? "").ToLower().Contains(keyword.ToLower()));
return isCol1Header || isCol2Header;
}
/// <summary>
/// 發送 JSON 響應
/// </summary>
private void SendJsonResponse(HttpContext context, object data)
{
context.Response.ContentType = "application/json; charset=utf-8";
try
{
// 使用 JavaScriptSerializer 序列化為 JSON
System.Web.Script.Serialization.JavaScriptSerializer serializer =
new System.Web.Script.Serialization.JavaScriptSerializer();
// 設定遞迴深度以支援複雜物件
serializer.MaxJsonLength = int.MaxValue;
string json = serializer.Serialize(data);
context.Response.Write(json);
}
catch (Exception ex)
{
// 備用的簡單 JSON 序列化
context.Response.Write("{\"Success\":false,\"Message\":\"序列化錯誤:" + ex.Message.Replace("\"", "\\\"") + "\"}");
}
}
public bool IsReusable
{
get { return false; }
}
}
}