Найти в Дзене
NJ_Gang

программа сайта аптеки со складом на C#

``` PharmacySolution/ ├── Pharmacy.Core/ (Ядро системы) ├── Pharmacy.Data/ (Доступ к данным) ├── Pharmacy.Services/ (Бизнес-логика) ├── Pharmacy.Web/ (Веб-интерфейс) └── Pharmacy.Tests/ (Тесты) ``` ## 1. Pharmacy.Core (Основные модели и интерфейсы) ```csharp // Pharmacy.Core/Models/Product.cs namespace Pharmacy.Core.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public string ActiveSubstance { get; set; } public string Manufacturer { get; set; } public string Barcode { get; set; } public ProductCategory Category { get; set; } public bool IsPrescriptionRequired { get; set; } public decimal Price { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } public ICollection<StockItem> StockItems { get; set; } public ICollection<ProductImage> Images { get; set; } } public enum ProductCategory { Medicines, Vitamins

```

PharmacySolution/

├── Pharmacy.Core/ (Ядро системы)

├── Pharmacy.Data/ (Доступ к данным)

├── Pharmacy.Services/ (Бизнес-логика)

├── Pharmacy.Web/ (Веб-интерфейс)

└── Pharmacy.Tests/ (Тесты)

```

## 1. Pharmacy.Core (Основные модели и интерфейсы)

```csharp

// Pharmacy.Core/Models/Product.cs

namespace Pharmacy.Core.Models

{

public class Product

{

public int Id { get; set; }

public string Name { get; set; }

public string Description { get; set; }

public string ActiveSubstance { get; set; }

public string Manufacturer { get; set; }

public string Barcode { get; set; }

public ProductCategory Category { get; set; }

public bool IsPrescriptionRequired { get; set; }

public decimal Price { get; set; }

public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

public DateTime? UpdatedAt { get; set; }

public ICollection<StockItem> StockItems { get; set; }

public ICollection<ProductImage> Images { get; set; }

}

public enum ProductCategory

{

Medicines,

Vitamins,

MedicalDevices,

Cosmetics,

Hygiene,

BabyCare,

Other

}

}

// Pharmacy.Core/Models/StockItem.cs

namespace Pharmacy.Core.Models

{

public class StockItem

{

public int Id { get; set; }

public int ProductId { get; set; }

public Product Product { get; set; }

public int Quantity { get; set; }

public int WarehouseId { get; set; }

public Warehouse Warehouse { get; set; }

public DateTime ExpirationDate { get; set; }

public string BatchNumber { get; set; }

public DateTime ArrivalDate { get; set; }

public decimal PurchasePrice { get; set; }

}

}

// Pharmacy.Core/Models/Warehouse.cs

namespace Pharmacy.Core.Models

{

public class Warehouse

{

public int Id { get; set; }

public string Name { get; set; }

public string Address { get; set; }

public decimal Capacity { get; set; } // в куб. метрах

public decimal CurrentOccupancy { get; set; }

public bool IsActive { get; set; } = true;

public ICollection<StockItem> StockItems { get; set; }

}

}

// Pharmacy.Core/Interfaces/IRepository.cs

namespace Pharmacy.Core.Interfaces

{

public interface IRepository<T> where T : class

{

Task<T> GetByIdAsync(int id);

Task<IEnumerable<T>> GetAllAsync();

Task AddAsync(T entity);

Task UpdateAsync(T entity);

Task DeleteAsync(T entity);

Task<bool> ExistsAsync(int id);

}

}

// Pharmacy.Core/Interfaces/IProductService.cs

namespace Pharmacy.Core.Interfaces

{

public interface IProductService

{

Task<Product> GetProductByIdAsync(int id);

Task<IEnumerable<Product>> GetAllProductsAsync();

Task<IEnumerable<Product>> SearchProductsAsync(string searchTerm, ProductCategory? category);

Task AddProductAsync(Product product);

Task UpdateProductAsync(Product product);

Task DeleteProductAsync(int id);

Task<StockInfo> GetStockInfoAsync(int productId);

Task AdjustStockAsync(int productId, int warehouseId, int quantityChange, string batchNumber, DateTime? expirationDate);

}

public class StockInfo

{

public int TotalQuantity { get; set; }

public IEnumerable<StockItem> Items { get; set; }

}

}

```

