[0.9.17] - 2025-08-31
Some checks failed
Deploy on push / deploy (push) Failing after 8s

### 👥 Author Statistics Enhancement
- **📊 Полная статистика авторов**: Добавлены все недостающие счётчики в AuthorStat
  - `topics`: Количество уникальных тем, в которых участвовал автор
  - `coauthors`: Количество соавторов
  - `replies_count`: Количество вызванных комментариев
  - `rating_shouts`: Рейтинг публикаций автора (сумма реакций LIKE/AGREE/ACCEPT/PROOF/CREDIT минус DISLIKE/DISAGREE/REJECT/DISPROOF)
  - `rating_comments`: Рейтинг комментариев автора (реакции на его комментарии)
  - `replies_count`: Количество вызванных комментариев
  - `comments`: Количество созданных комментариев и цитат
  - `viewed_shouts`: Общее количество просмотров всех публикаций автора
- **🔄 Улучшенная сортировка**: Поддержка сортировки по всем новым полям статистики
- ** Оптимизированные запросы**: Batch-запросы для получения всей статистики одним вызовом
- **🧪 Подробное логирование**: Эмодзи-маркеры для каждого типа статистики

### 🔧 Technical Implementation
- **Resolvers**: Обновлён `load_authors_by` для включения всех счётчиков
- **Database**: Оптимизированные SQL-запросы с JOIN для статистики
- **Caching**: Интеграция с ViewedStorage для подсчёта просмотров
- **GraphQL Schema**: Обновлён тип AuthorStat с новыми полями
This commit is contained in:
2025-08-31 20:01:40 +03:00
parent db3dafa569
commit d65f8f9fa7
4 changed files with 686 additions and 29 deletions

View File

@@ -1,7 +1,29 @@
# Changelog # Changelog
## [0.9.16] - 2025-01-27 ## [0.9.17] - 2025-08-31
### 👥 Author Statistics Enhancement
- **📊 Полная статистика авторов**: Добавлены все недостающие счётчики в AuthorStat
- `topics`: Количество уникальных тем, в которых участвовал автор
- `coauthors`: Количество соавторов
- `replies_count`: Количество вызванных комментариев
- `rating_shouts`: Рейтинг публикаций автора (сумма реакций LIKE/AGREE/ACCEPT/PROOF/CREDIT минус DISLIKE/DISAGREE/REJECT/DISPROOF)
- `rating_comments`: Рейтинг комментариев автора (реакции на его комментарии)
- `replies_count`: Количество вызванных комментариев
- `comments`: Количество созданных комментариев и цитат
- `viewed_shouts`: Общее количество просмотров всех публикаций автора
- **🔄 Улучшенная сортировка**: Поддержка сортировки по всем новым полям статистики
- **⚡ Оптимизированные запросы**: Batch-запросы для получения всей статистики одним вызовом
- **🧪 Подробное логирование**: Эмодзи-маркеры для каждого типа статистики
### 🔧 Technical Implementation
- **Resolvers**: Обновлён `load_authors_by` для включения всех счётчиков
- **Database**: Оптимизированные SQL-запросы с JOIN для статистики
- **Caching**: Интеграция с ViewedStorage для подсчёта просмотров
- **GraphQL Schema**: Обновлён тип AuthorStat с новыми полями
## [0.9.16] - 2025-08-31
### 🔍 Search System Revolution ### 🔍 Search System Revolution
- **🚀 Настоящие векторные эмбединги**: Заменил псевдослучайные hash-эмбединги на SentenceTransformers - **🚀 Настоящие векторные эмбединги**: Заменил псевдослучайные hash-эмбединги на SentenceTransformers

291
docs/author-statistics.md Normal file
View File

