[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
## [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
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.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)

View File

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