diff --git a/BMA.EHR.Application/Repositories/Leaves/TimeAttendants/CheckInJobStatusRepository.cs b/BMA.EHR.Application/Repositories/Leaves/TimeAttendants/CheckInJobStatusRepository.cs index 85c575d3..302bdd12 100644 --- a/BMA.EHR.Application/Repositories/Leaves/TimeAttendants/CheckInJobStatusRepository.cs +++ b/BMA.EHR.Application/Repositories/Leaves/TimeAttendants/CheckInJobStatusRepository.cs @@ -114,60 +114,6 @@ namespace BMA.EHR.Application.Repositories.Leaves.TimeAttendants return job!; } - /// - /// ดึงข้อมูลงานที่ค้างอยู่ในสถานะ PENDING หรือ PROCESSING เกินเวลาที่กำหนด (นาที) - /// - public async Task> GetStalePendingOrProcessingJobsAsync(int timeoutMinutes = 30) - { - var cutoffDate = DateTime.Now.AddMinutes(-timeoutMinutes); - var staleJobs = await _dbContext.Set() - .Where(x => (x.Status == "PENDING" || x.Status == "PROCESSING") - && x.CreatedDate < cutoffDate) - .OrderBy(x => x.CreatedDate) - .ToListAsync(); - - return staleJobs; - } - - /// - /// ดึงข้อมูลงานที่ค้างอยู่ในสถานะ PENDING หรือ PROCESSING เกินเวลาที่กำหนด (นาที) ของ user คนใดคนหนึ่ง - /// - public async Task> GetStalePendingOrProcessingJobsByUserAsync(Guid userId, int timeoutMinutes = 30) - { - var cutoffDate = DateTime.Now.AddMinutes(-timeoutMinutes); - var staleJobs = await _dbContext.Set() - .Where(x => x.KeycloakUserId == userId - && (x.Status == "PENDING" || x.Status == "PROCESSING") - && x.CreatedDate < cutoffDate) - .OrderBy(x => x.CreatedDate) - .ToListAsync(); - - return staleJobs; - } - - /// - /// Mark งานที่ค้างเกินเวลาที่กำหนดเป็น FAILED - /// - public async Task MarkStaleJobsAsFailedAsync(int timeoutMinutes = 30) - { - var staleJobs = await GetStalePendingOrProcessingJobsAsync(timeoutMinutes); - - foreach (var job in staleJobs) - { - job.Status = "FAILED"; - job.CompletedDate = DateTime.Now; - job.ErrorMessage = $"งานค้างในสถานะ {job.Status} เกิน {timeoutMinutes} นาที ระบบทำเครื่องหมายเป็น FAILED อัตโนมัติ"; - } - - if (staleJobs.Any()) - { - _dbContext.Set().UpdateRange(staleJobs); - await _dbContext.SaveChangesAsync(); - } - - return staleJobs.Count; - } - /// /// ล้างข้อมูล Job Status ที่เก่าเกิน X วัน /// diff --git a/BMA.EHR.Leave/BMA.EHR.Leave.csproj b/BMA.EHR.Leave/BMA.EHR.Leave.csproj index e7c58efa..28f42590 100644 --- a/BMA.EHR.Leave/BMA.EHR.Leave.csproj +++ b/BMA.EHR.Leave/BMA.EHR.Leave.csproj @@ -74,9 +74,6 @@ PreserveNewest - - PreserveNewest - diff --git a/BMA.EHR.Leave/Controllers/LeaveController.cs b/BMA.EHR.Leave/Controllers/LeaveController.cs index 78594aef..3b1ec5b7 100644 --- a/BMA.EHR.Leave/Controllers/LeaveController.cs +++ b/BMA.EHR.Leave/Controllers/LeaveController.cs @@ -536,18 +536,7 @@ namespace BMA.EHR.Leave.Service.Controllers // prepare data and convert request body and send to queue var userId = UserId == null ? Guid.Empty : Guid.Parse(UserId); var currentDate = DateTime.Now; - - // ตรวจสอบและ mark งานเก่าที่ค้างเกิน 30 นาทีเป็น FAILED อัตโนมัติ - var staleJobs = await _checkInJobStatusRepository.GetStalePendingOrProcessingJobsByUserAsync(userId, 30); - if (staleJobs != null && staleJobs.Count > 0) - { - foreach (var staleJob in staleJobs) - { - await _checkInJobStatusRepository.UpdateToFailedAsync(staleJob.TaskId, - $"งานค้างในสถานะ {staleJob.Status} เกิน 30 นาที ระบบทำเครื่องหมายเป็น FAILED อัตโนมัติ"); - } - } - + // ตรวจสอบว่ามีงานที่กำลัง pending หรือ processing อยู่หรือไม่ var existingJobs = await _checkInJobStatusRepository.GetPendingOrProcessingJobsAsync(userId); if (existingJobs != null && existingJobs.Count > 0) @@ -555,10 +544,10 @@ namespace BMA.EHR.Leave.Service.Controllers // กรองเฉพาะงานที่เป็นประเภทเดียวกัน (CHECK_IN หรือ CHECK_OUT) var checkType = data.CheckInId == null ? "CHECK_IN" : "CHECK_OUT"; var sameTypeJob = existingJobs.FirstOrDefault(j => j.CheckType == checkType); - + if (sameTypeJob != null) { - + return Error($"มีงาน {checkType} กำลังดำเนินการอยู่", StatusCodes.Status500InternalServerError); // var timeDiff = (currentDate - sameTypeJob.CreatedDate).TotalMinutes; // if (timeDiff < 2) @@ -597,7 +586,7 @@ namespace BMA.EHR.Leave.Service.Controllers LocationName = data.LocationName, Remark = data.Remark, CheckInFileName = data.Img == null ? "no-file" : data.Img.FileName, - //CheckInFileBytes = checkFileBytes, + CheckInFileBytes = checkFileBytes, Token = AccessToken ?? "" }; @@ -624,7 +613,14 @@ namespace BMA.EHR.Leave.Service.Controllers Status = "PENDING", CheckType = data.CheckInId == null ? "CHECK_IN" : "CHECK_OUT", CheckInId = data.CheckInId, - AdditionalData = JsonConvert.SerializeObject(checkData) + AdditionalData = JsonConvert.SerializeObject(new + { + IsLocation = data.IsLocation, + LocationName = data.LocationName, + POI = data.POI, + KeycloakId = userId, + Token = AccessToken, + }) }; await _checkInJobStatusRepository.AddAsync(jobStatus); @@ -731,117 +727,6 @@ namespace BMA.EHR.Leave.Service.Controllers return Success(new { count = result.Count, jobs = result }); } - /// - /// ประมวลผลงาน CheckIn ที่ค้างอยู่ในสถานะ PENDING/PROCESSING เกินเวลาที่กำหนดใหม่อีกครั้ง - /// - /// - /// เมื่อทำรายการสำเร็จ - /// ไม่ได้ Login เข้าระบบ - /// เมื่อเกิดข้อผิดพลาดในการทำงาน - [HttpPost("reprocess-stale-checkin-jobs")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> ReprocessStaleCheckInJobsAsync([FromQuery] int timeoutMinutes = 30) - { - try - { - var staleJobs = await _checkInJobStatusRepository.GetStalePendingOrProcessingJobsAsync(timeoutMinutes); - - if (staleJobs == null || staleJobs.Count == 0) - { - return Success(new { message = "ไม่พบงานที่ค้างอยู่", count = 0 }); - } - - var results = new List(); - foreach (var job in staleJobs) - { - try - { - // อ่านข้อมูลเดิมจาก AdditionalData - if (string.IsNullOrEmpty(job.AdditionalData)) - { - await _checkInJobStatusRepository.UpdateToFailedAsync(job.TaskId, - "ไม่พบข้อมูลสำหรับประมวลผลซ้ำ (AdditionalData is null)"); - results.Add(new - { - taskId = job.TaskId, - keycloakUserId = job.KeycloakUserId, - checkType = job.CheckType, - createdDate = job.CreatedDate, - previousStatus = job.Status, - newStatus = "FAILED", - errorMessage = "ไม่พบข้อมูลสำหรับประมวลผลซ้ำ" - }); - continue; - } - - var checkData = JsonConvert.DeserializeObject(job.AdditionalData); - checkData.UserId = job.KeycloakUserId; - checkData.CurrentDate = job.CreatedDate; - if (checkData == null) - { - await _checkInJobStatusRepository.UpdateToFailedAsync(job.TaskId, - "ไม่สามารถอ่านข้อมูลสำหรับประมวลผลซ้ำได้"); - results.Add(new - { - taskId = job.TaskId, - keycloakUserId = job.KeycloakUserId, - checkType = job.CheckType, - createdDate = job.CreatedDate, - previousStatus = job.Status, - newStatus = "FAILED", - errorMessage = "ไม่สามารถอ่านข้อมูลสำหรับประมวลผลซ้ำได้" - }); - continue; - } - - // ตั้ง TaskId ให้ตรงกับ job เดิม - checkData.TaskId = job.TaskId; - - // เรียก ProcessCheckInAsync ด้วยข้อมูลเดิม - var processResult = await ProcessCheckInAsync(checkData); - - results.Add(new - { - taskId = job.TaskId, - keycloakUserId = job.KeycloakUserId, - checkType = job.CheckType, - createdDate = job.CreatedDate, - previousStatus = job.Status, - result = processResult - }); - } - catch (Exception ex) - { - await _checkInJobStatusRepository.UpdateToFailedAsync(job.TaskId, - $"เกิดข้อผิดพลาดในการประมวลผลซ้ำ: {ex.Message}"); - results.Add(new - { - taskId = job.TaskId, - keycloakUserId = job.KeycloakUserId, - checkType = job.CheckType, - createdDate = job.CreatedDate, - previousStatus = job.Status, - newStatus = "FAILED", - errorMessage = ex.Message - }); - } - } - - return Success(new - { - message = $"ประมวลผลซ้ำงาน {staleJobs.Count} รายการเสร็จสิ้น", - count = staleJobs.Count, - jobs = results - }); - } - catch (Exception ex) - { - return Error(ex); - } - } - [HttpGet("check-status")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -1073,18 +958,16 @@ namespace BMA.EHR.Leave.Service.Controllers var currentDate = data.CurrentDate ?? DateTime.Now; - if (data.CheckInFileName == "no-file") + if (data.CheckInFileName == "no-file") { //throw new Exception(GlobalMessages.NoFileToUpload); await _checkInJobStatusRepository.UpdateToFailedAsync(taskId, GlobalMessages.NoFileToUpload); - await _notificationService.SendNotificationAsync(data.Token, true, - $"ลงเวลาไม่สำเร็จ \r\nเนื่องจาก {GlobalMessages.NoFileToUpload}\r\nกรุณาลองใหม่อีกครั้ง"); + await _notificationService.SendNotificationAsync(data.Token, true, $"ลงเวลาไม่สำเร็จ \r\nเนื่องจาก {GlobalMessages.NoFileToUpload}\r\nกรุณาลองใหม่อีกครั้ง"); // send notification to user var noti1 = new Notification { - Body = - $"ประมวลผลการลงเวลาวันที่ {currentDate.ToString("dd-MM-yyyy")} ไม่สำเร็จ \r\nเนื่องจาก {GlobalMessages.NoFileToUpload}", + Body = $"ประมวลผลการลงเวลาวันที่ {currentDate.ToString("dd-MM-yyyy")} ไม่สำเร็จ \r\nเนื่องจาก {GlobalMessages.NoFileToUpload}", ReceiverUserId = profile.Id, Type = "", Payload = "", @@ -1094,54 +977,46 @@ namespace BMA.EHR.Leave.Service.Controllers return Error(GlobalMessages.NoFileToUpload, StatusCodes.Status400BadRequest); } - + // last check-in record var lastCheckIn = await _userTimeStampRepository.GetLastRecord(userId); var check_status = data.CheckInId == null ? "check-in-picture" : "check-out-picture"; var check_out_status = "check-out-picture"; - // ถ้าไม่มี CheckInFileBytes ให้ใช้ภาพ blank.jpeg แทน - var fileBytes = data.CheckInFileBytes; - if (fileBytes == null || fileBytes.Length == 0) - { - var blankPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot", "blank.jpeg"); - fileBytes = await System.IO.File.ReadAllBytesAsync(blankPath); - data.CheckInFileName = "blank.jpeg"; - } - - var fileName = - $"{_bucketName}/{userId}/{currentDate.ToString("dd-MM-yyyy")}/{check_status}/{data.CheckInFileName}"; - var fileNameCheckOut = - $"{_bucketName}/{userId}/{currentDate.ToString("dd-MM-yyyy")}/{check_out_status}/{data.CheckInFileName}"; - using (var ms = new MemoryStream(fileBytes)) + var fileName = $"{_bucketName}/{userId}/{currentDate.ToString("dd-MM-yyyy")}/{check_status}/{data.CheckInFileName}"; + var fileNameCheckOut = $"{_bucketName}/{userId}/{currentDate.ToString("dd-MM-yyyy")}/{check_out_status}/{data.CheckInFileName}"; + using (var ms = new MemoryStream(data.CheckInFileBytes ?? new byte[0])) { try { await _minIOService.UploadFileAsync(fileName, ms); + // if (lastCheckIn != null && lastCheckIn.CheckOut == null) + // { + // // ยังไม่เคย check-out มาก่อน หรือ check-out เป็น null ให้ใช้ชื่อไฟล์แบบ check-out + // await _minIOService.UploadFileAsync(fileNameCheckOut, ms); + // } } catch (Exception ex) { - await _checkInJobStatusRepository.UpdateToFailedAsync(taskId, - $"ไม่สามารถอัปโหลดรูปภาพได้: {ex.Message}"); - await _notificationService.SendNotificationAsync(data.Token, true, - $"ลงเวลาไม่สำเร็จ \r\nเนื่องจากไม่สามารถอัปโหลดรูปภาพได้ {ex.Message}\r\nกรุณาลองใหม่อีกครั้ง"); + await _checkInJobStatusRepository.UpdateToFailedAsync(taskId, $"ไม่สามารถอัปโหลดรูปภาพได้: {ex.Message}"); + await _notificationService.SendNotificationAsync(data.Token, true, $"ลงเวลาไม่สำเร็จ \r\nเนื่องจากไม่สามารถอัปโหลดรูปภาพได้ {ex.Message}\r\nกรุณาลองใหม่อีกครั้ง"); // send notification to user - var noti2 = new Notification + var noti1 = new Notification { - Body = - $"ประมวลผลการลงเวลาวันที่ {currentDate.ToString("dd-MM-yyyy")} ไม่สำเร็จ \r\nเนื่องจากไม่สามารถอัปโหลดรูปภาพได้ {ex.Message}", + Body = $"ประมวลผลการลงเวลาวันที่ {currentDate.ToString("dd-MM-yyyy")} ไม่สำเร็จ \r\nเนื่องจากไม่สามารถอัปโหลดรูปภาพได้ {ex.Message}", ReceiverUserId = profile.Id, Type = "", Payload = "", }; - _appDbContext.Set().Add(noti2); + _appDbContext.Set().Add(noti1); await _appDbContext.SaveChangesAsync(); - return Error($"ไม่สามารถอัปโหลดรูปภาพได้: {ex.Message}", - StatusCodes.Status500InternalServerError); + + return Error($"ไม่สามารถอัปโหลดรูปภาพได้: {ex.Message}", StatusCodes.Status500InternalServerError); } + } if (lastCheckIn != null && lastCheckIn.CheckOut == null) @@ -1151,32 +1026,36 @@ namespace BMA.EHR.Leave.Service.Controllers try { await _minIOService.UploadFileAsync(fileNameCheckOut, ms2); + // if (lastCheckIn != null && lastCheckIn.CheckOut == null) + // { + // // ยังไม่เคย check-out มาก่อน หรือ check-out เป็น null ให้ใช้ชื่อไฟล์แบบ check-out + // await _minIOService.UploadFileAsync(fileNameCheckOut, ms); + // } } catch (Exception ex) { - await _checkInJobStatusRepository.UpdateToFailedAsync(taskId, - $"ไม่สามารถอัปโหลดรูปภาพได้: {ex.Message}"); - await _notificationService.SendNotificationAsync(data.Token, true, - $"ลงเวลาไม่สำเร็จ \r\nเนื่องจากไม่สามารถอัปโหลดรูปภาพได้ {ex.Message}\r\nกรุณาลองใหม่อีกครั้ง"); + await _checkInJobStatusRepository.UpdateToFailedAsync(taskId, $"ไม่สามารถอัปโหลดรูปภาพได้: {ex.Message}"); + await _notificationService.SendNotificationAsync(data.Token, true, $"ลงเวลาไม่สำเร็จ \r\nเนื่องจากไม่สามารถอัปโหลดรูปภาพได้ {ex.Message}\r\nกรุณาลองใหม่อีกครั้ง"); // send notification to user - var noti3 = new Notification + var noti1 = new Notification { - Body = - $"ประมวลผลการลงเวลาวันที่ {currentDate.ToString("dd-MM-yyyy")} ไม่สำเร็จ \r\nเนื่องจากไม่สามารถอัปโหลดรูปภาพได้ {ex.Message}", + Body = $"ประมวลผลการลงเวลาวันที่ {currentDate.ToString("dd-MM-yyyy")} ไม่สำเร็จ \r\nเนื่องจากไม่สามารถอัปโหลดรูปภาพได้ {ex.Message}", ReceiverUserId = profile.Id, Type = "", Payload = "", }; - _appDbContext.Set().Add(noti3); + _appDbContext.Set().Add(noti1); await _appDbContext.SaveChangesAsync(); - return Error($"ไม่สามารถอัปโหลดรูปภาพได้: {ex.Message}", - StatusCodes.Status500InternalServerError); + + return Error($"ไม่สามารถอัปโหลดรูปภาพได้: {ex.Message}", StatusCodes.Status500InternalServerError); } + } } + var defaultRound = await _dutyTimeRepository.GetDefaultAsync(); if (defaultRound == null) { diff --git a/BMA.EHR.Leave/Program.cs b/BMA.EHR.Leave/Program.cs index 3dd83203..9d42133e 100644 --- a/BMA.EHR.Leave/Program.cs +++ b/BMA.EHR.Leave/Program.cs @@ -198,20 +198,12 @@ if (manager != null) // ทำความสะอาดข้อมูล CheckIn Job Status ที่เก่ากว่า 30 วัน - รันทุกวันเวลา 02:00 น. manager.AddOrUpdate("ทำความสะอาดข้อมูล CheckIn Job Status", Job.FromExpression(x => x.CleanupOldJobsAsync(30)), "0 2 * * *", bangkokTimeZone); - // ตรวจสอบและ mark งาน CheckIn ที่ค้างเกิน 30 นาทีเป็น FAILED - รันทุก 15 นาที - // manager.AddOrUpdate("ตรวจสอบงาน CheckIn ที่ค้างเกินเวลา", Job.FromExpression(x => x.MarkStaleJobsAsFailedAsync(30)), "*/15 * * * *", - // new RecurringJobOptions - // { - // TimeZone = bangkokTimeZone, - // QueueName = "leave" - // }); - // - // manager.AddOrUpdate("ประมวลผลงานที่ค้างอยู่ในสถานะ Pending หรือ Processing", Job.FromExpression(x => x.ProcessPendingJobsAsync()), "0 3 * * *", - // new RecurringJobOptions - // { - // TimeZone = bangkokTimeZone, - // QueueName = "leave" // ← กำหนด queue - // }); + manager.AddOrUpdate("ประมวลผลงานที่ค้างอยู่ในสถานะ Pending หรือ Processing", Job.FromExpression(x => x.ProcessPendingJobsAsync()), "0 3 * * *", + new RecurringJobOptions + { + TimeZone = bangkokTimeZone, + QueueName = "leave" // ← กำหนด queue + }); } // apply migrations diff --git a/BMA.EHR.Leave/wwwroot/blank.jpeg b/BMA.EHR.Leave/wwwroot/blank.jpeg deleted file mode 100644 index 2b153486..00000000 Binary files a/BMA.EHR.Leave/wwwroot/blank.jpeg and /dev/null differ