## 2. Pharmacy.Data (Доступ к данным)

```csharp

// Pharmacy.Data/Repositories/Repository.cs

namespace Pharmacy.Data.Repositories

{

public class Repository<T> : IRepository<T> where T : class

{

protected readonly PharmacyDbContext _context;

protected readonly DbSet<T> _dbSet;

public Repository(PharmacyDbContext context)

{

_context = context;

_dbSet = context.Set<T>();

}

public async Task<T> GetByIdAsync(int id) => await _dbSet.FindAsync(id);

public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();

public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);

public async Task UpdateAsync(T entity) => _context.Entry(entity).State = EntityState.Modified;

public async Task DeleteAsync(T entity) => _dbSet.Remove(entity);

public async Task<bool> ExistsAsync(int id) => await _dbSet.AnyAsync(e => EF.Property<int>(e, "Id") == id);

}

}

// Pharmacy.Data/Repositories/ProductRepository.cs

namespace Pharmacy.Data.Repositories

{

public interface IProductRepository : IRepository<Product>

{

Task<IEnumerable<Product>> GetProductsByCategoryAsync(ProductCategory category);

Task<IEnumerable<Product>> SearchProductsAsync(string searchTerm, ProductCategory? category);

Task<StockInfo> GetStockInfoAsync(int productId);

}

public class ProductRepository : Repository<Product>, IProductRepository

{

public ProductRepository(PharmacyDbContext context) : base(context) { }

public async Task<IEnumerable<Product>> GetProductsByCategoryAsync(ProductCategory category)

=> await _context.Products.Where(p => p.Category == category).ToListAsync();

public async Task<IEnumerable<Product>> SearchProductsAsync(string searchTerm, ProductCategory? category)

{

var query = _context.Products.AsQueryable();

if (!string.IsNullOrWhiteSpace(searchTerm))

{

query = query.Where(p =>

p.Name.Contains(searchTerm) ||

p.Description.Contains(searchTerm) ||

p.ActiveSubstance.Contains(searchTerm) ||

p.Barcode == searchTerm);

}

if (category.HasValue)

{

query = query.Where(p => p.Category == category.Value);

}

return await query.ToListAsync();

}

public async Task<StockInfo> GetStockInfoAsync(int productId)

{

var stockItems = await _context.StockItems

.Include(si => si.Warehouse)

.Where(si => si.ProductId == productId)

.ToListAsync();

return new StockInfo

{

TotalQuantity = stockItems.Sum(si => si.Quantity),

Items = stockItems

};

}

}

}

// Pharmacy.Data/PharmacyDbContext.cs

namespace Pharmacy.Data

{

public class PharmacyDbContext : DbContext

{

public PharmacyDbContext(DbContextOptions<PharmacyDbContext> options) : base(options) { }

public DbSet<Product> Products { get; set; }

public DbSet<StockItem> StockItems { get; set; }

public DbSet<Warehouse> Warehouses { get; set; }

public DbSet<ProductImage> ProductImages { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)

{

modelBuilder.Entity<Product>(entity =>

{

entity.HasIndex(p => p.Barcode).IsUnique();

entity.Property(p => p.Price).HasColumnType("decimal(18,2)");

});

modelBuilder.Entity<StockItem>(entity =>

{

entity.Property(si => si.PurchasePrice).HasColumnType("decimal(18,2)");

entity.HasIndex(si => new { si.ProductId, si.WarehouseId, si.BatchNumber }).IsUnique();

});

modelBuilder.Entity<Warehouse>(entity =>

{

entity.Property(w => w.Capacity).HasColumnType("decimal(18,2)");

entity.Property(w => w.CurrentOccupancy).HasColumnType("decimal(18,2)");

});

}

}

}

```

## 3. Pharmacy.Services (Бизнес-логика)

