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
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>
84 lines
3.8 KiB
C#
84 lines
3.8 KiB
C#
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<TDto>(MapsterConfig.Config)</c>
|
||
/// — Mapster кодогенерирует SQL-friendly `.Select(...)`-выражение,
|
||
/// что эквивалентно ручному `Select(p => new TDto(...))` по
|
||
/// производительности, но компактнее в контроллере.
|
||
///
|
||
/// Главное правило записи:
|
||
/// 1. Все computed-поля (joins, агрегаты) — через `.Map(...)`.
|
||
/// 2. Все коллекции (Prices, Barcodes) — через `.Map(...)` с
|
||
/// `.Adapt<TItemDto>()` на каждом элементе.
|
||
/// 3. PreserveReference = false (default) — для EF-проекций
|
||
/// циклы не нужны.
|
||
///
|
||
/// Регистрация — в `Program.cs`:
|
||
/// <code>
|
||
/// var cfg = MapsterConfig.Build();
|
||
/// services.AddSingleton(cfg);
|
||
/// services.AddScoped<IMapper, ServiceMapper>();
|
||
/// </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;
|
||
}
|
||
}
|