From 55e5810e651c98f2465dc3a4e765735ebb164a6d Mon Sep 17 00:00:00 2001 From: IK Date: Mon, 21 Sep 2026 20:21:12 +0700 Subject: [PATCH] feat: search official records --- apps/api/app/routers/public_data.py | 11 +++++++++-- apps/web/src/pages/records.astro | 5 +++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/api/app/routers/public_data.py b/apps/api/app/routers/public_data.py index 0f95fb5..5cde2aa 100644 --- a/apps/api/app/routers/public_data.py +++ b/apps/api/app/routers/public_data.py @@ -1,13 +1,14 @@ from datetime import datetime, timedelta, timezone -from fastapi import APIRouter, Query -from sqlalchemy import func, select +from fastapi import APIRouter, HTTPException, Query +from sqlalchemy import func, or_, select from sqlalchemy.orm import joinedload from ..config import settings from ..dependencies import Db from ..models import ( CatchReport, + Bait, CommunityImportRun, DataSource, ExternalObservation, @@ -117,9 +118,12 @@ def records( fish: str | None = None, waterbody: str | None = None, category: str | None = None, + q: str | None = None, limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0), ) -> PaginatedOfficialRecordOut: + if q is not None and len(q) > 100: + raise HTTPException(status_code=422, detail="record search query is too long") query = select(CatchReport).options( joinedload(CatchReport.fish), joinedload(CatchReport.waterbody), @@ -131,6 +135,9 @@ def records( query = query.join(CatchReport.waterbody).where(Waterbody.slug == waterbody) if category: query = query.where(CatchReport.raw_payload["category"].as_string() == category) + if q and q.strip(): + needle = f"%{q.strip()}%" + query = query.outerjoin(Bait, CatchReport.bait_id == Bait.id).where(or_(CatchReport.player_name.ilike(needle), Bait.name.ilike(needle))) total = db.scalar( query.with_only_columns(func.count(CatchReport.id), maintain_column_froms=True).order_by(None) ) or 0 diff --git a/apps/web/src/pages/records.astro b/apps/web/src/pages/records.astro index c16efe7..c5cec93 100644 --- a/apps/web/src/pages/records.astro +++ b/apps/web/src/pages/records.astro @@ -9,13 +9,14 @@ import { api, kg, type DictionaryItem, type ImportRun, type OfficialRecord, type const params = Astro.url.searchParams; const fish = params.get("fish") ?? ""; const waterbody = params.get("waterbody") ?? ""; +const search = params.get("q") ?? ""; const recordsLimit = 50; const requestedOffset = Number(params.get("offset") ?? 0); let offset = Number.isInteger(requestedOffset) && requestedOffset >= 0 ? requestedOffset : 0; let items: OfficialRecord[] = [], runs: ImportRun[] = [], fishes: DictionaryItem[] = [], waterbodies: DictionaryItem[] = []; let unavailable = false, showNoIndex = false, totalRecords = 0; try { - const query = new URLSearchParams({ fish, waterbody, limit: String(recordsLimit), offset: String(offset) }); + const query = new URLSearchParams({ fish, waterbody, q: search, limit: String(recordsLimit), offset: String(offset) }); const paginated = await api(`/api/v1/records?${query}`); items = paginated.items; totalRecords = paginated.total; @@ -36,7 +37,7 @@ const last = runs[0]; ---
Публичные данные RF4

Официальные
рекорды

{last ? `Импорт: ${last.status}` : "Импорт ещё не запускался"}{last?.finished_at && {new Date(last.finished_at).toLocaleString("ru-RU")} · {last.rows_seen} строк}
-
{(fish || waterbody) && Сбросить}
+
{(fish || waterbody || search) && Сбросить}
{unavailable && } {!unavailable && items.length &&
РыбаВесВодоёмПриманкаИгрокДата и источник
{items.map(record =>
{record.fish}{kg(record.weight_g)}{record.waterbody}{record.bait ?? "—"}{record.player_name ?? "—"}
)}
}