```csharp

// Pharmacy.Services/ProductService.cs

namespace Pharmacy.Services

{

public class ProductService : IProductService

{

private readonly IProductRepository _productRepository;

private readonly IRepository<StockItem> _stockItemRepository;

private readonly IRepository<Warehouse> _warehouseRepository;

private readonly ILogger<ProductService> _logger;

public ProductService(

IProductRepository productRepository,

IRepository<StockItem> stockItemRepository,

IRepository<Warehouse> warehouseRepository,

ILogger<ProductService> logger)

{

_productRepository = productRepository;

_stockItemRepository = stockItemRepository;

_warehouseRepository = warehouseRepository;

_logger = logger;

}

public async Task<Product> GetProductByIdAsync(int id)

{

var product = await _productRepository.GetByIdAsync(id);

if (product == null)

{

_logger.LogWarning("Product with id {ProductId} not found", id);

throw new KeyNotFoundException($"Product with id {id} not found");

}

return product;

}

public async Task<IEnumerable<Product>> GetAllProductsAsync()

=> await _productRepository.GetAllAsync();

public async Task<IEnumerable<Product>> SearchProductsAsync(string searchTerm, ProductCategory? category)

=> await _productRepository.SearchProductsAsync(searchTerm, category);

public async Task AddProductAsync(Product product)

{

if (product == null)

throw new ArgumentNullException(nameof(product));

await _productRepository.AddAsync(product);

_logger.LogInformation("Product {ProductName} added with id {ProductId}", product.Name, product.Id);

}

public async Task UpdateProductAsync(Product product)

{

if (product == null)

throw new ArgumentNullException(nameof(product));

if (!await _productRepository.ExistsAsync(product.Id))

throw new KeyNotFoundException($"Product with id {product.Id} not found");

product.UpdatedAt = DateTime.UtcNow;

await _productRepository.UpdateAsync(product);

_logger.LogInformation("Product with id {ProductId} updated", product.Id);

}

public async Task DeleteProductAsync(int id)

{

var product = await _productRepository.GetByIdAsync(id);

if (product == null)

throw new KeyNotFoundException($"Product with id {id} not found");

await _productRepository.DeleteAsync(product);

_logger.LogInformation("Product with id {ProductId} deleted", id);

}

public async Task<StockInfo> GetStockInfoAsync(int productId)

{

if (!await _productRepository.ExistsAsync(productId))

throw new KeyNotFoundException($"Product with id {productId} not found");

return await _productRepository.GetStockInfoAsync(productId);

}

public async Task AdjustStockAsync(int productId, int warehouseId, int quantityChange, string batchNumber, DateTime? expirationDate)

{

if (quantityChange == 0)

return;

var product = await _productRepository.GetByIdAsync(productId);

if (product == null)

throw new KeyNotFoundException($"Product with id {productId} not found");

var warehouse = await _warehouseRepository.GetByIdAsync(warehouseId);

if (warehouse == null)

throw new KeyNotFoundException($"Warehouse with id {warehouseId} not found");

var stockItem = await _context.StockItems

.FirstOrDefaultAsync(si => si.ProductId == productId &&

si.WarehouseId == warehouseId &&

si.BatchNumber == batchNumber);

if (stockItem == null)

{

if (quantityChange < 0)

throw new InvalidOperationException("Cannot reduce stock for non-existent batch");

if (!expirationDate.HasValue)

throw new ArgumentException("Expiration date is required for new batch");

stockItem = new StockItem

{

ProductId = productId,

WarehouseId = warehouseId,

Quantity = quantityChange,

BatchNumber = batchNumber,

ExpirationDate = expirationDate.Value,

ArrivalDate = DateTime.UtcNow,

PurchasePrice = 0 // Можно добавить логику для установки цены

};

await _stockItemRepository.AddAsync(stockItem);

}

else

{

var newQuantity = stockItem.Quantity + quantityChange;

if (newQuantity < 0)

throw new InvalidOperationException("Insufficient stock");

stockItem.Quantity = newQuantity;

await _stockItemRepository.UpdateAsync(stockItem);

}

_logger.LogInformation("Stock adjusted for product {ProductId} in warehouse {WarehouseId}. Change: {QuantityChange}",

productId, warehouseId, quantityChange);

}

}

}

```

