Add backend Windows service project
Adds a Windows worker service host for running the canteen backend independently, including backend configuration and worker startup logic.feature/centralized-offline-canteen
parent
e59a3f8af6
commit
eff36f0e9f
|
|
@ -0,0 +1,48 @@
|
||||||
|
using UtopiaCanteen.Shared;
|
||||||
|
using UtopiaCanteenSystem.Api;
|
||||||
|
using UtopiaCanteenSystem.Models;
|
||||||
|
|
||||||
|
namespace UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Meal schedule CRUD via central backend API (client never touches HRMS/MySQL).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BackendApiMealScheduleService : IMealScheduleService
|
||||||
|
{
|
||||||
|
private readonly ICanteenBackendApiClient _api;
|
||||||
|
|
||||||
|
public BackendApiMealScheduleService(ICanteenBackendApiClient api)
|
||||||
|
{
|
||||||
|
_api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<MealSchedule> GetActiveSchedulesForSite(string siteId)
|
||||||
|
{
|
||||||
|
var all = GetAllSchedulesAsync().GetAwaiter().GetResult();
|
||||||
|
if (string.IsNullOrWhiteSpace(siteId))
|
||||||
|
return all;
|
||||||
|
var site = siteId.Trim().Replace("SITE : ", "", StringComparison.OrdinalIgnoreCase).Trim();
|
||||||
|
return all.Where(s => string.Equals(s.LocationSiteId?.Trim(), site, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<MealSchedule>> GetAllSchedulesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var dtos = await _api.GetMealSchedulesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return dtos.Select(ApiDtoMapper.ToMealSchedule).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> CreateAsync(MealSchedule schedule, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var dto = ApiDtoMapper.ToMealScheduleDto(schedule);
|
||||||
|
return await _api.CreateMealScheduleAsync(dto, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(MealSchedule schedule, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var dto = ApiDtoMapper.ToMealScheduleDto(schedule);
|
||||||
|
await _api.UpdateMealScheduleAsync(dto, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default) =>
|
||||||
|
await _api.DeleteMealScheduleAsync(id, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>UtopiaCanteenSystem</RootNamespace>
|
||||||
|
<AssemblyName>UtopiaCanteen.Backend</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\UtopiaCanteen.Shared\UtopiaCanteen.Shared.csproj" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||||
|
<PackageReference Include="MySqlConnector" Version="2.3.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\Data\**\*.cs" LinkBase="Data" />
|
||||||
|
<Compile Include="..\Models\**\*.cs" LinkBase="Models" />
|
||||||
|
<Compile Include="..\Helpers\**\*.cs" LinkBase="Helpers" />
|
||||||
|
<Compile Include="..\Api\CanteenBackendHost.cs" Link="Api\CanteenBackendHost.cs" />
|
||||||
|
<Compile Include="..\Api\ApiDtoMapper.cs" Link="Api\ApiDtoMapper.cs" />
|
||||||
|
<Compile Include="..\Services\**\*.cs" LinkBase="Services" />
|
||||||
|
<Compile Remove="..\Services\CanteenBackendApiClient.cs" />
|
||||||
|
<Compile Remove="..\Services\ICanteenBackendApiClient.cs" />
|
||||||
|
<Compile Remove="..\Services\EmptyMenuLookupService.cs" />
|
||||||
|
<Compile Remove="..\Services\AuthService.cs" />
|
||||||
|
<Compile Remove="..\Services\IAuthService.cs" />
|
||||||
|
<Compile Remove="..\Services\AuthResult.cs" />
|
||||||
|
<Compile Remove="..\Services\NavigationService.cs" />
|
||||||
|
<Compile Remove="..\Services\INavigationService.cs" />
|
||||||
|
<Compile Remove="..\Services\AppSession.cs" />
|
||||||
|
<Compile Remove="..\Services\AdminAuditService.cs" />
|
||||||
|
<Compile Remove="..\Services\IAdminAuditService.cs" />
|
||||||
|
<Compile Remove="..\Services\EmployeePhotoService.cs" />
|
||||||
|
<Compile Remove="..\Services\IEmployeePhotoService.cs" />
|
||||||
|
<Compile Remove="..\Services\SampleSyncApiController.cs" />
|
||||||
|
<Compile Remove="..\Services\ClientConfigService.cs" />
|
||||||
|
<Compile Remove="..\Services\NoOpEmployeePhotoService.cs" />
|
||||||
|
<Compile Remove="..\Services\NoOpMealScheduleService.cs" />
|
||||||
|
<Compile Remove="..\Models\OrderHistoryItem.cs" />
|
||||||
|
<Compile Remove="..\Services\ConfigService.cs" />
|
||||||
|
<Compile Remove="..\Services\BackendApiMealScheduleService.cs" />
|
||||||
|
<Compile Remove="..\Services\AdminLoginHelper.cs" />
|
||||||
|
<Compile Remove="..\Api\RfidApiDtos.cs" />
|
||||||
|
<Compile Remove="..\Api\BackendApiDtos.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using UtopiaCanteenSystem.Data;
|
||||||
|
using UtopiaCanteenSystem.Models;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
namespace UtopiaCanteen.BackendService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Backend service configuration: appsettings.json + persisted JSON under LocalApplicationData.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BackendHostConfigService : IConfigService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly string _persistedPath;
|
||||||
|
private PersistedConfig _persisted;
|
||||||
|
|
||||||
|
public BackendHostConfigService(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_persistedPath = Path.Combine(DatabasePath.GetAppDataFolder(), "backend-settings.json");
|
||||||
|
_persisted = LoadPersisted();
|
||||||
|
MergeFromAppSettingsIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetListenUrls() =>
|
||||||
|
_configuration["ListenUrls"] ?? "http://0.0.0.0:5000";
|
||||||
|
|
||||||
|
public string GetSyncApiEndpoint() => _configuration["SyncApiEndpoint"] ?? _persisted.SyncApiEndpoint ?? string.Empty;
|
||||||
|
public void SetSyncApiEndpoint(string endpoint) { _persisted.SyncApiEndpoint = endpoint ?? string.Empty; SavePersisted(); }
|
||||||
|
|
||||||
|
public bool GetSyncServiceEnabled() =>
|
||||||
|
_configuration.GetValue("SyncServiceEnabled", _persisted.SyncServiceEnabled);
|
||||||
|
|
||||||
|
public void SetSyncServiceEnabled(bool enabled) { _persisted.SyncServiceEnabled = enabled; SavePersisted(); }
|
||||||
|
|
||||||
|
public bool GetScannerConnected() => false;
|
||||||
|
public void SetScannerConnected(bool connected) { }
|
||||||
|
|
||||||
|
public int GetScanIntervalDays() => 0;
|
||||||
|
public void SetScanIntervalDays(int value) { }
|
||||||
|
public int GetScanIntervalHours() => 0;
|
||||||
|
public void SetScanIntervalHours(int value) { }
|
||||||
|
public int GetScanIntervalMinutes() => 1;
|
||||||
|
public void SetScanIntervalMinutes(int value) { }
|
||||||
|
public int GetScanIntervalSeconds() => 0;
|
||||||
|
public void SetScanIntervalSeconds(int value) { }
|
||||||
|
public TimeSpan GetScanInterval() => TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
public string GetAdminCardId() => "ADMIN";
|
||||||
|
public void SetAdminCardId(string cardId) { }
|
||||||
|
|
||||||
|
public string GetSiteId() => _persisted.SiteId ?? "02";
|
||||||
|
public void SetSiteId(string siteId) { _persisted.SiteId = siteId ?? string.Empty; SavePersisted(); }
|
||||||
|
|
||||||
|
public void ApplyLocationSiteIdFromAuth(string? locationSiteId) { }
|
||||||
|
|
||||||
|
public string GetDeviceId() => _persisted.DeviceId ?? string.Empty;
|
||||||
|
public void SetDeviceId(string deviceId) { _persisted.DeviceId = deviceId ?? string.Empty; SavePersisted(); }
|
||||||
|
|
||||||
|
public bool GetRememberAdminCredentials() => false;
|
||||||
|
public void SetRememberAdminCredentials(bool remember) { }
|
||||||
|
public string GetSavedAdminUsername() => string.Empty;
|
||||||
|
public void SetSavedAdminUsername(string username) { }
|
||||||
|
public string GetSavedAdminPassword() => string.Empty;
|
||||||
|
public void SetSavedAdminPassword(string password) { }
|
||||||
|
|
||||||
|
public string GetMySqlConnectionString() =>
|
||||||
|
FirstNonEmpty(_configuration["MySqlConnectionString"], _persisted.MySqlConnectionString);
|
||||||
|
|
||||||
|
public void SetMySqlConnectionString(string connectionString)
|
||||||
|
{
|
||||||
|
_persisted.MySqlConnectionString = connectionString ?? string.Empty;
|
||||||
|
SavePersisted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetHrmsLookupConnectionString() =>
|
||||||
|
ConfigConnectionHelper.GetHrmsOrProductionConnectionString(
|
||||||
|
FirstNonEmpty(_configuration["HrmsLookupConnectionString"], _persisted.HrmsLookupConnectionString),
|
||||||
|
GetMySqlConnectionString());
|
||||||
|
|
||||||
|
public void SetHrmsLookupConnectionString(string connectionString)
|
||||||
|
{
|
||||||
|
_persisted.HrmsLookupConnectionString = connectionString ?? string.Empty;
|
||||||
|
SavePersisted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime? GetLastEmployeeRfidCacheSyncUtc() => _persisted.LastEmployeeRfidCacheSyncUtc;
|
||||||
|
public void SetLastEmployeeRfidCacheSyncUtc(DateTime utc)
|
||||||
|
{
|
||||||
|
_persisted.LastEmployeeRfidCacheSyncUtc = utc;
|
||||||
|
SavePersisted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime? GetLastMealMenuCacheSyncUtc() => _persisted.LastMealMenuCacheSyncUtc;
|
||||||
|
public void SetLastMealMenuCacheSyncUtc(DateTime utc)
|
||||||
|
{
|
||||||
|
_persisted.LastMealMenuCacheSyncUtc = utc;
|
||||||
|
SavePersisted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppMode GetAppMode() => AppMode.Server;
|
||||||
|
public void SetAppMode(AppMode mode) { }
|
||||||
|
|
||||||
|
public string GetCentralServerBaseUrl() => string.Empty;
|
||||||
|
public void SetCentralServerBaseUrl(string url) { }
|
||||||
|
|
||||||
|
public string GetBackendBaseUrl() => DeriveLocalhostApiBaseUrl(GetListenUrls());
|
||||||
|
|
||||||
|
public string GetLocalServerListenUrls() => GetListenUrls();
|
||||||
|
public void SetLocalServerListenUrls(string urls) { }
|
||||||
|
|
||||||
|
private void MergeFromAppSettingsIfNeeded()
|
||||||
|
{
|
||||||
|
var mysql = _configuration["MySqlConnectionString"];
|
||||||
|
if (!string.IsNullOrWhiteSpace(mysql) && string.IsNullOrWhiteSpace(_persisted.MySqlConnectionString))
|
||||||
|
_persisted.MySqlConnectionString = mysql;
|
||||||
|
|
||||||
|
var hrms = _configuration["HrmsLookupConnectionString"];
|
||||||
|
if (!string.IsNullOrWhiteSpace(hrms) && string.IsNullOrWhiteSpace(_persisted.HrmsLookupConnectionString))
|
||||||
|
_persisted.HrmsLookupConnectionString = hrms;
|
||||||
|
|
||||||
|
SavePersisted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FirstNonEmpty(params string?[] values)
|
||||||
|
{
|
||||||
|
foreach (var v in values)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(v))
|
||||||
|
return v!;
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DeriveLocalhostApiBaseUrl(string listenUrls)
|
||||||
|
{
|
||||||
|
var first = listenUrls
|
||||||
|
.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.FirstOrDefault() ?? "http://0.0.0.0:5000";
|
||||||
|
first = first.TrimEnd('/');
|
||||||
|
if (first.Contains("0.0.0.0", StringComparison.Ordinal))
|
||||||
|
first = first.Replace("0.0.0.0", "localhost", StringComparison.Ordinal);
|
||||||
|
if (first.Contains('+'))
|
||||||
|
first = first.Replace("+", "localhost", StringComparison.Ordinal);
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PersistedConfig LoadPersisted()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_persistedPath))
|
||||||
|
return new PersistedConfig();
|
||||||
|
var json = File.ReadAllText(_persistedPath);
|
||||||
|
return JsonSerializer.Deserialize<PersistedConfig>(json) ?? new PersistedConfig();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new PersistedConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SavePersisted()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(_persisted, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(_persistedPath, json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Log(ex, "BackendHostConfigService.SavePersisted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PersistedConfig
|
||||||
|
{
|
||||||
|
public string? SyncApiEndpoint { get; set; }
|
||||||
|
public bool SyncServiceEnabled { get; set; } = true;
|
||||||
|
public string? MySqlConnectionString { get; set; }
|
||||||
|
public string? HrmsLookupConnectionString { get; set; }
|
||||||
|
public DateTime? LastEmployeeRfidCacheSyncUtc { get; set; }
|
||||||
|
public DateTime? LastMealMenuCacheSyncUtc { get; set; }
|
||||||
|
public string? SiteId { get; set; }
|
||||||
|
public string? DeviceId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UtopiaCanteenSystem.Api;
|
||||||
|
using UtopiaCanteenSystem.Data;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
namespace UtopiaCanteen.BackendService;
|
||||||
|
|
||||||
|
public sealed class CanteenBackendWorker : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IConfigService _config;
|
||||||
|
private readonly DbContextFactory _dbFactory;
|
||||||
|
private readonly IHostApplicationLifetime _lifetime;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private CancellationTokenSource? _apiCts;
|
||||||
|
private Task? _apiTask;
|
||||||
|
private System.Timers.Timer? _cacheSyncTimer;
|
||||||
|
private System.Timers.Timer? _orderSyncTimer;
|
||||||
|
private int _cacheSyncRunning;
|
||||||
|
private int _orderSyncRunning;
|
||||||
|
private CanteenBackendServices? _backend;
|
||||||
|
|
||||||
|
public CanteenBackendWorker(
|
||||||
|
IConfigService config,
|
||||||
|
DbContextFactory dbFactory,
|
||||||
|
IHostApplicationLifetime lifetime,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_lifetime = lifetime;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Logger.Log(new Exception("UtopiaCanteen backend service starting."), "CanteenBackendWorker");
|
||||||
|
|
||||||
|
using (var db = _dbFactory.CreateDbContext())
|
||||||
|
db.EnsureDatabaseCreated();
|
||||||
|
|
||||||
|
var employeeRfidTagSync = new EmployeeRfidTagSyncService(_dbFactory, _config);
|
||||||
|
var mealMenuCacheSync = new MealMenuCacheSyncService(_dbFactory, _config);
|
||||||
|
var offlineCacheSync = new OfflineCacheSyncService(employeeRfidTagSync, mealMenuCacheSync);
|
||||||
|
var employeeLookup = new EmployeeLookupService(_dbFactory, _config);
|
||||||
|
var menuLookup = new MenuLookupService(_dbFactory);
|
||||||
|
var mealSessionResolver = new DbMealSessionResolver(_dbFactory);
|
||||||
|
var productionSync = new SyncService(_dbFactory, _config);
|
||||||
|
var rfid = new RfidService(_dbFactory, _config, employeeLookup, mealSessionResolver, menuLookup, productionSync);
|
||||||
|
_backend = new CanteenBackendServices(rfid, offlineCacheSync, productionSync, _config);
|
||||||
|
|
||||||
|
_apiCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
var listenUrls = _config.GetLocalServerListenUrls();
|
||||||
|
var backend = _backend;
|
||||||
|
var cts = _apiCts;
|
||||||
|
_apiTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(500, cts.Token).ConfigureAwait(false);
|
||||||
|
await CanteenBackendHost.RunAsync(backend, listenUrls, cts.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Shutdown
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Log(ex, "CanteenBackendWorker.ApiHost");
|
||||||
|
}
|
||||||
|
}, CancellationToken.None);
|
||||||
|
|
||||||
|
_ = RunStartupCacheSyncAsync(cancellationToken);
|
||||||
|
|
||||||
|
var cacheMinutes = Math.Max(1, _configuration.GetValue("CacheSyncIntervalMinutes", 15));
|
||||||
|
if (!string.IsNullOrWhiteSpace(_config.GetHrmsLookupConnectionString()))
|
||||||
|
{
|
||||||
|
_cacheSyncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(cacheMinutes).TotalMilliseconds) { AutoReset = true };
|
||||||
|
_cacheSyncTimer.Elapsed += async (_, _) => await RunCacheSyncSafeAsync().ConfigureAwait(false);
|
||||||
|
_cacheSyncTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderMinutes = Math.Max(1, _configuration.GetValue("ProductionSyncIntervalMinutes", 1));
|
||||||
|
if (_config.GetSyncServiceEnabled() && !string.IsNullOrWhiteSpace(_config.GetMySqlConnectionString()))
|
||||||
|
{
|
||||||
|
_orderSyncTimer = new System.Timers.Timer(TimeSpan.FromMinutes(orderMinutes).TotalMilliseconds) { AutoReset = true };
|
||||||
|
_orderSyncTimer.Elapsed += async (_, _) => await RunOrderSyncSafeAsync().ConfigureAwait(false);
|
||||||
|
_orderSyncTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Logger.Log(new Exception("UtopiaCanteen backend service stopping."), "CanteenBackendWorker");
|
||||||
|
_cacheSyncTimer?.Stop();
|
||||||
|
_orderSyncTimer?.Stop();
|
||||||
|
_apiCts?.Cancel();
|
||||||
|
if (_apiTask != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _apiTask.WaitAsync(TimeSpan.FromSeconds(10), cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_cacheSyncTimer?.Dispose();
|
||||||
|
_orderSyncTimer?.Dispose();
|
||||||
|
_apiCts?.Dispose();
|
||||||
|
await base.StopAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task ExecuteAsync(CancellationToken stoppingToken) =>
|
||||||
|
Task.Delay(Timeout.Infinite, stoppingToken);
|
||||||
|
|
||||||
|
private async Task RunStartupCacheSyncAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_backend == null || string.IsNullOrWhiteSpace(_config.GetHrmsLookupConnectionString()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (var i = 0; i < 30 && !cancellationToken.IsCancellationRequested; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _backend.RunCacheSyncExclusiveAsync(async () =>
|
||||||
|
{
|
||||||
|
await _backend.OfflineCacheSync.SyncEmployeeAndMenuCacheAsync().ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Log(ex, "CanteenBackendWorker.RunStartupCacheSyncAsync");
|
||||||
|
}
|
||||||
|
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunCacheSyncSafeAsync()
|
||||||
|
{
|
||||||
|
if (_backend == null || Interlocked.Exchange(ref _cacheSyncRunning, 1) == 1)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _backend.RunCacheSyncExclusiveAsync(async () =>
|
||||||
|
{
|
||||||
|
await _backend.OfflineCacheSync.SyncEmployeeAndMenuCacheAsync().ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Log(ex, "CanteenBackendWorker.RunCacheSyncSafeAsync");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _cacheSyncRunning, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunOrderSyncSafeAsync()
|
||||||
|
{
|
||||||
|
if (_backend == null || Interlocked.Exchange(ref _orderSyncRunning, 1) == 1)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _backend.RunOrderSyncExclusiveAsync(async () =>
|
||||||
|
{
|
||||||
|
await _backend.ProductionSync.SyncNowAsync().ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Log(ex, "CanteenBackendWorker.RunOrderSyncSafeAsync");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _orderSyncRunning, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using UtopiaCanteen.BackendService;
|
||||||
|
using UtopiaCanteenSystem.Data;
|
||||||
|
using UtopiaCanteenSystem.Services;
|
||||||
|
|
||||||
|
DatabasePath.UseBackendServiceStorage();
|
||||||
|
|
||||||
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
builder.Services.AddWindowsService(options =>
|
||||||
|
{
|
||||||
|
options.ServiceName = "UtopiaCanteenBackend";
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IConfigService, BackendHostConfigService>();
|
||||||
|
builder.Services.AddSingleton<DbContextFactory>();
|
||||||
|
builder.Services.AddHostedService<CanteenBackendWorker>();
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
await host.RunAsync();
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>UtopiaCanteen.BackendService</RootNamespace>
|
||||||
|
<AssemblyName>UtopiaCanteen.BackendService</AssemblyName>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\UtopiaCanteen.Backend\UtopiaCanteen.Backend.csproj" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
Loading…
Reference in New Issue