Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
using Deal.Api.Http;
|
||||
using Deal.Modules.Settings.Application;
|
||||
using Deal.Modules.Settings.Application.Models;
|
||||
|
||||
namespace Deal.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP-эндпоинты курсов валют: GET /api/rates, POST /api/rates/refresh (Ruling 8, api-map §3.4 L149–150).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// «Только для Settings-экрана» (Ruling 8): фронт читает курсы на boot (store.js L571–581) и обновляет
|
||||
/// по кнопке (refreshRates L1843–1848). GET — текущий кэш (ratesCache) или дефолт-мок; при протухании/
|
||||
/// смене источника/отсутствии кэша (Ruling 6) фоново запускает RefreshAsync через
|
||||
/// <see cref="RatesRefreshScheduler"/> и отвечает текущим кэшем (план Task 8 L317–318). POST — синхронный
|
||||
/// refresh 1:1 с прототипом: <c>{ok: bool, rates: {base, rates, source, updatedAt}}</c> (ok=false при сбое
|
||||
/// ЦБ, кэш не тронут). Оба требуют сессию: 401 {detail} (Ruling 10). Резолв scoped-зависимостей — через
|
||||
/// RequestServices ПОСЛЕ проверки сессии (как SettingsEndpoints/AiCheckEndpoint: ISettingsStore требует
|
||||
/// tenant-контекст запроса).
|
||||
/// </remarks>
|
||||
public static class RatesEndpoints
|
||||
{
|
||||
// Префикс группы API (общий для эндпоинтов этапа, Ruling 8).
|
||||
private const string ApiGroupPrefix = "/api";
|
||||
|
||||
// Путь текущих курсов (GET).
|
||||
private const string RatesPath = "/rates";
|
||||
|
||||
// Путь принудительного обновления (POST).
|
||||
private const string RatesRefreshPath = "/rates/refresh";
|
||||
|
||||
// OpenAPI-тег группы (в прототипе роутер settings — settings_routes.py).
|
||||
private const string OpenApiTag = "settings";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует GET /api/rates и POST /api/rates/refresh.
|
||||
/// </summary>
|
||||
/// <param name="app">Построитель маршрутов приложения.</param>
|
||||
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
||||
public static IEndpointRouteBuilder MapRatesEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(ApiGroupPrefix).WithTags(OpenApiTag);
|
||||
group.MapGet(RatesPath, GetRatesAsync);
|
||||
group.MapPost(RatesRefreshPath, RefreshRatesAsync);
|
||||
return app;
|
||||
}
|
||||
|
||||
// GET /api/rates: текущий кэш курсов тенанта (+ ленивый фоновый refresh при необходимости).
|
||||
private static async Task<IResult> GetRatesAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
// Резолв после 401-гейта: RatesService — scoped на TenantDbContext (tenant-контекст запроса).
|
||||
RatesService ratesService = context.RequestServices.GetRequiredService<RatesService>();
|
||||
|
||||
RatesDto current = await ratesService.GetAsync(ct);
|
||||
|
||||
// Ленивое обновление (Ruling 6, план Task 8): протухший кэш / смена источника / нет кэша —
|
||||
// фоновый RefreshAsync в отдельном scope; ответ — текущий кэш.
|
||||
if (await ratesService.ShouldFetchAsync(ct))
|
||||
{
|
||||
context.RequestServices.GetRequiredService<RatesRefreshScheduler>().Schedule();
|
||||
}
|
||||
|
||||
return Results.Ok(current);
|
||||
}
|
||||
|
||||
// POST /api/rates/refresh: принудительное обновление; ответ {ok, rates} (1:1 settings_routes.py L229–232).
|
||||
private static async Task<IResult> RefreshRatesAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
RatesService ratesService = context.RequestServices.GetRequiredService<RatesService>();
|
||||
|
||||
// Синхронно: refresh по текущей настройке rateSource; при сбое ЦБ ok=false и кэш не тронут.
|
||||
bool ok = await ratesService.RefreshAsync(ct);
|
||||
RatesDto current = await ratesService.GetAsync(ct);
|
||||
return Results.Ok(new { ok, rates = current });
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
Reference in New Issue
Block a user