Add migration to create CheckInJobStatuses table for RMQ task control

- Introduced a new migration that creates the CheckInJobStatuses table.
- The table includes fields for tracking job statuses, timestamps, user information, and error messages.
- Supports various statuses such as PENDING, PROCESSING, COMPLETED, and FAILED.
This commit is contained in:
Suphonchai Phoonsawat 2026-01-20 10:49:13 +07:00
parent 3532df32fd
commit a463df5716
12 changed files with 2259 additions and 126 deletions

View file

@ -53,6 +53,7 @@ namespace BMA.EHR.Application
services.AddTransient<UserDutyTimeRepository>(); services.AddTransient<UserDutyTimeRepository>();
services.AddTransient<AdditionalCheckRequestRepository>(); services.AddTransient<AdditionalCheckRequestRepository>();
services.AddTransient<UserCalendarRepository>(); services.AddTransient<UserCalendarRepository>();
services.AddTransient<CheckInJobStatusRepository>();
services.AddTransient<LeaveTypeRepository>(); services.AddTransient<LeaveTypeRepository>();
services.AddTransient<LeaveRequestRepository>(); services.AddTransient<LeaveRequestRepository>();

View file

@ -0,0 +1,135 @@
using BMA.EHR.Application.Common.Interfaces;
using BMA.EHR.Domain.Models.Leave.TimeAttendants;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
namespace BMA.EHR.Application.Repositories.Leaves.TimeAttendants
{
public class CheckInJobStatusRepository : GenericLeaveRepository<Guid, CheckInJobStatus>
{
#region " Fields "
private readonly ILeaveDbContext _dbContext;
#endregion
#region " Constructor and Destructor "
public CheckInJobStatusRepository(ILeaveDbContext dbContext,
IHttpContextAccessor httpContextAccessor) : base(dbContext, httpContextAccessor)
{
_dbContext = dbContext;
}
#endregion
#region " Methods "
/// <summary>
/// ดึงข้อมูล Job Status จาก TaskId
/// </summary>
public async Task<CheckInJobStatus?> GetByTaskIdAsync(Guid taskId)
{
var data = await _dbContext.Set<CheckInJobStatus>()
.Where(x => x.TaskId == taskId)
.FirstOrDefaultAsync();
return data;
}
/// <summary>
/// ดึงข้อมูล Job Status จาก UserId และสถานะ
/// </summary>
public async Task<List<CheckInJobStatus>> GetByUserIdAndStatusAsync(Guid userId, string status)
{
var data = await _dbContext.Set<CheckInJobStatus>()
.Where(x => x.KeycloakUserId == userId && x.Status == status)
.OrderByDescending(x => x.CreatedDate)
.ToListAsync();
return data;
}
/// <summary>
/// ดึงข้อมูล Job Status ที่ยัง pending หรือ processing
/// </summary>
public async Task<List<CheckInJobStatus>> GetPendingOrProcessingJobsAsync(Guid userId)
{
var data = await _dbContext.Set<CheckInJobStatus>()
.Where(x => x.KeycloakUserId == userId &&
(x.Status == "PENDING" || x.Status == "PROCESSING"))
//.OrderByDescending(x => x.CreatedDate)
.ToListAsync();
return data;
}
/// <summary>
/// อัปเดตสถานะเป็น Processing
/// </summary>
public async Task<CheckInJobStatus> UpdateToProcessingAsync(Guid taskId)
{
var job = await GetByTaskIdAsync(taskId);
if (job != null)
{
job.Status = "PROCESSING";
job.ProcessingDate = DateTime.Now;
await UpdateAsync(job);
}
return job!;
}
/// <summary>
/// อัปเดตสถานะเป็น Completed
/// </summary>
public async Task<CheckInJobStatus> UpdateToCompletedAsync(Guid taskId, string? additionalData = null)
{
var job = await GetByTaskIdAsync(taskId);
if (job != null)
{
job.Status = "COMPLETED";
job.CompletedDate = DateTime.Now;
if (!string.IsNullOrEmpty(additionalData))
{
job.AdditionalData = additionalData;
}
await UpdateAsync(job);
}
return job!;
}
/// <summary>
/// อัปเดตสถานะเป็น Failed
/// </summary>
public async Task<CheckInJobStatus> UpdateToFailedAsync(Guid taskId, string errorMessage)
{
var job = await GetByTaskIdAsync(taskId);
if (job != null)
{
job.Status = "FAILED";
job.CompletedDate = DateTime.Now;
job.ErrorMessage = errorMessage;
await UpdateAsync(job);
}
return job!;
}
/// <summary>
/// ล้างข้อมูล Job Status ที่เก่าเกิน X วัน
/// </summary>
public async Task<int> CleanupOldJobsAsync(int daysOld = 30)
{
var cutoffDate = DateTime.Now.AddDays(-daysOld);
var oldJobs = await _dbContext.Set<CheckInJobStatus>()
.Where(x => x.CreatedDate < cutoffDate)
.ToListAsync();
_dbContext.Set<CheckInJobStatus>().RemoveRange(oldJobs);
await _dbContext.SaveChangesAsync();
return oldJobs.Count;
}
#endregion
}
}