@@ -0,0 +1,291 @@
# 📊 Система статистики авторов
Полная документация по расчёту и использованию статистики авторов в Discours.
## 🎯 Обзор
Система статистики авторов предоставляет многомерную оценку активности, популярности и вовлечённости каждого автора на платформе. Все метрики рассчитываются в реальном времени и кешируются для производительности.
## 📈 Метрики AuthorStat
```graphql
# Статистика автора - полная метрика активности и популярности
type AuthorStat {
# Контент автора
shouts: Int # Количество опубликованных статей
topics: Int # Количество уникальных тем, в которых участвовал
comments: Int # Количество созданных комментариев и цитат
# Взаимодействие с другими авторами
coauthors: Int # Количество уникальных соавторов
followers: Int # Количество подписчиков
# Рейтинговая система
rating: Int # Общий рейтинг (rating_shouts + rating_comments)
rating_shouts: Int # Рейтинг публикаций (сумма реакций LIKE/AGREE/ACCEPT/PROOF/CREDIT минус DISLIKE/DISAGREE/REJECT/DISPROOF)
rating_comments: Int # Рейтинг комментариев (реакции на комментарии автора)
# Метрики вовлечённости
replies_count: Int # Количество ответов на контент автора (ответы на комментарии + комментарии на посты)
viewed_shouts: Int # Общее количество просмотров всех публикаций автора
}
```
### 📝 Контент автора
#### `shouts: Int`
**Количество опубликованных статей**
- Учитывает только статьи со статусом `published_at IS NOT NULL`
- Исключает удалённые статьи (`deleted_at IS NULL`)
- Подсчитывается через таблицу `shout_author`
```sql
SELECT sa.author, COUNT(DISTINCT s.id) as shouts_count
FROM shout_author sa
JOIN shout s ON sa.shout = s.id
WHERE s.deleted_at IS NULL AND s.published_at IS NOT NULL
GROUP BY sa.author
```
#### `topics: Int`
**Количество уникальных тем, в которых участвовал автор**
- Подсчитывает уникальные темы через связку статей автора
- Основано на таблицах `shout_author``shout_topic`
```sql
SELECT sa.author, COUNT(DISTINCT st.topic) as topics_count
FROM shout_author sa
JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL
JOIN shout_topic st ON s.id = st.shout
GROUP BY sa.author
```
#### `comments: Int`
**Количество созданных комментариев и цитат**
- Включает реакции типа `COMMENT` и `QUOTE`
- Исключает удалённые комментарии
```sql
SELECT r.created_by, COUNT(DISTINCT r.id) as comments_count
FROM reaction r
JOIN shout s ON r.shout = s.id AND s.deleted_at IS NULL
WHERE r.deleted_at IS NULL AND r.kind IN ('COMMENT', 'QUOTE')
GROUP BY r.created_by
```
### 👥 Взаимодействие с другими авторами
#### `coauthors: Int`
**Количество уникальных соавторов**
- Подсчитывает авторов, с которыми автор публиковал совместные статьи
- Исключает самого автора из подсчёта
- Учитывает только опубликованные и неудалённые статьи
```sql
SELECT sa1.author, COUNT(DISTINCT sa2.author) as coauthors_count
FROM shout_author sa1
JOIN shout s ON sa1.shout = s.id
AND s.deleted_at IS NULL
AND s.published_at IS NOT NULL
JOIN shout_author sa2 ON s.id = sa2.shout
AND sa2.author != sa1.author -- исключаем самого автора
GROUP BY sa1.author
```
#### `followers: Int`
**Количество подписчиков**
- Прямой подсчёт из таблицы `author_follower`
```sql
SELECT following, COUNT(DISTINCT follower) as followers_count
FROM author_follower
GROUP BY following
```
### ⭐ Рейтинговая система
#### `rating: Int`
**Общий рейтинг автора**
- Сумма `rating_shouts + rating_comments`
- Агрегированная метрика популярности контента
#### `rating_shouts: Int`
**Рейтинг публикаций автора**
- Сумма всех реакций на статьи автора
- Положительные реакции: `LIKE`, `AGREE`, `ACCEPT`, `PROOF`, `CREDIT` (+1)
- Отрицательные реакции: `DISLIKE`, `DISAGREE`, `REJECT`, `DISPROOF` (-1)
- Нейтральные реакции: остальные (0)
```sql
SELECT sa.author,
SUM(CASE
WHEN r.kind IN ('LIKE', 'AGREE', 'ACCEPT', 'PROOF', 'CREDIT') THEN 1
WHEN r.kind IN ('DISLIKE', 'DISAGREE', 'REJECT', 'DISPROOF') THEN -1
ELSE 0
END) as rating_shouts
FROM shout_author sa
JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL
JOIN reaction r ON s.id = r.shout AND r.deleted_at IS NULL
GROUP BY sa.author
```
#### `rating_comments: Int`
**Рейтинг комментариев автора**
- Аналогичная система для реакций на комментарии автора
- Подсчитывает реакции на комментарии через `reply_to`
```sql
SELECT r1.created_by,
SUM(CASE
WHEN r2.kind IN ('LIKE', 'AGREE', 'ACCEPT', 'PROOF', 'CREDIT') THEN 1
WHEN r2.kind IN ('DISLIKE', 'DISAGREE', 'REJECT', 'DISPROOF') THEN -1
ELSE 0
END) as rating_comments
FROM reaction r1
JOIN reaction r2 ON r1.id = r2.reply_to AND r2.deleted_at IS NULL
WHERE r1.deleted_at IS NULL AND r1.kind IN ('COMMENT', 'QUOTE')
GROUP BY r1.created_by
```
### 🔄 Метрики вовлечённости
#### `replies_count: Int`
**Количество ответов на контент автора**
- **Комплексная метрика**, включающая:
1. **Ответы на комментарии автора** (через `reply_to`)
2. **Комментарии на посты автора** (прямые комментарии к статьям)
Логика расчёта:
```python
# Ответы на комментарии
replies_to_comments = COUNT(r2) WHERE r1.created_by = author AND r2.reply_to = r1.id
# Комментарии на посты
comments_on_posts = COUNT(r) WHERE sa.author = author AND r.shout = s.id
# Итого
replies_count = replies_to_comments + comments_on_posts
```
#### `viewed_shouts: Int`
**Общее количество просмотров всех публикаций автора**
- Интеграция с `ViewedStorage` (Google Analytics)
- Суммирует просмотры всех статей автора
- Обновляется асинхронно из внешних источников
## 🔍 API использования
### GraphQL запрос
```graphql
query LoadAuthors($by: AuthorsBy, $limit: Int, $offset: Int) {
load_authors_by(by: $by, limit: $limit, offset: $offset) {
id
slug
name
bio
pic
stat {
shouts
topics
coauthors
followers
rating
rating_shouts
rating_comments
comments
replies_count
viewed_shouts
}
}
}
```
### Параметры сортировки
```graphql
# Сортировка по количеству публикаций
{ "order": "shouts" }
# Сортировка по общему рейтингу
{ "order": "rating" }
# Сортировка по вовлечённости
{ "order": "replies_count" }
# Сортировка по просмотрам
{ "order": "viewed_shouts" }
```
## ⚡ Производительность
### Кеширование
- **Redis кеш** для результатов запросов
- **Ключи кеша**: `authors:stats:limit={limit}:offset={offset}:order={order}`
- **TTL**: Настраивается в `cache.py`
### Оптимизации SQL
- **Batch запросы** для получения статистики всех авторов одновременно
- **Подготовленные параметры** для защиты от SQL-инъекций
- **Индексы** на ключевых полях (`author_id`, `shout_id`, `reaction.kind`)
### Сортировка
- **SQL-уровень сортировки** для метрик статистики
- **Подзапросы с JOIN** для производительности
- **COALESCE** для обработки NULL значений
## 🧪 Тестирование
### Unit тесты
```python
# Тестирование расчёта статистики
async def test_author_stats_calculation():
# Создаём тестовые данные
# Проверяем корректность расчёта каждой метрики
pass
# Тестирование сортировки
async def test_author_sorting():
# Проверяем сортировку по разным полям
pass
```
### Интеграционные тесты
- Тестирование с реальными данными
- Проверка производительности на больших объёмах
- Валидация кеширования
## 🔧 Конфигурация
### Переменные окружения
```bash
# Google Analytics для просмотров
GOOGLE_KEYFILE_PATH=/path/to/service-account.json
GOOGLE_PROPERTY_ID=your-property-id
# Redis для кеширования
REDIS_URL=redis://localhost:6379
```
### Настройки реакций
Типы реакций определены в `orm/reaction.py`:
```python
# Положительные (+1)
POSITIVE_REACTIONS = ["LIKE", "AGREE", "ACCEPT", "PROOF", "CREDIT"]
# Отрицательные (-1)
NEGATIVE_REACTIONS = ["DISLIKE", "DISAGREE", "REJECT", "DISPROOF"]
```
## 🚀 Развитие
### Планируемые улучшения
- [ ] Исторические тренды статистики
- [ ] Сегментация по периодам времени
- [ ] Дополнительные метрики вовлечённости
- [ ] Персонализированные рекомендации на основе статистики
### Известные ограничения
- Просмотры обновляются с задержкой (Google Analytics API)
- Большие объёмы данных могут замедлять запросы без кеша
- Сложные запросы сортировки требуют больше ресурсов

