Построчный журнал сообщений больше не нужен
Счётчики сообщений теперь живут только в analytics.messages_daily: /инфо, /рейтинг и админка суммируют дневные значения, а bot.add_user больше не пишет строку на каждое сообщение. Старые строки bot.messages один раз перенесены в дневную таблицу и сама таблица удалена; в карточке пользователя «последнее сообщение» стало датой.
This commit is contained in:
parent
d7bbacb67c
commit
cf14dfa80d
|
|
@ -121,6 +121,7 @@ HttpOnly-cookie (`ADMIN_SESSION_SECRET`), все формы защищены CSR
|
||||||
- схема — `sql/V26__analytics_schema.sql`;
|
- схема — `sql/V26__analytics_schema.sql`;
|
||||||
- клиент и батчинг — `common/analytics/` (запись неблокирующая, сбой Postgres не влияет на бота);
|
- клиент и батчинг — `common/analytics/` (запись неблокирующая, сбой Postgres не влияет на бота);
|
||||||
- сообщения не пишутся поштучно: на каждое сообщение инкрементится дневной счётчик;
|
- сообщения не пишутся поштучно: на каждое сообщение инкрементится дневной счётчик;
|
||||||
|
- построчная `bot.messages` удалена (V27), старые данные перенесены в `analytics.messages_daily`;
|
||||||
- партиции создаются приложением и **не удаляются** — история хранится бессрочно;
|
- партиции создаются приложением и **не удаляются** — история хранится бессрочно;
|
||||||
- выключается флагом `ANALYTICS_ENABLED=false`.
|
- выключается флагом `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`).
|
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
||||||
- Docker-образы тегируются версией (Блок 2), события аналитики получают `bot_version`.
|
- Docker-образы тегируются версией (Блок 2), события аналитики получают `bot_version`.
|
||||||
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
<div class="text-xs uppercase tracking-wide text-slate-500">Сообщения</div>
|
<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="mt-1 text-xl font-semibold">{{ user.msg_count }}</div>
|
||||||
<div class="text-xs text-slate-500">
|
<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>
|
</div>
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,13 @@ async def fetch_messages(
|
||||||
|
|
||||||
async with get_pool().connection() as conn:
|
async with get_pool().connection() as conn:
|
||||||
cur = await conn.execute(
|
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),
|
tuple(params),
|
||||||
)
|
)
|
||||||
row = await cur.fetchone()
|
row = await cur.fetchone()
|
||||||
|
|
@ -110,9 +116,11 @@ async def fetch_messages(
|
||||||
|
|
||||||
cur = await conn.execute(
|
cur = await conn.execute(
|
||||||
f"""
|
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}
|
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
|
LIMIT %s OFFSET %s
|
||||||
""",
|
""",
|
||||||
(*params, limit, offset),
|
(*params, limit, offset),
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ _FROM = """
|
||||||
LEFT JOIN bot.de_users AS du ON du.id_user = tg.id_user
|
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 bot.titles AS t ON t.id_title = du.id_title
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT id_user, count(*) AS cnt, max(dt) AS last_dt
|
SELECT user_id, sum(count) AS cnt, max(day) AS last_dt
|
||||||
FROM bot.messages GROUP BY id_user
|
FROM analytics.messages_daily GROUP BY user_id
|
||||||
) AS msg ON msg.id_user = tg.id_user
|
) AS msg ON msg.user_id = tg.id_user
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT id_user, sum(amount) AS balance
|
SELECT id_user, sum(amount) AS balance
|
||||||
FROM bot.money GROUP BY id_user
|
FROM bot.money GROUP BY id_user
|
||||||
|
|
|
||||||
|
|
@ -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 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:
|
async with get_pool().connection() as conn, conn.cursor() as cur:
|
||||||
await cur.execute(
|
await cur.execute(
|
||||||
"CALL bot.add_user(%s, %s, %s, %s)",
|
"CALL bot.add_user(%s, %s, %s, %s)",
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__version__ = "0.7.0"
|
__version__ = "0.7.1"
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ DDL — `sql/V26__analytics_schema.sql`.
|
||||||
поэтому вместо строки на каждое сообщение растёт только счётчик за день.
|
поэтому вместо строки на каждое сообщение растёт только счётчик за день.
|
||||||
|
|
||||||
Сообщения **не** пишутся в `analytics.events` отдельным событием `message`.
|
Сообщения **не** пишутся в `analytics.events` отдельным событием `message`.
|
||||||
|
Построчная таблица `bot.messages` удалена миграцией `V27`: старые строки один раз
|
||||||
|
пересчитаны в `analytics.messages_daily`, новые пишутся только счётчиком.
|
||||||
|
|
||||||
## Хранение
|
## Хранение
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ CREATE OR REPLACE FUNCTION bot.get_top_list()
|
||||||
AS $$
|
AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
RETURN QUERY
|
RETURN QUERY
|
||||||
SELECT tu.username, du.nick, count(*) AS cnt
|
SELECT tu.username, du.nick, sum(m.count)::bigint AS cnt
|
||||||
FROM bot.messages m
|
FROM analytics.messages_daily m
|
||||||
INNER JOIN bot.tg_users tu ON m.id_user = tu.id_user
|
INNER JOIN bot.tg_users tu ON m.user_id = tu.id_user
|
||||||
INNER JOIN bot.de_users du ON m.id_user = du.id_user
|
INNER JOIN bot.de_users du ON m.user_id = du.id_user
|
||||||
GROUP BY tu.username, du.nick
|
GROUP BY tu.username, du.nick
|
||||||
ORDER BY 3 DESC
|
ORDER BY 3 DESC
|
||||||
LIMIT 10;
|
LIMIT 10;
|
||||||
|
|
|
||||||
|
|
@ -75,15 +75,15 @@ BEGIN
|
||||||
LEFT JOIN bot.de_users de ON tg.id_user = de.id_user
|
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 bot.titles t ON de.id_title = t.id_title
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT id_user, count(*) AS msg_count
|
SELECT user_id, sum(count)::bigint AS msg_count
|
||||||
FROM bot.messages
|
FROM analytics.messages_daily
|
||||||
GROUP BY id_user
|
GROUP BY user_id
|
||||||
) m ON tg.id_user = m.id_user
|
) m ON tg.id_user = m.user_id
|
||||||
WHERE tg.id_user = p_id_user;
|
WHERE tg.id_user = p_id_user;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- Создать/обновить пользователя и записать сообщение.
|
-- Создать/обновить пользователя.
|
||||||
CREATE OR REPLACE PROCEDURE bot.add_user(
|
CREATE OR REPLACE PROCEDURE bot.add_user(
|
||||||
IN p_id_user bigint,
|
IN p_id_user bigint,
|
||||||
IN p_username character varying,
|
IN p_username character varying,
|
||||||
|
|
@ -102,8 +102,6 @@ BEGIN
|
||||||
INSERT INTO bot.de_users (id_user, nick)
|
INSERT INTO bot.de_users (id_user, nick)
|
||||||
VALUES (p_id_user, NULL)
|
VALUES (p_id_user, NULL)
|
||||||
ON CONFLICT (id_user) DO NOTHING;
|
ON CONFLICT (id_user) DO NOTHING;
|
||||||
|
|
||||||
INSERT INTO bot.messages (id_user) VALUES (p_id_user);
|
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|
@ -127,7 +125,7 @@ DECLARE
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM bot.tg_users WHERE id_user = p_id_user;
|
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.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;
|
DELETE FROM bot.purifications WHERE id_user = p_id_user;
|
||||||
|
|
||||||
v_money := bot.get_money(p_id_user);
|
v_money := bot.get_money(p_id_user);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -7,4 +7,4 @@ from common import __version__
|
||||||
|
|
||||||
def test_version_is_set():
|
def test_version_is_set():
|
||||||
assert __version__
|
assert __version__
|
||||||
assert __version__ == "0.7.0"
|
assert __version__ == "0.7.1"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue