Files
news_all_spo/api/route/public.py
2026-05-13 19:05:07 +05:00

133 lines
4.0 KiB
Python

import json
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
import redis.asyncio as aioredis
from bd.database import get_db, get_redis
from bd.models import Article, Category, Tag, ArticleStatus
router = APIRouter()
def _cat_dict(c: Category) -> dict:
return {"id": str(c.id), "name": c.name, "slug": c.slug}
def _article_card(a: Article) -> dict:
return {
"id": str(a.id),
"title": a.title,
"slug": a.slug,
"excerpt": a.excerpt,
"cover_url": a.cover_url,
"published_at": a.published_at.isoformat() if a.published_at else None,
"view_count": a.view_count,
"category": _cat_dict(a.category) if a.category else None,
"tags": [{"id": str(t.id), "name": t.name, "slug": t.slug} for t in a.tags],
}
def _article_full(a: Article) -> dict:
d = _article_card(a)
d["content"] = a.content
d["font_family"] = a.font_family
return d
@router.get("")
async def list_news(
offset: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
category: str | None = Query(None),
tag: str | None = Query(None),
db: AsyncSession = Depends(get_db),
redis: aioredis.Redis = Depends(get_redis),
):
cache_key = f"news:list:{offset}:{limit}:{category or ''}:{tag or ''}"
cached = await redis.get(cache_key)
if cached:
return json.loads(cached)
base = (
select(Article)
.options(selectinload(Article.category), selectinload(Article.tags))
.where(Article.status == ArticleStatus.published)
)
if category:
cat = (await db.execute(select(Category).where(Category.slug == category))).scalar_one_or_none()
if cat:
base = base.where(Article.category_id == cat.id)
else:
return {"total": 0, "offset": offset, "limit": limit, "has_more": False, "items": []}
if tag:
t = (await db.execute(select(Tag).where(Tag.slug == tag))).scalar_one_or_none()
if t:
base = base.where(Article.tags.any(Tag.id == t.id))
count_q = select(func.count()).select_from(base.subquery())
total: int = (await db.execute(count_q)).scalar_one()
articles = (
await db.execute(base.order_by(Article.published_at.desc()).offset(offset).limit(limit))
).scalars().all()
result = {
"total": total,
"offset": offset,
"limit": limit,
"has_more": (offset + limit) < total,
"items": [_article_card(a) for a in articles],
}
await redis.setex(cache_key, 30, json.dumps(result, default=str))
return result
@router.get("/categories")
async def list_categories(
db: AsyncSession = Depends(get_db),
redis: aioredis.Redis = Depends(get_redis),
):
cached = await redis.get("news:categories")
if cached:
return json.loads(cached)
cats = (await db.execute(select(Category).order_by(Category.name))).scalars().all()
result = [_cat_dict(c) for c in cats]
await redis.setex("news:categories", 60, json.dumps(result))
return result
@router.get("/{slug}")
async def get_article(
slug: str,
db: AsyncSession = Depends(get_db),
redis: aioredis.Redis = Depends(get_redis),
):
cache_key = f"news:article:{slug}"
cached = await redis.get(cache_key)
q = (
select(Article)
.options(selectinload(Article.category), selectinload(Article.tags))
.where(Article.slug == slug, Article.status == ArticleStatus.published)
)
article = (await db.execute(q)).scalar_one_or_none()
if not article:
raise HTTPException(status_code=404, detail="Article not found")
article.view_count += 1
await db.commit()
if cached:
data = json.loads(cached)
data["view_count"] = article.view_count
return data
await db.refresh(article)
result = _article_full(article)
await redis.setex(cache_key, 60, json.dumps(result, default=str))
return result