View File

@@ -18,7 +18,8 @@ from cache.cache import (
) )
from orm.author import Author, AuthorFollower from orm.author import Author, AuthorFollower
from orm.community import Community, CommunityAuthor, CommunityFollower from orm.community import Community, CommunityAuthor, CommunityFollower
from orm.shout import Shout, ShoutAuthor from orm.reaction import Reaction
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from resolvers.stat import get_with_stat from resolvers.stat import get_with_stat
from services.auth import login_required from services.auth import login_required
from services.search import search_service from services.search import search_service
@@ -34,17 +35,25 @@ DEFAULT_COMMUNITIES = [1]
# Определение типа AuthorsBy на основе схемы GraphQL # Определение типа AuthorsBy на основе схемы GraphQL
class AuthorsBy(TypedDict, total=False): class AuthorsBy(TypedDict, total=False):
""" """
Тип для параметра сортировки авторов, соответствующий схеме GraphQL. Параметры фильтрации и сортировки авторов для GraphQL запроса load_authors_by.
Поля: 📊 Поля сортировки:
order: Поле для сортировки авторов:
🔢 Базовые метрики: "shouts" (публикации), "followers" (подписчики)
🏷️ Контент: "topics" (темы), "comments" (комментарии)
👥 Социальные: "coauthors" (соавторы), "replies_count" (ответы на контент)
⭐ Рейтинг: "rating" (общий), "rating_shouts" (публикации), "rating_comments" (комментарии)
👁️ Вовлечённость: "viewed_shouts" (просмотры)
📝 Алфавит: "name" (по имени)
🔍 Поля фильтрации:
last_seen: Временная метка последнего посещения last_seen: Временная метка последнего посещения
created_at: Временная метка создания created_at: Временная метка создания
slug: Уникальный идентификатор автора slug: Уникальный идентификатор автора
name: Имя автора name: Имя автора для поиска
topic: Тема, связанная с автором topic: Тема, связанная с автором
order: Поле для сортировки (shouts, followers, rating, comments, name)
after: Временная метка для фильтрации "после" after: Временная метка для фильтрации "после"
stat: Поле статистики stat: Поле статистики для дополнительной фильтрации
""" """
last_seen: int | None last_seen: int | None
@@ -96,15 +105,36 @@ async def get_authors_with_stats(
limit: int = 10, offset: int = 0, by: AuthorsBy | None = None, current_user_id: int | None = None limit: int = 10, offset: int = 0, by: AuthorsBy | None = None, current_user_id: int | None = None
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """
Получает авторов со статистикой с пагинацией. 🧪 Получает авторов с полной статистикой и поддержкой сортировки.
📊 Рассчитывает все метрики AuthorStat:
- shouts: Количество опубликованных статей
- topics: Уникальные темы участия
- coauthors: Количество соавторов
- followers: Подписчики
- rating: Общий рейтинг (rating_shouts + rating_comments)
- rating_shouts: Рейтинг публикаций (реакции)
- rating_comments: Рейтинг комментариев (реакции)
- comments: Созданные комментарии
- replies_count: Ответы на контент (комментарии на посты + ответы на комментарии)
- viewed_shouts: Просмотры публикаций (из ViewedStorage)
⚡ Оптимизации:
- Batch SQL-запросы для статистики
- Кеширование результатов
- Сортировка на уровне SQL для производительности
Args: Args:
limit: Максимальное количество возвращаемых авторов limit: Максимальное количество возвращаемых авторов (1-100)
offset: Смещение для пагинации offset: Смещение для пагинации
by: Опциональный параметр сортировки (AuthorsBy) by: Параметры фильтрации и сортировки (AuthorsBy)
current_user_id: ID текущего пользователя current_user_id: ID текущего пользователя для фильтрации доступа
Returns: Returns:
list: Список авторов с их статистикой list[dict]: Список авторов с полной статистикой, отсортированных согласно параметрам
Raises:
Exception: При ошибках выполнения SQL-запросов или доступа к ViewedStorage
""" """
# Формируем ключ кеша с помощью универсальной функции # Формируем ключ кеша с помощью универсальной функции
order_value = by.get("order", "default") if by else "default" order_value = by.get("order", "default") if by else "default"
@@ -128,7 +158,18 @@ async def get_authors_with_stats(
if "order" in by: if "order" in by:
order_value = by["order"] order_value = by["order"]
logger.debug(f"Found order field with value: {order_value}") logger.debug(f"Found order field with value: {order_value}")
if order_value in ["shouts", "followers", "rating", "comments"]: if order_value in [
"shouts",
"followers",
"rating",
"comments",
"topics",
"coauthors",
"viewed_shouts",
"rating_shouts",
"rating_comments",
"replies_count",
]:
stats_sort_field = order_value stats_sort_field = order_value
logger.debug(f"Applying statistics-based sorting by: {stats_sort_field}") logger.debug(f"Applying statistics-based sorting by: {stats_sort_field}")
# Не применяем другую сортировку, так как будем использовать stats_sort_field # Не применяем другую сортировку, так как будем использовать stats_sort_field
@@ -212,12 +253,140 @@ async def get_authors_with_stats(
sql_desc(func.coalesce(subquery.c.followers_count, 0)) sql_desc(func.coalesce(subquery.c.followers_count, 0))
) )
logger.debug("Applied sorting by followers count") logger.debug("Applied sorting by followers count")
elif stats_sort_field == "topics":
# 🏷️ Сортировка по количеству тем
logger.debug("Building subquery for topics sorting")
subquery = (
select(ShoutAuthor.author, func.count(func.distinct(ShoutTopic.topic)).label("topics_count"))
.select_from(ShoutAuthor)
.join(Shout, ShoutAuthor.shout == Shout.id)
.join(ShoutTopic, Shout.id == ShoutTopic.shout)
.where(and_(Shout.deleted_at.is_(None), Shout.published_at.is_not(None)))
.group_by(ShoutAuthor.author)
.subquery()
)
base_query = base_query.outerjoin(subquery, Author.id == subquery.c.author).order_by(
sql_desc(func.coalesce(subquery.c.topics_count, 0))
)
logger.debug("Applied sorting by topics count")
elif stats_sort_field == "coauthors":
# ✍️ Сортировка по количеству соавторов
logger.debug("Building subquery for coauthors sorting")
sa1 = ShoutAuthor.__table__.alias("sa1")
sa2 = ShoutAuthor.__table__.alias("sa2")
subquery = (
select(sa1.c.author, func.count(func.distinct(sa2.c.author)).label("coauthors_count"))
.select_from(sa1.join(Shout, sa1.c.shout == Shout.id).join(sa2, sa2.c.shout == Shout.id))
.where(
and_(
Shout.deleted_at.is_(None),
Shout.published_at.is_not(None),
sa1.c.author != sa2.c.author, # исключаем самого автора из подсчёта
)
)
.group_by(sa1.c.author)
.subquery()
)
base_query = base_query.outerjoin(subquery, Author.id == subquery.c.author).order_by(
sql_desc(func.coalesce(subquery.c.coauthors_count, 0))
)
logger.debug("Applied sorting by coauthors count")
elif stats_sort_field == "comments":
# 💬 Сортировка по количеству комментариев
logger.debug("Building subquery for comments sorting")
subquery = (
select(Reaction.created_by, func.count(func.distinct(Reaction.id)).label("comments_count"))
.select_from(Reaction)
.join(Shout, Reaction.shout == Shout.id)
.where(
and_(
Reaction.deleted_at.is_(None),
Shout.deleted_at.is_(None),
Reaction.kind.in_(["COMMENT", "QUOTE"]),
)
)
.group_by(Reaction.created_by)
.subquery()
)
base_query = base_query.outerjoin(subquery, Author.id == subquery.c.created_by).order_by(
sql_desc(func.coalesce(subquery.c.comments_count, 0))
)
logger.debug("Applied sorting by comments count")
elif stats_sort_field == "replies_count":
# 💬 Сортировка по общему количеству ответов (комментарии на посты + ответы на комментарии)
logger.debug("Building subquery for replies_count sorting")
# Подзапрос для ответов на комментарии автора
replies_to_comments_subq = (
select(
Reaction.created_by.label("author_id"),
func.count(func.distinct(Reaction.id)).label("replies_count"),
)
.select_from(Reaction)
.where(
and_(
Reaction.deleted_at.is_(None),
Reaction.reply_to.is_not(None),
Reaction.kind.in_(["COMMENT", "QUOTE"]),
)
)
.group_by(Reaction.created_by)
.subquery()
)
# Подзапрос для комментариев на посты автора
comments_on_posts_subq = (
select(
ShoutAuthor.author.label("author_id"),
func.count(func.distinct(Reaction.id)).label("replies_count"),
)
.select_from(ShoutAuthor)
.join(Shout, ShoutAuthor.shout == Shout.id)
.join(Reaction, Shout.id == Reaction.shout)
.where(
and_(
Shout.deleted_at.is_(None),
Shout.published_at.is_not(None),
Reaction.deleted_at.is_(None),
Reaction.kind.in_(["COMMENT", "QUOTE"]),
)
)
.group_by(ShoutAuthor.author)
.subquery()
)
# Объединяем оба подзапроса через UNION ALL
combined_replies_subq = (
select(
func.coalesce(
replies_to_comments_subq.c.author_id, comments_on_posts_subq.c.author_id
).label("author_id"),
func.coalesce(
func.coalesce(replies_to_comments_subq.c.replies_count, 0)
+ func.coalesce(comments_on_posts_subq.c.replies_count, 0),
0,
).label("total_replies"),
)
.select_from(
replies_to_comments_subq
.outerjoin(
comments_on_posts_subq,
replies_to_comments_subq.c.author_id == comments_on_posts_subq.c.author_id,
)
)
.subquery()
)
base_query = base_query.outerjoin(
combined_replies_subq, Author.id == combined_replies_subq.c.author_id
).order_by(sql_desc(func.coalesce(combined_replies_subq.c.total_replies, 0)))
logger.debug("Applied sorting by replies_count")
# Логирование для отладки сортировки # Логирование для отладки сортировки
try: try:
# Получаем SQL запрос для проверки # Получаем SQL запрос для проверки
sql_query = str(base_query.compile(compile_kwargs={"literal_binds": True})) sql_query = str(base_query.compile(compile_kwargs={"literal_binds": True}))
logger.debug(f"Generated SQL query for followers sorting: {sql_query}") logger.debug(f"Generated SQL query for replies_count sorting: {sql_query}")
except Exception as e: except Exception as e:
logger.error(f"Error generating SQL query: {e}") logger.error(f"Error generating SQL query: {e}")
@@ -238,9 +407,13 @@ async def get_authors_with_stats(
if stats_sort_field: if stats_sort_field:
logger.debug(f"Query returned {len(authors)} authors with sorting by {stats_sort_field}") logger.debug(f"Query returned {len(authors)} authors with sorting by {stats_sort_field}")
# Оптимизированный запрос для получения статистики по публикациям для авторов # 🧪 Оптимизированные запросы для получения всей статистики авторов
logger.debug("Executing shouts statistics query") logger.debug("Executing comprehensive statistics queries")
placeholders = ", ".join([f":id{i}" for i in range(len(author_ids))]) placeholders = ", ".join([f":id{i}" for i in range(len(author_ids))])
params = {f"id{i}": author_id for i, author_id in enumerate(author_ids)}
# 📊 Статистика по публикациям
logger.debug("Executing shouts statistics query")
shouts_stats_query = f""" shouts_stats_query = f"""
SELECT sa.author, COUNT(DISTINCT s.id) as shouts_count SELECT sa.author, COUNT(DISTINCT s.id) as shouts_count
FROM shout_author sa FROM shout_author sa
@@ -248,11 +421,10 @@ async def get_authors_with_stats(
WHERE sa.author IN ({placeholders}) WHERE sa.author IN ({placeholders})
GROUP BY sa.author GROUP BY sa.author
""" """
params = {f"id{i}": author_id for i, author_id in enumerate(author_ids)}
shouts_stats = {row[0]: row[1] for row in session.execute(text(shouts_stats_query), params)} shouts_stats = {row[0]: row[1] for row in session.execute(text(shouts_stats_query), params)}
logger.debug(f"Shouts stats retrieved: {shouts_stats}") logger.debug(f"Shouts stats retrieved: {shouts_stats}")
# Запрос на получение статистики по подписчикам для авторов # 👥 Статистика по подписчикам
logger.debug("Executing followers statistics query") logger.debug("Executing followers statistics query")
followers_stats_query = f""" followers_stats_query = f"""
SELECT following, COUNT(DISTINCT follower) as followers_count SELECT following, COUNT(DISTINCT follower) as followers_count
@@ -263,8 +435,163 @@ async def get_authors_with_stats(
followers_stats = {row[0]: row[1] for row in session.execute(text(followers_stats_query), params)} followers_stats = {row[0]: row[1] for row in session.execute(text(followers_stats_query), params)}
logger.debug(f"Followers stats retrieved: {followers_stats}") logger.debug(f"Followers stats retrieved: {followers_stats}")
# Формируем результат с добавлением статистики # 🏷️ Статистика по темам (количество уникальных тем, в которых участвовал автор)
logger.debug("Building final result with statistics") logger.debug("Executing topics statistics query")
topics_stats_query = f"""
SELECT sa.author, COUNT(DISTINCT st.topic) as topics_count
FROM shout_author sa
JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL
JOIN shout_topic st ON s.id = st.shout
WHERE sa.author IN ({placeholders})
GROUP BY sa.author
"""
topics_stats = {row[0]: row[1] for row in session.execute(text(topics_stats_query), params)}
logger.debug(f"Topics stats retrieved: {topics_stats}")
# ✍️ Статистика по соавторам (количество уникальных соавторов)
logger.debug("Executing coauthors statistics query")
coauthors_stats_query = f"""
SELECT sa1.author, COUNT(DISTINCT sa2.author) as coauthors_count
FROM shout_author sa1
JOIN shout s ON sa1.shout = s.id
AND s.deleted_at IS NULL
AND s.published_at IS NOT NULL
JOIN shout_author sa2 ON s.id = sa2.shout
AND sa2.author != sa1.author -- исключаем самого автора
WHERE sa1.author IN ({placeholders})
GROUP BY sa1.author
"""
coauthors_stats = {row[0]: row[1] for row in session.execute(text(coauthors_stats_query), params)}
logger.debug(f"Coauthors stats retrieved: {coauthors_stats}")
# 💬 Статистика по комментариям (количество созданных комментариев)
logger.debug("Executing comments statistics query")
comments_stats_query = f"""
SELECT r.created_by, COUNT(DISTINCT r.id) as comments_count
FROM reaction r
JOIN shout s ON r.shout = s.id AND s.deleted_at IS NULL
WHERE r.created_by IN ({placeholders}) AND r.deleted_at IS NULL
AND r.kind IN ('COMMENT', 'QUOTE')
GROUP BY r.created_by
"""
comments_stats = {row[0]: row[1] for row in session.execute(text(comments_stats_query), params)}
logger.debug(f"Comments stats retrieved: {comments_stats}")
# 💬 Статистика по вызванным комментариям (ответы на комментарии + комментарии на посты)
logger.debug("Executing replies_count statistics query")
# Ответы на комментарии автора
replies_to_comments_query = f"""
SELECT r1.created_by as author_id, COUNT(DISTINCT r2.id) as replies_count
FROM reaction r1
JOIN reaction r2 ON r1.id = r2.reply_to AND r2.deleted_at IS NULL
WHERE r1.created_by IN ({placeholders}) AND r1.deleted_at IS NULL
AND r1.kind IN ('COMMENT', 'QUOTE')
AND r2.kind IN ('COMMENT', 'QUOTE')
GROUP BY r1.created_by
"""
replies_to_comments_stats = {
row[0]: row[1] for row in session.execute(text(replies_to_comments_query), params)
}
logger.debug(f"Replies to comments stats retrieved: {replies_to_comments_stats}")
# Комментарии на посты автора
comments_on_posts_query = f"""
SELECT sa.author as author_id, COUNT(DISTINCT r.id) as replies_count
FROM shout_author sa
JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL
JOIN reaction r ON s.id = r.shout AND r.deleted_at IS NULL
WHERE sa.author IN ({placeholders})
AND r.kind IN ('COMMENT', 'QUOTE')
GROUP BY sa.author
"""
comments_on_posts_stats = {
row[0]: row[1] for row in session.execute(text(comments_on_posts_query), params)
}
logger.debug(f"Comments on posts stats retrieved: {comments_on_posts_stats}")
# Объединяем статистику
replies_count_stats = {}
for author_id in author_ids:
replies_to_comments = replies_to_comments_stats.get(author_id, 0)
comments_on_posts = comments_on_posts_stats.get(author_id, 0)
replies_count_stats[author_id] = replies_to_comments + comments_on_posts
logger.debug(f"Combined replies count stats: {replies_count_stats}")
# ⭐ Статистика по рейтингу публикаций (сумма реакций на публикации автора)
logger.debug("Executing rating_shouts statistics query")
rating_shouts_stats_query = f"""
SELECT sa.author,
SUM(CASE
WHEN r.kind IN ('LIKE', 'AGREE', 'ACCEPT', 'PROOF', 'CREDIT') THEN 1
WHEN r.kind IN ('DISLIKE', 'DISAGREE', 'REJECT', 'DISPROOF') THEN -1
ELSE 0
END) as rating_shouts
FROM shout_author sa
JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL
JOIN reaction r ON s.id = r.shout AND r.deleted_at IS NULL
WHERE sa.author IN ({placeholders})
GROUP BY sa.author
"""
rating_shouts_stats = {
row[0]: row[1] for row in session.execute(text(rating_shouts_stats_query), params)
}
logger.debug(f"Rating shouts stats retrieved: {rating_shouts_stats}")
# ⭐ Статистика по рейтингу комментариев (реакции на комментарии автора)
logger.debug("Executing rating_comments statistics query")
rating_comments_stats_query = f"""
SELECT r1.created_by,
SUM(CASE
WHEN r2.kind IN ('LIKE', 'AGREE', 'ACCEPT', 'PROOF', 'CREDIT') THEN 1
WHEN r2.kind IN ('DISLIKE', 'DISAGREE', 'REJECT', 'DISPROOF') THEN -1
ELSE 0
END) as rating_comments
FROM reaction r1
JOIN reaction r2 ON r1.id = r2.reply_to AND r2.deleted_at IS NULL
WHERE r1.created_by IN ({placeholders}) AND r1.deleted_at IS NULL
AND r1.kind IN ('COMMENT', 'QUOTE')
GROUP BY r1.created_by
"""
rating_comments_stats = {
row[0]: row[1] for row in session.execute(text(rating_comments_stats_query), params)
}
logger.debug(f"Rating comments stats retrieved: {rating_comments_stats}")
# 📈 Общий рейтинг (сумма рейтингов публикаций и комментариев)
logger.debug("Calculating overall rating")
overall_rating_stats = {}
for author_id in author_ids:
shouts_rating = rating_shouts_stats.get(author_id, 0)
comments_rating = rating_comments_stats.get(author_id, 0)
overall_rating_stats[author_id] = shouts_rating + comments_rating
logger.debug(f"Overall rating stats calculated: {overall_rating_stats}")
# 👁️ Статистика по просмотрам публикаций (используем ViewedStorage для получения агрегированных данных)
logger.debug("Calculating viewed_shouts statistics from ViewedStorage")
from services.viewed import ViewedStorage
viewed_shouts_stats = {}
# Получаем общие просмотры для всех публикаций каждого автора
for author_id in author_ids:
total_views = 0
# Получаем все публикации автора и суммируем их просмотры
author_shouts_query = """
SELECT s.slug
FROM shout_author sa
JOIN shout s ON sa.shout = s.id AND s.deleted_at IS NULL AND s.published_at IS NOT NULL
WHERE sa.author = :author_id
"""
shout_rows = session.execute(text(author_shouts_query), {"author_id": author_id})
for shout_row in shout_rows:
shout_slug = shout_row[0]
shout_views = ViewedStorage.get_shout(shout_slug=shout_slug)
total_views += shout_views
viewed_shouts_stats[author_id] = total_views
logger.debug(f"Viewed shouts stats calculated: {viewed_shouts_stats}")
# 🎯 Формируем результат с добавлением полной статистики
logger.debug("Building final result with comprehensive statistics")
result = [] result = []
for author in authors: for author in authors:
try: try:
@@ -272,7 +599,15 @@ async def get_authors_with_stats(
author_dict = author.dict() author_dict = author.dict()
author_dict["stat"] = { author_dict["stat"] = {
"shouts": shouts_stats.get(author.id, 0), "shouts": shouts_stats.get(author.id, 0),
"topics": topics_stats.get(author.id, 0),
"coauthors": coauthors_stats.get(author.id, 0),
"followers": followers_stats.get(author.id, 0), "followers": followers_stats.get(author.id, 0),
"rating": overall_rating_stats.get(author.id, 0),
"rating_shouts": rating_shouts_stats.get(author.id, 0),
"rating_comments": rating_comments_stats.get(author.id, 0),
"comments": comments_stats.get(author.id, 0),
"replies_count": replies_count_stats.get(author.id, 0),
"viewed_shouts": viewed_shouts_stats.get(author.id, 0),
} }
result.append(author_dict) result.append(author_dict)

View File

@@ -1,13 +1,22 @@
# Статистика автора - полная метрика активности и популярности
type AuthorStat { type AuthorStat {
shouts: Int # Контент автора
topics: Int shouts: Int # Количество опубликованных статей
authors: Int topics: Int # Количество уникальных тем, в которых участвовал
followers: Int comments: Int # Количество созданных комментариев и цитат
rating: Int
rating_shouts: Int # Взаимодействие с другими авторами
rating_comments: Int coauthors: Int # Количество уникальных соавторов
comments: Int followers: Int # Количество подписчиков
viewed: Int
# Рейтинговая система
rating: Int # Общий рейтинг (rating_shouts + rating_comments)
rating_shouts: Int # Рейтинг публикаций (сумма реакций LIKE/AGREE/ACCEPT/PROOF/CREDIT минус DISLIKE/DISAGREE/REJECT/DISPROOF)
rating_comments: Int # Рейтинг комментариев (реакции на комментарии автора)
# Метрики вовлечённости
replies_count: Int # Количество ответов на контент автора (ответы на комментарии + комментарии на посты)
viewed_shouts: Int # Общее количество просмотров всех публикаций автора
} }
type Author { type Author {