Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 934e441e89 | |||
| b76a22641a | |||
| 6160732a66 |
30
ProductMicroservice/.dockerignore
Normal file
30
ProductMicroservice/.dockerignore
Normal file
@@ -0,0 +1,30 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
!**/.gitignore
|
||||
!.git/HEAD
|
||||
!.git/config
|
||||
!.git/packed-refs
|
||||
!.git/refs/heads/**
|
||||
11
ProductMicroservice/CategoryMicroservice.csproj
Normal file
11
ProductMicroservice/CategoryMicroservice.csproj
Normal file
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>4a831111-1a8f-4dd4-b364-44bd98f36e3b</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>.</DockerfileContext>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
61
ProductMicroservice/Controllers/CategoriesController.cs
Normal file
61
ProductMicroservice/Controllers/CategoriesController.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using ProductMicroservice.Domain;
|
||||
using ProductMicroservice.Infrastructure;
|
||||
|
||||
namespace ProductMicroservice.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class CategoriesController : ControllerBase
|
||||
{
|
||||
private readonly CategoryDbContext _db;
|
||||
|
||||
public CategoriesController(CategoryDbContext db) => _db = db;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Category>>> GetAll()
|
||||
{
|
||||
return await _db.Categories.AsNoTracking().ToListAsync();
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<ActionResult<Category>> Get(int id)
|
||||
{
|
||||
var cat = await _db.Categories.FindAsync(id);
|
||||
if (cat is null) return NotFound();
|
||||
return cat;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Category>> Create(Category model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.Name))
|
||||
return BadRequest("Name is required");
|
||||
|
||||
_db.Categories.Add(model);
|
||||
await _db.SaveChangesAsync();
|
||||
return CreatedAtAction(nameof(Get), new { id = model.Id }, model);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<IActionResult> Update(int id, Category model)
|
||||
{
|
||||
if (id != model.Id) return BadRequest();
|
||||
if (!_db.Categories.Any(c => c.Id == id)) return NotFound();
|
||||
|
||||
_db.Entry(model).State = EntityState.Modified;
|
||||
await _db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var cat = await _db.Categories.FindAsync(id);
|
||||
if (cat is null) return NotFound();
|
||||
_db.Categories.Remove(cat);
|
||||
await _db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
30
ProductMicroservice/Dockerfile
Normal file
30
ProductMicroservice/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
# См. статью по ссылке https://aka.ms/customizecontainer, чтобы узнать как настроить контейнер отладки и как Visual Studio использует этот Dockerfile для создания образов для ускорения отладки.
|
||||
|
||||
# Этот этап используется при запуске из VS в быстром режиме (по умолчанию для конфигурации отладки)
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
|
||||
# Этот этап используется для сборки проекта службы
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["ProductMicroservice.csproj", "."]
|
||||
RUN dotnet restore "./ProductMicroservice.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/."
|
||||
RUN dotnet build "./ProductMicroservice.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# Этот этап используется для публикации проекта службы, который будет скопирован на последний этап
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./ProductMicroservice.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Этот этап используется в рабочей среде или при запуске из VS в обычном режиме (по умолчанию, когда конфигурация отладки не используется)
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "ProductMicroservice.dll"]
|
||||
7
ProductMicroservice/Domain/Category.cs
Normal file
7
ProductMicroservice/Domain/Category.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ProductMicroservice.Domain;
|
||||
|
||||
public class Category
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
}
|
||||
12
ProductMicroservice/Infrastructure/CategoryDbContext.cs
Normal file
12
ProductMicroservice/Infrastructure/CategoryDbContext.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using ProductMicroservice.Domain;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ProductMicroservice.Infrastructure;
|
||||
|
||||
public class CategoryDbContext : DbContext
|
||||
{
|
||||
public CategoryDbContext(DbContextOptions<CategoryDbContext> options)
|
||||
: base(options) { }
|
||||
|
||||
public DbSet<Category> Categories => Set<Category>();
|
||||
}
|
||||
6
ProductMicroservice/ProductMicroservice.http
Normal file
6
ProductMicroservice/ProductMicroservice.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@ProductMicroservice_HostAddress = http://localhost:5186
|
||||
|
||||
GET {{ProductMicroservice_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
31
ProductMicroservice/ProductMicroservice.sln
Normal file
31
ProductMicroservice/ProductMicroservice.sln
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36401.2
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CategoryMicroservice", "CategoryMicroservice.csproj", "{CA014D50-626B-4303-80DC-362AA95C15F6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProductService", "..\ProductService\ProductService.csproj", "{183F680C-0F79-4B0E-B1EB-7E5F3005D600}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{CA014D50-626B-4303-80DC-362AA95C15F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CA014D50-626B-4303-80DC-362AA95C15F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CA014D50-626B-4303-80DC-362AA95C15F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CA014D50-626B-4303-80DC-362AA95C15F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{183F680C-0F79-4B0E-B1EB-7E5F3005D600}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{183F680C-0F79-4B0E-B1EB-7E5F3005D600}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{183F680C-0F79-4B0E-B1EB-7E5F3005D600}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{183F680C-0F79-4B0E-B1EB-7E5F3005D600}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {2EC5F8EE-6748-4CEB-9C6F-A266F67E1176}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
34
ProductMicroservice/Program.cs
Normal file
34
ProductMicroservice/Program.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using ProductMicroservice.Infrastructure;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services.AddDbContext<CategoryDbContext>(options =>
|
||||
{
|
||||
var connectionString = builder.Configuration.GetConnectionString("Default");
|
||||
options.UseNpgsql(connectionString);
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<CategoryDbContext>();
|
||||
db.Database.Migrate();
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
52
ProductMicroservice/Properties/launchSettings.json
Normal file
52
ProductMicroservice/Properties/launchSettings.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5186"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7112;http://localhost:5186"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
||||
"ASPNETCORE_HTTP_PORTS": "8080"
|
||||
},
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:46293",
|
||||
"sslPort": 44338
|
||||
}
|
||||
}
|
||||
}
|
||||
8
ProductMicroservice/appsettings.Development.json
Normal file
8
ProductMicroservice/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
ProductMicroservice/appsettings.json
Normal file
8
ProductMicroservice/appsettings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=db;Port=5432;Database=products;Username=postgres;Password=1234"
|
||||
},
|
||||
"CategoryService": {
|
||||
"BaseUrl": "http://category-api:8080/"
|
||||
}
|
||||
}
|
||||
135
ProductService/Controllers/ProductsController.cs
Normal file
135
ProductService/Controllers/ProductsController.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using ProductService.Data;
|
||||
using ProductService.Models;
|
||||
using ProductService.Services;
|
||||
|
||||
namespace ProductService.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ProductsController : ControllerBase
|
||||
{
|
||||
private readonly ProductDbContext _db;
|
||||
private readonly ICategoryClient _categoryClient;
|
||||
|
||||
public ProductsController(ProductDbContext db, ICategoryClient categoryClient)
|
||||
{
|
||||
_db = db ?? throw new ArgumentNullException(nameof(db));
|
||||
_categoryClient = categoryClient ?? throw new ArgumentNullException(nameof(categoryClient));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<ProductDto>>> GetAll(CancellationToken cancellationToken)
|
||||
{
|
||||
var products = await _db.Products
|
||||
.AsNoTracking()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
|
||||
|
||||
var result = products.Select(p => new ProductDto
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Description = p.Description,
|
||||
CategoryId = p.CategoryId,
|
||||
CategoryName = categories.TryGetValue(p.CategoryId, out var name) ? name : "UNKNOWN",
|
||||
Quantity = p.Quantity
|
||||
});
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
public async Task<ActionResult<ProductDto>> GetById(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var product = await _db.Products
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||
|
||||
if (product is null)
|
||||
return NotFound();
|
||||
|
||||
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
|
||||
|
||||
var dto = new ProductDto
|
||||
{
|
||||
Id = product.Id,
|
||||
Name = product.Name,
|
||||
Description = product.Description,
|
||||
CategoryId = product.CategoryId,
|
||||
CategoryName = categories.TryGetValue(product.CategoryId, out var name) ? name : "UNKNOWN",
|
||||
Quantity = product.Quantity
|
||||
};
|
||||
|
||||
return Ok(dto);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Product>> Create([FromBody] Product model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return ValidationProblem(ModelState);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(model.Name))
|
||||
return BadRequest("Name is required.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(model.Description))
|
||||
return BadRequest("Description is required.");
|
||||
|
||||
if (model.Quantity is < 0)
|
||||
return BadRequest("Quantity cannot be negative.");
|
||||
|
||||
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
|
||||
|
||||
if (!categories.ContainsKey(model.CategoryId))
|
||||
return BadRequest($"Category with id {model.CategoryId} does not exist.");
|
||||
|
||||
_db.Products.Add(model);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetById),
|
||||
new { id = model.Id },
|
||||
model);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<IActionResult> Update(int id, [FromBody] Product model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (id != model.Id)
|
||||
return BadRequest("Route id and body id must match.");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return ValidationProblem(ModelState);
|
||||
|
||||
if (model.Quantity is < 0)
|
||||
return BadRequest("Quantity cannot be negative.");
|
||||
|
||||
var exists = await _db.Products.AnyAsync(p => p.Id == id, cancellationToken);
|
||||
if (!exists)
|
||||
return NotFound();
|
||||
|
||||
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
|
||||
if (!categories.ContainsKey(model.CategoryId))
|
||||
return BadRequest($"Category with id {model.CategoryId} does not exist.");
|
||||
|
||||
_db.Entry(model).State = EntityState.Modified;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var product = await _db.Products.FindAsync(new object[] { id }, cancellationToken);
|
||||
if (product is null)
|
||||
return NotFound();
|
||||
|
||||
_db.Products.Remove(product);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
229
ProductService/Controllers/ReportsController.cs
Normal file
229
ProductService/Controllers/ReportsController.cs
Normal file
@@ -0,0 +1,229 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using ProductService.Data;
|
||||
using ProductService.Services;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection.Metadata;
|
||||
using static System.Net.Mime.MediaTypeNames;
|
||||
|
||||
namespace ProductService.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ReportsController : ControllerBase
|
||||
{
|
||||
private readonly ProductDbContext _db;
|
||||
private readonly ICategoryClient _categoryClient;
|
||||
|
||||
public ReportsController(ProductDbContext db, ICategoryClient categoryClient)
|
||||
{
|
||||
_db = db ?? throw new ArgumentNullException(nameof(db));
|
||||
_categoryClient = categoryClient ?? throw new ArgumentNullException(nameof(categoryClient));
|
||||
}
|
||||
|
||||
[HttpGet("products/in-stock/excel")]
|
||||
public async Task<IActionResult> GetProductsInStockExcel(CancellationToken cancellationToken)
|
||||
{
|
||||
var productsInStock = await _db.Products
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Quantity.HasValue && p.Quantity.Value > 0)
|
||||
.OrderBy(p => p.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
|
||||
|
||||
using var package = new ExcelPackage();
|
||||
var worksheet = package.Workbook.Worksheets.Add("В наличии");
|
||||
|
||||
worksheet.Cells[1, 1].Value = "Продукты в наличии";
|
||||
worksheet.Cells[1, 1].Style.Font.Bold = true;
|
||||
|
||||
int row = 3;
|
||||
|
||||
foreach (var p in productsInStock)
|
||||
{
|
||||
worksheet.Cells[row, 1].Value = $"{p.Name}: {p.Description}";
|
||||
row++;
|
||||
}
|
||||
|
||||
worksheet.Cells.AutoFitColumns();
|
||||
|
||||
var bytes = package.GetAsByteArray();
|
||||
var fileName = "ProductsInStock.xlsx";
|
||||
|
||||
return File(
|
||||
fileContents: bytes,
|
||||
contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
fileDownloadName: fileName);
|
||||
}
|
||||
|
||||
[HttpGet("products/out-of-stock/pie-pdf")]
|
||||
public async Task<IActionResult> GetProductsOutOfStockPiePdf(CancellationToken cancellationToken)
|
||||
{
|
||||
var productsOutOfStock = await _db.Products
|
||||
.AsNoTracking()
|
||||
.Where(p => !p.Quantity.HasValue || p.Quantity.Value == 0)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
|
||||
|
||||
var grouped = productsOutOfStock
|
||||
.GroupBy(p => categories.TryGetValue(p.CategoryId, out var name) ? name : "UNKNOWN")
|
||||
.Select(g => new
|
||||
{
|
||||
CategoryName = g.Key,
|
||||
Count = g.Count()
|
||||
})
|
||||
.OrderByDescending(x => x.Count)
|
||||
.ToList();
|
||||
|
||||
if (!grouped.Any())
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
var document = new Document();
|
||||
document.Info.Title = "Продукты, отсутствующие в наличии";
|
||||
document.Info.Subject = "Круговая диаграмма по категориям";
|
||||
document.Info.Author = "ProductService";
|
||||
|
||||
var section = document.AddSection();
|
||||
section.AddParagraph("Продукты, отсутствующие в наличии", "Heading1").Format.SpaceAfter = "1cm";
|
||||
|
||||
var chart = section.AddChart(ChartType.Pie2D);
|
||||
chart.Width = "12cm";
|
||||
chart.Height = "8cm";
|
||||
|
||||
var series = chart.SeriesCollection.AddSeries();
|
||||
var xSeries = chart.XValues.AddXSeries();
|
||||
|
||||
foreach (var item in grouped)
|
||||
{
|
||||
series.Add(item.Count);
|
||||
xSeries.Add(item.CategoryName);
|
||||
}
|
||||
|
||||
series.HasDataLabel = true;
|
||||
series.DataLabel.Type = DataLabelType.Percent;
|
||||
|
||||
var renderer = new PdfDocumentRenderer(unicode: true)
|
||||
{
|
||||
Document = document
|
||||
};
|
||||
renderer.RenderDocument();
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
renderer.PdfDocument.Save(ms, false);
|
||||
var bytes = ms.ToArray();
|
||||
|
||||
var fileName = "ProductsOutOfStockByCategory.pdf";
|
||||
|
||||
return File(
|
||||
fileContents: bytes,
|
||||
contentType: "application/pdf",
|
||||
fileDownloadName: fileName);
|
||||
}
|
||||
|
||||
[HttpGet("products/all/word")]
|
||||
public async Task<IActionResult> GetAllProductsWord(CancellationToken cancellationToken)
|
||||
{
|
||||
var products = await _db.Products
|
||||
.AsNoTracking()
|
||||
.OrderBy(p => p.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var categories = await _categoryClient.GetCategoriesAsync(cancellationToken);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using (var wordDoc = WordprocessingDocument.Create(ms, WordprocessingDocumentType.Document, true))
|
||||
{
|
||||
var mainPart = wordDoc.AddMainDocumentPart();
|
||||
mainPart.Document = new Document();
|
||||
var body = mainPart.Document.AppendChild(new Body());
|
||||
|
||||
var headingParagraph = new Paragraph(
|
||||
new Run(
|
||||
new Text("Список продуктов")))
|
||||
{
|
||||
ParagraphProperties = new ParagraphProperties(
|
||||
new ParagraphStyleId { Val = "Heading1" })
|
||||
};
|
||||
body.AppendChild(headingParagraph);
|
||||
|
||||
body.AppendChild(new Paragraph(new Run(new Text(""))));
|
||||
|
||||
var table = new Table();
|
||||
|
||||
var tableProps = new TableProperties(
|
||||
new TableBorders(
|
||||
new TopBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
|
||||
new BottomBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
|
||||
new LeftBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
|
||||
new RightBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
|
||||
new InsideHorizontalBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 },
|
||||
new InsideVerticalBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4 }
|
||||
)
|
||||
);
|
||||
table.AppendChild(tableProps);
|
||||
|
||||
var headerRow = new TableRow();
|
||||
|
||||
headerRow.Append(
|
||||
CreateCell("ID", isHeader: true),
|
||||
CreateCell("Название", isHeader: true),
|
||||
CreateCell("Категория", isHeader: true),
|
||||
CreateCell("Количество", isHeader: true));
|
||||
|
||||
table.AppendChild(headerRow);
|
||||
|
||||
foreach (var p in products)
|
||||
{
|
||||
var row = new TableRow();
|
||||
|
||||
var categoryName = categories.TryGetValue(p.CategoryId, out var name)
|
||||
? name
|
||||
: "UNKNOWN";
|
||||
|
||||
var quantityText = p.Quantity.HasValue
|
||||
? p.Quantity.Value.ToString()
|
||||
: "отсутствует";
|
||||
|
||||
row.Append(
|
||||
CreateCell(p.Id.ToString()),
|
||||
CreateCell(p.Name),
|
||||
CreateCell(categoryName),
|
||||
CreateCell(quantityText));
|
||||
|
||||
table.AppendChild(row);
|
||||
}
|
||||
|
||||
body.AppendChild(table);
|
||||
}
|
||||
|
||||
var bytes = ms.ToArray();
|
||||
var fileName = "AllProducts.docx";
|
||||
|
||||
return File(
|
||||
fileContents: bytes,
|
||||
contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
fileDownloadName: fileName);
|
||||
}
|
||||
|
||||
private static TableCell CreateCell(string text, bool isHeader = false)
|
||||
{
|
||||
var runProps = new RunProperties();
|
||||
if (isHeader)
|
||||
{
|
||||
runProps.Bold = new Bold();
|
||||
}
|
||||
|
||||
var run = new Run();
|
||||
run.Append(runProps);
|
||||
run.Append(new Text(text ?? string.Empty));
|
||||
|
||||
var paragraph = new Paragraph(run);
|
||||
|
||||
var cell = new TableCell(paragraph);
|
||||
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
34
ProductService/Data/ProductDbContext.cs
Normal file
34
ProductService/Data/ProductDbContext.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using ProductService.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
namespace ProductService.Data;
|
||||
|
||||
public class ProductDbContext : DbContext
|
||||
{
|
||||
public ProductDbContext(DbContextOptions<ProductDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Product> Products => Set<Product>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
var product = modelBuilder.Entity<Product>();
|
||||
|
||||
product.Property(p => p.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(200);
|
||||
|
||||
product.Property(p => p.Description)
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000);
|
||||
|
||||
product.Property(p => p.CategoryId)
|
||||
.IsRequired();
|
||||
|
||||
}
|
||||
}
|
||||
19
ProductService/Models/Product.cs
Normal file
19
ProductService/Models/Product.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ProductService.Models;
|
||||
|
||||
public class Product
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required, MaxLength(200)]
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
[Required, MaxLength(1000)]
|
||||
public string Description { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
public int CategoryId { get; set; }
|
||||
|
||||
public int? Quantity { get; set; }
|
||||
}
|
||||
11
ProductService/Models/ProductDto.cs
Normal file
11
ProductService/Models/ProductDto.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ProductService.Models;
|
||||
|
||||
public class ProductDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public string Description { get; set; } = null!;
|
||||
public int CategoryId { get; set; }
|
||||
public string CategoryName { get; set; } = null!;
|
||||
public int? Quantity { get; set; }
|
||||
}
|
||||
15
ProductService/ProductService.csproj
Normal file
15
ProductService/ProductService.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\ProductMicroservice</DockerfileContext>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
ProductService/ProductService.http
Normal file
6
ProductService/ProductService.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@ProductService_HostAddress = http://localhost:5249
|
||||
|
||||
GET {{ProductService_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
50
ProductService/Program.cs
Normal file
50
ProductService/Program.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services.AddDbContext<ProductDbContext>(options =>
|
||||
{
|
||||
var connectionString = builder.Configuration.GetConnectionString("Default");
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
throw new InvalidOperationException("Connection string 'Default' is not configured.");
|
||||
}
|
||||
|
||||
options.UseNpgsql(connectionString);
|
||||
});
|
||||
|
||||
builder.Services.AddHttpClient<ICategoryClient, CategoryClient>(client =>
|
||||
{
|
||||
var baseUrl = builder.Configuration["CategoryService:BaseUrl"];
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
throw new InvalidOperationException("Configuration 'CategoryService:BaseUrl' is not set.");
|
||||
}
|
||||
|
||||
client.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<ProductDbContext>();
|
||||
db.Database.Migrate();
|
||||
}
|
||||
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
41
ProductService/Properties/launchSettings.json
Normal file
41
ProductService/Properties/launchSettings.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:11752",
|
||||
"sslPort": 44385
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5249",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7100;http://localhost:5249",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
ProductService/Services/CategoryClient.cs
Normal file
26
ProductService/Services/CategoryClient.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace ProductService.Services;
|
||||
|
||||
public class CategoryClient : ICategoryClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public CategoryClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
private sealed class CategoryDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<int, string>> GetCategoriesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var categories = await _httpClient.GetFromJsonAsync<List<CategoryDto>>(
|
||||
"api/categories",
|
||||
cancellationToken
|
||||
) ?? new List<CategoryDto>();
|
||||
|
||||
return categories.ToDictionary(c => c.Id, c => c.Name);
|
||||
}
|
||||
}
|
||||
6
ProductService/Services/ICategoryClient.cs
Normal file
6
ProductService/Services/ICategoryClient.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace ProductService.Services;
|
||||
|
||||
public interface ICategoryClient
|
||||
{
|
||||
Task<Dictionary<int, string>> GetCategoriesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
8
ProductService/appsettings.Development.json
Normal file
8
ProductService/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
ProductService/appsettings.json
Normal file
15
ProductService/appsettings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=db;Port=5432;Database=kop5_products;Username=postgres;Password=1234"
|
||||
},
|
||||
"CategoryService": {
|
||||
"BaseUrl": "http://category-api:8080/"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user