453 lines
16 KiB
C#
453 lines
16 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using MySqlConnector;
|
|
using UtopiaCanteenSystem.Data;
|
|
using UtopiaCanteenSystem.Models;
|
|
using UtopiaCanteenSystem.Services.Logging;
|
|
|
|
namespace UtopiaCanteenSystem.Services;
|
|
|
|
/// <summary>
|
|
/// Pulls meal_schedule, lunch_menu_week, lunch_menu_item, and menu_item from HRMS into SQLite.
|
|
/// </summary>
|
|
public class MealMenuCacheSyncService : IMealMenuCacheSyncService
|
|
{
|
|
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
|
private readonly IConfigService _configService;
|
|
|
|
public MealMenuCacheSyncService(IDbContextFactory<AppDbContext> dbFactory, IConfigService configService)
|
|
{
|
|
_dbFactory = dbFactory;
|
|
_configService = configService;
|
|
}
|
|
|
|
public async Task<MealMenuCacheSyncResult> SyncAllAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
FileLogger.Info("CacheSync", "Meal/menu cache sync started.");
|
|
|
|
var connectionString = _configService.GetHrmsLookupConnectionString();
|
|
if (string.IsNullOrWhiteSpace(connectionString))
|
|
{
|
|
FileLogger.Warn("CacheSync", "Meal/menu sync skipped. MySQL connection string is not configured.");
|
|
return new MealMenuCacheSyncResult
|
|
{
|
|
Success = false,
|
|
ErrorMessage = "MySQL connection string is not configured."
|
|
};
|
|
}
|
|
|
|
try
|
|
{
|
|
var syncedAt = DateTime.UtcNow;
|
|
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
|
|
|
var menuItems = await FetchMenuItemsAsync(connectionString, cancellationToken).ConfigureAwait(false);
|
|
var mealSchedules = await FetchMealSchedulesAsync(connectionString, cancellationToken).ConfigureAwait(false);
|
|
var lunchWeeks = await FetchLunchMenuWeeksAsync(connectionString, cancellationToken).ConfigureAwait(false);
|
|
var lunchItems = await FetchLunchMenuItemsAsync(connectionString, cancellationToken).ConfigureAwait(false);
|
|
|
|
var menuItemCount = await UpsertMenuItemsAsync(db, menuItems, syncedAt, cancellationToken).ConfigureAwait(false);
|
|
var mealScheduleCount = await UpsertMealSchedulesAsync(db, mealSchedules, syncedAt, cancellationToken).ConfigureAwait(false);
|
|
var lunchWeekCount = await UpsertLunchMenuWeeksAsync(db, lunchWeeks, syncedAt, cancellationToken).ConfigureAwait(false);
|
|
var lunchItemCount = await UpsertLunchMenuItemsAsync(db, lunchItems, syncedAt, cancellationToken).ConfigureAwait(false);
|
|
|
|
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
|
_configService.SetLastMealMenuCacheSyncUtc(syncedAt);
|
|
|
|
FileLogger.Info(
|
|
"CacheSync",
|
|
$"Meal/menu sync completed. MealSchedules={mealScheduleCount}, MenuWeeks={lunchWeekCount}, " +
|
|
$"MenuItems={lunchItemCount}, CatalogItems={menuItemCount}.");
|
|
|
|
return new MealMenuCacheSyncResult
|
|
{
|
|
Success = true,
|
|
MealScheduleCount = mealScheduleCount,
|
|
LunchMenuWeekCount = lunchWeekCount,
|
|
LunchMenuItemCount = lunchItemCount,
|
|
MenuItemCount = menuItemCount
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Log(ex, "MealMenuCacheSyncService.SyncAllAsync");
|
|
FileLogger.Error("CacheSync", $"Meal/menu sync failed. Error={ex.Message}", ex);
|
|
return new MealMenuCacheSyncResult
|
|
{
|
|
Success = false,
|
|
ErrorMessage = ex.Message
|
|
};
|
|
}
|
|
}
|
|
|
|
private static async Task<int> UpsertMenuItemsAsync(
|
|
AppDbContext db,
|
|
List<MenuItemRow> rows,
|
|
DateTime syncedAt,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var count = 0;
|
|
foreach (var row in rows)
|
|
{
|
|
if (row.HrmsId <= 0)
|
|
continue;
|
|
|
|
var existing = await db.MenuItemCache
|
|
.FirstOrDefaultAsync(x => x.HrmsId == row.HrmsId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (existing == null)
|
|
{
|
|
db.MenuItemCache.Add(new MenuItemCache
|
|
{
|
|
HrmsId = row.HrmsId,
|
|
ItemName = row.ItemName,
|
|
ItemType = row.ItemType,
|
|
Price = row.Price,
|
|
ItemFor = row.ItemFor,
|
|
LocationSiteId = row.LocationSiteId,
|
|
LastSyncedAtUtc = syncedAt
|
|
});
|
|
}
|
|
else
|
|
{
|
|
existing.ItemName = row.ItemName;
|
|
existing.ItemType = row.ItemType;
|
|
existing.Price = row.Price;
|
|
existing.ItemFor = row.ItemFor;
|
|
existing.LocationSiteId = row.LocationSiteId;
|
|
existing.LastSyncedAtUtc = syncedAt;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static async Task<int> UpsertMealSchedulesAsync(
|
|
AppDbContext db,
|
|
List<MealScheduleRow> rows,
|
|
DateTime syncedAt,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var count = 0;
|
|
foreach (var row in rows)
|
|
{
|
|
if (row.HrmsId <= 0)
|
|
continue;
|
|
|
|
var existing = await db.MealScheduleCache
|
|
.FirstOrDefaultAsync(x => x.HrmsId == row.HrmsId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (existing == null)
|
|
{
|
|
db.MealScheduleCache.Add(new MealScheduleCache
|
|
{
|
|
HrmsId = row.HrmsId,
|
|
MealName = row.MealName,
|
|
StartTime = row.StartTime,
|
|
EndTime = row.EndTime,
|
|
CreatedAt = row.CreatedAt,
|
|
UpdatedAt = row.UpdatedAt,
|
|
LocationSiteId = row.LocationSiteId,
|
|
LastSyncedAtUtc = syncedAt
|
|
});
|
|
}
|
|
else
|
|
{
|
|
existing.MealName = row.MealName;
|
|
existing.StartTime = row.StartTime;
|
|
existing.EndTime = row.EndTime;
|
|
existing.CreatedAt = row.CreatedAt;
|
|
existing.UpdatedAt = row.UpdatedAt;
|
|
existing.LocationSiteId = row.LocationSiteId;
|
|
existing.LastSyncedAtUtc = syncedAt;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static async Task<int> UpsertLunchMenuWeeksAsync(
|
|
AppDbContext db,
|
|
List<LunchMenuWeekRow> rows,
|
|
DateTime syncedAt,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var count = 0;
|
|
foreach (var row in rows)
|
|
{
|
|
if (row.HrmsId <= 0)
|
|
continue;
|
|
|
|
var existing = await db.LunchMenuWeekCache
|
|
.FirstOrDefaultAsync(x => x.HrmsId == row.HrmsId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (existing == null)
|
|
{
|
|
db.LunchMenuWeekCache.Add(new LunchMenuWeekCache
|
|
{
|
|
HrmsId = row.HrmsId,
|
|
WeekStartDate = row.WeekStartDate,
|
|
WeekEndDate = row.WeekEndDate,
|
|
CreatedBy = row.CreatedBy,
|
|
CreatedAt = row.CreatedAt,
|
|
LocationSiteId = row.LocationSiteId,
|
|
LastSyncedAtUtc = syncedAt
|
|
});
|
|
}
|
|
else
|
|
{
|
|
existing.WeekStartDate = row.WeekStartDate;
|
|
existing.WeekEndDate = row.WeekEndDate;
|
|
existing.CreatedBy = row.CreatedBy;
|
|
existing.CreatedAt = row.CreatedAt;
|
|
existing.LocationSiteId = row.LocationSiteId;
|
|
existing.LastSyncedAtUtc = syncedAt;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static async Task<int> UpsertLunchMenuItemsAsync(
|
|
AppDbContext db,
|
|
List<LunchMenuItemRow> rows,
|
|
DateTime syncedAt,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var count = 0;
|
|
foreach (var row in rows)
|
|
{
|
|
if (row.HrmsId <= 0)
|
|
continue;
|
|
|
|
var existing = await db.LunchMenuItemCache
|
|
.FirstOrDefaultAsync(x => x.HrmsId == row.HrmsId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (existing == null)
|
|
{
|
|
db.LunchMenuItemCache.Add(new LunchMenuItemCache
|
|
{
|
|
HrmsId = row.HrmsId,
|
|
LunchMenuWeekHrmsId = row.LunchMenuWeekHrmsId,
|
|
DayOfWeek = row.DayOfWeek,
|
|
MealName = row.MealName,
|
|
MenuItemHrmsId = row.MenuItemHrmsId,
|
|
CreatedAt = row.CreatedAt,
|
|
MenuDate = row.MenuDate,
|
|
LastSyncedAtUtc = syncedAt
|
|
});
|
|
}
|
|
else
|
|
{
|
|
existing.LunchMenuWeekHrmsId = row.LunchMenuWeekHrmsId;
|
|
existing.DayOfWeek = row.DayOfWeek;
|
|
existing.MealName = row.MealName;
|
|
existing.MenuItemHrmsId = row.MenuItemHrmsId;
|
|
existing.CreatedAt = row.CreatedAt;
|
|
existing.MenuDate = row.MenuDate;
|
|
existing.LastSyncedAtUtc = syncedAt;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
private static async Task<List<MealScheduleRow>> FetchMealSchedulesAsync(string connectionString, CancellationToken cancellationToken)
|
|
{
|
|
const string sql = @"
|
|
SELECT id, meal_name, start_time, end_time, created_at, updated_at, location_site_id
|
|
FROM meal_schedule";
|
|
|
|
return await QueryRowsAsync(connectionString, sql, cancellationToken, reader => new MealScheduleRow
|
|
{
|
|
HrmsId = GetInt64(reader, 0),
|
|
MealName = GetString(reader, 1),
|
|
StartTime = GetTimeString(reader, 2),
|
|
EndTime = GetTimeString(reader, 3),
|
|
CreatedAt = GetDateTimeNullable(reader, 4),
|
|
UpdatedAt = GetDateTimeNullable(reader, 5),
|
|
LocationSiteId = GetInt(reader, 6)
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task<List<LunchMenuWeekRow>> FetchLunchMenuWeeksAsync(string connectionString, CancellationToken cancellationToken)
|
|
{
|
|
const string sql = @"
|
|
SELECT id, week_start_date, week_end_date, created_by, created_at, location_site_id
|
|
FROM lunch_menu_week";
|
|
|
|
return await QueryRowsAsync(connectionString, sql, cancellationToken, reader => new LunchMenuWeekRow
|
|
{
|
|
HrmsId = GetInt64(reader, 0),
|
|
WeekStartDate = GetDateTimeNullable(reader, 1),
|
|
WeekEndDate = GetDateTimeNullable(reader, 2),
|
|
CreatedBy = GetString(reader, 3),
|
|
CreatedAt = GetDateTimeNullable(reader, 4),
|
|
LocationSiteId = GetInt(reader, 5)
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task<List<LunchMenuItemRow>> FetchLunchMenuItemsAsync(string connectionString, CancellationToken cancellationToken)
|
|
{
|
|
const string sql = @"
|
|
SELECT id, lunch_menu_week_id, day_of_week, meal_name, menu_item_id, created_at, menu_date
|
|
FROM lunch_menu_item";
|
|
|
|
return await QueryRowsAsync(connectionString, sql, cancellationToken, reader => new LunchMenuItemRow
|
|
{
|
|
HrmsId = GetInt64(reader, 0),
|
|
LunchMenuWeekHrmsId = GetInt64(reader, 1),
|
|
DayOfWeek = GetString(reader, 2),
|
|
MealName = GetString(reader, 3),
|
|
MenuItemHrmsId = GetInt64(reader, 4),
|
|
CreatedAt = GetDateTimeNullable(reader, 5),
|
|
MenuDate = GetDateTimeNullable(reader, 6)
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task<List<MenuItemRow>> FetchMenuItemsAsync(string connectionString, CancellationToken cancellationToken)
|
|
{
|
|
const string sql = @"
|
|
SELECT id, item_name, item_type, price, item_for, location_site_id
|
|
FROM menu_item";
|
|
|
|
return await QueryRowsAsync(connectionString, sql, cancellationToken, reader => new MenuItemRow
|
|
{
|
|
HrmsId = GetInt64(reader, 0),
|
|
ItemName = GetString(reader, 1),
|
|
ItemType = GetString(reader, 2),
|
|
Price = GetDecimal(reader, 3),
|
|
ItemFor = GetString(reader, 4),
|
|
LocationSiteId = GetInt(reader, 5)
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task<List<T>> QueryRowsAsync<T>(
|
|
string connectionString,
|
|
string sql,
|
|
CancellationToken cancellationToken,
|
|
Func<MySqlDataReader, T> map)
|
|
{
|
|
var rows = new List<T>();
|
|
await using var conn = new MySqlConnection(connectionString);
|
|
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
|
|
await using var cmd = new MySqlCommand(sql, conn);
|
|
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
|
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
|
rows.Add(map(reader));
|
|
return rows;
|
|
}
|
|
|
|
private static string GetString(MySqlDataReader reader, int ordinal)
|
|
{
|
|
if (reader.IsDBNull(ordinal))
|
|
return string.Empty;
|
|
return reader.GetValue(ordinal)?.ToString() ?? string.Empty;
|
|
}
|
|
|
|
private static int GetInt(MySqlDataReader reader, int ordinal)
|
|
{
|
|
if (reader.IsDBNull(ordinal))
|
|
return 0;
|
|
var v = reader.GetValue(ordinal);
|
|
if (v is int i)
|
|
return i;
|
|
if (v is long l)
|
|
return (int)l;
|
|
return int.TryParse(v?.ToString(), out var parsed) ? parsed : 0;
|
|
}
|
|
|
|
private static long GetInt64(MySqlDataReader reader, int ordinal)
|
|
{
|
|
if (reader.IsDBNull(ordinal))
|
|
return 0;
|
|
var v = reader.GetValue(ordinal);
|
|
if (v is long l)
|
|
return l;
|
|
if (v is int i)
|
|
return i;
|
|
return long.TryParse(v?.ToString(), out var parsed) ? parsed : 0;
|
|
}
|
|
|
|
private static decimal GetDecimal(MySqlDataReader reader, int ordinal)
|
|
{
|
|
if (reader.IsDBNull(ordinal))
|
|
return 0m;
|
|
var v = reader.GetValue(ordinal);
|
|
return v is decimal d ? d : Convert.ToDecimal(v);
|
|
}
|
|
|
|
private static DateTime? GetDateTimeNullable(MySqlDataReader reader, int ordinal)
|
|
{
|
|
if (reader.IsDBNull(ordinal))
|
|
return null;
|
|
var v = reader.GetValue(ordinal);
|
|
if (v is DateTime dt)
|
|
return dt;
|
|
return DateTime.TryParse(v?.ToString(), out var parsed) ? parsed : null;
|
|
}
|
|
|
|
private static string GetTimeString(MySqlDataReader reader, int ordinal)
|
|
{
|
|
if (reader.IsDBNull(ordinal))
|
|
return "00:00:00";
|
|
var v = reader.GetValue(ordinal);
|
|
if (v is TimeSpan ts)
|
|
return ts.ToString(@"hh\:mm\:ss");
|
|
return v?.ToString()?.Trim() ?? "00:00:00";
|
|
}
|
|
|
|
private sealed class MealScheduleRow
|
|
{
|
|
public long HrmsId { get; set; }
|
|
public string MealName { get; set; } = string.Empty;
|
|
public string StartTime { get; set; } = string.Empty;
|
|
public string EndTime { get; set; } = string.Empty;
|
|
public DateTime? CreatedAt { get; set; }
|
|
public DateTime? UpdatedAt { get; set; }
|
|
public int LocationSiteId { get; set; }
|
|
}
|
|
|
|
private sealed class LunchMenuWeekRow
|
|
{
|
|
public long HrmsId { get; set; }
|
|
public DateTime? WeekStartDate { get; set; }
|
|
public DateTime? WeekEndDate { get; set; }
|
|
public string CreatedBy { get; set; } = string.Empty;
|
|
public DateTime? CreatedAt { get; set; }
|
|
public int LocationSiteId { get; set; }
|
|
}
|
|
|
|
private sealed class LunchMenuItemRow
|
|
{
|
|
public long HrmsId { get; set; }
|
|
public long LunchMenuWeekHrmsId { get; set; }
|
|
public string DayOfWeek { get; set; } = string.Empty;
|
|
public string MealName { get; set; } = string.Empty;
|
|
public long MenuItemHrmsId { get; set; }
|
|
public DateTime? CreatedAt { get; set; }
|
|
public DateTime? MenuDate { get; set; }
|
|
}
|
|
|
|
private sealed class MenuItemRow
|
|
{
|
|
public long HrmsId { get; set; }
|
|
public string ItemName { get; set; } = string.Empty;
|
|
public string ItemType { get; set; } = string.Empty;
|
|
public decimal Price { get; set; }
|
|
public string ItemFor { get; set; } = string.Empty;
|
|
public int LocationSiteId { get; set; }
|
|
}
|
|
}
|