View file

@ -21,63 +21,52 @@ var queue = configuration["Rabbit:Queue"] ?? "basic-queue";
// create connection // create connection
var factory = new ConnectionFactory() var factory = new ConnectionFactory()
{ {
HostName = host, //Uri = new Uri("amqp://admin:P@ssw0rd@192.168.4.11:5672")
UserName = user, HostName = host,// หรือ hostname ของ RabbitMQ Server ที่คุณใช้
Password = pass UserName = user, // ใส่ชื่อผู้ใช้ของคุณ
Password = pass // ใส่รหัสผ่านของคุณ
}; };
using var connection = factory.CreateConnection(); using var connection = factory.CreateConnection();
using var channel = connection.CreateModel(); using var channel = connection.CreateModel();
//channel.QueueDeclare(queue: "bma-checkin-queue", durable: true, exclusive: false, autoDelete: false, arguments: null);
channel.QueueDeclare(queue: queue, durable: true, exclusive: false, autoDelete: false, arguments: null); channel.QueueDeclare(queue: queue, durable: true, exclusive: false, autoDelete: false, arguments: null);
var consumer = new EventingBasicConsumer(channel); var consumer = new EventingBasicConsumer(channel);
string? consumerTag = null;
bool isConsuming = false;
consumer.Received += async (model, ea) => consumer.Received += async (model, ea) =>
{ {
var body = ea.Body.ToArray(); var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body); var message = Encoding.UTF8.GetString(body);
// Double-check time before processing (safety check)
if (!IsWithinOperatingHours())
{
WriteToConsole($"Message received outside operating hours. Requeuing message.");
channel.BasicNack(ea.DeliveryTag, false, true); // Requeue the message
return;
}
await CallRestApi(message); await CallRestApi(message);
// convert string into object
//var request = JsonConvert.DeserializeObject<CheckInRequest>(message);
//using (var db = new ApplicationDbContext())
//{
// var item = new AttendantItem
// {
// Name = request.Name,
// CheckInDateTime = request.CheckInDateTime,
// };
// db.AttendantItems.Add(item);
// db.SaveChanges();
// WriteToConsole($"ได้รับคำขอจาก Queue: {message}");
// WriteToConsole($"ตอบกลับจาก REST API: {JsonConvert.SerializeObject(item)}");
//}
WriteToConsole($"ได้รับคำขอจาก Queue: {message}"); WriteToConsole($"ได้รับคำขอจาก Queue: {message}");
//WriteToConsole($"ตอบกลับจาก REST API: {JsonConvert.SerializeObject(item)}");
}; };
// Monitor and control consumer based on time schedule //channel.BasicConsume(queue: "bma-checkin-queue", autoAck: true, consumer: consumer);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1)); channel.BasicConsume(queue: queue, autoAck: true, consumer: consumer);
while (await timer.WaitForNextTickAsync()) //Console.WriteLine("\nPress 'Enter' to exit the process...");
{
var shouldBeConsuming = IsWithinOperatingHours();
if (shouldBeConsuming && !isConsuming) await Task.Delay(-1);
{
// Start consuming
consumerTag = channel.BasicConsume(queue: queue, autoAck: true, consumer: consumer);
isConsuming = true;
WriteToConsole($"✅ Started consuming messages at {GetCurrentBangkokTime():yyyy-MM-dd HH:mm:ss}");
}
else if (!shouldBeConsuming && isConsuming)
{
// Stop consuming
if (consumerTag != null)
{
channel.BasicCancel(consumerTag);
consumerTag = null;
}
isConsuming = false;
WriteToConsole($"⏸️ Stopped consuming messages at {GetCurrentBangkokTime():yyyy-MM-dd HH:mm:ss}. Will resume after 08:10 tomorrow.");
}
}
static void WriteToConsole(string message) static void WriteToConsole(string message)
{ {
@ -106,25 +95,6 @@ async Task CallRestApi(string requestData)
} }
} }
DateTime GetCurrentBangkokTime()
{
var bangkokTimeZone = TimeZoneInfo.FindSystemTimeZoneById("SE Asia Standard Time");
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, bangkokTimeZone);
}
bool IsWithinOperatingHours()
{
var currentTime = GetCurrentBangkokTime();
var startTime = new TimeSpan(8, 10, 0); // 8:10 AM
var endTime = new TimeSpan(23, 59, 59); // End of day
var currentTimeOfDay = currentTime.TimeOfDay;
// Consumer should only work from 8:10 AM to end of day
return currentTimeOfDay >= startTime && currentTimeOfDay <= endTime;
}
public class ResponseObject public class ResponseObject
{ {
[JsonPropertyName("status")] [JsonPropertyName("status")]

View file

@ -1,9 +1,9 @@
{ {
"Rabbit": { "Rabbit": {
"Host": "192.168.1.40", "Host": "192.168.1.63",
"User": "admin", "User": "admin",
"Password": "Test123456", "Password": "12345678",
"Queue": "bma-checkin-queue" "Queue": "hrms-checkin-queue-dev"
}, },
"API": "https://localhost:7283/api/v1" "API": "https://localhost:7283/api/v1"
} }

View file

@ -0,0 +1,39 @@
using BMA.EHR.Domain.Models.Base;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace BMA.EHR.Domain.Models.Leave.TimeAttendants
{
public class CheckInJobStatus : EntityBase
{
[Required, Comment("Task ID สำหรับติดตามสถานะงาน")]
public Guid TaskId { get; set; } = Guid.Empty;
[Required, Comment("รหัส User ของ Keycloak")]
public Guid KeycloakUserId { get; set; } = Guid.Empty;
[Comment("วันเวลาที่สร้างงาน")]
public DateTime CreatedDate { get; set; } = DateTime.Now;
[Comment("วันเวลาที่เริ่มประมวลผล")]
public DateTime? ProcessingDate { get; set; }
[Comment("วันเวลาที่เสร็จสิ้นการประมวลผล")]
public DateTime? CompletedDate { get; set; }
[Required, Comment("สถานะงาน: PENDING, PROCESSING, COMPLETED, FAILED")]
public string Status { get; set; } = "PENDING";
[Comment("ประเภทการลงเวลา: CHECK_IN, CHECK_OUT")]
public string? CheckType { get; set; }
[Comment("CheckInId สำหรับ Check-Out")]
public Guid? CheckInId { get; set; }
[Comment("ข้อความแสดงข้อผิดพลาด")]
public string? ErrorMessage { get; set; }
[Comment("ข้อมูลเพิ่มเติม (JSON)")]
public string? AdditionalData { get; set; }
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BMA.EHR.Infrastructure.Migrations.LeaveDb
{
/// <inheritdoc />
public partial class AddRMQTaskControl : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CheckInJobStatuses",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, comment: "PrimaryKey", collation: "ascii_general_ci"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false, comment: "สร้างข้อมูลเมื่อ"),
CreatedUserId = table.Column<string>(type: "varchar(40)", maxLength: 40, nullable: false, comment: "User Id ที่สร้างข้อมูล")
.Annotation("MySql:CharSet", "utf8mb4"),
LastUpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true, comment: "แก้ไขข้อมูลล่าสุดเมื่อ"),
LastUpdateUserId = table.Column<string>(type: "varchar(40)", maxLength: 40, nullable: false, comment: "User Id ที่แก้ไขข้อมูลล่าสุด")
.Annotation("MySql:CharSet", "utf8mb4"),
CreatedFullName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false, comment: "ชื่อ User ที่สร้างข้อมูล")
.Annotation("MySql:CharSet", "utf8mb4"),
LastUpdateFullName = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: false, comment: "ชื่อ User ที่แก้ไขข้อมูลล่าสุด")
.Annotation("MySql:CharSet", "utf8mb4"),
TaskId = table.Column<Guid>(type: "char(36)", nullable: false, comment: "Task ID สำหรับติดตามสถานะงาน", collation: "ascii_general_ci"),
KeycloakUserId = table.Column<Guid>(type: "char(36)", nullable: false, comment: "รหัส User ของ Keycloak", collation: "ascii_general_ci"),
CreatedDate = table.Column<DateTime>(type: "datetime(6)", nullable: false, comment: "วันเวลาที่สร้างงาน"),
ProcessingDate = table.Column<DateTime>(type: "datetime(6)", nullable: true, comment: "วันเวลาที่เริ่มประมวลผล"),
CompletedDate = table.Column<DateTime>(type: "datetime(6)", nullable: true, comment: "วันเวลาที่เสร็จสิ้นการประมวลผล"),
Status = table.Column<string>(type: "longtext", nullable: false, comment: "สถานะงาน: PENDING, PROCESSING, COMPLETED, FAILED")
.Annotation("MySql:CharSet", "utf8mb4"),
CheckType = table.Column<string>(type: "longtext", nullable: true, comment: "ประเภทการลงเวลา: CHECK_IN, CHECK_OUT")
.Annotation("MySql:CharSet", "utf8mb4"),
CheckInId = table.Column<Guid>(type: "char(36)", nullable: true, comment: "CheckInId สำหรับ Check-Out", collation: "ascii_general_ci"),
ErrorMessage = table.Column<string>(type: "longtext", nullable: true, comment: "ข้อความแสดงข้อผิดพลาด")
.Annotation("MySql:CharSet", "utf8mb4"),
AdditionalData = table.Column<string>(type: "longtext", nullable: true, comment: "ข้อมูลเพิ่มเติม (JSON)")
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_CheckInJobStatuses", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CheckInJobStatuses");
}
}
}

