"""Чтение данных пользователей для админки (Блок 4.3). Баланс — контракт ``SUM(bot.money.amount)``; яички — ``SUM(bot.eggs.amount)``. Подписка на рассылки — отсутствие строки в ``bot.unsubscribe``. """ from __future__ import annotations from typing import Any from common.db.pool import get_pool #: Общая часть выборки: агрегаты по сообщениям, деньгам, яичкам и очищениям. _FROM = """ FROM bot.tg_users AS tg LEFT JOIN bot.de_users AS du ON du.id_user = tg.id_user LEFT JOIN bot.titles AS t ON t.id_title = du.id_title LEFT JOIN ( SELECT id_user, count(*) AS cnt, max(dt) AS last_dt FROM bot.messages GROUP BY id_user ) AS msg ON msg.id_user = tg.id_user LEFT JOIN ( SELECT id_user, sum(amount) AS balance FROM bot.money GROUP BY id_user ) AS money ON money.id_user = tg.id_user LEFT JOIN ( SELECT id_user, sum(amount) AS eggs FROM bot.eggs GROUP BY id_user ) AS eggs ON eggs.id_user = tg.id_user LEFT JOIN ( SELECT id_user, count(*) AS cnt, max(dt) AS last_dt FROM bot.purifications GROUP BY id_user ) AS pur ON pur.id_user = tg.id_user LEFT JOIN bot.unsubscribe AS unsub ON unsub.id_user = tg.id_user """ _SELECT = """ SELECT tg.id_user, tg.username, tg.first_name, tg.last_name, du.nick, du.lvl, du.expr, du.expr_streak, du.dt_create, t.title, coalesce(msg.cnt, 0) AS msg_count, msg.last_dt AS last_message_at, coalesce(money.balance, 0) AS balance, coalesce(eggs.eggs, 0) AS eggs, coalesce(pur.cnt, 0) AS purification_count, pur.last_dt AS last_purification_at, (unsub.id_user IS NULL) AS subscribed """ #: Колонки, по которым идёт поиск (в порядке подстановки параметров). _SEARCH_COLUMNS = ("tg.username", "tg.first_name", "tg.last_name", "du.nick") def _search_clause(term: str) -> tuple[str, list[Any]]: """WHERE по нику, username, имени и Telegram-ID.""" term = term.strip() if not term: return "", [] like = f"%{term}%" clauses = [f"{column} ILIKE %s" for column in _SEARCH_COLUMNS] params: list[Any] = [like] * len(_SEARCH_COLUMNS) telegram_id = term.lstrip("@") if telegram_id.isdigit(): clauses.append("tg.id_user = %s") params.append(int(telegram_id)) return " WHERE (" + " OR ".join(clauses) + ")", params async def count_users(search: str = "") -> int: """Сколько пользователей подходит под поиск.""" where, params = _search_clause(search) async with get_pool().connection() as conn: cur = await conn.execute("SELECT count(*) AS total " + _FROM + where, tuple(params)) row = await cur.fetchone() return int(row["total"]) if row else 0 async def list_users(search: str = "", *, limit: int = 25, offset: int = 0) -> list[dict[str, Any]]: """Страница списка пользователей (с поиском).""" where, params = _search_clause(search) async with get_pool().connection() as conn: cur = await conn.execute( _SELECT + _FROM + where + " ORDER BY du.nick NULLS LAST, tg.id_user LIMIT %s OFFSET %s", (*params, limit, offset), ) return await cur.fetchall() async def get_user(user_id: int) -> dict[str, Any] | None: """Карточка пользователя со сводными показателями.""" async with get_pool().connection() as conn: cur = await conn.execute(_SELECT + _FROM + " WHERE tg.id_user = %s", (user_id,)) return await cur.fetchone() async def money_history(user_id: int, *, limit: int = 10) -> list[dict[str, Any]]: """Последние операции по балансу с названием действия.""" async with get_pool().connection() as conn: cur = await conn.execute( """ SELECT m.id_money, m.amount, m.dt_action, a.title AS action FROM bot.money AS m LEFT JOIN bot.actions AS a ON a.id_action = m.id_action WHERE m.id_user = %s ORDER BY m.dt_action DESC, m.id_money DESC LIMIT %s """, (user_id, limit), ) return await cur.fetchall()