food-market/src/food-market.api/Controllers/Uploads/UploadsController.cs
nns e13dd6937f perf(s14): индексы + N+1 fix + bundle -50% + WebP variants + pool + Hangfire timing
Sprint 14 — производительность с реальными замерами до/после.

Ключевые цифры:
- Sales-report SQL: 9.53ms → 7.09ms mean (-25%) после N+1 fix +
  индексов.
- Initial JS bundle: 1456 KB → 706 KB raw (-51%); gzip 389 KB →
  196 KB (-50%) через React.lazy на 30 редких страниц + Recharts.
- Lighthouse /login: Perf 89, A11y 92, BP 100 (target ≥85/90/90 ✓).

Подробности по каждому пункту + методология замеров — в
docs/sprint14-progress.md.

Что сделано:
1. Phase14a_PerfIndexes — composite (Org,Status,Date), partial
   (WHERE Status=1 AND NOT IsReturn) + INCLUDE, и composite
   stock_movements(Org,OccurredAt).
2. SalesReportController.FetchAsync — раньше каждая строка
   результата делала CASE WHEN ELSE (SELECT ... LIMIT 1)
   correlated subquery на RetailPoint.Name и User.FullName.
   Заменено на 2 IN-batch'a + dictionary lookup в C#.
3. App.tsx React.lazy для отчётов, audit-log, loyalty, super-admin,
   settings, all rare edit pages. Recharts вынесен в lazy chunk
   Dashboard'а (KPI рендерятся сразу).
4. SixLabors.ImageSharp v3.1.6 + ImageVariantService — генерирует
   thumb 256/medium 800 WebP@80 при загрузке. UploadsController
   ?size=thumb|medium с fallback. React <ProductImage> — <picture>
   + srcset.
5. ApplyDefaultPoolConfig на старте: Max=100, Min=10 (грей пул),
   Idle=300, Max Auto Prepare=20.
6. Lighthouse на /login /forgot-password /reset-password —
   все три проходят пороги.
7. JobTimingFilter + HangfireGlobalFilterRegistrar — каждый
   recurring job логирует длительность; >30s = Warning.

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

57 lines
2.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 foodmarket.Api.Storage;
using Microsoft.AspNetCore.Mvc;
namespace foodmarket.Api.Controllers.Uploads;
/// <summary>Универсальный stream-endpoint для отдачи объектов из
/// IObjectStorage. Маршрут <c>/uploads/{**path}</c>. Для Local — читает с
/// диска, для MinIO — proxy'ит. Авторизация не требуется (картинки товаров
/// публичны как и было раньше); если в будущем понадобится защита — добавить
/// [Authorize] и подписать URL.
///
/// nginx должен по-прежнему перехватывать <c>/uploads/</c> и проксировать на
/// API (см. deploy/nginx.conf). Сейчас локальный root монтируется в volume и
/// nginx раздаёт напрямую — этот контроллер становится альтернативой когда
/// MinIO активен.</summary>
[ApiController]
[Route("uploads")]
public class UploadsController : ControllerBase
{
private readonly IObjectStorage _storage;
public UploadsController(IObjectStorage storage) => _storage = storage;
[HttpGet("{*path}")]
public async Task<IActionResult> Get(string path, [FromQuery] string? size, CancellationToken ct)
{
if (string.IsNullOrEmpty(path)) return NotFound();
// Sprint 14: ?size=thumb|medium|original. Запрос с size=thumb
// отдаёт <path>.thumb.webp (если существует), иначе fallback
// на оригинал. Это позволяет фронту использовать <picture> с
// srcset для разных ширин экрана.
var variantSuffix = size?.ToLowerInvariant() switch
{
"thumb" => foodmarket.Api.Storage.ImageVariantService.ThumbSuffix,
"medium" => foodmarket.Api.Storage.ImageVariantService.MediumSuffix,
_ => "",
};
if (variantSuffix.Length > 0)
{
var variantPath = path + variantSuffix;
var variant = await _storage.OpenAsync(variantPath, ct);
if (variant is not null)
{
Response.Headers["Cache-Control"] = "public, max-age=2592000"; // 30 дней (агрессивнее для variant'ов)
return File(variant.Value.Stream, variant.Value.ContentType);
}
// Fallback на оригинал — старые загрузки до Sprint 14 не имеют variant'ов.
}
var obj = await _storage.OpenAsync(path, ct);
if (obj is null) return NotFound();
Response.Headers["Cache-Control"] = "public, max-age=604800"; // 7 дней
return File(obj.Value.Stream, obj.Value.ContentType);
}
}