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

1235 lines
49 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.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
};
}
}
}
}