diff --git a/README.md b/README.md index db93412..8453134 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ HttpOnly-cookie (`ADMIN_SESSION_SECRET`), все формы защищены CSR - схема — `sql/V26__analytics_schema.sql`; - клиент и батчинг — `common/analytics/` (запись неблокирующая, сбой Postgres не влияет на бота); - сообщения не пишутся поштучно: на каждое сообщение инкрементится дневной счётчик; +- построчная `bot.messages` удалена (V27), старые данные перенесены в `analytics.messages_daily`; - партиции создаются приложением и **не удаляются** — история хранится бессрочно; - выключается флагом `ANALYTICS_ENABLED=false`. @@ -170,7 +171,7 @@ pytest ## Версионирование -- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.7.0`. +- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.7.1`. - Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`). - Docker-образы тегируются версией (Блок 2), события аналитики получают `bot_version`. - Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`. diff --git a/admin/templates/users/card.html b/admin/templates/users/card.html index b0dd9dc..62d74f7 100644 --- a/admin/templates/users/card.html +++ b/admin/templates/users/card.html @@ -68,7 +68,7 @@
Сообщения
{{ user.msg_count }}
- последнее: {{ user.last_message_at.strftime("%d.%m.%Y %H:%M") if user.last_message_at else "—" }} + последняя активность: {{ user.last_message_at.strftime("%d.%m.%Y") if user.last_message_at else "—" }}
diff --git a/common/analytics/read.py b/common/analytics/read.py index a4d9dca..7190441 100644 --- a/common/analytics/read.py +++ b/common/analytics/read.py @@ -102,7 +102,13 @@ async def fetch_messages( async with get_pool().connection() as conn: cur = await conn.execute( - f"SELECT count(*) AS total FROM analytics.messages_daily{where}", + f""" + SELECT count(*) AS total + FROM ( + SELECT 1 FROM analytics.messages_daily{where} + GROUP BY day, user_id + ) AS grouped + """, tuple(params), ) row = await cur.fetchone() @@ -110,9 +116,11 @@ async def fetch_messages( cur = await conn.execute( f""" - SELECT day, user_id, username, chat_id, count + SELECT day, user_id, max(username) AS username, max(chat_id) AS chat_id, + sum(count)::bigint AS count FROM analytics.messages_daily{where} - ORDER BY day DESC, count DESC + GROUP BY day, user_id + ORDER BY day DESC, sum(count) DESC LIMIT %s OFFSET %s """, (*params, limit, offset), diff --git a/common/repositories/users_admin_repo.py b/common/repositories/users_admin_repo.py index 6be072a..f63311c 100644 --- a/common/repositories/users_admin_repo.py +++ b/common/repositories/users_admin_repo.py @@ -16,9 +16,9 @@ _FROM = """ 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 + SELECT user_id, sum(count) AS cnt, max(day) AS last_dt + FROM analytics.messages_daily GROUP BY user_id + ) AS msg ON msg.user_id = tg.id_user LEFT JOIN ( SELECT id_user, sum(amount) AS balance FROM bot.money GROUP BY id_user diff --git a/common/repositories/users_repo.py b/common/repositories/users_repo.py index 18f5f7e..9dcd020 100644 --- a/common/repositories/users_repo.py +++ b/common/repositories/users_repo.py @@ -8,7 +8,7 @@ from common.db.pool import get_pool async def add_user(id_user: int, username: str, first_name: str, last_name: str) -> None: - """Создать/обновить пользователя и добавить запись о сообщении.""" + """Создать/обновить пользователя (счётчик сообщений ведёт аналитика).""" async with get_pool().connection() as conn, conn.cursor() as cur: await cur.execute( "CALL bot.add_user(%s, %s, %s, %s)", diff --git a/common/version.py b/common/version.py index e2b7506..8239d83 100644 --- a/common/version.py +++ b/common/version.py @@ -7,4 +7,4 @@ from __future__ import annotations -__version__ = "0.7.0" +__version__ = "0.7.1" diff --git a/docs/events.md b/docs/events.md index 40c761f..d50cd0a 100644 --- a/docs/events.md +++ b/docs/events.md @@ -21,6 +21,8 @@ DDL — `sql/V26__analytics_schema.sql`. поэтому вместо строки на каждое сообщение растёт только счётчик за день. Сообщения **не** пишутся в `analytics.events` отдельным событием `message`. +Построчная таблица `bot.messages` удалена миграцией `V27`: старые строки один раз +пересчитаны в `analytics.messages_daily`, новые пишутся только счётчиком. ## Хранение diff --git a/sql/R__functions_stats.sql b/sql/R__functions_stats.sql index 7246c5f..c66e82f 100644 --- a/sql/R__functions_stats.sql +++ b/sql/R__functions_stats.sql @@ -6,10 +6,10 @@ CREATE OR REPLACE FUNCTION bot.get_top_list() AS $$ BEGIN RETURN QUERY - SELECT tu.username, du.nick, count(*) AS cnt - FROM bot.messages m - INNER JOIN bot.tg_users tu ON m.id_user = tu.id_user - INNER JOIN bot.de_users du ON m.id_user = du.id_user + SELECT tu.username, du.nick, sum(m.count)::bigint AS cnt + FROM analytics.messages_daily m + INNER JOIN bot.tg_users tu ON m.user_id = tu.id_user + INNER JOIN bot.de_users du ON m.user_id = du.id_user GROUP BY tu.username, du.nick ORDER BY 3 DESC LIMIT 10; diff --git a/sql/R__functions_users.sql b/sql/R__functions_users.sql index 13d309f..7d44da4 100644 --- a/sql/R__functions_users.sql +++ b/sql/R__functions_users.sql @@ -75,15 +75,15 @@ BEGIN LEFT JOIN bot.de_users de ON tg.id_user = de.id_user LEFT JOIN bot.titles t ON de.id_title = t.id_title LEFT JOIN ( - SELECT id_user, count(*) AS msg_count - FROM bot.messages - GROUP BY id_user - ) m ON tg.id_user = m.id_user + SELECT user_id, sum(count)::bigint AS msg_count + FROM analytics.messages_daily + GROUP BY user_id + ) m ON tg.id_user = m.user_id WHERE tg.id_user = p_id_user; END; $$; --- Создать/обновить пользователя и записать сообщение. +-- Создать/обновить пользователя. CREATE OR REPLACE PROCEDURE bot.add_user( IN p_id_user bigint, IN p_username character varying, @@ -102,8 +102,6 @@ BEGIN INSERT INTO bot.de_users (id_user, nick) VALUES (p_id_user, NULL) ON CONFLICT (id_user) DO NOTHING; - - INSERT INTO bot.messages (id_user) VALUES (p_id_user); END; $$; @@ -127,7 +125,7 @@ DECLARE BEGIN DELETE FROM bot.tg_users WHERE id_user = p_id_user; DELETE FROM bot.de_users WHERE id_user = p_id_user; - DELETE FROM bot.messages WHERE id_user = p_id_user; + DELETE FROM analytics.messages_daily WHERE user_id = p_id_user; DELETE FROM bot.purifications WHERE id_user = p_id_user; v_money := bot.get_money(p_id_user); diff --git a/sql/V27__drop_message_rows.sql b/sql/V27__drop_message_rows.sql new file mode 100644 index 0000000..2f8dba3 --- /dev/null +++ b/sql/V27__drop_message_rows.sql @@ -0,0 +1,51 @@ +-- Переход на дневные счётчики: переносим старые построчные сообщения +-- в analytics.messages_daily и удаляем журнал bot.messages. +-- +-- Для старых строк chat_id неизвестен (в bot.messages его не было), поэтому +-- пишем 0. Чтение группирует по (day, user_id) и суммирует count, так что +-- старые и новые записи одного дня не дублируются в интерфейсе. +DO $$ +DECLARE + min_day date; + max_day date; + d date; +BEGIN + SELECT min((dt AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Moscow')::date), + max((dt AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Moscow')::date) + INTO min_day, max_day + FROM bot.messages + WHERE dt IS NOT NULL; + + IF min_day IS NULL THEN + RETURN; + END IF; + + -- Партиции на все исторические дни, иначе вставка ниже не найдёт партицию. + FOR d IN + SELECT min_day + i + FROM generate_series(0, max_day - min_day) AS i + LOOP + EXECUTE format( + 'CREATE TABLE IF NOT EXISTS analytics.messages_daily_%s ' + 'PARTITION OF analytics.messages_daily ' + 'FOR VALUES FROM (%L) TO (%L)', + to_char(d, 'YYYYMMDD'), d, d + 1 + ); + END LOOP; +END $$; + +INSERT INTO analytics.messages_daily (day, user_id, username, chat_id, count) +SELECT (m.dt AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Moscow')::date AS day, + m.id_user, + coalesce(tg.username, '') AS username, + 0 AS chat_id, + count(*) AS count +FROM bot.messages AS m + LEFT JOIN bot.tg_users AS tg ON tg.id_user = m.id_user +WHERE m.dt IS NOT NULL +GROUP BY 1, 2, 3 +ON CONFLICT (day, user_id, chat_id) DO UPDATE +SET count = analytics.messages_daily.count + EXCLUDED.count, + username = EXCLUDED.username; + +DROP TABLE IF EXISTS bot.messages; diff --git a/tests/test_version.py b/tests/test_version.py index 9d434b6..4581ecc 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -7,4 +7,4 @@ from common import __version__ def test_version_is_set(): assert __version__ - assert __version__ == "0.7.0" + assert __version__ == "0.7.1"