food-market/src/food-market.application/Mapping/MapsterConfig.cs
nns 346b7bfd48
Some checks failed
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Public / Build + push Public (push) Has been cancelled
Docker Public / Deploy Public on stage (push) Has been cancelled
feat(s20): Mapster + SSO scaffold + maintenance automation (7 пунктов)
1. TD-3 Mapster — Application/Mapping/MapsterConfig.cs с
   TypeAdapterConfig для Product, Counterparty + collections.
   ProductsController.List/Get/GetInternalAsync + CounterpartiesController.
   List/Get переведены на .ProjectToType<TDto>(MapsterConfig.Config).
   Inline Projection-Expression удалён.

2. SSO scaffold — Microsoft.AspNetCore.Authentication.Google + .MicrosoftAccount
   пакеты, условная регистрация в Program.cs (только если ClientId задан).
   ExternalAuthController с GET /api/auth/external/{provider} (Challenge или
   503 если не настроено), /callback (501 с email — invite-flow TODO),
   /providers (булевый список). docs/sso.md инструкция.

3. Stale-data cleanup — HousekeepingJobs расширен:
   PruneOrgAuditLogAsync (>90д из Cleanup:OrgAuditLogDays),
   PruneDraftsAsync (Supply/RetailSale/Demand старше 30д),
   PruneRevokedRefreshTokensAsync (raw SQL DELETE из OpenIddictTokens).
   3 новых cron'a в HangfireJobsConfigurator (03:00-03:20 UTC).

4. DB VACUUM automation — DatabaseMaintenanceJobs.VacuumTopTablesAsync:
   pg_total_relation_size → топ-5 таблиц → VACUUM (ANALYZE) per table
   с замером времени. Default cron еженедельно вс 04:00 UTC.

5. Disk usage monitoring — DiskMonitoringJob ежечасно: DriveInfo.AvailableFreeSpace
   на пути из Monitoring:DiskPaths (default "/opt,/var/lib/docker").
   <1GB → Telegram-alert на Monitoring:SuperAdminTelegramChatIds.
   Anti-spam cooldown 6h. Gauge food_market_disk_free_bytes{mount}.

6. Performance regression detection — ~/nightly-perf-check.sh после
   nightly-verify. Парсит /metrics, считает db_avg_ms, сравнивает с
   baseline в ~/.fm-watchdog/perf-baseline.json. Δ>30% → Telegram alert
   + baseline НЕ обновляется (sliding window).

7. Public-site analytics placeholder — Astro BaseLayout рендерит
   gtag/Yandex.Metrika только если задан PUBLIC_GA_ID / PUBLIC_YM_ID;
   иначе <script data-id="REPLACE_ME" data-doc="docs/analytics.md">
   маркер. docs/analytics.md с инструкцией подключения.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 21:54:12 +05:00

84 lines
3.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Mapster;
using foodmarket.Application.Catalog;
using foodmarket.Domain.Catalog;
namespace foodmarket.Application.Mapping;
/// <summary>Sprint 20 / TD-3: централизованная Mapster-конфигурация
/// для проекций domain → DTO. Используется через
/// <c>queryable.ProjectToType&lt;TDto&gt;(MapsterConfig.Config)</c>
/// — Mapster кодогенерирует SQL-friendly `.Select(...)`-выражение,
/// что эквивалентно ручному `Select(p =&gt; new TDto(...))` по
/// производительности, но компактнее в контроллере.
///
/// Главное правило записи:
/// 1. Все computed-поля (joins, агрегаты) — через `.Map(...)`.
/// 2. Все коллекции (Prices, Barcodes) — через `.Map(...)` с
/// `.Adapt&lt;TItemDto&gt;()` на каждом элементе.
/// 3. PreserveReference = false (default) — для EF-проекций
/// циклы не нужны.
///
/// Регистрация — в `Program.cs`:
/// <code>
/// var cfg = MapsterConfig.Build();
/// services.AddSingleton(cfg);
/// services.AddScoped&lt;IMapper, ServiceMapper&gt;();
/// </code>
/// </summary>
public static class MapsterConfig
{
private static TypeAdapterConfig? _cached;
/// <summary>Singleton TypeAdapterConfig. Lazy-initialized чтобы
/// тесты тоже могли вызвать без DI.</summary>
public static TypeAdapterConfig Config => _cached ??= Build();
public static TypeAdapterConfig Build()
{
var cfg = new TypeAdapterConfig();
cfg.NewConfig<ProductBarcode, ProductBarcodeDto>()
.ConstructUsing(src => new ProductBarcodeDto(
src.Id, src.Code, src.Type, src.IsPrimary));
cfg.NewConfig<ProductPrice, ProductPriceDto>()
.ConstructUsing(src => new ProductPriceDto(
src.Id, src.PriceTypeId, src.PriceType!.Name,
src.Amount, src.CurrencyId, src.Currency!.Code));
cfg.NewConfig<Product, ProductDto>()
.ConstructUsing(src => new ProductDto(
src.Id, src.Name, src.Article, src.Description,
src.UnitOfMeasureId, src.UnitOfMeasure!.Name,
src.Vat, src.VatEnabled,
src.ProductGroupId, src.ProductGroup!.Name,
src.DefaultSupplierId,
src.DefaultSupplier != null ? src.DefaultSupplier.Name : null,
src.CountryOfOriginId,
src.CountryOfOrigin != null ? src.CountryOfOrigin.Name : null,
src.IsService, src.Packaging, src.IsMarked,
src.MinStock, src.MaxStock,
src.ReferencePrice, src.ReferencePriceUpdatedAt,
src.PurchaseCurrencyId,
src.PurchaseCurrency != null ? src.PurchaseCurrency.Code : null,
src.Cost, src.LastSupplyAt,
src.ImageUrl,
src.IsArchived, src.IsAvailableForSale,
src.Prices.Select(pr => new ProductPriceDto(
pr.Id, pr.PriceTypeId, pr.PriceType!.Name,
pr.Amount, pr.CurrencyId, pr.Currency!.Code)).ToList(),
src.Barcodes.Select(b => new ProductBarcodeDto(
b.Id, b.Code, b.Type, b.IsPrimary)).ToList()));
cfg.NewConfig<Counterparty, CounterpartyDto>()
.ConstructUsing(src => new CounterpartyDto(
src.Id, src.Name, src.LegalName, src.Type,
src.Bin, src.Iin, src.TaxNumber,
src.CountryId, src.Country != null ? src.Country.Name : null,
src.Address, src.Phone, src.Email,
src.BankName, src.BankAccount, src.Bik,
src.ContactPerson, src.Notes));
return cfg;
}
}