327 lines
13 KiB
Python
327 lines
13 KiB
Python
import time
|
||
|
||
from sqlalchemy import func, select, text
|
||
|
||
from cache.cache import (
|
||
cache_topic,
|
||
get_cached_topic_authors,
|
||
get_cached_topic_by_slug,
|
||
get_cached_topic_followers,
|
||
redis_operation,
|
||
)
|
||
from cache.memorycache import cache_region
|
||
from orm.author import Author
|
||
from orm.shout import Shout, ShoutTopic
|
||
from orm.topic import Topic, TopicFollower
|
||
from resolvers.stat import get_with_stat
|
||
from services.auth import login_required
|
||
from services.db import local_session
|
||
from services.redis import redis
|
||
from services.schema import mutation, query
|
||
from utils.logger import root_logger as logger
|
||
|
||
|
||
# Вспомогательная функция для получения всех тем без статистики
|
||
async def get_all_topics():
|
||
"""
|
||
Получает все темы без статистики.
|
||
Используется для случаев, когда нужен полный список тем без дополнительной информации.
|
||
|
||
Returns:
|
||
list: Список всех тем без статистики
|
||
"""
|
||
# Пытаемся получить данные из кеша
|
||
cached_topics = await redis_operation("GET", "topics:all:basic")
|
||
|
||
if cached_topics:
|
||
logger.debug("Используем кешированные базовые данные о темах из Redis")
|
||
try:
|
||
import json
|
||
|
||
return json.loads(cached_topics)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при десериализации тем из Redis: {e}")
|
||
|
||
# Если в кеше нет данных, выполняем запрос в БД
|
||
logger.debug("Получаем список всех тем из БД и кешируем результат")
|
||
|
||
with local_session() as session:
|
||
# Запрос на получение базовой информации о темах
|
||
topics_query = select(Topic)
|
||
topics = session.execute(topics_query).scalars().all()
|
||
|
||
# Преобразуем темы в словари
|
||
result = [topic.dict() for topic in topics]
|
||
|
||
# Кешируем результат в Redis без TTL (будет обновляться только при изменениях)
|
||
try:
|
||
import json
|
||
|
||
await redis_operation("SET", "topics:all:basic", json.dumps(result))
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при кешировании тем в Redis: {e}")
|
||
|
||
return result
|
||
|
||
|
||
# Вспомогательная функция для получения тем со статистикой с пагинацией
|
||
async def get_topics_with_stats(limit=100, offset=0, community_id=None):
|
||
"""
|
||
Получает темы со статистикой с пагинацией.
|
||
|
||
Args:
|
||
limit: Максимальное количество возвращаемых тем
|
||
offset: Смещение для пагинации
|
||
community_id: Опциональный ID сообщества для фильтрации
|
||
|
||
Returns:
|
||
list: Список тем с их статистикой
|
||
"""
|
||
# Формируем ключ кеша с учетом параметров
|
||
cache_key = f"topics:stats:limit={limit}:offset={offset}"
|
||
if community_id:
|
||
cache_key += f":community={community_id}"
|
||
|
||
# Пытаемся получить данные из кеша
|
||
cached_topics = await redis_operation("GET", cache_key)
|
||
|
||
if cached_topics:
|
||
logger.debug(f"Используем кешированные данные о темах из Redis: {cache_key}")
|
||
try:
|
||
import json
|
||
|
||
return json.loads(cached_topics)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при десериализации тем из Redis: {e}")
|
||
|
||
# Если в кеше нет данных, выполняем оптимизированный запрос
|
||
logger.debug(f"Выполняем запрос на получение тем со статистикой: limit={limit}, offset={offset}")
|
||
|
||
with local_session() as session:
|
||
# Базовый запрос для получения тем
|
||
base_query = select(Topic)
|
||
|
||
# Добавляем фильтр по сообществу, если указан
|
||
if community_id:
|
||
base_query = base_query.where(Topic.community == community_id)
|
||
|
||
# Применяем лимит и смещение
|
||
base_query = base_query.limit(limit).offset(offset)
|
||
|
||
# Получаем темы
|
||
topics = session.execute(base_query).scalars().all()
|
||
topic_ids = [topic.id for topic in topics]
|
||
|
||
if not topic_ids:
|
||
return []
|
||
|
||
# Запрос на получение статистики по публикациям для выбранных тем
|
||
shouts_stats_query = f"""
|
||
SELECT st.topic, COUNT(DISTINCT s.id) as shouts_count
|
||
FROM shout_topic st
|
||
JOIN shout s ON st.shout = s.id AND s.deleted_at IS NULL
|
||
WHERE st.topic IN ({",".join(map(str, topic_ids))})
|
||
GROUP BY st.topic
|
||
"""
|
||
shouts_stats = {row[0]: row[1] for row in session.execute(text(shouts_stats_query))}
|
||
|
||
# Запрос на получение статистики по подписчикам для выбранных тем
|
||
followers_stats_query = f"""
|
||
SELECT topic, COUNT(DISTINCT follower) as followers_count
|
||
FROM topic_followers
|
||
WHERE topic IN ({",".join(map(str, topic_ids))})
|
||
GROUP BY topic
|
||
"""
|
||
followers_stats = {row[0]: row[1] for row in session.execute(text(followers_stats_query))}
|
||
|
||
# Формируем результат с добавлением статистики
|
||
result = []
|
||
for topic in topics:
|
||
topic_dict = topic.dict()
|
||
topic_dict["stat"] = {
|
||
"shouts": shouts_stats.get(topic.id, 0),
|
||
"followers": followers_stats.get(topic.id, 0),
|
||
}
|
||
result.append(topic_dict)
|
||
|
||
# Кешируем каждую тему отдельно для использования в других функциях
|
||
await cache_topic(topic_dict)
|
||
|
||
# Кешируем полный результат в Redis без TTL (будет обновляться только при изменениях)
|
||
try:
|
||
import json
|
||
|
||
await redis_operation("SET", cache_key, json.dumps(result))
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при кешировании тем в Redis: {e}")
|
||
|
||
return result
|
||
|
||
|
||
# Функция для инвалидации кеша тем
|
||
async def invalidate_topics_cache():
|
||
"""
|
||
Инвалидирует все кеши тем при изменении данных.
|
||
"""
|
||
logger.debug("Инвалидация кеша тем")
|
||
|
||
# Получаем все ключи, начинающиеся с "topics:"
|
||
topic_keys = await redis.execute("KEYS", "topics:*")
|
||
|
||
if topic_keys:
|
||
# Удаляем все найденные ключи
|
||
await redis.execute("DEL", *topic_keys)
|
||
logger.debug(f"Удалено {len(topic_keys)} ключей кеша тем")
|
||
|
||
|
||
# Запрос на получение всех тем
|
||
@query.field("get_topics_all")
|
||
async def get_topics_all(_, _info):
|
||
"""
|
||
Получает список всех тем без статистики.
|
||
|
||
Returns:
|
||
list: Список всех тем
|
||
"""
|
||
return await get_all_topics()
|
||
|
||
|
||
# Запрос на получение тем с пагинацией и статистикой
|
||
@query.field("get_topics_paginated")
|
||
async def get_topics_paginated(_, _info, limit=100, offset=0):
|
||
"""
|
||
Получает список тем с пагинацией и статистикой.
|
||
|
||
Args:
|
||
limit: Максимальное количество возвращаемых тем
|
||
offset: Смещение для пагинации
|
||
|
||
Returns:
|
||
list: Список тем с их статистикой
|
||
"""
|
||
return await get_topics_with_stats(limit, offset)
|
||
|
||
|
||
# Запрос на получение тем по сообществу
|
||
@query.field("get_topics_by_community")
|
||
async def get_topics_by_community(_, _info, community_id: int, limit=100, offset=0):
|
||
"""
|
||
Получает список тем, принадлежащих указанному сообществу с пагинацией и статистикой.
|
||
|
||
Args:
|
||
community_id: ID сообщества
|
||
limit: Максимальное количество возвращаемых тем
|
||
offset: Смещение для пагинации
|
||
|
||
Returns:
|
||
list: Список тем с их статистикой
|
||
"""
|
||
return await get_topics_with_stats(limit, offset, community_id)
|
||
|
||
|
||
# Запрос на получение тем по автору
|
||
@query.field("get_topics_by_author")
|
||
async def get_topics_by_author(_, _info, author_id=0, slug="", user=""):
|
||
topics_by_author_query = select(Topic)
|
||
if author_id:
|
||
topics_by_author_query = topics_by_author_query.join(Author).where(Author.id == author_id)
|
||
elif slug:
|
||
topics_by_author_query = topics_by_author_query.join(Author).where(Author.slug == slug)
|
||
elif user:
|
||
topics_by_author_query = topics_by_author_query.join(Author).where(Author.user == user)
|
||
|
||
return get_with_stat(topics_by_author_query)
|
||
|
||
|
||
# Запрос на получение одной темы по её slug
|
||
@query.field("get_topic")
|
||
async def get_topic(_, _info, slug: str):
|
||
topic = await get_cached_topic_by_slug(slug, get_with_stat)
|
||
if topic:
|
||
return topic
|
||
|
||
|
||
# Мутация для создания новой темы
|
||
@mutation.field("create_topic")
|
||
@login_required
|
||
async def create_topic(_, _info, topic_input):
|
||
with local_session() as session:
|
||
# TODO: проверить права пользователя на создание темы для конкретного сообщества
|
||
# и разрешение на создание
|
||
new_topic = Topic(**topic_input)
|
||
session.add(new_topic)
|
||
session.commit()
|
||
|
||
# Инвалидируем кеш всех тем
|
||
await invalidate_topics_cache()
|
||
|
||
return {"topic": new_topic}
|
||
|
||
|
||
# Мутация для обновления темы
|
||
@mutation.field("update_topic")
|
||
@login_required
|
||
async def update_topic(_, _info, topic_input):
|
||
slug = topic_input["slug"]
|
||
with local_session() as session:
|
||
topic = session.query(Topic).filter(Topic.slug == slug).first()
|
||
if not topic:
|
||
return {"error": "topic not found"}
|
||
else:
|
||
Topic.update(topic, topic_input)
|
||
session.add(topic)
|
||
session.commit()
|
||
|
||
# Инвалидируем кеш всех тем и конкретной темы
|
||
await invalidate_topics_cache()
|
||
await redis.execute("DEL", f"topic:slug:{slug}")
|
||
await redis.execute("DEL", f"topic:id:{topic.id}")
|
||
|
||
return {"topic": topic}
|
||
|
||
|
||
# Мутация для удаления темы
|
||
@mutation.field("delete_topic")
|
||
@login_required
|
||
async def delete_topic(_, info, slug: str):
|
||
user_id = info.context["user_id"]
|
||
with local_session() as session:
|
||
t: Topic = session.query(Topic).filter(Topic.slug == slug).first()
|
||
if not t:
|
||
return {"error": "invalid topic slug"}
|
||
author = session.query(Author).filter(Author.user == user_id).first()
|
||
if author:
|
||
if t.created_by != author.id:
|
||
return {"error": "access denied"}
|
||
|
||
session.delete(t)
|
||
session.commit()
|
||
|
||
# Инвалидируем кеш всех тем и конкретной темы
|
||
await invalidate_topics_cache()
|
||
await redis.execute("DEL", f"topic:slug:{slug}")
|
||
await redis.execute("DEL", f"topic:id:{t.id}")
|
||
|
||
return {}
|
||
return {"error": "access denied"}
|
||
|
||
|
||
# Запрос на получение подписчиков темы
|
||
@query.field("get_topic_followers")
|
||
async def get_topic_followers(_, _info, slug: str):
|
||
logger.debug(f"getting followers for @{slug}")
|
||
topic = await get_cached_topic_by_slug(slug, get_with_stat)
|
||
topic_id = topic.id if isinstance(topic, Topic) else topic.get("id")
|
||
followers = await get_cached_topic_followers(topic_id)
|
||
return followers
|
||
|
||
|
||
# Запрос на получение авторов темы
|
||
@query.field("get_topic_authors")
|
||
async def get_topic_authors(_, _info, slug: str):
|
||
logger.debug(f"getting authors for @{slug}")
|
||
topic = await get_cached_topic_by_slug(slug, get_with_stat)
|
||
topic_id = topic.id if isinstance(topic, Topic) else topic.get("id")
|
||
authors = await get_cached_topic_authors(topic_id)
|
||
return authors
|