From ed206e63dcfb86329ee9b9830bad18dced39bba8 Mon Sep 17 00:00:00 2001 From: Pancake2021 <92122200+Pancake2021@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:50:15 +0400 Subject: [PATCH 1/5] =?UTF-8?q?revert:=20=D0=BE=D1=82=D0=BA=D0=B0=D1=82=20?= =?UTF-8?q?=D0=BB=D0=B0=D0=B1.1=20=E2=80=94=20=D0=B1=D1=8B=D0=BB=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B1=D1=80=D0=B0=D0=BD=20=D0=BD=D0=B5=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0=D0=BD=D1=82?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F.=20=D0=9F=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D1=8B=D0=B2=D0=B0=D1=8E=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0=D0=BD=D1=82=20=E2=84=96?= =?UTF-8?q?30=20(=D0=9A=D1=80=D0=B5=D0=B4=D0=B8=D1=82=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B0,=20Query=20Based,=20SQS,?= =?UTF-8?q?=20Localstack)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 22144d278f0f212fb2725d606b4155c5d17b15b1 Mon Sep 17 00:00:00 2001 From: Pancake2021 <92122200+Pancake2021@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:03:07 +0400 Subject: [PATCH 2/5] =?UTF-8?q?feat(domain):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20=D0=BA=D1=80=D0=B5=D0=B4=D0=B8=D1=82=D0=BD=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B8=20=D0=B8=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/LoansController.cs | 21 ++++++++ CreditSystem.Api/CreditSystem.Api.csproj | 19 +++++++ CreditSystem.Api/Program.cs | 29 +++++++++++ .../Services/LoanDataGenerator.cs | 34 +++++++++++++ CreditSystem.Api/Services/LoanService.cs | 45 +++++++++++++++++ .../CreditSystem.Domain.csproj | 9 ++++ .../Entities/LoanApplication.cs | 49 +++++++++++++++++++ 7 files changed, 206 insertions(+) create mode 100644 CreditSystem.Api/Controllers/LoansController.cs create mode 100644 CreditSystem.Api/CreditSystem.Api.csproj create mode 100644 CreditSystem.Api/Program.cs create mode 100644 CreditSystem.Api/Services/LoanDataGenerator.cs create mode 100644 CreditSystem.Api/Services/LoanService.cs create mode 100644 CreditSystem.Domain/CreditSystem.Domain.csproj create mode 100644 CreditSystem.Domain/Entities/LoanApplication.cs diff --git a/CreditSystem.Api/Controllers/LoansController.cs b/CreditSystem.Api/Controllers/LoansController.cs new file mode 100644 index 00000000..2f5a21ed --- /dev/null +++ b/CreditSystem.Api/Controllers/LoansController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; +using CreditSystem.Api.Services; +using CreditSystem.Domain.Entities; + +namespace CreditSystem.Api.Controllers; + +[ApiController] +[Route("api/v1/[controller]")] +public class LoansController(LoanService loanService, ILogger logger) : ControllerBase +{ + /// + /// Получить данные по кредитной заявке (с генерацией при отсутствии) + /// + [HttpGet("{id:guid}")] + public async Task> Get(Guid id, CancellationToken ct) + { + logger.LogInformation("Запрос на получение данных по заявке: {Id}", id); + var result = await loanService.GetApplicationAsync(id, ct); + return Ok(result); + } +} diff --git a/CreditSystem.Api/CreditSystem.Api.csproj b/CreditSystem.Api/CreditSystem.Api.csproj new file mode 100644 index 00000000..6046a4b6 --- /dev/null +++ b/CreditSystem.Api/CreditSystem.Api.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/CreditSystem.Api/Program.cs b/CreditSystem.Api/Program.cs new file mode 100644 index 00000000..8613b7f8 --- /dev/null +++ b/CreditSystem.Api/Program.cs @@ -0,0 +1,29 @@ +var builder = WebApplication.CreateBuilder(args); + +// Подключаем стандартные настройки (ServiceDefaults) +builder.AddServiceDefaults(); + +// Наши сервисы +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +// Кэш (Redis подхватится через Aspire) +builder.AddRedisDistributedCache("cache"); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapControllers(); + +app.Run(); diff --git a/CreditSystem.Api/Services/LoanDataGenerator.cs b/CreditSystem.Api/Services/LoanDataGenerator.cs new file mode 100644 index 00000000..5a88c8d1 --- /dev/null +++ b/CreditSystem.Api/Services/LoanDataGenerator.cs @@ -0,0 +1,34 @@ +using Bogus; +using CreditSystem.Domain.Entities; + +namespace CreditSystem.Api.Services; + +/// +/// Генератор тестовых данных для кредитных заявок +/// +public class LoanDataGenerator +{ + private readonly Faker _faker; + + public LoanDataGenerator() + { + // Используем русскую локаль для правдоподобности + _faker = new Faker("ru") + .RuleFor(l => l.Id, f => f.Random.Guid()) + .RuleFor(l => l.ApplicantName, f => f.Name.FullName()) + .RuleFor(l => l.RequestedAmount, f => f.Finance.Amount(50000, 5000000, 0)) + .RuleFor(l => l.TermMonths, f => f.Random.Int(6, 120)) + .RuleFor(l => l.MonthlyIncome, f => f.Finance.Amount(30000, 300000, 0)) + .RuleFor(l => l.CreditScore, f => f.Random.Int(300, 850)) + .RuleFor(l => l.CreatedAt, f => f.Date.Recent(30)) + .RuleFor(l => l.Status, (f, l) => + { + // Простая логика: если рейтинг совсем плохой — сразу отказ + return l.CreditScore < 400 ? "Rejected" : "Pending"; + }); + } + + public LoanApplication Generate() => _faker.Generate(); + + public IEnumerable Generate(int count) => _faker.Generate(count); +} diff --git a/CreditSystem.Api/Services/LoanService.cs b/CreditSystem.Api/Services/LoanService.cs new file mode 100644 index 00000000..5e20c97b --- /dev/null +++ b/CreditSystem.Api/Services/LoanService.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; +using CreditSystem.Domain.Entities; +using CreditSystem.Api.Services; + +namespace CreditSystem.Api.Services; + +/// +/// Сервис управления кредитными заявками (с кэшированием) +/// +public class LoanService( + IDistributedCache cache, + LoanDataGenerator generator, + ILogger logger) +{ + private const int CacheExpirationMinutes = 15; + + public async Task GetApplicationAsync(Guid id, CancellationToken ct = default) + { + var key = $"loan:app:{id}"; + + // Пытаемся взять из редиса + var data = await cache.GetStringAsync(key, ct); + if (!string.IsNullOrEmpty(data)) + { + logger.LogInformation("Заявка {Id} найдена в кэше", id); + return JsonSerializer.Deserialize(data)!; + } + + // Если нет — генерим + logger.LogWarning("Заявка {Id} не найдена. Генерируем новую...", id); + var app = generator.Generate(); + app.Id = id; + + // И кладем обратно + var options = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CacheExpirationMinutes) + }; + + await cache.SetStringAsync(key, JsonSerializer.Serialize(app), options, ct); + + return app; + } +} diff --git a/CreditSystem.Domain/CreditSystem.Domain.csproj b/CreditSystem.Domain/CreditSystem.Domain.csproj new file mode 100644 index 00000000..30402ac0 --- /dev/null +++ b/CreditSystem.Domain/CreditSystem.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/CreditSystem.Domain/Entities/LoanApplication.cs b/CreditSystem.Domain/Entities/LoanApplication.cs new file mode 100644 index 00000000..8399a0bd --- /dev/null +++ b/CreditSystem.Domain/Entities/LoanApplication.cs @@ -0,0 +1,49 @@ +using System; + +namespace CreditSystem.Domain.Entities; + +/// +/// Сущность кредитной заявки (Вариант 30) +/// +public class LoanApplication +{ + /// + /// Уникальный номер заявки + /// + public Guid Id { get; set; } + + /// + /// ФИО заемщика + /// + public required string ApplicantName { get; set; } + + /// + /// Сумма кредита (в рублях) + /// + public decimal RequestedAmount { get; set; } + + /// + /// Срок кредитования (в месяцах) + /// + public int TermMonths { get; set; } + + /// + /// Ежемесячный доход + /// + public decimal MonthlyIncome { get; set; } + + /// + /// Кредитный рейтинг (0-1000) + /// + public int CreditScore { get; set; } + + /// + /// Текущий статус заявки + /// + public string Status { get; set; } = "New"; + + /// + /// Дата и время подачи + /// + public DateTime CreatedAt { get; set; } +} From e63c60a777611f98a55c8f17be88edaa9a3f4bd9 Mon Sep 17 00:00:00 2001 From: Pancake2021 <92122200+Pancake2021@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:46:19 +0400 Subject: [PATCH 3/5] =?UTF-8?q?infra:=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=D0=BA=D0=B0=20.NET=20Aspire:=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20Redis=20=D0=B8=20LocalStack=20?= =?UTF-8?q?(SQS)=20=D0=BF=D0=BE=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreditSystem.AppHost.csproj | 22 +++++ CreditSystem.AppHost/Program.cs | 17 ++++ .../CreditSystem.ServiceDefaults.csproj | 19 ++++ CreditSystem.ServiceDefaults/Extensions.cs | 92 +++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 CreditSystem.AppHost/CreditSystem.AppHost.csproj create mode 100644 CreditSystem.AppHost/Program.cs create mode 100644 CreditSystem.ServiceDefaults/CreditSystem.ServiceDefaults.csproj create mode 100644 CreditSystem.ServiceDefaults/Extensions.cs diff --git a/CreditSystem.AppHost/CreditSystem.AppHost.csproj b/CreditSystem.AppHost/CreditSystem.AppHost.csproj new file mode 100644 index 00000000..54e9b46c --- /dev/null +++ b/CreditSystem.AppHost/CreditSystem.AppHost.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + true + 7f82b7c4-5d5d-4f8a-8b8b-8b8b8b8b8b8b + + + + + + + + + + + + + diff --git a/CreditSystem.AppHost/Program.cs b/CreditSystem.AppHost/Program.cs new file mode 100644 index 00000000..ce0308fe --- /dev/null +++ b/CreditSystem.AppHost/Program.cs @@ -0,0 +1,17 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// Используем Redis для кэширования (согласно общему заданию лабы №1) +var redis = builder.AddRedis("cache"); + +// Добавляем LocalStack для SQS (согласно варианту №30) +var localstack = builder.AddLocalStack("localstack") + .WithServices("sqs"); + +// Наш основной API сервис (Query Based) +builder.AddProject("creditsystem-api") + .WithReference(redis) + .WithReference(localstack) // Передаем ссылку на LocalStack/SQS + .WaitFor(redis) + .WaitFor(localstack); + +builder.Build().Run(); diff --git a/CreditSystem.ServiceDefaults/CreditSystem.ServiceDefaults.csproj b/CreditSystem.ServiceDefaults/CreditSystem.ServiceDefaults.csproj new file mode 100644 index 00000000..92166a3a --- /dev/null +++ b/CreditSystem.ServiceDefaults/CreditSystem.ServiceDefaults.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/CreditSystem.ServiceDefaults/Extensions.cs b/CreditSystem.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..ff14e2ef --- /dev/null +++ b/CreditSystem.ServiceDefaults/Extensions.cs @@ -0,0 +1,92 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace CreditSystem.ServiceDefaults; + +/// +/// Стандартные расширения для сервисов (логирование, метрики, проверки здоровья) +/// +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} From e69bfa204ee3c31c15ade1be2293df837136f134 Mon Sep 17 00:00:00 2001 From: Pancake2021 <92122200+Pancake2021@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:26:55 +0400 Subject: [PATCH 4/5] =?UTF-8?q?chore:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20Solution=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=D0=B0=20=D0=B8=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=D0=B4=2030?= =?UTF-8?q?=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CloudDevelopment.sln | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241d..eeaf7e74 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -1,9 +1,15 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36811.4 +VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditSystem.Domain", "CreditSystem.Domain\CreditSystem.Domain.csproj", "{D1D1D1D1-D1D1-D1D1-D1D1-D1D1D1D1D1D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditSystem.Api", "CreditSystem.Api\CreditSystem.Api.csproj", "{A2A2A2A2-A2A2-A2A2-A2A2-A2A2A2A2A2A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditSystem.AppHost", "CreditSystem.AppHost\CreditSystem.AppHost.csproj", "{B3B3B3B3-B3B3-B3B3-B3B3-B3B3B3B3B3B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditSystem.ServiceDefaults", "CreditSystem.ServiceDefaults\CreditSystem.ServiceDefaults.csproj", "{C4C4C4C4-C4C4-C4C4-C4C4-C4C4C4C4C4C4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,15 +17,13 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {90FE6B04-8381-437E-893A-FEBA1DA10AEE} + {D1D1D1D1-D1D1-D1D1-D1D1-D1D1D1D1D1D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1D1D1D1-D1D1-D1D1-D1D1-D1D1D1D1D1D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2A2A2A2-A2A2-A2A2-A2A2-A2A2A2A2A2A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2A2A2A2-A2A2-A2A2-A2A2-A2A2A2A2A2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3B3B3B3-B3B3-B3B3-B3B3-B3B3B3B3B3B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3B3B3B3-B3B3-B3B3-B3B3-B3B3B3B3B3B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4C4C4C4-C4C4-C4C4-C4C4-C4C4C4C4C4C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4C4C4C4-C4C4-C4C4-C4C4-C4C4C4C4C4C4}.Debug|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection EndGlobal From 957eb653ef6af17c62466c629c9552786c1d2c83 Mon Sep 17 00:00:00 2001 From: Pancake2021 <92122200+Pancake2021@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:05:07 +0400 Subject: [PATCH 5/5] Initial commit: Variant 30 Credit Application with SQS and Localstack --- .editorconfig | 153 +++++++ .github/DISCUSSION_TEMPLATE/questions.yml | 38 ++ ...20\276\321\200\320\275\320\276\320\271.md" | 33 ++ .github/PULL_REQUEST_TEMPLATE.md | 6 + .github/workflows/setup_pr.yml | 76 ++++ .gitignore | 418 ++++++++++++++++++ Client.Wasm/App.razor | 12 + Client.Wasm/Client.Wasm.csproj | 21 + Client.Wasm/Components/DataCard.razor | 74 ++++ Client.Wasm/Components/StudentCard.razor | 13 + Client.Wasm/Layout/MainLayout.razor | 12 + Client.Wasm/Layout/MainLayout.razor.css | 77 ++++ Client.Wasm/Pages/Home.razor | 3 + Client.Wasm/Program.cs | 17 + Client.Wasm/Properties/launchSettings.json | 41 ++ Client.Wasm/_Imports.razor | 15 + Client.Wasm/wwwroot/appsettings.json | 10 + Client.Wasm/wwwroot/css/app.css | 103 +++++ Client.Wasm/wwwroot/favicon.png | Bin 0 -> 1148 bytes Client.Wasm/wwwroot/icon-192.png | Bin 0 -> 2626 bytes Client.Wasm/wwwroot/index.html | 35 ++ CloudDevelopment.sln | 48 ++ LICENSE | 21 + .../CreditApplicationController.cs | 26 ++ ProjectApp.Api/Program.cs | 68 +++ ProjectApp.Api/ProjectApp.Api.csproj | 23 + ProjectApp.Api/Properties/launchSettings.json | 41 ++ .../CreditApplicationGenerator.cs | 81 ++++ .../CreditApplicationService.cs | 110 +++++ .../ICreditApplicationService.cs | 11 + ProjectApp.Api/appsettings.Development.json | 12 + ProjectApp.Api/appsettings.json | 12 + ProjectApp.AppHost/Program.cs | 18 + ProjectApp.AppHost/ProjectApp.AppHost.csproj | 24 + .../Properties/launchSettings.json | 29 ++ .../appsettings.Development.json | 8 + ProjectApp.AppHost/appsettings.json | 9 + .../Entities/CreditApplication.cs | 67 +++ ProjectApp.Domain/ProjectApp.Domain.csproj | 11 + ProjectApp.ServiceDefaults/Extensions.cs | 104 +++++ .../ProjectApp.ServiceDefaults.csproj | 20 + README.md | 46 ++ 42 files changed, 1946 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/DISCUSSION_TEMPLATE/questions.yml create mode 100644 ".github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/setup_pr.yml create mode 100644 .gitignore create mode 100644 Client.Wasm/App.razor create mode 100644 Client.Wasm/Client.Wasm.csproj create mode 100644 Client.Wasm/Components/DataCard.razor create mode 100644 Client.Wasm/Components/StudentCard.razor create mode 100644 Client.Wasm/Layout/MainLayout.razor create mode 100644 Client.Wasm/Layout/MainLayout.razor.css create mode 100644 Client.Wasm/Pages/Home.razor create mode 100644 Client.Wasm/Program.cs create mode 100644 Client.Wasm/Properties/launchSettings.json create mode 100644 Client.Wasm/_Imports.razor create mode 100644 Client.Wasm/wwwroot/appsettings.json create mode 100644 Client.Wasm/wwwroot/css/app.css create mode 100644 Client.Wasm/wwwroot/favicon.png create mode 100644 Client.Wasm/wwwroot/icon-192.png create mode 100644 Client.Wasm/wwwroot/index.html create mode 100644 CloudDevelopment.sln create mode 100644 LICENSE create mode 100644 ProjectApp.Api/Controllers/CreditApplicationController.cs create mode 100644 ProjectApp.Api/Program.cs create mode 100644 ProjectApp.Api/ProjectApp.Api.csproj create mode 100644 ProjectApp.Api/Properties/launchSettings.json create mode 100644 ProjectApp.Api/Services/CreditApplicationService/CreditApplicationGenerator.cs create mode 100644 ProjectApp.Api/Services/CreditApplicationService/CreditApplicationService.cs create mode 100644 ProjectApp.Api/Services/CreditApplicationService/ICreditApplicationService.cs create mode 100644 ProjectApp.Api/appsettings.Development.json create mode 100644 ProjectApp.Api/appsettings.json create mode 100644 ProjectApp.AppHost/Program.cs create mode 100644 ProjectApp.AppHost/ProjectApp.AppHost.csproj create mode 100644 ProjectApp.AppHost/Properties/launchSettings.json create mode 100644 ProjectApp.AppHost/appsettings.Development.json create mode 100644 ProjectApp.AppHost/appsettings.json create mode 100644 ProjectApp.Domain/Entities/CreditApplication.cs create mode 100644 ProjectApp.Domain/ProjectApp.Domain.csproj create mode 100644 ProjectApp.ServiceDefaults/Extensions.cs create mode 100644 ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj create mode 100644 README.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..0f3bba5c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,153 @@ +[*.cs] +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:error +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:error +csharp_style_prefer_tuple_swap = true:suggestion +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_var_for_built_in_types = true:error +csharp_style_var_when_type_is_apparent = true:error +csharp_style_var_elsewhere = false:silent +csharp_space_around_binary_operators = before_and_after +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = error +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = error +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:error +dotnet_style_namespace_match_folder = true:error +dotnet_style_require_accessibility_modifiers = always:error +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.underscored.capitalization = camel_case +dotnet_naming_style.underscored.required_prefix = _ + +dotnet_naming_rule.private_fields_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_underscored.style = underscored +dotnet_naming_rule.private_fields_underscored.severity = error + +dotnet_naming_rule.constants_must_be_uppercase.symbols = public_constants +dotnet_naming_symbols.public_constants.applicable_kinds = field +dotnet_naming_symbols.public_constants.applicable_accessibilities = * +dotnet_naming_symbols.public_constants.required_modifiers = const + +dotnet_naming_rule.constants_must_be_uppercase.style = pascal_case +dotnet_naming_style.uppercase_with_underscore_separator.capitalization = pascal_case + +dotnet_naming_rule.constants_must_be_uppercase.severity = error +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent + +# For variables +dotnet_naming_symbols.local_symbol.applicable_kinds = local +dotnet_naming_style.local_style.capitalization = camel_case +dotnet_naming_rule.variables_are_camel_case.severity = error +dotnet_naming_rule.variables_are_camel_case.symbols = local_symbol +dotnet_naming_rule.variables_are_camel_case.style = local_style \ No newline at end of file diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml new file mode 100644 index 00000000..6c8409e8 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -0,0 +1,38 @@ +labels: [q&a] +body: + - type: input + id: fio + attributes: + label: Привет, меня зовут + description: | + Напиши свои ФИО и номер группы, чтобы тебе ответил преподаватель, который ведет у тебя пары + placeholder: | + Фамилия И.О. 651Х + validations: + required: true + + - type: dropdown + id: lab + attributes: + label: У меня вопрос по + description: | + Выбери лабораторную, которая вызвала трудности + multiple: true + options: + - 1 лабораторной работе (Кэширование) + - 2 лабораторной работе (Балансировка нагрузки) + - 3 лабораторной работе (Интеграционное тестирование) + - 4 лабораторной работе (Переход на облачную инфраструктуру) + validations: + required: true + + - type: textarea + id: details + attributes: + label: Описание проблемы + description: | + Подробно опиши проблему, с которой ты столкнулся при выполнении лабораторной + placeholder: | + Также было бы крайне полезно привести помимо текстового описания проблемы скриншоты и фрагменты кода + validations: + required: true \ No newline at end of file diff --git "a/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" "b/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" new file mode 100644 index 00000000..02038ea5 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\320\262\320\276\320\277\321\200\320\276\321\201-\320\277\320\276-\320\273\320\260\320\261\320\276\321\200\320\260\321\202\320\276\321\200\320\275\320\276\320\271.md" @@ -0,0 +1,33 @@ +--- +name: Вопрос по лабораторной +about: Этот шаблон предназначен для того, чтобы студенты могли задать вопрос по лабораторной +title: Вопрос по лабораторной +labels: '' +assignees: Gwymlas, alxmcs, danlla + +--- + +**Меня зовут:** +Укажите свои ФИО + +**Я из группы:** +Укажите номер группы + +**У меня вопрос по лабе:** +Укажите номер и название лабораторной, по которой появился вопрос. + +**Мой вопрос:** +Максимально подробно опишите, что вы хотите узнать/что у вас не получается/что у вас не работает. При необходимости, добавьте примеры кода. Примеры кода должны быть оформлены с использованием md разметки, чтобы их можно было удобно воспринимать: + +```cs +public class Program +{ + public static void Main(string[] args) + { + System.Console.WriteLine("Hello, World!"); + } +} +``` + +**Дополнительная информация** +Опишите тут все, что не попадает под перечисленные ранее категории (если в том есть необходимость). \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..647b33e3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ +**ФИО:** Фамилия Имя +**Номер группы:** 651Х +**Номер лабораторной:** Х +**Номер варианта:** ХХ +**Краткое описание предметной области:** Товар на складе/учебный курс/т.д. +**Краткое описание добавленных фич:** Добавлен сервис генерации/интеграционные тесты/т.д. \ No newline at end of file diff --git a/.github/workflows/setup_pr.yml b/.github/workflows/setup_pr.yml new file mode 100644 index 00000000..40fdb46b --- /dev/null +++ b/.github/workflows/setup_pr.yml @@ -0,0 +1,76 @@ +name: Setup PR for code review + +on: + pull_request_target: + types: [opened, reopened] + +permissions: write-all + +jobs: + + assign: + runs-on: ubuntu-latest + steps: + - name: Parsing your PR title for lab number + env: + TITLE: ${{ github.event.pull_request.title }} + run: | + SUB='Лаб.' + for VAR in 1 2 3 4 + do + if (echo $TITLE | grep -iqF "$SUB$VAR" )|| (echo $TITLE | grep -iqF "$SUB $VAR"); then + echo "LABEL=Lab $VAR" >> "$GITHUB_ENV" + break + fi + done + for VAR in 6511 6512 6513 + do + if (echo $TITLE | grep -iqF "$VAR" ); then + echo "GROUP=$VAR" >> "$GITHUB_ENV" + break + fi + done + + - name: Checking your lab number + run: | + if [[ $LABEL == '' ]]; then + echo "Your PR caption is not composed correctly" + exit 1 + fi + echo Your number was parsed correctly - ${{ env.LABEL }} + + - name: Checking your group number + run: | + if [[ $GROUP == '' ]]; then + echo "Your PR caption is not composed correctly" + exit 1 + fi + echo Your group was parsed correctly - ${{ env.GROUP }} + + - name: Setting PR labels + uses: actions-ecosystem/action-add-labels@v1 + with: + labels: | + ${{ env.LABEL }} + In progress + + - name: Setting reviewer + if: env.GROUP == '6511' + uses: AveryCameronUofR/add-reviewer-gh-action@1.0.3 + with: + reviewers: "danlla" + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setting reviewer + if: env.GROUP == '6512' + uses: AveryCameronUofR/add-reviewer-gh-action@1.0.3 + with: + reviewers: "Gwymlas" + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setting reviewer + if: env.GROUP == '6513' + uses: AveryCameronUofR/add-reviewer-gh-action@1.0.3 + with: + reviewers: "alxmcs" + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ce892922 --- /dev/null +++ b/.gitignore @@ -0,0 +1,418 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +**/.paket/paket.exe +paket-files/ + +# FAKE - F# Make +**/.fake/ + +# CodeRush personal settings +**/.cr/personal + +# Python Tools for Visual Studio (PTVS) +**/__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +#tools/** +#!tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +**/.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp diff --git a/Client.Wasm/App.razor b/Client.Wasm/App.razor new file mode 100644 index 00000000..6fd3ed1b --- /dev/null +++ b/Client.Wasm/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/Client.Wasm/Client.Wasm.csproj b/Client.Wasm/Client.Wasm.csproj new file mode 100644 index 00000000..0ba9f90c --- /dev/null +++ b/Client.Wasm/Client.Wasm.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/Client.Wasm/Components/DataCard.razor b/Client.Wasm/Components/DataCard.razor new file mode 100644 index 00000000..791ebb59 --- /dev/null +++ b/Client.Wasm/Components/DataCard.razor @@ -0,0 +1,74 @@ +@inject IConfiguration Configuration +@inject HttpClient Client + + + + + Характеристики текущего объекта + + + + + + # + Характеристика + Значение + + + + @if(Value is null) + { + + 1 + нет данных + нет данных + + } + else + { + var array = Value.ToArray(); + foreach (var property in array) + { + + @(Array.IndexOf(array, property)+1) + @property.Key + @property.Value?.ToString() + + } + } + +
+
+
+ + + + Запросить новый объект + + + + + Идентификатор нового объекта: + + + + + + + + + + +
+ +@code { + private JsonObject? Value { get; set; } + private int Id { get; set; } + + private async Task RequestNewData() + { + var baseAddress = Configuration["BaseAddress"] ?? throw new KeyNotFoundException("Конфигурация клиента не содержит параметра BaseAddress"); + Value = await Client.GetFromJsonAsync($"{baseAddress}?id={Id}", new JsonSerializerOptions { }); + StateHasChanged(); + } +} diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor new file mode 100644 index 00000000..6f172737 --- /dev/null +++ b/Client.Wasm/Components/StudentCard.razor @@ -0,0 +1,13 @@ + + + Лабораторная работа + + + + Номер №1 "Кэширование" + Вариант №30 "Кредитная заявка" + Выполнена Панкеевым Глебом 6512 + Ссылка на мой форк + + + diff --git a/Client.Wasm/Layout/MainLayout.razor b/Client.Wasm/Layout/MainLayout.razor new file mode 100644 index 00000000..d6f90808 --- /dev/null +++ b/Client.Wasm/Layout/MainLayout.razor @@ -0,0 +1,12 @@ +@inherits LayoutComponentBase +
+
+
+ +
+ +
+ @Body +
+
+
diff --git a/Client.Wasm/Layout/MainLayout.razor.css b/Client.Wasm/Layout/MainLayout.razor.css new file mode 100644 index 00000000..ecf25e5b --- /dev/null +++ b/Client.Wasm/Layout/MainLayout.razor.css @@ -0,0 +1,77 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/Client.Wasm/Pages/Home.razor b/Client.Wasm/Pages/Home.razor new file mode 100644 index 00000000..b22b00ed --- /dev/null +++ b/Client.Wasm/Pages/Home.razor @@ -0,0 +1,3 @@ +@page "/" + + \ No newline at end of file diff --git a/Client.Wasm/Program.cs b/Client.Wasm/Program.cs new file mode 100644 index 00000000..a182a920 --- /dev/null +++ b/Client.Wasm/Program.cs @@ -0,0 +1,17 @@ +using Blazorise; +using Blazorise.Bootstrap; +using Blazorise.Icons.FontAwesome; +using Client.Wasm; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +builder.Services.AddBlazorise(options => { options.Immediate = true; }) + .AddBootstrapProviders() + .AddFontAwesomeIcons(); + +await builder.Build().RunAsync(); diff --git a/Client.Wasm/Properties/launchSettings.json b/Client.Wasm/Properties/launchSettings.json new file mode 100644 index 00000000..0d824ea7 --- /dev/null +++ b/Client.Wasm/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36545", + "sslPort": 44337 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5127", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7282;http://localhost:5127", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Client.Wasm/_Imports.razor b/Client.Wasm/_Imports.razor new file mode 100644 index 00000000..cedf9977 --- /dev/null +++ b/Client.Wasm/_Imports.razor @@ -0,0 +1,15 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using Client.Wasm.Layout +@using Client.Wasm.Components +@using Blazorise +@using Blazorise.Bootstrap +@using Blazorise.Icons.FontAwesome +@using System.Text.Json +@using System.Text.Json.Nodes \ No newline at end of file diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json new file mode 100644 index 00000000..007f63f6 --- /dev/null +++ b/Client.Wasm/wwwroot/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "BaseAddress": "http://localhost:5179/api/creditapplication" +} diff --git a/Client.Wasm/wwwroot/css/app.css b/Client.Wasm/wwwroot/css/app.css new file mode 100644 index 00000000..54a8aa38 --- /dev/null +++ b/Client.Wasm/wwwroot/css/app.css @@ -0,0 +1,103 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} diff --git a/Client.Wasm/wwwroot/favicon.png b/Client.Wasm/wwwroot/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8422b59695935d180d11d5dbe99653e711097819 GIT binary patch literal 1148 zcmV-?1cUpDP)9h26h2-Cs%i*@Moc3?#6qJID|D#|3|2Hn7gTIYEkr|%Xjp);YgvFmB&0#2E2b=| zkVr)lMv9=KqwN&%obTp-$<51T%rx*NCwceh-E+=&e(oLO`@Z~7gybJ#U|^tB2Pai} zRN@5%1qsZ1e@R(XC8n~)nU1S0QdzEYlWPdUpH{wJ2Pd4V8kI3BM=)sG^IkUXF2-j{ zrPTYA6sxpQ`Q1c6mtar~gG~#;lt=s^6_OccmRd>o{*=>)KS=lM zZ!)iG|8G0-9s3VLm`bsa6e ze*TlRxAjXtm^F8V`M1%s5d@tYS>&+_ga#xKGb|!oUBx3uc@mj1%=MaH4GR0tPBG_& z9OZE;->dO@`Q)nr<%dHAsEZRKl zedN6+3+uGHejJp;Q==pskSAcRcyh@6mjm2z-uG;s%dM-u0*u##7OxI7wwyCGpS?4U zBFAr(%GBv5j$jS@@t@iI8?ZqE36I^4t+P^J9D^ELbS5KMtZ z{Qn#JnSd$15nJ$ggkF%I4yUQC+BjDF^}AtB7w348EL>7#sAsLWs}ndp8^DsAcOIL9 zTOO!!0!k2`9BLk25)NeZp7ev>I1Mn={cWI3Yhx2Q#DnAo4IphoV~R^c0x&nw*MoIV zPthX?{6{u}sMS(MxD*dmd5rU(YazQE59b|TsB5Tm)I4a!VaN@HYOR)DwH1U5y(E)z zQqQU*B%MwtRQ$%x&;1p%ANmc|PkoFJZ%<-uq%PX&C!c-7ypis=eP+FCeuv+B@h#{4 zGx1m0PjS~FJt}3mdt4c!lel`1;4W|03kcZRG+DzkTy|7-F~eDsV2Tx!73dM0H0CTh zl)F-YUkE1zEzEW(;JXc|KR5{ox%YTh{$%F$a36JP6Nb<0%#NbSh$dMYF-{ z1_x(Vx)}fs?5_|!5xBTWiiIQHG<%)*e=45Fhjw_tlnmlixq;mUdC$R8v#j( zhQ$9YR-o%i5Uc`S?6EC51!bTRK=Xkyb<18FkCKnS2;o*qlij1YA@-nRpq#OMTX&RbL<^2q@0qja!uIvI;j$6>~k@IMwD42=8$$!+R^@5o6HX(*n~v0A9xRwxP|bki~~&uFk>U z#P+PQh zyZ;-jwXKqnKbb6)@RaxQz@vm={%t~VbaZrdbaZrdbaeEeXj>~BG?&`J0XrqR#sSlO zg~N5iUk*15JibvlR1f^^1czzNKWvoJtc!Sj*G37QXbZ8LeD{Fzxgdv#Q{x}ytfZ5q z+^k#NaEp>zX_8~aSaZ`O%B9C&YLHb(mNtgGD&Kezd5S@&C=n~Uy1NWHM`t07VQP^MopUXki{2^#ryd94>UJMYW|(#4qV`kb7eD)Q=~NN zaVIRi@|TJ!Rni8J=5DOutQ#bEyMVr8*;HU|)MEKmVC+IOiDi9y)vz=rdtAUHW$yjt zrj3B7v(>exU=IrzC<+?AE=2vI;%fafM}#ShGDZx=0Nus5QHKdyb9pw&4>4XCpa-o?P(Gnco1CGX|U> z$f+_tA3+V~<{MU^A%eP!8R*-sD9y<>Jc7A(;aC5hVbs;kX9&Sa$JMG!W_BLFQa*hM zri__C@0i0U1X#?)Y=)>JpvTnY6^s;fu#I}K9u>OldV}m!Ch`d1Vs@v9 zb}w(!TvOmSzmMBa9gYvD4xocL2r0ds6%Hs>Z& z#7#o9PGHDmfG%JQq`O5~dt|MAQN@2wyJw_@``7Giyy(yyk(m8U*kk5$X1^;3$a3}N^Lp6hE5!#8l z#~NYHmKAs6IAe&A;bvM8OochRmXN>`D`{N$%#dZCRxp4-dJ?*3P}}T`tYa3?zz5BA zTu7uE#GsDpZ$~j9q=Zq!LYjLbZPXFILZK4?S)C-zE1(dC2d<7nO4-nSCbV#9E|E1MM|V<9>i4h?WX*r*ul1 z5#k6;po8z=fdMiVVz*h+iaTlz#WOYmU^SX5#97H~B32s-#4wk<1NTN#g?LrYieCu> zF7pbOLR;q2D#Q`^t%QcY06*X-jM+ei7%ZuanUTH#9Y%FBi*Z#22({_}3^=BboIsbg zR0#jJ>9QR8SnmtSS6x($?$}6$x+q)697#m${Z@G6Ujf=6iO^S}7P`q8DkH!IHd4lB zDzwxt3BHsPAcXFFY^Fj}(073>NL_$A%v2sUW(CRutd%{G`5ow?L`XYSO*Qu?x+Gzv zBtR}Y6`XF4xX7)Z04D+fH;TMapdQFFameUuHL34NN)r@aF4RO%x&NApeWGtr#mG~M z6sEIZS;Uj1HB1*0hh=O@0q1=Ia@L>-tETu-3n(op+97E z#&~2xggrl(LA|giII;RwBlX2^Q`B{_t}gxNL;iB11gEPC>v` zb4SJ;;BFOB!{chn>?cCeGDKuqI0+!skyWTn*k!WiPNBf=8rn;@y%( znhq%8fj2eAe?`A5mP;TE&iLEmQ^xV%-kmC-8mWao&EUK_^=GW-Y3z ksi~={si~={skwfB0gq6itke#r1ONa407*qoM6N<$g11Kq@c;k- literal 0 HcmV?d00001 diff --git a/Client.Wasm/wwwroot/index.html b/Client.Wasm/wwwroot/index.html new file mode 100644 index 00000000..b74ee328 --- /dev/null +++ b/Client.Wasm/wwwroot/index.html @@ -0,0 +1,35 @@ + + + + + + + Client.Wasm + + + + + + + + + + + +
+ + + + +
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln new file mode 100644 index 00000000..7e69b888 --- /dev/null +++ b/CloudDevelopment.sln @@ -0,0 +1,48 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35931.197 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.Api", "ProjectApp.Api\ProjectApp.Api.csproj", "{E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.Domain", "ProjectApp.Domain\ProjectApp.Domain.csproj", "{CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.ServiceDefaults", "ProjectApp.ServiceDefaults\ProjectApp.ServiceDefaults.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectApp.AppHost", "ProjectApp.AppHost\ProjectApp.AppHost.csproj", "{2A5FB573-9376-4FEB-9289-A8387F435C13}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.Build.0 = Release|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC5A9873-4CC3-4B71-83AF-E4FD09F7B1AD}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A5FB573-9376-4FEB-9289-A8387F435C13}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {90FE6B04-8381-437E-893A-FEBA1DA10AEE} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..fa20eb9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 alxmcs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ProjectApp.Api/Controllers/CreditApplicationController.cs b/ProjectApp.Api/Controllers/CreditApplicationController.cs new file mode 100644 index 00000000..f5696ba6 --- /dev/null +++ b/ProjectApp.Api/Controllers/CreditApplicationController.cs @@ -0,0 +1,26 @@ +using ProjectApp.Api.Services.CreditApplicationService; +using ProjectApp.Domain.Entities; +using Microsoft.AspNetCore.Mvc; + +namespace ProjectApp.Api.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class CreditApplicationController(ICreditApplicationService creditService, ILogger logger) : ControllerBase +{ + /// + /// Получить кредитную заявку по ID, если не найдена в кэше — сгенерировать новую + /// + /// ID кредитной заявки + /// Токен отмены операции + /// Кредитная заявка + [HttpGet] + public async Task> GetById([FromQuery] int id, CancellationToken cancellationToken) + { + logger.LogInformation("Received request to retrieve/generate credit application {Id}", id); + + var application = await creditService.GetByIdAsync(id, cancellationToken); + + return Ok(application); + } +} diff --git a/ProjectApp.Api/Program.cs b/ProjectApp.Api/Program.cs new file mode 100644 index 00000000..260fdd6f --- /dev/null +++ b/ProjectApp.Api/Program.cs @@ -0,0 +1,68 @@ +using ProjectApp.Api.Services.CreditApplicationService; +using ProjectApp.ServiceDefaults; +using Amazon.SQS; +using Amazon.Runtime; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// SQS configuration for Localstack +var awsCredentials = new BasicAWSCredentials("test", "test"); +var sqsConfig = new AmazonSQSConfig +{ + ServiceURL = builder.Configuration["Services:localstack:HttpEndpoint"] ?? "http://localhost:4566", + AuthenticationRegion = "us-east-1" +}; +builder.Services.AddSingleton(new AmazonSQSClient(awsCredentials, sqsConfig)); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .WithMethods("GET") + .WithHeaders("Content-Type"); + }); +}); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new Microsoft.OpenApi.OpenApiInfo + { + Title = "Credit Application Generator API" + }); + + var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename); + if (File.Exists(xmlPath)) + { + options.IncludeXmlComments(xmlPath); + } + + var domainXmlPath = Path.Combine(AppContext.BaseDirectory, "ProjectApp.Domain.xml"); + if (File.Exists(domainXmlPath)) + { + options.IncludeXmlComments(domainXmlPath); + } +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors(); +app.UseHttpsRedirection(); +app.MapControllers(); +app.MapDefaultEndpoints(); + +app.Run(); \ No newline at end of file diff --git a/ProjectApp.Api/ProjectApp.Api.csproj b/ProjectApp.Api/ProjectApp.Api.csproj new file mode 100644 index 00000000..f443dc0a --- /dev/null +++ b/ProjectApp.Api/ProjectApp.Api.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + + + + + + + + + diff --git a/ProjectApp.Api/Properties/launchSettings.json b/ProjectApp.Api/Properties/launchSettings.json new file mode 100644 index 00000000..bbe9ee66 --- /dev/null +++ b/ProjectApp.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:46825", + "sslPort": 44333 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5179", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7170;http://localhost:5179", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ProjectApp.Api/Services/CreditApplicationService/CreditApplicationGenerator.cs b/ProjectApp.Api/Services/CreditApplicationService/CreditApplicationGenerator.cs new file mode 100644 index 00000000..9074ace5 --- /dev/null +++ b/ProjectApp.Api/Services/CreditApplicationService/CreditApplicationGenerator.cs @@ -0,0 +1,81 @@ +using Bogus; +using ProjectApp.Domain.Entities; + +namespace ProjectApp.Api.Services.CreditApplicationService; + +/// +/// Генератор случайных кредитных заявок с использованием Bogus +/// +public class CreditApplicationGenerator +{ + private readonly Faker _faker; + + public CreditApplicationGenerator() + { + _faker = new Faker("ru") + .RuleFor(c => c.Id, f => f.IndexFaker + 1) + .RuleFor(c => c.ClientFullName, f => f.Name.FullName()) + .RuleFor(c => c.ApplicationDate, f => f.Date.PastDateOnly(1)) + .RuleFor(c => c.CreditAmount, f => Math.Round(f.Finance.Amount(50000, 5000000), 2)) + .RuleFor(c => c.CreditTermMonths, f => f.Random.Int(6, 120)) + .RuleFor(c => c.CreditPurpose, f => f.PickRandom( + "Покупка автомобиля", + "Покупка недвижимости", + "Ремонт квартиры", + "Образование", + "Лечение", + "Погашение других кредитов", + "Отпуск", + "Покупка техники", + "Иное" + )) + .RuleFor(c => c.ClientIncome, f => Math.Round(f.Finance.Amount(20000, 500000), 2)) + .RuleFor(c => c.CreditScore, f => f.Random.Int(300, 1000)) + .RuleFor(c => c.Approved, f => + { + var hasDecision = f.Random.Bool(0.7f); + if (!hasDecision) + { + return null; + } + return f.Random.Bool(0.6f); + }) + .RuleFor(c => c.DecisionDate, (f, c) => + { + if (!c.Approved.HasValue) + { + return null; + } + + var minDate = c.ApplicationDate.ToDateTime(TimeOnly.MinValue).AddDays(1); + var maxDate = DateTime.Now; + + if (minDate > maxDate) + { + return DateOnly.FromDateTime(minDate); + } + + var endDate = f.Date.Between(minDate, maxDate); + return DateOnly.FromDateTime(endDate); + }) + .RuleFor(c => c.InterestRate, f => Math.Round(f.Random.Decimal(4.9m, 24.9m), 2)) + .RuleFor(c => c.MonthlyPayment, (f, c) => + { + var monthlyRate = c.InterestRate / 100 / 12; + var numerator = c.CreditAmount * monthlyRate; + var denominator = (decimal)(1 - Math.Pow((double)(1 + monthlyRate), -c.CreditTermMonths)); + + if (denominator == 0) + { + return Math.Round(c.CreditAmount / c.CreditTermMonths, 2); + } + + return Math.Round(numerator / denominator, 2); + }); + } + + /// + /// Генерирует одну случайную кредитную заявку + /// + public CreditApplication Generate() => _faker.Generate(); +} diff --git a/ProjectApp.Api/Services/CreditApplicationService/CreditApplicationService.cs b/ProjectApp.Api/Services/CreditApplicationService/CreditApplicationService.cs new file mode 100644 index 00000000..49f60698 --- /dev/null +++ b/ProjectApp.Api/Services/CreditApplicationService/CreditApplicationService.cs @@ -0,0 +1,110 @@ +using ProjectApp.Domain.Entities; +using Amazon.SQS; +using Amazon.SQS.Model; +using System.Text.Json; + +namespace ProjectApp.Api.Services.CreditApplicationService; + +/// +/// Сервис получения кредитной заявки с использованием SQS для кэширования +/// +public class CreditApplicationService( + IAmazonSQS sqsClient, + CreditApplicationGenerator generator, + IConfiguration configuration, + ILogger logger) : ICreditApplicationService +{ + private readonly string _queueUrl = configuration["SQS:QueueUrl"] ?? "http://localhost:4566/000000000000/credit-application-cache"; + + /// + /// Возвращает кредитную заявку по идентификатору. + /// Если заявка найдена в SQS — возвращается из него; иначе генерируется, сохраняется в SQS и возвращается. + /// + /// Идентификатор заявки + /// Токен отмены операции + /// Кредитная заявка + public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) + { + logger.LogInformation("Attempting to retrieve credit application {Id} from SQS", id); + + // Получаем сообщения из SQS + CreditApplication? application = null; + try + { + var receiveRequest = new ReceiveMessageRequest + { + QueueUrl = _queueUrl, + MaxNumberOfMessages = 10, + WaitTimeSeconds = 0 + }; + + var response = await sqsClient.ReceiveMessageAsync(receiveRequest, cancellationToken); + + foreach (var message in response.Messages) + { + try + { + var app = JsonSerializer.Deserialize(message.Body); + if (app != null && app.Id == id) + { + logger.LogInformation("Credit application {Id} found in SQS", id); + + // Удаляем сообщение после получения + await sqsClient.DeleteMessageAsync(new DeleteMessageRequest + { + QueueUrl = _queueUrl, + ReceiptHandle = message.ReceiptHandle + }, cancellationToken); + + return app; + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to deserialize message from SQS"); + } + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to receive messages from SQS (error ignored)"); + } + + // Если в SQS нет или ошибка — генерируем новую заявку + logger.LogInformation("Credit application {Id} not found in SQS or SQS unavailable, generating a new one", id); + application = generator.Generate(); + application.Id = id; + + // Попытка сохранить в SQS + try + { + logger.LogInformation("Saving credit application {Id} to SQS", id); + + var sendRequest = new SendMessageRequest + { + QueueUrl = _queueUrl, + MessageBody = JsonSerializer.Serialize(application), + MessageAttributes = new Dictionary + { + { "ApplicationId", new MessageAttributeValue { StringValue = id.ToString(), DataType = "String" } } + } + }; + + await sqsClient.SendMessageAsync(sendRequest, cancellationToken); + + logger.LogInformation( + "Credit application generated and sent to SQS: Id={Id}, Client={ClientFullName}, Amount={CreditAmount}, Purpose={CreditPurpose}, Score={CreditScore}", + application.Id, + application.ClientFullName, + application.CreditAmount, + application.CreditPurpose, + application.CreditScore); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to send credit application {Id} to SQS (error ignored)", id); + } + + return application; + } +} diff --git a/ProjectApp.Api/Services/CreditApplicationService/ICreditApplicationService.cs b/ProjectApp.Api/Services/CreditApplicationService/ICreditApplicationService.cs new file mode 100644 index 00000000..475bd44c --- /dev/null +++ b/ProjectApp.Api/Services/CreditApplicationService/ICreditApplicationService.cs @@ -0,0 +1,11 @@ +using ProjectApp.Domain.Entities; + +namespace ProjectApp.Api.Services.CreditApplicationService; + +/// +/// Сервис получения кредитной заявки +/// +public interface ICreditApplicationService +{ + public Task GetByIdAsync(int id, CancellationToken cancellationToken = default); +} diff --git a/ProjectApp.Api/appsettings.Development.json b/ProjectApp.Api/appsettings.Development.json new file mode 100644 index 00000000..b642d7aa --- /dev/null +++ b/ProjectApp.Api/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CacheSettings": { + "ExpirationMinutes": 10 + } +} diff --git a/ProjectApp.Api/appsettings.json b/ProjectApp.Api/appsettings.json new file mode 100644 index 00000000..b642d7aa --- /dev/null +++ b/ProjectApp.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "CacheSettings": { + "ExpirationMinutes": 10 + } +} diff --git a/ProjectApp.AppHost/Program.cs b/ProjectApp.AppHost/Program.cs new file mode 100644 index 00000000..13dea731 --- /dev/null +++ b/ProjectApp.AppHost/Program.cs @@ -0,0 +1,18 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var localstack = builder.AddContainer("localstack", "localstack/localstack") + .WithEnvironment("SERVICES", "sqs,s3") + .WithEnvironment("DEFAULT_REGION", "us-east-1") + .WithEnvironment("AWS_ACCESS_KEY_ID", "test") + .WithEnvironment("AWS_SECRET_ACCESS_KEY", "test") + .WithHttpEndpoint(port: 4566, targetPort: 4566); + +var api = builder.AddProject("projectapp-api") + .WaitFor(localstack) + .WithReplicas(3); + +builder.AddProject("client") + .WithReference(api) + .WaitFor(api); + +builder.Build().Run(); diff --git a/ProjectApp.AppHost/ProjectApp.AppHost.csproj b/ProjectApp.AppHost/ProjectApp.AppHost.csproj new file mode 100644 index 00000000..7704751d --- /dev/null +++ b/ProjectApp.AppHost/ProjectApp.AppHost.csproj @@ -0,0 +1,24 @@ + + + + + + Exe + net8.0 + enable + enable + true + b8f3eae0-771a-4f3a-8df3-ef0a21b09b55 + + + + + + + + + + + + + diff --git a/ProjectApp.AppHost/Properties/launchSettings.json b/ProjectApp.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..fa890ea9 --- /dev/null +++ b/ProjectApp.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17214;http://localhost:15105", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21185", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22273" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15105", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19190", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20136" + } + } + } +} diff --git a/ProjectApp.AppHost/appsettings.Development.json b/ProjectApp.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ProjectApp.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ProjectApp.AppHost/appsettings.json b/ProjectApp.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/ProjectApp.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/ProjectApp.Domain/Entities/CreditApplication.cs b/ProjectApp.Domain/Entities/CreditApplication.cs new file mode 100644 index 00000000..38ab73c9 --- /dev/null +++ b/ProjectApp.Domain/Entities/CreditApplication.cs @@ -0,0 +1,67 @@ +namespace ProjectApp.Domain.Entities; + +/// +/// Кредитная заявка +/// +public class CreditApplication +{ + /// + /// Идентификатор в системе + /// + public required int Id { get; set; } + + /// + /// ФИО клиента + /// + public required string ClientFullName { get; set; } + + /// + /// Дата подачи заявки + /// + public DateOnly ApplicationDate { get; set; } + + /// + /// Сумма кредита + /// + public decimal CreditAmount { get; set; } + + /// + /// Срок кредита в месяцах + /// + public int CreditTermMonths { get; set; } + + /// + /// Цель кредита + /// + public required string CreditPurpose { get; set; } + + /// + /// Доход клиента + /// + public decimal ClientIncome { get; set; } + + /// + /// Кредитный рейтинг (0-1000) + /// + public int CreditScore { get; set; } + + /// + /// Дата принятия решения + /// + public DateOnly? DecisionDate { get; set; } + + /// + /// Решение по заявке: одобрено (true), отклонено (false), null - ещё не решено + /// + public bool? Approved { get; set; } + + /// + /// Процентная ставка (годовых) + /// + public decimal InterestRate { get; set; } + + /// + /// Ежемесячный платёж + /// + public decimal MonthlyPayment { get; set; } +} diff --git a/ProjectApp.Domain/ProjectApp.Domain.csproj b/ProjectApp.Domain/ProjectApp.Domain.csproj new file mode 100644 index 00000000..fcfb2654 --- /dev/null +++ b/ProjectApp.Domain/ProjectApp.Domain.csproj @@ -0,0 +1,11 @@ + + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + diff --git a/ProjectApp.ServiceDefaults/Extensions.cs b/ProjectApp.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..ac558a7d --- /dev/null +++ b/ProjectApp.ServiceDefaults/Extensions.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace ProjectApp.ServiceDefaults; + +/// +/// Расширения для настройки сервисов Aspire (структурное логирование, телеметрия, health checks) +/// +public static class Extensions +{ + /// + /// Добавляет стандартные настройки Aspire для сервисов + /// + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + /// + /// Настройка OpenTelemetry для структурного логирования и телеметрии + /// + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + /// + /// Добавляет стандартные health checks + /// + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + /// + /// Настройка стандартных endpoints (health checks) + /// + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj b/ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj new file mode 100644 index 00000000..bf9f33a7 --- /dev/null +++ b/ProjectApp.ServiceDefaults/ProjectApp.ServiceDefaults.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 00000000..921efb13 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Лабораторная работа №1 — «Кэширование» + +**Вариант:** №30 — «Кредитная заявка» +**Балансировка:** Query Based +**Брокер:** SQS +**Хостинг S3:** Localstack +**Выполнил:** Панкеев Глеб, 6512 + +## Что реализовано + +- Генерация сущности «Кредитная заявка» через Bogus. +- Кэширование результатов генерации через IDistributedCache (Redis) с TTL 10 минут. +- Структурное логирование запросов и результатов генерации. +- Оркестрация сервисов через .NET Aspire. +- REST endpoint: `GET /api/creditapplication?id={id}`. + +## Характеристики генерируемой кредитной заявки + +1. Идентификатор в системе — `int` +2. ФИО клиента — `string` +3. Дата подачи заявки — `DateOnly` +4. Сумма кредита — `decimal` +5. Срок кредита в месяцах — `int` +6. Цель кредита — `string` +7. Доход клиента — `decimal` +8. Кредитный рейтинг — `int` (0-1000) +9. Дата принятия решения — `DateOnly?` +10. Решение по заявке — `bool?` (одобрено/отклонено/null - в процессе) +11. Процентная ставка — `decimal` (годовых) +12. Ежемесячный платёж — `decimal` + +## Правила генерации + +- ФИО клиента: полное имя из генератора Name. +- Дата подачи заявки: случайная дата за последний год. +- Сумма кредита: от 50 000 до 5 000 000 рублей. +- Срок кредита: от 6 до 120 месяцев. +- Цель кредита: случайный выбор из списка типовых целей. +- Доход клиента: от 20 000 до 500 000 рублей. +- Кредитный рейтинг: от 300 до 1000 баллов. +- Вероятность принятия решения: 70%. +- При наличии решения дата принятия всегда позже даты подачи. +- Процент одобрения среди решённых заявок: 60%. +- Процентная ставка: от 4.9% до 24.9% годовых. +- Ежемесячный платёж рассчитывается по формуле аннуитетного платежа. +