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"