## 4. Pharmacy.Web (Веб-интерфейс)

```csharp

// Pharmacy.Web/Controllers/ProductsController.cs

namespace Pharmacy.Web.Controllers

{

[Authorize]

[ApiController]

[Route("api/[controller]")]

public class ProductsController : ControllerBase

{

private readonly IProductService _productService;

private readonly IMapper _mapper;

public ProductsController(IProductService productService, IMapper mapper)

{

_productService = productService;

_mapper = mapper;

}

[HttpGet]

[AllowAnonymous]

public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts(

[FromQuery] string searchTerm = "",

[FromQuery] ProductCategory? category = null)

{

var products = await _productService.SearchProductsAsync(searchTerm, category);

return Ok(_mapper.Map<IEnumerable<ProductDto>>(products));

}

[HttpGet("{id}")]

[AllowAnonymous]

public async Task<ActionResult<ProductDetailsDto>> GetProduct(int id)

{

var product = await _productService.GetProductByIdAsync(id);

var stockInfo = await _productService.GetStockInfoAsync(id);

var productDetails = _mapper.Map<ProductDetailsDto>(product);

productDetails.StockInfo = _mapper.Map<StockInfoDto>(stockInfo);

return Ok(productDetails);

}

[HttpPost]

[Authorize(Roles = "Admin,Manager")]

public async Task<ActionResult<ProductDto>> CreateProduct(ProductCreateDto productDto)

{

var product = _mapper.Map<Product>(productDto);

await _productService.AddProductAsync(product);

return CreatedAtAction(nameof(GetProduct),

new { id = product.Id },

_mapper.Map<ProductDto>(product));

}

[HttpPut("{id}")]

[Authorize(Roles = "Admin,Manager")]

public async Task<IActionResult> UpdateProduct(int id, ProductUpdateDto productDto)

{

if (id != productDto.Id)

return BadRequest();

var product = await _productService.GetProductByIdAsync(id);

_mapper.Map(productDto, product);

await _productService.UpdateProductAsync(product);

return NoContent();

}

[HttpDelete("{id}")]

[Authorize(Roles = "Admin")]

public async Task<IActionResult> DeleteProduct(int id)

{

await _productService.DeleteProductAsync(id);

return NoContent();

}

[HttpPost("{id}/stock")]

[Authorize(Roles = "Admin,Manager")]

public async Task<IActionResult> AdjustStock(int id, StockAdjustmentDto adjustment)

{

await _productService.AdjustStockAsync(

id,

adjustment.WarehouseId,

adjustment.Quantity,

adjustment.BatchNumber,

adjustment.ExpirationDate);

return NoContent();

}

}

}

// Pharmacy.Web/DTOs/ProductDto.cs

namespace Pharmacy.Web.DTOs

{

public class ProductDto

{

public int Id { get; set; }

public string Name { get; set; }

public string Description { get; set; }

public string ActiveSubstance { get; set; }

public string Manufacturer { get; set; }

public string Barcode { get; set; }

public ProductCategory Category { get; set; }

public bool IsPrescriptionRequired { get; set; }

public decimal Price { get; set; }

public DateTime CreatedAt { get; set; }

public DateTime? UpdatedAt { get; set; }

}

public class ProductDetailsDto : ProductDto

{

public StockInfoDto StockInfo { get; set; }

public IEnumerable<ProductImageDto> Images { get; set; }

}

public class ProductCreateDto

{

[Required, MaxLength(100)]

public string Name { get; set; }

[MaxLength(500)]

public string Description { get; set; }

[Required, MaxLength(100)]

public string ActiveSubstance { get; set; }

[Required, MaxLength(100)]

public string Manufacturer { get; set; }

[Required, MaxLength(50)]

public string Barcode { get; set; }

[Required]

public ProductCategory Category { get; set; }

public bool IsPrescriptionRequired { get; set; }

[Range(0, 100000)]

public decimal Price { get; set; }

}

public class ProductUpdateDto

{

public int Id { get; set; }

[Required, MaxLength(100)]

public string Name { get; set; }

[MaxLength(500)]

public string Description { get; set; }

public bool IsPrescriptionRequired { get; set; }

[Range(0, 100000)]

public decimal Price { get; set; }

}

public class StockInfoDto

{

public int TotalQuantity { get; set; }

public IEnumerable<StockItemDto> Items { get; set; }

}

public class StockItemDto

{

public int WarehouseId { get; set; }

public string WarehouseName { get; set; }

public int Quantity { get; set; }

public string BatchNumber { get; set; }

public DateTime ExpirationDate { get; set; }

}

public class StockAdjustmentDto

{

[Required]

public int WarehouseId { get; set; }

[Required]

public int Quantity { get; set; }

[Required]

public string BatchNumber { get; set; }

public DateTime? ExpirationDate { get; set; }

}

public class ProductImageDto

{

public int Id { get; set; }

public string Url { get; set; }

public bool IsMain { get; set; }

}

}

```

