Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bdbc971
init
Dec 17, 2025
7566576
классы по моей предметной области
Dec 17, 2025
71025a0
дополнительные классы
Dec 17, 2025
3d845ee
добавление тестов и недостающих классов
Dec 24, 2025
86b2ff1
некоторые правки
Dec 24, 2025
96915a7
git action + README
Dec 24, 2025
e591a7a
некоторые правки, часть 1
Dec 25, 2025
17f2efa
некоторые правки, часть 2
Dec 26, 2025
aa1837f
упрощение моделей
Feb 13, 2026
4b2cf5c
исправленные тестовые данные
Feb 13, 2026
de2283e
измененные тесты
Feb 13, 2026
668f1f9
Merge branch 'main' into lab_1
ahewbu Feb 13, 2026
58dfcd0
изменение названия проекта, выравнивание, cleanup
Feb 16, 2026
cae8e05
изменение dotnet-tests
Feb 16, 2026
98aebdb
измененный readme, удален ненужный проект
Feb 16, 2026
a1306d4
изменен domain
Feb 16, 2026
cf8f1fc
добавлены DTO + mapping
Feb 16, 2026
76e4218
добавлены сервисы, контроллеры и конфиги AppHost
Feb 17, 2026
1becd0e
API + Apphost
Feb 19, 2026
427594a
исправлен AnalyticsController, добавлены миграции, исправлены пакеты
Feb 20, 2026
579ec27
исправлены тесты
Feb 21, 2026
5e29d35
добавлен запуск браузера
Feb 22, 2026
ba69c4d
добавлен Kafka консюмер с подключением в API
Feb 22, 2026
0ad4626
добавлен Kafka продюсер, генератор RentalEditDto на основе Bogus и ко…
Feb 22, 2026
fd7fb08
измененный readme
Feb 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CarRental/CarRental/.github/workflows/dotnet-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: .NET Tests

