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>
57 lines
2.8 KiB
C#
57 lines
2.8 KiB
C#
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);
|
||
}
|
||
}
|