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 { /// /// 查詢參加人員 /// [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 }; } } /// /// 進階搜尋參加人員 - 支援序號、身分別、身分證字號、餐點、電子郵件等條件 /// [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 }; } } /// /// 儲存參加人員 (新增或更新) /// [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 }; } } /// /// 刪除參加人員 /// [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 }; } } /// /// 確認匯入 - 將驗證後的資料入庫 /// 若姓名+手機號重複則跳過 /// [WebMethod(EnableSession = true)] [ScriptMethod(ResponseFormat = ResponseFormat.Json)] public BaseResponse ConfirmImport(int courseID, List 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 existingCombinations = new HashSet(); 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 importErrors = new List(); List skippedRecords = new List(); 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 }; } } /// /// 建立複合鍵 (姓名+手機號),用於檢查重複 /// private string CreateCompositeKey(string name, string mobile) { return (name ?? "").Trim() + "|" + (mobile ?? "").Trim(); } /// /// 匯出參加人員 - 使用 NPOI 生成 Excel 檔案並回傳 Base64 /// [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 }; } } /// /// 取得單元格的值(文字) /// 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 ""; } } /// /// 判斷是否為表頭行 /// 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; } /// /// 轉換日期時間字串 yyyy-MM-dd HH:mm:ss 為日期 /// 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; } /// /// 批發簡訊 - 向選定的人員批量發送簡訊 /// [WebMethod(EnableSession = true)] [ScriptMethod(ResponseFormat = ResponseFormat.Json)] public BaseResponse SendSmsBatch(int courseID, List 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 errors = new List(); List successRecords = new List(); // 建立 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 }; } } /// /// 查詢簡訊點數餘額 /// [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 = "三竹簡訊餘額請至三竹網站查詢" }; } } /// /// 查詢單筆參加人員 /// [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 }; } } /// /// 批量上傳簽到簽退資料 - 由 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="現場"),計入成功筆數 /// [WebMethod(EnableSession = true)] [ScriptMethod(ResponseFormat = ResponseFormat.Json)] public BaseResponse UploadCheckIns(int courseID, List 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 errors = new List(); List successIDs = new List(); 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 }; } } } }