on:
push:
branches: [ main, lab_1 ]
pull_request:
branches: [ main, lab_1 ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Restore dependencies
run: dotnet restore ./CarRental/CarRental.slnx

- name: Build
run: dotnet build ./CarRental/CarRental.slnx --no-restore --configuration Release

- name: Test
run: dotnet test ./CarRental/CarRental.Tests/CarRental.Tests.csproj --configuration Release --verbosity normal
27 changes: 27 additions & 0 deletions CarRental/CarRental/CarRental.API/CarRental.API.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Confluent.Kafka" Version="13.0.2" />
<PackageReference Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer" Version="13.0.2" />
<PackageReference Include="AutoMapper" Version="15.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CarRental.Application.Contracts\CarRental.Application.Contracts.csproj" />
<ProjectReference Include="..\CarRental.Infrastructure.Kafka\CarRental.Infrastructure.Kafka.csproj" />
<ProjectReference Include="..\CarRental.Infrastructure\CarRental.Infrastructure.csproj" />
<ProjectReference Include="..\CarRental.ServiceDefaults\CarRental.ServiceDefaults.csproj" />
</ItemGroup>
</Project>
199 changes: 199 additions & 0 deletions CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using AutoMapper;
using CarRental.Application.Contracts.Dto;
using CarRental.Domain.Entities;
using CarRental.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace CarRental.Api.Controllers;

/// <summary>
/// Контроллер для аналитических запросов и отчетов
/// </summary>
[ApiController]
[Route("api/analytics")]
public class AnalyticsController(
IRepository<Rental> rentalsRepo,
IRepository<Car> carsRepo,
IRepository<Client> clientsRepo,
IRepository<ModelGeneration> generationsRepo,
IMapper mapper) : ControllerBase
{
/// <summary>
/// Получает список клиентов, арендовавших автомобили указанной модели, отсортированный по названию
/// </summary>
/// <param name="modelName">Название модели автомобиля</param>
/// <returns>Список клиентов</returns>
[HttpGet("clients-by-model")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ClientGetDto>>> GetClientsByModelSortedByName(
[FromQuery] string modelName)
{
var rentalsQuery = rentalsRepo.GetQueryable(
include: query => query
.Include(r => r.Car)
.ThenInclude(c => c!.ModelGeneration)
.ThenInclude(mg => mg!.Model)
.Include(r => r.Client));

var clients = await rentalsQuery
.Where(r => r.Car!.ModelGeneration!.Model!.Name == modelName)
.Select(r => r.Client)
.Distinct()
.OrderBy(c => c!.FullName)
.ToListAsync();

var result = clients
.Select(mapper.Map<ClientGetDto>)
.ToList();

return Ok(result);
}

/// <summary>
/// Получает арендованные в данный момент автомобили
/// </summary>
/// <param name="currentDate">Текущая дата проверки</param>
/// <returns>Список арендованных автомобилей</returns>
[HttpGet("currently-rented-cars")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CarGetDto>>> GetCurrentlyRentedCars(
[FromQuery] DateTime currentDate)
{
var rentedCarIds = await rentalsRepo.GetQueryable()
.Where(r => r.RentalDate.AddHours(r.RentalHours) > currentDate)
.Select(r => r.CarId)
.Distinct()
.ToListAsync();

var rentedCars = await carsRepo.GetQueryable()
.Where(c => rentedCarIds.Contains(c.Id))
.Include(c => c.ModelGeneration)
.ThenInclude(m => m!.Model)
.ToListAsync();

var result = rentedCars
.Select(mapper.Map<CarGetDto>)
.ToList();

return Ok(result);
}

/// <summary>
/// Получает топ 5 самых популярных арендованных автомобилей
/// </summary>
/// <returns>Список автомобилей, которые можно взять напрокат</returns>
[HttpGet("top-5-most-rented-cars")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CarRentalCountDto>>> GetTop5MostRentedCars()
{
var topCarStats = await rentalsRepo.GetQueryable()
.GroupBy(r => r.CarId)
.Select(g => new { CarId = g.Key, RentalCount = g.Count() })
.OrderByDescending(x => x.RentalCount)
.Take(5)
.ToListAsync();

var topCarIds = topCarStats.Select(x => x.CarId).ToList();

var cars = await carsRepo.GetQueryable()
.Where(c => topCarIds.Contains(c.Id))
.Include(c => c.ModelGeneration)
.ThenInclude(m => m!.Model)
.ToListAsync();

var carsDict = cars.ToDictionary(c => c.Id);

var topCarsResult = topCarStats
.Where(x => carsDict.ContainsKey(x.CarId))
.Select(x => new CarRentalCountDto(
mapper.Map<CarGetDto>(carsDict[x.CarId]),
x.RentalCount))
.ToList();

return Ok(topCarsResult);
}

/// <summary>
/// Получает количество арендованных автомобилей для каждого автомобиля
/// </summary>
/// <returns>Список всех автомобилей, которые были взяты в аренду</returns>
[HttpGet("rental-count-per-car")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CarRentalCountDto>>> GetRentalCountPerCar()
{
var rentalCounts = await rentalsRepo.GetQueryable()
.GroupBy(r => r.CarId)
.Select(g => new { CarId = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.CarId, x => x.Count);

var cars = await carsRepo.GetQueryable()
.Include(c => c.ModelGeneration)
.ThenInclude(m => m!.Model)
.ToListAsync();

var carsWithRentalCount = cars
.Select(car => new CarRentalCountDto(
mapper.Map<CarGetDto>(car),
rentalCounts.GetValueOrDefault(car.Id, 0)))
.OrderByDescending(x => x.RentalCount)
.ToList();

return Ok(carsWithRentalCount);
}

/// <summary>
/// Получает топ 5 клиентов по общей сумме аренды
/// </summary>
/// <returns>Список клиентов с общей суммой арендной платы</returns>
[HttpGet("top-5-clients-by-rental-amount")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ClientRentalAmountDto>>> GetTop5ClientsByRentalAmount()
{
var rentals = await rentalsRepo.GetQueryable()
.Select(r => new { r.ClientId, r.CarId, r.RentalHours })
.ToListAsync();

var cars = await carsRepo.GetQueryable()
.Select(c => new { c.Id, c.ModelGenerationId })
.ToListAsync();

var generations = await generationsRepo.GetQueryable()
.Select(g => new { g.Id, g.RentalPricePerHour })
.ToListAsync();

var carPrices = cars.Join(generations,
c => c.ModelGenerationId,
g => g.Id,
(c, g) => new { CarId = c.Id, Price = g.RentalPricePerHour })
.ToDictionary(x => x.CarId, x => x.Price);

var topClientStats = rentals
.GroupBy(r => r.ClientId)
.Select(g => new
{
ClientId = g.Key,
TotalAmount = g.Sum(r => r.RentalHours * carPrices.GetValueOrDefault(r.CarId, 0))
})
.OrderByDescending(x => x.TotalAmount)
.Take(5)
.ToList();

var topClientIds = topClientStats.Select(x => x.ClientId).ToList();

var clients = await clientsRepo.GetQueryable()
.Where(c => topClientIds.Contains(c.Id))
.ToListAsync();

var clientsDict = clients.ToDictionary(c => c.Id);

var result = topClientStats
.Where(x => clientsDict.ContainsKey(x.ClientId))
.Select(x => new ClientRentalAmountDto(
mapper.Map<ClientGetDto>(clientsDict[x.ClientId]),
x.TotalAmount))
.ToList();

return Ok(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using AutoMapper;
using CarRental.Application.Contracts.Dto;
using CarRental.Domain.Entities;
using CarRental.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace CarRental.Api.Controllers;

/// <summary>
/// Контроллер для управления моделями автомобилей
/// </summary>
[ApiController]
[Route("api/car-models")]
public class CarModelsController(
IRepository<CarModel> repo,
IMapper mapper) : ControllerBase
{
/// <summary>
/// Получает доступ ко всем моделям автомобилей
/// </summary>
/// <returns>Список всех моделей автомобилей</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CarModelGetDto>>> GetAll()
{
var entities = await repo.GetAllAsync();
var dtos = mapper.Map<IEnumerable<CarModelGetDto>>(entities);
return Ok(dtos);
}

/// <summary>
/// Получает модель автомобиля по идентификатору
/// </summary>
/// <param name="id">Идентификатор модели</param>
/// <returns>Модель автомобиля</returns>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CarModelGetDto>> Get(int id)
{
var entity = await repo.GetByIdAsync(id);
if (entity == null) return NotFound();
var dto = mapper.Map<CarModelGetDto>(entity);
return Ok(dto);
}

/// <summary>
/// Создает новую модель автомобиля
/// </summary>
/// <param name="dto">Данные для создания модели</param>
/// <returns>Созданная модель автомобиля</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<ActionResult<CarModelGetDto>> Create([FromBody] CarModelEditDto dto)
{
var entity = mapper.Map<CarModel>(dto);
var created = await repo.AddAsync(entity);
var resultDto = mapper.Map<CarModelGetDto>(created);
return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto);
}

/// <summary>
/// Обновляет существующую модель автомобиля
/// </summary>
/// <param name="id">Идентификатор модели</param>
/// <param name="dto">Обновленные данные модели</param>
/// <returns>Обновленная модель автомобиля</returns>
[HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CarModelGetDto>> Update(int id, [FromBody] CarModelEditDto dto)
{
var entity = await repo.GetByIdAsync(id);
if (entity == null) return NotFound();
mapper.Map(dto, entity);
await repo.UpdateAsync(entity);
var resultDto = mapper.Map<CarModelGetDto>(entity);
return Ok(resultDto);
}

/// <summary>
/// Удалить модель автомобиля
/// </summary>
/// <param name="id">Идентификатор модели</param>
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> Delete(int id)
{
await repo.DeleteAsync(id);
return NoContent();
}
}
Loading