### 👥 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:
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,7 +1,29 @@
|
||||
# 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
|
||||
- **🚀 Настоящие векторные эмбединги**: Заменил псевдослучайные hash-эмбединги на SentenceTransformers
|
||||
|
||||
291
docs/author-statistics.md
Normal file
291
docs/author-statistics.md
Normal 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)
|
||||
- Большие объёмы данных могут замедлять запросы без кеша
|
||||
- Сложные запросы сортировки требуют больше ресурсов
|
||||
@@ -18,7 +18,8 @@ from cache.cache import (
|
||||
)
|
||||
from orm.author import Author, AuthorFollower
|
||||
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 services.auth import login_required
|
||||
from services.search import search_service
|
||||
@@ -34,17 +35,25 @@ DEFAULT_COMMUNITIES = [1]
|
||||
# Определение типа AuthorsBy на основе схемы GraphQL
|
||||
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: Временная метка последнего посещения
|
||||
created_at: Временная метка создания
|
||||
slug: Уникальный идентификатор автора
|
||||
name: Имя автора
|
||||
name: Имя автора для поиска
|
||||
topic: Тема, связанная с автором
|
||||
order: Поле для сортировки (shouts, followers, rating, comments, name)
|
||||
after: Временная метка для фильтрации "после"
|
||||
stat: Поле статистики
|
||||
stat: Поле статистики для дополнительной фильтрации
|
||||
"""
|
||||
|
||||
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
|
||||
) -> 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:
|
||||
limit: Максимальное количество возвращаемых авторов
|
||||
limit: Максимальное количество возвращаемых авторов (1-100)
|
||||
offset: Смещение для пагинации
|
||||
by: Опциональный параметр сортировки (AuthorsBy)
|
||||
current_user_id: ID текущего пользователя
|
||||
by: Параметры фильтрации и сортировки (AuthorsBy)
|
||||
current_user_id: ID текущего пользователя для фильтрации доступа
|
||||
|
||||
Returns:
|
||||
list: Список авторов с их статистикой
|
||||
list[dict]: Список авторов с полной статистикой, отсортированных согласно параметрам
|
||||
|
||||
Raises:
|
||||
Exception: При ошибках выполнения SQL-запросов или доступа к ViewedStorage
|
||||
"""
|
||||
# Формируем ключ кеша с помощью универсальной функции
|
||||
order_value = by.get("order", "default") if by else "default"
|
||||
@@ -128,7 +158,18 @@ async def get_authors_with_stats(
|
||||
if "order" in by:
|
||||
order_value = by["order"]
|
||||
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
|
||||
logger.debug(f"Applying statistics-based sorting by: {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))
|
||||
)
|
||||
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:
|
||||
# Получаем SQL запрос для проверки
|
||||
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:
|
||||
logger.error(f"Error generating SQL query: {e}")
|
||||
|
||||
@@ -238,9 +407,13 @@ async def get_authors_with_stats(
|
||||
if 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))])
|
||||
params = {f"id{i}": author_id for i, author_id in enumerate(author_ids)}
|
||||
|
||||
# 📊 Статистика по публикациям
|
||||
logger.debug("Executing shouts statistics query")
|
||||
shouts_stats_query = f"""
|
||||
SELECT sa.author, COUNT(DISTINCT s.id) as shouts_count
|
||||
FROM shout_author sa
|
||||
@@ -248,11 +421,10 @@ async def get_authors_with_stats(
|
||||
WHERE sa.author IN ({placeholders})
|
||||
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)}
|
||||
logger.debug(f"Shouts stats retrieved: {shouts_stats}")
|
||||
|
||||
# Запрос на получение статистики по подписчикам для авторов
|
||||
# 👥 Статистика по подписчикам
|
||||
logger.debug("Executing followers statistics query")
|
||||
followers_stats_query = f"""
|
||||
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)}
|
||||
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 = []
|
||||
for author in authors:
|
||||
try:
|
||||
@@ -272,7 +599,15 @@ async def get_authors_with_stats(
|
||||
author_dict = author.dict()
|
||||
author_dict["stat"] = {
|
||||
"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),
|
||||
"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)
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
# Статистика автора - полная метрика активности и популярности
|
||||
type AuthorStat {
|
||||
shouts: Int
|
||||
topics: Int
|
||||
authors: Int
|
||||
followers: Int
|
||||
rating: Int
|
||||
rating_shouts: Int
|
||||
rating_comments: Int
|
||||
comments: Int
|
||||
viewed: Int
|
||||
# Контент автора
|
||||
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 # Общее количество просмотров всех публикаций автора
|
||||
}
|
||||
|
||||
type Author {
|
||||
|
||||
Reference in New Issue
Block a user