test ferst

This commit is contained in:
jze9
2026-05-13 19:05:07 +05:00
commit 0ac1c8a862
33 changed files with 3702 additions and 0 deletions

132
api/route/public.py Normal file
View File

@@ -0,0 +1,132 @@
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