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
});
}
}
///
/// 處理匯入檔案 - 讀取 Excel 檔案並進行驗證,使用 NPOI
///
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> previewData = new List>();
List validationErrors = new List();
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 rowErrors = new List();
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();
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
});
}
}
///
/// 驗證中華民國身分證字號
/// 規則:第一碼為英文字母(A-Z),後接九位數字
/// 使用檢查碼算法驗證
///
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;
}
///
/// 驗證電子郵件格式
///
private bool ValidateEmail(string email)
{
if (string.IsNullOrEmpty(email))
return true;
return System.Text.RegularExpressions.Regex.IsMatch(
email.Trim(),
@"^[^\s@]+@[^\s@]+\.[^\s@]+$");
}
///
/// 取得單元格的值(文字)
///
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 "";
}
}
///
/// 判斷是否為表頭行
///
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;
}
///
/// 發送 JSON 響應
///
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; }
}
}
}