Построчный журнал сообщений больше не нужен
CI / lint-test (push) Successful in 20s Details
CI / build-push (push) Successful in 1m6s Details

Счётчики сообщений теперь живут только в analytics.messages_daily: /инфо, /рейтинг и админка суммируют дневные значения, а bot.add_user больше не пишет строку на каждое сообщение. Старые строки bot.messages один раз перенесены в дневную таблицу и сама таблица удалена; в карточке пользователя «последнее сообщение» стало датой.
This commit is contained in:
MpakobecEx 2026-11-03 02:43:05 +05:00
parent d7bbacb67c
commit cf14dfa80d
11 changed files with 83 additions and 23 deletions

View File

@ -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`.

View File

@ -68,7 +68,7 @@
<div class="text-xs uppercase tracking-wide text-slate-500">Сообщения</div>
<div class="mt-1 text-xl font-semibold">{{ user.msg_count }}</div>
<div class="text-xs text-slate-500">
последнее: {{ 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 "—" }}
</div>
</div>
<div class="rounded-lg border border-slate-200 bg-white p-4">

View File

@ -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),

View File

@ -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

View File

@ -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)",

View File

@ -7,4 +7,4 @@
from __future__ import annotations
__version__ = "0.7.0"
__version__ = "0.7.1"

View File

@ -21,6 +21,8 @@ DDL — `sql/V26__analytics_schema.sql`.
поэтому вместо строки на каждое сообщение растёт только счётчик за день.
Сообщения **не** пишутся в `analytics.events` отдельным событием `message`.
Построчная таблица `bot.messages` удалена миграцией `V27`: старые строки один раз
пересчитаны в `analytics.messages_daily`, новые пишутся только счётчиком.
## Хранение

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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"