83 lines
3.6 KiB
Python
83 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
from collections import Counter
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session, joinedload
|
|
|
|
from .models import CatchReport, ModerationStatus
|
|
from .schemas import ActivityOut
|
|
|
|
|
|
def activity_rows(
|
|
session: Session,
|
|
*,
|
|
hours: int,
|
|
waterbody: str | None = None,
|
|
fish: str | None = None,
|
|
method: str | None = None,
|
|
) -> list[ActivityOut]:
|
|
now = datetime.now(timezone.utc)
|
|
query = (
|
|
select(CatchReport)
|
|
.options(
|
|
joinedload(CatchReport.fish), joinedload(CatchReport.waterbody),
|
|
joinedload(CatchReport.spot), joinedload(CatchReport.bait),
|
|
)
|
|
.where(
|
|
CatchReport.moderation_status == ModerationStatus.approved,
|
|
CatchReport.spot_id.is_not(None),
|
|
CatchReport.reported_at >= now - timedelta(hours=hours),
|
|
)
|
|
)
|
|
reports = list(session.scalars(query))
|
|
if waterbody:
|
|
reports = [r for r in reports if r.waterbody.slug == waterbody]
|
|
if fish:
|
|
reports = [r for r in reports if r.fish.slug == fish]
|
|
if method:
|
|
reports = [r for r in reports if r.fishing_method == method]
|
|
|
|
groups: dict[tuple[object, object], list[CatchReport]] = {}
|
|
for report in reports:
|
|
groups.setdefault((report.spot_id, report.fish_id), []).append(report)
|
|
|
|
result: list[ActivityOut] = []
|
|
for items in groups.values():
|
|
first = items[0]
|
|
players = {r.player_name.strip().casefold() for r in items if r.player_name and r.player_name.strip()}
|
|
freshness = [math.exp(-max(0.0, (now - _aware(r.reported_at)).total_seconds()) / 3600 / 18) for r in items]
|
|
weighted = sum(value * r.source_confidence / 100 for value, r in zip(freshness, items))
|
|
trophies = sum(bool(r.fish.trophy_weight_g and r.weight_g >= r.fish.trophy_weight_g) for r in items)
|
|
activity = round(55 * min(1, weighted / 12) + 25 * min(1, len(players) / 6) + 20 * min(1, trophies / 3))
|
|
average_confidence = sum(r.source_confidence for r in items) / len(items)
|
|
confidence = round(45 * min(1, len(items) / 10) + 35 * min(1, len(players) / 5) + 20 * average_confidence / 100)
|
|
latest = max(_aware(r.caught_at or r.reported_at) for r in items)
|
|
baits = Counter(r.bait.name for r in items if r.bait)
|
|
freshness_text = _freshness_text(now - latest)
|
|
result.append(ActivityOut(
|
|
spot_id=first.spot.id, waterbody_slug=first.waterbody.slug,
|
|
waterbody=first.waterbody.name_ru, fish_slug=first.fish.slug,
|
|
fish=first.fish.name_ru, x=first.spot.x, y=first.spot.y,
|
|
best_bait=baits.most_common(1)[0][0] if baits else None,
|
|
catches=len(items), unique_players=len(players),
|
|
average_weight_g=round(sum(r.weight_g for r in items) / len(items)),
|
|
max_weight_g=max(r.weight_g for r in items), last_confirmed_at=latest,
|
|
activity_score=activity, confidence_score=confidence,
|
|
explanation=f"{len(items)} свежих уловов от {len(players)} игроков. Последнее подтверждение {freshness_text}." + (" Данных мало." if len(items) < 3 else ""),
|
|
))
|
|
return sorted(result, key=lambda row: (row.activity_score, row.last_confirmed_at), reverse=True)
|
|
|
|
|
|
def _aware(value: datetime) -> datetime:
|
|
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
|
|
|
|
def _freshness_text(delta: timedelta) -> str:
|
|
minutes = max(0, round(delta.total_seconds() / 60))
|
|
if minutes < 60:
|
|
return f"{minutes} мин. назад"
|
|
return f"{minutes // 60} ч. назад"
|