## 5. Startup и конфигурация

```csharp

// Pharmacy.Web/Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container

builder.Services.AddDbContext<PharmacyDbContext>(options =>

options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

builder.Services.AddScoped<IProductRepository, ProductRepository>();

builder.Services.AddScoped<IProductService, ProductService>();

builder.Services.AddAutoMapper(typeof(MappingProfile));

builder.Services.AddControllers();

builder.Services.AddEndpointsApiExplorer();

builder.Services.AddSwaggerGen(c =>

{

c.SwaggerDoc("v1", new OpenApiInfo { Title = "Pharmacy API", Version = "v1" });

});

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)

.AddJwtBearer(options =>

{

options.TokenValidationParameters = new TokenValidationParameters

{

ValidateIssuerSigningKey = true,

IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8

.GetBytes(builder.Configuration.GetSection("AppSettings:Token").Value)),

ValidateIssuer = false,

ValidateAudience = false

};

});

var app = builder.Build();

// Configure the HTTP request pipeline

if (app.Environment.IsDevelopment())

{

app.UseSwagger();

app.UseSwaggerUI();

}

app.UseHttpsRedirection();

app.UseAuthentication();

app.UseAuthorization();

app.MapControllers();

// Seed database

using (var scope = app.Services.CreateScope())

{

var services = scope.ServiceProvider;

try

{

var context = services.GetRequiredService<PharmacyDbContext>();

context.Database.Migrate();

await Seed.SeedData(context);

}

catch (Exception ex)

{

var logger = services.GetRequiredService<ILogger<Program>>();

logger.LogError(ex, "An error occurred during migration");

}

}

app.Run();

```

## 6. Дополнительные компоненты