View file

@ -882,6 +882,99 @@ namespace BMA.EHR.Infrastructure.Migrations.LeaveDb
b.ToTable("AdditionalCheckRequests"); b.ToTable("AdditionalCheckRequests");
}); });
modelBuilder.Entity("BMA.EHR.Domain.Models.Leave.TimeAttendants.CheckInJobStatus", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)")
.HasColumnOrder(0)
.HasComment("PrimaryKey")
.HasAnnotation("Relational:JsonPropertyName", "id");
b.Property<string>("AdditionalData")
.HasColumnType("longtext")
.HasComment("ข้อมูลเพิ่มเติม (JSON)");
b.Property<Guid?>("CheckInId")
.HasColumnType("char(36)")
.HasComment("CheckInId สำหรับ Check-Out");
b.Property<string>("CheckType")
.HasColumnType("longtext")
.HasComment("ประเภทการลงเวลา: CHECK_IN, CHECK_OUT");
b.Property<DateTime?>("CompletedDate")
.HasColumnType("datetime(6)")
.HasComment("วันเวลาที่เสร็จสิ้นการประมวลผล");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)")
.HasColumnOrder(100)
.HasComment("สร้างข้อมูลเมื่อ");
b.Property<DateTime>("CreatedDate")
.HasColumnType("datetime(6)")
.HasComment("วันเวลาที่สร้างงาน");
b.Property<string>("CreatedFullName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnOrder(104)
.HasComment("ชื่อ User ที่สร้างข้อมูล");
b.Property<string>("CreatedUserId")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("varchar(40)")
.HasColumnOrder(101)
.HasComment("User Id ที่สร้างข้อมูล");
b.Property<string>("ErrorMessage")
.HasColumnType("longtext")
.HasComment("ข้อความแสดงข้อผิดพลาด");
b.Property<Guid>("KeycloakUserId")
.HasColumnType("char(36)")
.HasComment("รหัส User ของ Keycloak");
b.Property<string>("LastUpdateFullName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("varchar(200)")
.HasColumnOrder(105)
.HasComment("ชื่อ User ที่แก้ไขข้อมูลล่าสุด");
b.Property<string>("LastUpdateUserId")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("varchar(40)")
.HasColumnOrder(103)
.HasComment("User Id ที่แก้ไขข้อมูลล่าสุด");
b.Property<DateTime?>("LastUpdatedAt")
.HasColumnType("datetime(6)")
.HasColumnOrder(102)
.HasComment("แก้ไขข้อมูลล่าสุดเมื่อ");
b.Property<DateTime?>("ProcessingDate")
.HasColumnType("datetime(6)")
.HasComment("วันเวลาที่เริ่มประมวลผล");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext")
.HasComment("สถานะงาน: PENDING, PROCESSING, COMPLETED, FAILED");
b.Property<Guid>("TaskId")
.HasColumnType("char(36)")
.HasComment("Task ID สำหรับติดตามสถานะงาน");
b.HasKey("Id");
b.ToTable("CheckInJobStatuses");
});
modelBuilder.Entity("BMA.EHR.Domain.Models.Leave.TimeAttendants.DutyTime", b => modelBuilder.Entity("BMA.EHR.Domain.Models.Leave.TimeAttendants.DutyTime", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")

View file

@ -22,6 +22,8 @@ namespace BMA.EHR.Infrastructure.Persistence
public DbSet<UserCalendar> UserCalendars { get; set; } public DbSet<UserCalendar> UserCalendars { get; set; }
public DbSet<CheckInJobStatus> CheckInJobStatuses { get; set; }
#endregion #endregion
#region " Leave System " #region " Leave System "

View file

@ -58,6 +58,7 @@ namespace BMA.EHR.Leave.Service.Controllers
private readonly LeaveRequestRepository _leaveRequestRepository; private readonly LeaveRequestRepository _leaveRequestRepository;
private readonly UserCalendarRepository _userCalendarRepository; private readonly UserCalendarRepository _userCalendarRepository;
private readonly PermissionRepository _permission; private readonly PermissionRepository _permission;
private readonly CheckInJobStatusRepository _checkInJobStatusRepository;
private readonly CommandRepository _commandRepository; private readonly CommandRepository _commandRepository;
@ -92,6 +93,7 @@ namespace BMA.EHR.Leave.Service.Controllers
ObjectPool<IModel> objectPool, ObjectPool<IModel> objectPool,
PermissionRepository permission, PermissionRepository permission,
NotificationRepository notificationRepository, NotificationRepository notificationRepository,
CheckInJobStatusRepository checkInJobStatusRepository,
HttpClient httpClient) HttpClient httpClient)
{ {
_dutyTimeRepository = dutyTimeRepository; _dutyTimeRepository = dutyTimeRepository;
@ -109,6 +111,7 @@ namespace BMA.EHR.Leave.Service.Controllers
_commandRepository = commandRepository; _commandRepository = commandRepository;
_leaveRequestRepository = leaveRequestRepository; _leaveRequestRepository = leaveRequestRepository;
_notificationRepository = notificationRepository; _notificationRepository = notificationRepository;
_checkInJobStatusRepository = checkInJobStatusRepository;
_objectPool = objectPool; _objectPool = objectPool;
_permission = permission; _permission = permission;
@ -540,11 +543,15 @@ namespace BMA.EHR.Leave.Service.Controllers
} }
} }
// add task id for check in queue
string taskId = Guid.NewGuid().ToString();
var checkData = new CheckTimeDtoRB var checkData = new CheckTimeDtoRB
{ {
UserId = userId, UserId = userId,
CurrentDate = currentDate, CurrentDate = currentDate,
CheckInId = data.CheckInId, CheckInId = data.CheckInId,
TaskId = Guid.Parse(taskId),
Lat = data.Lat, Lat = data.Lat,
Lon = data.Lon, Lon = data.Lon,
POI = data.POI, POI = data.POI,
@ -564,11 +571,27 @@ namespace BMA.EHR.Leave.Service.Controllers
var serializedObject = JsonConvert.SerializeObject(checkData); var serializedObject = JsonConvert.SerializeObject(checkData);
var body = Encoding.UTF8.GetBytes(serializedObject); var body = Encoding.UTF8.GetBytes(serializedObject);
// add task id for check in queue
string taskId = Guid.NewGuid().ToString();
var properties = channel.CreateBasicProperties(); var properties = channel.CreateBasicProperties();
properties.Persistent = true; properties.Persistent = true;
properties.MessageId = userId.ToString("D");// ระบบลงเวลาต้องมีการเช็คสถานะใน rabbitMQ ด้วยว่ามีการรอรันอยู่ไหม ลงเวลาเข้า/ออกงาน #894 properties.MessageId = taskId;
// บันทึกสถานะงานก่อนส่งไป RabbitMQ
var jobStatus = new CheckInJobStatus
{
TaskId = Guid.Parse(taskId),
KeycloakUserId = userId,
CreatedDate = currentDate,
Status = "PENDING",
CheckType = data.CheckInId == null ? "CHECK_IN" : "CHECK_OUT",
CheckInId = data.CheckInId,
AdditionalData = JsonConvert.SerializeObject(new
{
IsLocation = data.IsLocation,
LocationName = data.LocationName,
POI = data.POI
})
};
await _checkInJobStatusRepository.AddAsync(jobStatus);
channel.BasicPublish(exchange: "", channel.BasicPublish(exchange: "",
routingKey: queue, routingKey: queue,
@ -583,6 +606,78 @@ namespace BMA.EHR.Leave.Service.Controllers
} }
} }
/// <summary>
/// ตรวจสอบสถานะงาน check-in ด้วย Task ID
/// </summary>
/// <param name="taskId">Task ID ที่ได้จากการเรียก CheckInAsync</param>
/// <returns>
/// </returns>
/// <response code="200">เมื่อทำรายการสำเร็จ</response>
/// <response code="401">ไม่ได้ Login เข้าระบบ</response>
/// <response code="404">ไม่พบข้อมูลงาน</response>
/// <response code="500">เมื่อเกิดข้อผิดพลาดในการทำงาน</response>
[HttpGet("job-status/{taskId:guid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<ResponseObject>> GetJobStatusAsync(Guid taskId)
{
var jobStatus = await _checkInJobStatusRepository.GetByTaskIdAsync(taskId);
if (jobStatus == null)
{
return Error("ไม่พบข้อมูลงาน", StatusCodes.Status404NotFound);
}
var result = new
{
taskId = jobStatus.TaskId,
keycloakUserId = jobStatus.KeycloakUserId,
status = jobStatus.Status,
checkType = jobStatus.CheckType,
checkInId = jobStatus.CheckInId,
createdDate = jobStatus.CreatedDate,
processingDate = jobStatus.ProcessingDate,
completedDate = jobStatus.CompletedDate,
errorMessage = jobStatus.ErrorMessage,
additionalData = jobStatus.AdditionalData != null ?
JsonConvert.DeserializeObject(jobStatus.AdditionalData) : null
};
return Success(result);
}
/// <summary>
/// ดึงรายการงานที่ยัง pending หรือ processing ของผู้ใช้
/// </summary>
/// <returns>
/// </returns>
/// <response code="200">เมื่อทำรายการสำเร็จ</response>
/// <response code="401">ไม่ได้ Login เข้าระบบ</response>
/// <response code="500">เมื่อเกิดข้อผิดพลาดในการทำงาน</response>
[HttpGet("pending-jobs")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<ResponseObject>> GetPendingJobsAsync()
{
var userId = UserId == null ? Guid.Empty : Guid.Parse(UserId);
var jobs = await _checkInJobStatusRepository.GetPendingOrProcessingJobsAsync(userId);
var result = jobs.Select(job => new
{
taskId = job.TaskId,
status = job.Status,
checkType = job.CheckType,
checkInId = job.CheckInId,
createdDate = job.CreatedDate,
processingDate = job.ProcessingDate
}).ToList();
return Success(new { count = result.Count, jobs = result });
}
[HttpGet("check-status")] [HttpGet("check-status")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
@ -590,61 +685,62 @@ namespace BMA.EHR.Leave.Service.Controllers
public async Task<ActionResult<ResponseObject>> CheckInCheckStatus() public async Task<ActionResult<ResponseObject>> CheckInCheckStatus()
{ {
var userId = UserId == null ? Guid.Empty : Guid.Parse(UserId); var userId = UserId == null ? Guid.Empty : Guid.Parse(UserId);
var currentDate = DateTime.Now; // var currentDate = DateTime.Now;
var channel = _objectPool.Get(); // var channel = _objectPool.Get();
try try
{ {
var _url = _configuration["Rabbit:URL"] ?? ""; // var _url = _configuration["Rabbit:URL"] ?? "";
var _queue = _configuration["Rabbit:Queue"] ?? "basic-queue"; // var _queue = _configuration["Rabbit:Queue"] ?? "basic-queue";
// Step 1: ตรวจสอบจำนวน message ทั้งหมดในคิว // // Step 1: ตรวจสอบจำนวน message ทั้งหมดในคิว
string queueUrl = $"{_url}{_queue}"; // string queueUrl = $"{_url}{_queue}";
var queueResponse = await _httpClient.GetAsync(queueUrl); // var queueResponse = await _httpClient.GetAsync(queueUrl);
if (!queueResponse.IsSuccessStatusCode) // if (!queueResponse.IsSuccessStatusCode)
{ // {
return Error("Error accessing RabbitMQ API", (int)queueResponse.StatusCode); // return Error("Error accessing RabbitMQ API", (int)queueResponse.StatusCode);
} // }
var queueContent = await queueResponse.Content.ReadAsStringAsync(); // var queueContent = await queueResponse.Content.ReadAsStringAsync();
var queueData = JObject.Parse(queueContent); // var queueData = JObject.Parse(queueContent);
int totalMessages = queueData["messages"]?.Value<int>() ?? 0; // int totalMessages = queueData["messages"]?.Value<int>() ?? 0;
// Step 2: วนลูปดึง message ทีละ 100 งาน // // Step 2: วนลูปดึง message ทีละ 100 งาน
int batchSize = 100; // int batchSize = 100;
var allMessages = new List<string>(); // var allMessages = new List<string>();
int processedMessages = 0; // int processedMessages = 0;
while (processedMessages < totalMessages) // while (processedMessages < totalMessages)
{ // {
var requestBody = new StringContent( // var requestBody = new StringContent(
$"{{\"count\":{batchSize},\"requeue\":true,\"encoding\":\"auto\",\"ackmode\":\"ack_requeue_true\"}}", // $"{{\"count\":{batchSize},\"requeue\":true,\"encoding\":\"auto\",\"ackmode\":\"ack_requeue_true\"}}",
Encoding.UTF8, // Encoding.UTF8,
"application/json" // "application/json"
); // );
string getMessagesUrl = $"{_url}{_queue}/get"; // string getMessagesUrl = $"{_url}{_queue}/get";
var response = await _httpClient.PostAsync(getMessagesUrl, requestBody); // var response = await _httpClient.PostAsync(getMessagesUrl, requestBody);
if (!response.IsSuccessStatusCode) // if (!response.IsSuccessStatusCode)
{ // {
return StatusCode((int)response.StatusCode, "Error retrieving messages from RabbitMQ."); // return StatusCode((int)response.StatusCode, "Error retrieving messages from RabbitMQ.");
} // }
var content = await response.Content.ReadAsStringAsync(); // var content = await response.Content.ReadAsStringAsync();
var messages = JArray.Parse(content); // var messages = JArray.Parse(content);
if (messages.Count == 0) // if (messages.Count == 0)
{ // {
break; // break;
} // }
processedMessages += messages.Count; // processedMessages += messages.Count;
allMessages.AddRange(messages.Select(m => m["properties"].ToString())); // allMessages.AddRange(messages.Select(m => m["properties"].ToString()));
} // }
var jobs = await _checkInJobStatusRepository.GetPendingOrProcessingJobsAsync(userId);
// Step 3: ค้นหา taskIds ที่อยู่ใน messages ทั้งหมด // Step 3: ค้นหา taskIds ที่อยู่ใน messages ทั้งหมด
var foundTasks = allMessages.FirstOrDefault(x => x.Contains(userId.ToString("D"))); //var foundTasks = allMessages.FirstOrDefault(x => x.Contains(userId.ToString("D")));
return Success(new { keycloakId = userId, InQueue = foundTasks != null }); return Success(new { keycloakId = userId, InQueue = (jobs != null && jobs.Count > 0) });
} }
catch (Exception ex) catch (Exception ex)
@ -653,7 +749,7 @@ namespace BMA.EHR.Leave.Service.Controllers
} }
finally finally
{ {
_objectPool.Return(channel); //_objectPool.Return(channel);
} }
} }
@ -790,21 +886,31 @@ namespace BMA.EHR.Leave.Service.Controllers
public async Task<ActionResult<ResponseObject>> ProcessCheckInAsync([FromBody] CheckTimeDtoRB data) public async Task<ActionResult<ResponseObject>> ProcessCheckInAsync([FromBody] CheckTimeDtoRB data)
{ {
var userId = data.UserId ?? Guid.Empty; var userId = data.UserId ?? Guid.Empty;
var profile = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, data.Token); var taskId = data.TaskId ?? Guid.Empty;
if (profile == null) try
return Error(GlobalMessages.DataNotFound, StatusCodes.Status404NotFound);
if (data.CheckInFileName == "no-file") throw new Exception(GlobalMessages.NoFileToUpload);
var currentDate = data.CurrentDate ?? DateTime.Now;
var check_status = data.CheckInId == null ? "check-in-picture" : "check-out-picture";
var fileName = $"{_bucketName}/{userId}/{currentDate.ToString("dd-MM-yyyy")}/{check_status}/{data.CheckInFileName}";
using (var ms = new MemoryStream(data.CheckInFileBytes ?? new byte[0]))
{ {
await _minIOService.UploadFileAsync(fileName, ms); // อัปเดตสถานะเป็น PROCESSING
} if (taskId != Guid.Empty)
{
await _checkInJobStatusRepository.UpdateToProcessingAsync(taskId);
}
var profile = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, data.Token);
if (profile == null)
return Error(GlobalMessages.DataNotFound, StatusCodes.Status404NotFound);
if (data.CheckInFileName == "no-file") throw new Exception(GlobalMessages.NoFileToUpload);
var currentDate = data.CurrentDate ?? DateTime.Now;
var check_status = data.CheckInId == null ? "check-in-picture" : "check-out-picture";
var fileName = $"{_bucketName}/{userId}/{currentDate.ToString("dd-MM-yyyy")}/{check_status}/{data.CheckInFileName}";
using (var ms = new MemoryStream(data.CheckInFileBytes ?? new byte[0]))
{
await _minIOService.UploadFileAsync(fileName, ms);
}
var defaultRound = await _dutyTimeRepository.GetDefaultAsync(); var defaultRound = await _dutyTimeRepository.GetDefaultAsync();
if (defaultRound == null) if (defaultRound == null)
@ -1058,9 +1164,32 @@ namespace BMA.EHR.Leave.Service.Controllers
} }
} }
// อัปเดตสถานะเป็น COMPLETED
if (taskId != Guid.Empty)
{
var additionalData = JsonConvert.SerializeObject(new
{
CheckInType = data.CheckInId == null ? "check-in" : "check-out",
FileName = fileName,
ProcessedDate = currentDate
});
await _checkInJobStatusRepository.UpdateToCompletedAsync(taskId, additionalData);
}
var checkInType = data.CheckInId == null ? "check-in" : "check-out"; var checkInType = data.CheckInId == null ? "check-in" : "check-out";
return Success(new { user = $"{profile.FirstName} {profile.LastName}", date = currentDate, type = checkInType }); ; return Success(new { user = $"{profile.FirstName} {profile.LastName}", date = currentDate, type = checkInType }); ;
} }
catch (Exception ex)
{
// อัปเดตสถานะเป็น FAILED
if (taskId != Guid.Empty)
{
await _checkInJobStatusRepository.UpdateToFailedAsync(taskId, ex.Message);
}
throw;
}
}
/// <summary> /// <summary>
/// LV1_005 - ลงเวลาเข้า-ออกงาน (USER) /// LV1_005 - ลงเวลาเข้า-ออกงาน (USER)

View file

@ -54,6 +54,7 @@ namespace BMA.EHR.Leave.Service.DTOs.CheckIn
public Guid? CheckInId { get; set; } public Guid? CheckInId { get; set; }
public Guid? TaskId { get; set; }
public double Lat { get; set; } = 0; public double Lat { get; set; } = 0;

View file

@ -53,7 +53,7 @@
"Host": "192.168.1.63", "Host": "192.168.1.63",
"User": "admin", "User": "admin",
"Password": "12345678", "Password": "12345678",
"Queue": "hrms-checkin-queue", "Queue": "hrms-checkin-queue-dev",
"URL": "http://192.168.1.63:9122/api/queues/%2F/" "URL": "http://192.168.1.63:9122/api/queues/%2F/"
}, },
"Mail": { "Mail": {