1235 lines
49 KiB
C#
1235 lines
49 KiB
C#
using NPOI.HSSF.UserModel;
|
||
using NPOI.SS.UserModel;
|
||
using NPOI.XSSF.UserModel;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net.NetworkInformation;
|
||
using System.Web;
|
||
using System.Web.Script.Services;
|
||
using System.Web.Services;
|
||
using System.Web.Services.Description;
|
||
using WebLib;
|
||
|
||
namespace 報到系統
|
||
{
|
||
[WebService(Namespace = "http://tempuri.org/")]
|
||
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
|
||
[System.ComponentModel.ToolboxItem(false)]
|
||
[ScriptService]
|
||
public class SignUpService : System.Web.Services.WebService
|
||
{
|
||
/// <summary>
|
||
/// 查詢參加人員
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse SearchSignUps(int pageIndex, int pageSize, int courseID, string name, string mobile)
|
||
{
|
||
try
|
||
{
|
||
// 計算起始索引 (pageIndex 從 1 開始)
|
||
int startRowIndex = (pageIndex - 1) * pageSize;
|
||
|
||
using (var conn = DAC.NewConnection())
|
||
{
|
||
DAC_SignUp dacSignUp = new DAC_SignUp(conn);
|
||
DAC_MessageLog dacMessageLog = new DAC_MessageLog(conn);
|
||
|
||
// 查詢資料
|
||
SignUpList result = dacSignUp.SelectPage(
|
||
noInputReturnAll: false,
|
||
startRowIndex: startRowIndex,
|
||
maximumRows: pageSize,
|
||
CourseID: courseID > 0 ? (int?)courseID : null,
|
||
SeqNo: null,
|
||
Name: name,
|
||
Mobile: mobile,
|
||
QRCode: null,
|
||
orderBy: "CourseID DESC, ID ASC"
|
||
);
|
||
|
||
// 取得總筆數
|
||
int totalCount = dacSignUp.SelectCount(
|
||
noInputReturnAll: false,
|
||
CourseID: courseID > 0 ? (int?)courseID : null,
|
||
SeqNo: null,
|
||
Name: name,
|
||
Mobile: mobile,
|
||
QRCode: null
|
||
);
|
||
|
||
// 查詢訊息發送紀錄
|
||
result.ForEach(p => {
|
||
var messageLogs = dacMessageLog.Select(false, p.CourseID, null, p.Name, p.Mobile);
|
||
bool hasSent = messageLogs.Find(q => q.Status == "SENT") != null;
|
||
p.MessageSending = messageLogs == null || messageLogs.Count == 0 ? "未發送" : (hasSent ? "發送成功" : "發送失敗");
|
||
});
|
||
|
||
return new BaseResponse
|
||
{
|
||
Success = true,
|
||
Message = "查詢成功",
|
||
Data = result ?? new SignUpList(),
|
||
TotalCount = totalCount
|
||
};
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "查詢參加人員發生錯誤:" + ex.Message,
|
||
Data = new SignUpList(),
|
||
TotalCount = 0
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 進階搜尋參加人員 - 支援序號、身分別、身分證字號、餐點、電子郵件等條件
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse SearchSignUpsAdvanced(int pageIndex, int pageSize, int courseID, int? seqNo, string name, string mobile,
|
||
string idType, string idNumber, bool idNumberEmpty, string mealType, string email, bool emailEmpty)
|
||
{
|
||
try
|
||
{
|
||
// 計算起始索引 (pageIndex 從 1 開始)
|
||
int startRowIndex = (pageIndex - 1) * pageSize;
|
||
|
||
using (var conn = DAC.NewConnection())
|
||
{
|
||
DAC_SignUp dacSignUp = new DAC_SignUp(conn);
|
||
DAC_MessageLog dacMessageLog = new DAC_MessageLog(conn);
|
||
|
||
// 查詢資料
|
||
SignUpList result = dacSignUp.SelectPageAdvanced(
|
||
noInputReturnAll: false,
|
||
startRowIndex: startRowIndex,
|
||
maximumRows: pageSize,
|
||
CourseID: courseID > 0 ? (int?)courseID : null,
|
||
SeqNo: seqNo,
|
||
Name: name,
|
||
Mobile: mobile,
|
||
IDType: idType,
|
||
IDNumber: idNumber,
|
||
IDNumberEmpty: idNumberEmpty,
|
||
MealType: mealType,
|
||
Email: email,
|
||
EmailEmpty: emailEmpty,
|
||
orderBy: "SeqNo ASC"
|
||
);
|
||
|
||
// 取得總筆數
|
||
int totalCount = dacSignUp.SelectCountAdvanced(
|
||
noInputReturnAll: false,
|
||
CourseID: courseID > 0 ? (int?)courseID : null,
|
||
SeqNo: seqNo,
|
||
Name: name,
|
||
Mobile: mobile,
|
||
IDType: idType,
|
||
IDNumber: idNumber,
|
||
IDNumberEmpty: idNumberEmpty,
|
||
MealType: mealType,
|
||
Email: email,
|
||
EmailEmpty: emailEmpty
|
||
);
|
||
|
||
// 查詢訊息發送紀錄
|
||
result.ForEach(p => {
|
||
var messageLogs = dacMessageLog.Select(false, p.CourseID, null, p.Name, p.Mobile);
|
||
bool hasSent = messageLogs.Find(q => q.Status == "SENT") != null;
|
||
p.MessageSending = messageLogs == null || messageLogs.Count == 0 ? "未發送" : (hasSent ? "發送成功" : "發送失敗");
|
||
});
|
||
|
||
return new BaseResponse
|
||
{
|
||
Success = true,
|
||
Message = "查詢成功",
|
||
Data = result ?? new SignUpList(),
|
||
TotalCount = totalCount
|
||
};
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "進階查詢參加人員發生錯誤:" + ex.Message,
|
||
Data = new SignUpList(),
|
||
TotalCount = 0
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 儲存參加人員 (新增或更新)
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse SaveSignUp(bool isNew, SignUpItem item)
|
||
{
|
||
try
|
||
{
|
||
if (item == null)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "參數不完整"
|
||
};
|
||
}
|
||
|
||
// 驗證必填欄位
|
||
if (item.CourseID <= 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "活動編號為必填欄位"
|
||
};
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(item.Name?.Trim()))
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "學員姓名為必填欄位"
|
||
};
|
||
}
|
||
|
||
if (item.Name.Length > 100)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "學員姓名最長 100 個字元"
|
||
};
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(item.Mobile?.Trim()))
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "手機號為必填欄位"
|
||
};
|
||
}
|
||
|
||
if (item.Mobile.Length > 20)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "手機號最長 20 個字元"
|
||
};
|
||
}
|
||
|
||
DAC_SignUp dac = new DAC_SignUp();
|
||
|
||
if (isNew)
|
||
{
|
||
// 新增 - 序號由 DAC_SignUp.BeforeInsertOne 自動生成
|
||
SignUpItem newItem = dac.InsertOne(item);
|
||
return new BaseResponse
|
||
{
|
||
Success = true,
|
||
Message = "新增成功",
|
||
Data = newItem
|
||
};
|
||
}
|
||
else
|
||
{
|
||
// 更新 - 保護現有的關鍵欄位(QRCode、SeqNo)
|
||
// 如果前端沒有傳遞這些欄位,則保留原有值
|
||
SignUpList existing = dac.SelectOne(item.ID);
|
||
if (existing != null && existing.Count > 0)
|
||
{
|
||
SignUpItem existingItem = existing[0];
|
||
|
||
// 如果新數據中的 QRCode 為空,保留原有值
|
||
if (string.IsNullOrEmpty(item.QRCode))
|
||
{
|
||
item.QRCode = existingItem.QRCode;
|
||
}
|
||
|
||
// 如果新數據中的 SeqNo 為 0,保留原有值
|
||
if (item.SeqNo <= 0)
|
||
{
|
||
item.SeqNo = existingItem.SeqNo;
|
||
}
|
||
}
|
||
|
||
SignUpItem updatedItem = dac.UpdateOne(item);
|
||
return new BaseResponse
|
||
{
|
||
Success = true,
|
||
Message = "更新成功",
|
||
Data = updatedItem
|
||
};
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "儲存參加人員發生錯誤:" + ex.Message
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刪除參加人員
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse DeleteSignUp(int id, int dbAppNo)
|
||
{
|
||
try
|
||
{
|
||
if (id <= 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "參數不完整"
|
||
};
|
||
}
|
||
|
||
DAC_SignUp dac = new DAC_SignUp();
|
||
int rowsAffected = dac.DeleteOne(id, dbAppNo);
|
||
|
||
if (rowsAffected > 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = true,
|
||
Message = "刪除成功"
|
||
};
|
||
}
|
||
else
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "找不到要刪除的資料或資料已被修改"
|
||
};
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "刪除參加人員發生錯誤:" + ex.Message
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 確認匯入 - 將驗證後的資料入庫
|
||
/// 若姓名+手機號重複則跳過
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse ConfirmImport(int courseID, List<SignUpItem> items, bool clearBefore = false)
|
||
{
|
||
try
|
||
{
|
||
if (courseID <= 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "活動編號為必填欄位"
|
||
};
|
||
}
|
||
|
||
if (items == null || items.Count == 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "沒有要匯入的資料"
|
||
};
|
||
}
|
||
|
||
using (var ts = DAC.NewTransactionScope())
|
||
{
|
||
DAC_SignUp dac = new DAC_SignUp();
|
||
|
||
// 如果選擇「先清除再匯入」,則刪除該活動的所有現有記錄
|
||
if (clearBefore)
|
||
{
|
||
dac.DeleteByCourseID(courseID);
|
||
}
|
||
|
||
// 查詢現有的參加人員(同一活動)
|
||
SignUpList existingSignups = dac.Select(
|
||
noInputReturnAll: true,
|
||
CourseID: courseID,
|
||
SeqNo: null,
|
||
Name: null,
|
||
Mobile: null,
|
||
QRCode: null,
|
||
orderBy: "ID ASC"
|
||
);
|
||
|
||
// 建立一個集合存儲現有的 (姓名, 手機號) 組合,用於快速查詢
|
||
HashSet<string> existingCombinations = new HashSet<string>();
|
||
if (existingSignups != null && existingSignups.Count > 0)
|
||
{
|
||
foreach (var signup in existingSignups)
|
||
{
|
||
string key = CreateCompositeKey(signup.Name, signup.Mobile);
|
||
existingCombinations.Add(key);
|
||
}
|
||
}
|
||
|
||
int successCount = 0;
|
||
int skippedCount = 0;
|
||
List<string> importErrors = new List<string>();
|
||
List<string> skippedRecords = new List<string>();
|
||
|
||
foreach (var item in items)
|
||
{
|
||
try
|
||
{
|
||
// 檢查是否重複
|
||
string compositeKey = CreateCompositeKey(item.Name, item.Mobile);
|
||
if (existingCombinations.Contains(compositeKey))
|
||
{
|
||
skippedCount++;
|
||
skippedRecords.Add($"{item.Name} ({item.Mobile})");
|
||
continue;
|
||
}
|
||
|
||
// 不重複,執行插入
|
||
// 序號由 DAC_SignUp.BeforeInsertOne 自動生成,清除任何手動設定的值
|
||
item.CourseID = courseID;
|
||
item.SeqNo = 0; // 重置序號,讓 BeforeInsertOne 自動生成
|
||
dac.InsertOne(item);
|
||
successCount++;
|
||
|
||
// 將新插入的記錄加入集合,避免同一批次中的重複
|
||
existingCombinations.Add(compositeKey);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
importErrors.Add($"匯入失敗:{item.Name} ({item.Mobile}) - {ex.Message}");
|
||
}
|
||
}
|
||
|
||
ts.Complete();
|
||
|
||
string message = $"成功匯入 {successCount} 筆資料";
|
||
if (skippedCount > 0)
|
||
{
|
||
message += $",跳過 {skippedCount} 筆重複資料";
|
||
}
|
||
if (importErrors.Count > 0)
|
||
{
|
||
message += $",失敗 {importErrors.Count} 筆";
|
||
}
|
||
|
||
return new BaseResponse
|
||
{
|
||
Success = true,
|
||
Message = message,
|
||
Data = new
|
||
{
|
||
SuccessCount = successCount,
|
||
SkippedCount = skippedCount,
|
||
ErrorCount = importErrors.Count,
|
||
Errors = importErrors,
|
||
SkippedRecords = skippedRecords
|
||
}
|
||
};
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "確認匯入發生錯誤:" + ex.Message
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 建立複合鍵 (姓名+手機號),用於檢查重複
|
||
/// </summary>
|
||
private string CreateCompositeKey(string name, string mobile)
|
||
{
|
||
return (name ?? "").Trim() + "|" + (mobile ?? "").Trim();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 匯出參加人員 - 使用 NPOI 生成 Excel 檔案並回傳 Base64
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse ExportSignUps(int courseID, string timeFormat = "datetime")
|
||
{
|
||
try
|
||
{
|
||
if (courseID <= 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "活動編號為必填欄位"
|
||
};
|
||
}
|
||
|
||
DAC_SignUp dac = new DAC_SignUp();
|
||
DAC_Classes dacClasses = new DAC_Classes();
|
||
|
||
// 查詢參加人員
|
||
SignUpList signups = dac.Select(
|
||
noInputReturnAll: true,
|
||
CourseID: courseID,
|
||
SeqNo: null,
|
||
Name: null,
|
||
Mobile: null,
|
||
QRCode: null,
|
||
orderBy: "ID ASC"
|
||
);
|
||
|
||
// 查詢活動資訊
|
||
ClassesList classes = dacClasses.SelectOne(courseID);
|
||
if (classes.Count == 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "找不到活動資訊"
|
||
};
|
||
}
|
||
|
||
ClassesItem classItem = classes[0];
|
||
|
||
// 使用 NPOI 建立 Excel 檔案
|
||
IWorkbook workbook = new XSSFWorkbook();
|
||
ISheet sheet = workbook.CreateSheet("參加人員");
|
||
|
||
// 建立字體和樣式
|
||
IFont headerFont = workbook.CreateFont();
|
||
headerFont.IsBold = true;
|
||
headerFont.FontHeightInPoints = 11;
|
||
|
||
ICellStyle headerStyle = workbook.CreateCellStyle();
|
||
headerStyle.SetFont(headerFont);
|
||
headerStyle.Alignment = HorizontalAlignment.Center;
|
||
headerStyle.VerticalAlignment = VerticalAlignment.Center;
|
||
|
||
ICellStyle dataStyle = workbook.CreateCellStyle();
|
||
dataStyle.Alignment = HorizontalAlignment.Left;
|
||
dataStyle.VerticalAlignment = VerticalAlignment.Center;
|
||
|
||
int rowIndex = 0;
|
||
|
||
// 活動資訊
|
||
IRow infoRow1 = sheet.CreateRow(rowIndex++);
|
||
infoRow1.CreateCell(0).SetCellValue("活動名稱");
|
||
infoRow1.CreateCell(1).SetCellValue(classItem.CourseName);
|
||
|
||
IRow infoRow2 = sheet.CreateRow(rowIndex++);
|
||
infoRow2.CreateCell(0).SetCellValue("活動日期");
|
||
infoRow2.CreateCell(1).SetCellValue(classItem.CourseDate.ToString("yyyy-MM-dd"));
|
||
|
||
IRow infoRow3 = sheet.CreateRow(rowIndex++);
|
||
infoRow3.CreateCell(0).SetCellValue("活動地點");
|
||
infoRow3.CreateCell(1).SetCellValue(classItem.CourseLocation);
|
||
|
||
// 空行
|
||
rowIndex++;
|
||
|
||
// 表頭
|
||
IRow headerRow = sheet.CreateRow(rowIndex++);
|
||
headerRow.CreateCell(0).SetCellValue("序號");
|
||
headerRow.GetCell(0).CellStyle = headerStyle;
|
||
headerRow.CreateCell(1).SetCellValue("學員姓名");
|
||
headerRow.GetCell(1).CellStyle = headerStyle;
|
||
headerRow.CreateCell(2).SetCellValue("手機號");
|
||
headerRow.GetCell(2).CellStyle = headerStyle;
|
||
headerRow.CreateCell(3).SetCellValue("簽到時間");
|
||
headerRow.GetCell(3).CellStyle = headerStyle;
|
||
headerRow.CreateCell(4).SetCellValue("簽退時間");
|
||
headerRow.GetCell(4).CellStyle = headerStyle;
|
||
headerRow.CreateCell(5).SetCellValue("身分別");
|
||
headerRow.GetCell(5).CellStyle = headerStyle;
|
||
headerRow.CreateCell(6).SetCellValue("身分證字號");
|
||
headerRow.GetCell(6).CellStyle = headerStyle;
|
||
headerRow.CreateCell(7).SetCellValue("餐點");
|
||
headerRow.GetCell(7).CellStyle = headerStyle;
|
||
headerRow.CreateCell(8).SetCellValue("電子郵件");
|
||
headerRow.GetCell(8).CellStyle = headerStyle;
|
||
headerRow.CreateCell(9).SetCellValue("失業給付認定次數");
|
||
headerRow.GetCell(9).CellStyle = headerStyle;
|
||
headerRow.CreateCell(10).SetCellValue("性別");
|
||
headerRow.GetCell(10).CellStyle = headerStyle;
|
||
headerRow.CreateCell(11).SetCellValue("報名方式");
|
||
headerRow.GetCell(11).CellStyle = headerStyle;
|
||
|
||
// 新增儲存格註解
|
||
IDrawing drawing = sheet.CreateDrawingPatriarch();
|
||
IClientAnchor anchor5 = workbook.GetCreationHelper().CreateClientAnchor();
|
||
anchor5.Col1 = 5;
|
||
anchor5.Row1 = headerRow.RowNum;
|
||
IComment comment5 = drawing.CreateCellComment(anchor5);
|
||
comment5.String = new XSSFRichTextString("失業給付 / 一般民眾 / 青年尋職");
|
||
headerRow.GetCell(5).CellComment = comment5;
|
||
|
||
IClientAnchor anchor7 = workbook.GetCreationHelper().CreateClientAnchor();
|
||
anchor7.Col1 = 7;
|
||
anchor7.Row1 = headerRow.RowNum;
|
||
IComment comment7 = drawing.CreateCellComment(anchor7);
|
||
comment7.String = new XSSFRichTextString("素食 / 非素食 / 不用餐");
|
||
headerRow.GetCell(7).CellComment = comment7;
|
||
|
||
// 資料行
|
||
foreach (var signup in signups)
|
||
{
|
||
IRow dataRow = sheet.CreateRow(rowIndex++);
|
||
dataRow.CreateCell(0).SetCellValue(signup.SeqNo);
|
||
dataRow.CreateCell(1).SetCellValue(signup.Name);
|
||
dataRow.CreateCell(2).SetCellValue(signup.Mobile);
|
||
dataRow.CreateCell(3).SetCellValue(Utils.FormatTimeValue(signup.CheckInTime, timeFormat));
|
||
dataRow.CreateCell(4).SetCellValue(Utils.FormatTimeValue(signup.CheckOutTime, timeFormat));
|
||
dataRow.CreateCell(5).SetCellValue(signup.IDType ?? "");
|
||
dataRow.CreateCell(6).SetCellValue(signup.IDNumber ?? "");
|
||
dataRow.CreateCell(7).SetCellValue(signup.MealType ?? "");
|
||
dataRow.CreateCell(8).SetCellValue(signup.Email ?? "");
|
||
dataRow.CreateCell(9).SetCellValue(signup.LossJobTimes.HasValue ? signup.LossJobTimes.Value.ToString() : "");
|
||
dataRow.CreateCell(10).SetCellValue(signup.Gender ?? "");
|
||
dataRow.CreateCell(11).SetCellValue(signup.SignUpType ?? "未設定");
|
||
|
||
for (int i = 0; i < 10; i++)
|
||
{
|
||
dataRow.GetCell(i).CellStyle = dataStyle;
|
||
}
|
||
}
|
||
|
||
// 設定欄寬
|
||
sheet.SetColumnWidth(0, 10 * 256);
|
||
sheet.SetColumnWidth(1, 15 * 256);
|
||
sheet.SetColumnWidth(2, 15 * 256);
|
||
sheet.SetColumnWidth(3, (timeFormat == "datetime" ? 20 : 10) * 256);
|
||
sheet.SetColumnWidth(4, (timeFormat == "datetime" ? 20 : 10) * 256);
|
||
sheet.SetColumnWidth(5, 12 * 256);
|
||
sheet.SetColumnWidth(6, 15 * 256);
|
||
sheet.SetColumnWidth(7, 12 * 256);
|
||
sheet.SetColumnWidth(8, 20 * 256);
|
||
sheet.SetColumnWidth(9, 20 * 256);
|
||
sheet.SetColumnWidth(10, 20 * 256);
|
||
sheet.SetColumnWidth(11, 12 * 256);
|
||
|
||
// 儲存到記憶體流並轉換為 Base64
|
||
MemoryStream ms = new MemoryStream();
|
||
workbook.Write(ms);
|
||
// ms.Position = 0;
|
||
|
||
byte[] fileBytes = ms.ToArray();
|
||
string fileBase64 = Convert.ToBase64String(fileBytes);
|
||
string fileName = $"{classItem.CourseName}_{DateTime.Now:yyyyMMdd}.xlsx";
|
||
|
||
// ms.Close();
|
||
// workbook.Close();
|
||
|
||
return new BaseResponse
|
||
{
|
||
Success = true,
|
||
Message = "匯出成功",
|
||
Data = new
|
||
{
|
||
FileName = fileName,
|
||
FileBase64 = fileBase64,
|
||
FileSize = fileBytes.Length,
|
||
DataCount = signups.Count
|
||
}
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "匯出參加人員發生錯誤:" + ex.Message
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取得單元格的值(文字)
|
||
/// </summary>
|
||
private string GetCellValue(ICell cell)
|
||
{
|
||
if (cell == null) return "";
|
||
|
||
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 "";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判斷是否為表頭行
|
||
/// </summary>
|
||
private bool IsHeaderRow(string col1, string col2)
|
||
{
|
||
string[] headerKeywords = { "姓名", "名字", "name", "Name", "手機", "電話", "mobile", "phone" };
|
||
|
||
foreach (var keyword in headerKeywords)
|
||
{
|
||
if ((col1 ?? "").ToLower().Contains(keyword.ToLower()) ||
|
||
(col2 ?? "").ToLower().Contains(keyword.ToLower()))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 轉換日期時間字串 yyyy-MM-dd HH:mm:ss 為日期
|
||
/// </summary>
|
||
private DateTime? TransferDateTimeString(string dateTime)
|
||
{
|
||
if (string.IsNullOrEmpty(dateTime))
|
||
return null;
|
||
|
||
string format = "yyyy-MM-dd HH:mm:ss";
|
||
DateTime result;
|
||
|
||
if (DateTime.TryParseExact(
|
||
dateTime.Trim(),
|
||
format,
|
||
CultureInfo.InvariantCulture,
|
||
DateTimeStyles.None,
|
||
out result))
|
||
return result;
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批發簡訊 - 向選定的人員批量發送簡訊
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse SendSmsBatch(int courseID, List<int> signupIds)
|
||
{
|
||
try
|
||
{
|
||
if (courseID <= 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "活動編號為必填欄位"
|
||
};
|
||
}
|
||
|
||
if (signupIds == null || signupIds.Count == 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "請先選擇要發送的人員"
|
||
};
|
||
}
|
||
|
||
int successCount = 0;
|
||
int failedCount = 0;
|
||
List<string> errors = new List<string>();
|
||
List<string> successRecords = new List<string>();
|
||
|
||
// 建立 ZXing writer 實例
|
||
var writer = new ZXing.BarcodeWriter
|
||
{
|
||
Format = ZXing.BarcodeFormat.QR_CODE,
|
||
Options = new ZXing.QrCode.QrCodeEncodingOptions
|
||
{
|
||
ErrorCorrection = ZXing.QrCode.Internal.ErrorCorrectionLevel.M,
|
||
Width = 150,
|
||
Height = 150,
|
||
Margin = 1,
|
||
CharacterSet = "UTF-8"
|
||
}
|
||
};
|
||
|
||
using (var conn = DAC.NewConnection())
|
||
{
|
||
DAC_Classes dacClasses = new DAC_Classes(conn);
|
||
DAC_SignUp dacSignUp = new DAC_SignUp(conn);
|
||
DAC_MessageQueue dacQueue = new DAC_MessageQueue(conn);
|
||
|
||
var classItem = dacClasses.SelectOne(courseID).FirstOrDefault();
|
||
if (classItem == null)
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "無此課程編號"
|
||
};
|
||
|
||
if (string.IsNullOrEmpty(classItem.NotificationTitle))
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "尚未設定此課程的通知標題"
|
||
};
|
||
|
||
if (string.IsNullOrEmpty(classItem.NotificationTemplate))
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "尚未設定此課程的通知模版"
|
||
};
|
||
|
||
// 逐筆查詢要發送的參加人員
|
||
foreach (var signupId in signupIds)
|
||
{
|
||
try
|
||
{
|
||
// 查詢該人員的資訊
|
||
SignUpList signupList = dacSignUp.SelectOne(signupId);
|
||
if (signupList == null || signupList.Count == 0)
|
||
{
|
||
errors.Add($"無法找到編號 {signupId} 的參加人員資料");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
SignUpItem signupItem = signupList[0];
|
||
|
||
// 檢查手機號是否存在
|
||
if (string.IsNullOrEmpty(signupItem.Mobile?.Trim()))
|
||
{
|
||
errors.Add($"參加人員「{signupItem.Name}」缺少手機號");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 檢查是否有 QRCode 資料
|
||
string qrCodeBase64 = null;
|
||
if (string.IsNullOrEmpty(signupItem.QRCode))
|
||
{
|
||
signupItem.QRCode = DAC_SignUp.GenQRCodeText(signupItem);
|
||
dacSignUp.UpdateOne(signupItem);
|
||
}
|
||
|
||
// 產生 QRCode 圖檔並轉成 BASE64
|
||
try
|
||
{
|
||
using (var bitmap = writer.Write(signupItem.QRCode))
|
||
using (var ms = new MemoryStream())
|
||
{
|
||
bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
|
||
qrCodeBase64 = Convert.ToBase64String(ms.ToArray());
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errors.Add($"參加人員「{signupItem.Name}」產生 QRCode 失敗:{ex.Message}");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 寫入 MessageQueue
|
||
dacQueue.InsertOne(new MessageQueueItem()
|
||
{
|
||
CourseID = signupItem.CourseID,
|
||
Status = "PENDING",
|
||
Name = signupItem.Name,
|
||
Mobile = signupItem.Mobile,
|
||
Subject = classItem.NotificationTitle,
|
||
Content = classItem.NotificationTemplate
|
||
.Replace("{課程日期}", classItem.CourseDate.ToString("yyyy-MM-dd HH:mm"))
|
||
.Replace("{課程名稱}", classItem.CourseName)
|
||
.Replace("{課程地點}", classItem.CourseLocation)
|
||
.Replace("{活動日期}", classItem.CourseDate.ToString("yyyy-MM-dd HH:mm"))
|
||
.Replace("{活動名稱}", classItem.CourseName)
|
||
.Replace("{活動地點}", classItem.CourseLocation)
|
||
.Replace("{學員姓名}", signupItem.Name)
|
||
.Replace("{開始時間}", classItem.StartTime)
|
||
.Replace("{結束時間}", classItem.EndTime)
|
||
.Replace("{講師}", classItem.Teacher)
|
||
,
|
||
QRCode = qrCodeBase64
|
||
});
|
||
|
||
successCount++;
|
||
successRecords.Add($"{signupItem.Name} ({signupItem.Mobile})");
|
||
|
||
Console.WriteLine($"[SMS] 預約發送簡訊給 {signupItem.Name} ({signupItem.Mobile})");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errors.Add($"預約發送給編號 {signupId} 的人員失敗:{ex.Message}");
|
||
failedCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
string message = $"成功預約發送 {successCount} 條簡訊";
|
||
if (failedCount > 0)
|
||
{
|
||
message += $",失敗 {failedCount} 條";
|
||
}
|
||
|
||
// 測試用,啟動發送簡訊服務
|
||
// SMSSendingService.Instance.Start();
|
||
|
||
return new BaseResponse
|
||
{
|
||
Success = successCount > 0,
|
||
Message = message,
|
||
Data = new
|
||
{
|
||
SuccessCount = successCount,
|
||
FailedCount = failedCount,
|
||
SuccessRecords = successRecords,
|
||
Errors = errors,
|
||
TotalRequested = signupIds.Count
|
||
}
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "批發簡訊發生錯誤:" + ex.Message
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查詢簡訊點數餘額
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse CheckBalance()
|
||
{
|
||
switch (PublicVariable.CurrentMMSPlatform)
|
||
{
|
||
case "EVERY8D":
|
||
var success = EVERY8DSMSProvider.CheckBalance(out int balance, out string errorMessage);
|
||
if (success)
|
||
{
|
||
return new BaseResponse { Success = true, Data = new { Balance = balance } };
|
||
}
|
||
else
|
||
{
|
||
return new BaseResponse { Success = false, Message = errorMessage };
|
||
}
|
||
case "MITAKE":
|
||
default:
|
||
return new BaseResponse { Success = false, Message = "三竹簡訊餘額請至三竹網站查詢" };
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 查詢單筆參加人員
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse GetSignUpById(int signupId)
|
||
{
|
||
try
|
||
{
|
||
if (signupId <= 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "參加人員編號為必填欄位"
|
||
};
|
||
}
|
||
|
||
DAC_SignUp dac = new DAC_SignUp();
|
||
SignUpList result = dac.SelectOne(signupId);
|
||
|
||
if (result == null || result.Count == 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "找不到該參加人員"
|
||
};
|
||
}
|
||
|
||
return new BaseResponse
|
||
{
|
||
Success = true,
|
||
Message = "查詢成功",
|
||
Data = result[0]
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "查詢參加人員發生錯誤:" + ex.Message
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量上傳簽到簽退資料 - 由 WinForm 客戶端上傳簽到簽退資料
|
||
/// 驗證規則:
|
||
/// - CourseID 必須存在於課程表
|
||
/// - Name、Mobile、SignUpType 必填
|
||
/// - SignUpType 允許值:預約 / 現場
|
||
/// - CheckInType 允許值:CHECKIN / CHECKOUT
|
||
/// - CheckInTime 驗證格式:yyyy-MM-dd HH:mm:ss
|
||
/// 處理邏輯:
|
||
/// - 用 CourseID 查詢 Classes.ID,若不存在則放棄這筆,記入錯誤筆數
|
||
/// - 按 (CourseID, Name, Mobile) 查詢現有 SignUp 記錄
|
||
/// - 若找到則依照 CheckInType 更新 CheckInTime/CheckOutTime,計入成功筆數
|
||
/// - 若沒找到則新增 (SignUpType="現場"),計入成功筆數
|
||
/// </summary>
|
||
[WebMethod(EnableSession = true)]
|
||
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
|
||
public BaseResponse UploadCheckIns(int courseID, List<CheckInItem> items)
|
||
{
|
||
try
|
||
{
|
||
if (courseID <= 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "活動編號為必填欄位"
|
||
};
|
||
}
|
||
|
||
if (items == null || items.Count == 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "沒有要同步的資料"
|
||
};
|
||
}
|
||
|
||
// 驗證 CourseID 是否存在
|
||
DAC_Classes dacClasses = new DAC_Classes();
|
||
ClassesList courseList = dacClasses.SelectOne(courseID);
|
||
if (courseList == null || courseList.Count == 0)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = $"課程編號 {courseID} 不存在"
|
||
};
|
||
}
|
||
|
||
DAC_SignUp dac = new DAC_SignUp();
|
||
int createdCount = 0;
|
||
int updatedCount = 0;
|
||
int failedCount = 0;
|
||
int skippedCount = 0;
|
||
List<string> errors = new List<string>();
|
||
List<string> successIDs = new List<string>();
|
||
|
||
for (int i = 0; i < items.Count; i++)
|
||
{
|
||
var item = items[i];
|
||
int itemIndex = i + 1;
|
||
|
||
try
|
||
{
|
||
// 驗證必填欄位
|
||
if (string.IsNullOrEmpty(item.Name?.Trim()))
|
||
{
|
||
errors.Add($"第 {itemIndex} 筆:姓名為必填欄位");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(item.Mobile?.Trim()))
|
||
{
|
||
errors.Add($"第 {itemIndex} 筆:手機號為必填欄位");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(item.SignUpType?.Trim()))
|
||
{
|
||
errors.Add($"第 {itemIndex} 筆:報名方式為必填欄位");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 驗證 SignUpType 允許值
|
||
string signUpType = item.SignUpType.Trim();
|
||
// 沒有值代表現場
|
||
if (string.IsNullOrEmpty(signUpType)) signUpType = "現場";
|
||
if (signUpType != "預約" && signUpType != "現場")
|
||
{
|
||
errors.Add($"第 {itemIndex} 筆:報名方式只允許『預約』或『現場』,目前值為『{signUpType}』");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 驗證 CheckInType
|
||
if (string.IsNullOrEmpty(item.CheckInType?.Trim()))
|
||
{
|
||
errors.Add($"第 {itemIndex} 筆:簽到類別為必填欄位");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
string checkInType = item.CheckInType.Trim();
|
||
if (checkInType != "CHECKIN" && checkInType != "CHECKOUT")
|
||
{
|
||
errors.Add($"第 {itemIndex} 筆:簽到類別只允許『CHECKIN』或『CHECKOUT』,目前值為『{checkInType}』");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 驗證 CheckInTime
|
||
if (string.IsNullOrEmpty(item.CheckInTime))
|
||
{
|
||
errors.Add($"第 {itemIndex} 筆:簽到/簽退時間為必填欄位");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 驗證 CheckInTime 格式(UTC 時間已經是有效格式)
|
||
var checkinTime = TransferDateTimeString(item.CheckInTime);
|
||
if (!checkinTime.HasValue)
|
||
{
|
||
errors.Add($"第 {itemIndex} 筆:簽到時間格式不正確,應為 yyyy-MM-dd HH:mm:ss");
|
||
failedCount++;
|
||
continue;
|
||
}
|
||
|
||
// 按 (CourseID, Name, Mobile) 查詢現有記錄
|
||
SignUpList existing = dac.SelectByCourseNameMobile(courseID, item.Name, item.Mobile);
|
||
|
||
if (existing != null && existing.Count > 0)
|
||
{
|
||
// 記錄存在,依照 CheckInType 更新相應的時間欄位
|
||
SignUpItem existingItem = existing[0];
|
||
bool needUpdate = false;
|
||
|
||
if (checkInType == "CHECKIN")
|
||
{
|
||
existingItem.CheckInTime = item.CheckInTime;
|
||
existingItem.AgreementStatus = item.AgreementStatus;
|
||
needUpdate = true;
|
||
}
|
||
else if (checkInType == "CHECKOUT")
|
||
{
|
||
existingItem.CheckOutTime = item.CheckInTime;
|
||
needUpdate = true;
|
||
}
|
||
|
||
if (needUpdate)
|
||
{
|
||
dac.UpdateOne(existingItem);
|
||
updatedCount++;
|
||
successIDs.Add(item.ID.ToString());
|
||
}
|
||
else
|
||
{
|
||
skippedCount++;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 記錄不存在,新增為現場簽到
|
||
// 序號由 DAC_SignUp.BeforeInsertOne 自動生成
|
||
SignUpItem newItem = new SignUpItem
|
||
{
|
||
CourseID = courseID,
|
||
Name = item.Name,
|
||
Mobile = item.Mobile,
|
||
IDNumber = item.IDNumber,
|
||
IDType = item.IDType,
|
||
MealType = item.MealType,
|
||
Email = item.Email,
|
||
SignUpType = "現場",
|
||
QRCode = null,
|
||
};
|
||
|
||
// 根據 CheckInType 設定簽到/簽退時間
|
||
if (checkInType == "CHECKIN")
|
||
{
|
||
newItem.CheckInTime = item.CheckInTime;
|
||
}
|
||
else if (checkInType == "CHECKOUT")
|
||
{
|
||
newItem.CheckOutTime = item.CheckInTime;
|
||
}
|
||
|
||
dac.InsertOne(newItem);
|
||
createdCount++;
|
||
successIDs.Add(item.ID.ToString());
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errors.Add($"第 {itemIndex} 筆:{item.Name} ({item.Mobile}) - {ex.Message}");
|
||
failedCount++;
|
||
}
|
||
}
|
||
|
||
string message = $"成功新增 {createdCount} 筆,更新 {updatedCount} 筆";
|
||
if (failedCount > 0)
|
||
{
|
||
message += $",失敗 {failedCount} 筆";
|
||
}
|
||
if (skippedCount > 0)
|
||
{
|
||
message += $",跳過 {skippedCount} 筆";
|
||
}
|
||
|
||
return new BaseResponse
|
||
{
|
||
Success = createdCount > 0 || updatedCount > 0,
|
||
Message = message,
|
||
Data = new
|
||
{
|
||
CreatedCount = createdCount,
|
||
UpdatedCount = updatedCount,
|
||
FailedCount = failedCount,
|
||
SkippedCount = skippedCount,
|
||
SuccessIDs = string.Join(",", successIDs.ToArray()),
|
||
Errors = errors
|
||
}
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new BaseResponse
|
||
{
|
||
Success = false,
|
||
Message = "批量上傳簽到簽退資料發生錯誤:" + ex.Message
|
||
};
|
||
}
|
||
}
|
||
}
|
||
}
|