```csharp

// Pharmacy.Core/Mapping/MappingProfile.cs

namespace Pharmacy.Core.Mapping

{

public class MappingProfile : Profile

{

public MappingProfile()

{

CreateMap<Product, ProductDto>();

CreateMap<ProductCreateDto, Product>();

CreateMap<ProductUpdateDto, Product>();

CreateMap<StockItem, StockItemDto>()

.ForMember(dest => dest.WarehouseName, opt => opt.MapFrom(src => src.Warehouse.Name));

CreateMap<StockInfo, StockInfoDto>();

}

}

}

// Pharmacy.Data/Seed.cs

namespace Pharmacy.Data

{

public static class Seed

{

public static async Task SeedData(PharmacyDbContext context)

{

if (!await context.Warehouses.AnyAsync())

{

var warehouses = new List<Warehouse>

{

new Warehouse { Name = "Основной склад", Address = "ул. Складская, 1", Capacity = 1000 },

new Warehouse { Name = "Дополнительный склад", Address = "ул. Запасная, 5", Capacity = 500 }

};

await context.Warehouses.AddRangeAsync(warehouses);

await context.SaveChangesAsync();

}

if (!await context.Products.AnyAsync())

{

var products = new List<Product>

{

new Product {

Name = "Аспирин",

Description = "Обезболивающее и жаропонижающее средство",

ActiveSubstance = "Ацетилсалициловая кислота",

Manufacturer = "Bayer",

Barcode = "123456789012",

Category = ProductCategory.Medicines,

IsPrescriptionRequired = false,

Price = 150.50m

},

new Product {

Name = "Нурофен",

Description = "Обезболивающее, жаропонижающее и противовоспалительное средство",

ActiveSubstance = "Ибупрофен",

Manufacturer = "Reckitt Benckiser",

Barcode = "987654321098",

Category = ProductCategory.Medicines,

IsPrescriptionRequired = false,

Price = 220.75m

}

};

await context.Products.AddRangeAsync(products);

await context.SaveChangesAsync();

var stockItems = new List<StockItem>

{

new StockItem {

ProductId = 1,

WarehouseId = 1,

Quantity = 100,

BatchNumber = "ASP202301",

ExpirationDate = DateTime.UtcNow.AddYears(2),

ArrivalDate = DateTime.UtcNow.AddMonths(-1),

PurchasePrice = 120.00m

},

new StockItem {

ProductId = 2,

WarehouseId = 1,

Quantity = 50,

BatchNumber = "NUR202302",

ExpirationDate = DateTime.UtcNow.AddYears(3),

ArrivalDate = DateTime.UtcNow.AddMonths(-2),

PurchasePrice = 180.00m

}

};

await context.StockItems.AddRangeAsync(stockItems);

await context.SaveChangesAsync();

}

}

}

}

```

## 7. Pharmacy.Tests (Тесты)

```csharp

// Pharmacy.Tests/Services/ProductServiceTests.cs

namespace Pharmacy.Tests.Services

{

public class ProductServiceTests

{

private readonly Mock<IProductRepository> _mockProductRepo;

private readonly Mock<IRepository<StockItem>> _mockStockItemRepo;

private readonly Mock<IRepository<Warehouse>> _mockWarehouseRepo;

private readonly Mock<ILogger<ProductService>> _mockLogger;

private readonly ProductService _service;

public ProductServiceTests()

{

_mockProductRepo = new Mock<IProductRepository>();

_mockStockItemRepo = new Mock<IRepository<StockItem>>();

_mockWarehouseRepo = new Mock<IRepository<Warehouse>>();

_mockLogger = new Mock<ILogger<ProductService>>();

_service = new ProductService(

_mockProductRepo.Object,

_mockStockItemRepo.Object,

_mockWarehouseRepo.Object,

_mockLogger.Object);

}

[Fact]

public async Task GetProductByIdAsync_ProductExists_ReturnsProduct()

{

// Arrange

var productId = 1;

var expectedProduct = new Product { Id = productId, Name = "Test Product" };

_mockProductRepo.Setup(x => x.GetByIdAsync(productId))

.ReturnsAsync(expectedProduct);

// Act

var result = await _service.GetProductByIdAsync(productId);

// Assert

result.Should().NotBeNull();

result.Id.Should().Be(productId);

result.Name.Should().Be(expectedProduct.Name);

}

[Fact]

public async Task AdjustStockAsync_NewBatch_AddsStockItem()

{

// Arrange

var productId = 1;

var warehouseId = 1;

var quantity = 10;

var batchNumber = "BATCH001";

var expirationDate = DateTime.UtcNow.AddYears(1);

_mockProductRepo.Setup(x => x.GetByIdAsync(productId))

.ReturnsAsync(new Product { Id = productId });

_mockWarehouseRepo.Setup(x => x.GetByIdAsync(warehouseId))

.ReturnsAsync(new Warehouse { Id = warehouseId });

_mockStockItemRepo.Setup(x => x.GetAllAsync())

.ReturnsAsync(new List<StockItem>());

// Act

await _service.AdjustStockAsync(productId, warehouseId, quantity, batchNumber, expirationDate);

// Assert

_mockStockItemRepo.Verify(x => x.AddAsync(It.Is<StockItem>(si =>

si.ProductId == productId &&

si.WarehouseId == warehouseId &&

si.Quantity == quantity &&

si.BatchNumber == batchNumber)),

Times.Once);

}

}

}

```