add architecture skeleton

This commit is contained in:
2026-04-19 13:12:04 +03:00
parent 312708531f
commit 2738f31f26
84 changed files with 2710 additions and 70 deletions
+21
View File
@@ -0,0 +1,21 @@
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base
USER $APP_UID
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["GymLogAI.Worker/GymLogAI.Worker.csproj", "GymLogAI.Worker/"]
COPY ["GymLogAI.Application/GymLogAI.Application.csproj", "GymLogAI.Application/"]
COPY ["GymLogAI.AI/GymLogAI.AI.csproj", "GymLogAI.AI/"]
COPY ["GymLogAI.Core/GymLogAI.Core.csproj", "GymLogAI.Core/"]
COPY ["GymLogAI.Persistence/GymLogAI.Persistence.csproj", "GymLogAI.Persistence/"]
RUN dotnet restore "GymLogAI.Worker/GymLogAI.Worker.csproj"
COPY . .
WORKDIR "/src/GymLogAI.Worker"
RUN dotnet publish "./GymLogAI.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "GymLogAI.Worker.dll"]
+20
View File
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-GymLogAI.Worker-bf7dded7-f871-4b7d-a513-0a677ed83bd3</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GymLogAI.Application\GymLogAI.Application.csproj" />
<ProjectReference Include="..\GymLogAI.AI\GymLogAI.AI.csproj" />
<ProjectReference Include="..\GymLogAI.Core\GymLogAI.Core.csproj" />
<ProjectReference Include="..\GymLogAI.Persistence\GymLogAI.Persistence.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,40 @@
using GymLogAI.Application.Handlers;
using GymLogAI.Application.Interfaces;
using Microsoft.Extensions.Options;
namespace GymLogAI.Worker;
public sealed class ParsePendingTelegramMessagesJob(
IServiceProvider serviceProvider,
IOptions<WorkerOptions> options,
ILogger<ParsePendingTelegramMessagesJob> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await using var scope = serviceProvider.CreateAsyncScope();
var repository = scope.ServiceProvider.GetRequiredService<ITelegramMessageRepository>();
var handler = scope.ServiceProvider.GetRequiredService<ParseTelegramMessageHandler>();
var pendingMessages = await repository.GetPendingAsync(options.Value.BatchSize, stoppingToken);
foreach (var pendingMessage in pendingMessages)
{
await handler.HandleAsync(pendingMessage.Id, stoppingToken);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to process pending telegram messages.");
}
await Task.Delay(TimeSpan.FromSeconds(options.Value.PollingIntervalSeconds), stoppingToken);
}
}
}
+26
View File
@@ -0,0 +1,26 @@
using GymLogAI.AI;
using GymLogAI.Application.Handlers;
using GymLogAI.Persistence;
using GymLogAI.Worker;
using Microsoft.EntityFrameworkCore;
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddOptions<WorkerOptions>()
.Bind(builder.Configuration.GetSection(WorkerOptions.SectionName));
builder.Services.AddPersistence(builder.Configuration);
builder.Services.AddAiServices(builder.Configuration);
builder.Services.AddScoped<ImportTelegramMessageHandler>();
builder.Services.AddScoped<ParseTelegramMessageHandler>();
builder.Services.AddScoped<RecommendWorkoutHandler>();
builder.Services.AddHostedService<ParsePendingTelegramMessagesJob>();
var host = builder.Build();
await using (var scope = host.Services.CreateAsyncScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await dbContext.Database.MigrateAsync();
}
await host.RunAsync();
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"GymLogAI.Worker": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace GymLogAI.Worker;
public sealed record WorkerOptions
{
public const string SectionName = "Worker";
public int BatchSize { get; init; } = 20;
public int PollingIntervalSeconds { get; init; } = 10;
}
@@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"Postgres": "Host=localhost;Port=5432;Database=gymlogai;Username=postgres;Password=postgres"
},
"Worker": {
"BatchSize": 20,
"PollingIntervalSeconds": 10
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"Postgres": "Host=localhost;Port=5432;Database=gymlogai;Username=postgres;Password=postgres"
},
"Worker": {
"BatchSize": 20,
"PollingIntervalSeconds": 10
},
"OpenRouter": {
"BaseUrl": "https://openrouter.ai/api/v1/",
"ApiKey": "",
"ChatModel": "openai/gpt-4.1-mini",
"EmbeddingModel": "text-embedding-3-small"
}
}